blob: 29d5f5c2f010239952d3e5d6bd58ffffa5ac62d5 [file] [log] [blame] [edit]
// Package api implements the REST API for the scrap exchange service.
//
// See also the Scrap Exchange Design Doc http://go/scrap-exchange.
package api
import (
"bytes"
"encoding/json"
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/metrics2"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/scrap/go/scrap"
)
// Endpoint names used for metrics.
const (
scrapsCreateCallMetric = "scrap_exchange_api_scraps_create"
scrapsGetCallMetric = "scrap_exchange_api_scraps_get"
scrapsDeleteCallMetric = "scrap_exchange_api_scraps_delete"
rawGetCallMetric = "scrap_exchange_api_raw_get"
templateGetCallMetric = "scrap_exchange_api_template_get"
namesPutCallMetric = "scrap_exchange_api_names_put"
namesGetCallMetric = "scrap_exchange_api_names_get"
namesDeleteCallMetric = "scrap_exchange_api_names_delete"
namesListCallMetric = "scrap_exchange_api_names_list"
)
// Api supplies the handlers for the scrap exchange REST API.
type Api struct {
scrapExchange scrap.ScrapExchange
}
// New returns a new Api instance.
func New(scrapExchange scrap.ScrapExchange) *Api {
return &Api{
scrapExchange: scrapExchange,
}
}
// The URL variable names used in the mux Path handlers. See ApplyHandlers.
const (
hashOrNameVar = "hashOrName"
nameVar = "name"
typeVar = "type"
langVar = "lang"
)
var (
errUnknownType = errors.New("Unknown Type.")
errUnknownLang = errors.New("Unknown Lang.")
)
// Option controls which endpoints get added in AddHandlers.
type Option int
const (
DoNotAddProtectedEndpoints Option = iota
AddProtectedEndpoints
)
// AddHandlers hooks up the Scrap Exchange REST API to the given router.
//
// The value of 'option' controls if Protected endpoints, such as the one for
// creating scraps, are added to the router.
//
// +----------------------------------------+--------+---------+---------------+-----------------------------------+------+
// | URL | Method | Request | Response | Description | Prot |
// +----------------------------------------+--------+---------+---------------+-----------------------------------+------+
// | /_/scraps/ | POST | Scrap | ScrapID | Creates a new scrap. | Y |
// +----------------------------------------+--------+---------+---------------+-----------------------------------+------+
// | /_/scraps/{type}/({hash}|{name}) | GET | | ScrapBody | Returns the scrap. | |
// +----------------------------------------+--------+---------+---------------+-----------------------------------+------+
// | /_/scraps/{type}/({hash}|{name}) | DELETE | | | Removes the scrap. | Y |
// +----------------------------------------+--------+---------+---------------+-----------------------------------+------+
// | /_/raw/{type}/({hash}|{name}) | GET | | text/plain | Returns the raw scrap. | |
// | | | | image/svg+xml | | |
// +----------------------------------------+--------+---------+---------------+-----------------------------------+------+
// | /_/tmpl/{type}/({hash}|{name})/{lang} | GET | | text/plain | Templated scrap. | |
// +----------------------------------------+--------+---------+---------------+-----------------------------------+------+
// | /_/names/{type}/{name} | PUT | Name | | Creates/Updates a named scrap. | Y |
// +----------------------------------------+--------+---------+---------------+-----------------------------------+------+
// | /_/names/{type}/{name} | GET | | Name | Retrieves a single named scrap | |
// +----------------------------------------+--------+---------+---------------+-----------------------------------+------+
// | /_/names/{type}/{name} | DELETE | | | Deletes the scrap name. | Y |
// +----------------------------------------+--------+---------+---------------+-----------------------------------+------+
// | /_/_names/{type}/ | GET | | []string | Returns all named scraps of type. | |
// +----------------------------------------+--------+---------+---------------+-----------------------------------+------+
func (a *Api) AddHandlers(r chi.Router, option Option) {
if option == AddProtectedEndpoints {
r.Post("/_/scraps/", a.scrapCreateHandler)
}
const scrapPath = "/_/scraps/{type:[a-z]+}/{hashOrName:[@0-9a-zA-Z-_]+}"
r.Get(scrapPath, a.scrapGetHandler)
if option == AddProtectedEndpoints {
r.Delete(scrapPath, a.scrapDeleteHandler)
}
r.Get("/_/raw/{type:[a-z]+}/{hashOrName:[@0-9a-zA-Z-_]+}", a.rawGetHandler)
r.Get("/_/tmpl/{type:[a-z]+}/{hashOrName:[@0-9a-zA-Z-_]+}/{lang:[a-z]+}", a.templateGetHandler)
const namePath = "/_/names/{type:[a-z]+}/{name:@[0-9a-zA-Z-_]+}"
r.Get(namePath, a.nameGetHandler)
if option == AddProtectedEndpoints {
r.Put(namePath, a.namePutHandler)
r.Delete(namePath, a.nameDeleteHandler)
}
r.Get("/_/names/{type:[a-z]+}/", a.namesListHandler)
}
// writeJSON writes 'body' as a JSON encoded HTTP response with the right
// mime-type, and logs errors if the body failed to write.
func writeJSON(w http.ResponseWriter, body interface{}) {
w.Header().Set("Content-Type", "application/json")
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(body); err != nil {
httputils.ReportError(w, err, "Failed to encode JSON response.", http.StatusInternalServerError)
return
}
if _, err := w.Write(b.Bytes()); err != nil {
sklog.Errorf("Failed to write JSON response.")
}
}
// scrapCreateHandler implements the REST API, see AddHandlers.
func (a *Api) scrapCreateHandler(w http.ResponseWriter, r *http.Request) {
metrics2.GetCounter(scrapsCreateCallMetric).Inc(1)
var body scrap.ScrapBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
httputils.ReportError(w, err, "Failed to decode ScrapBody", http.StatusBadRequest)
return
}
id, err := a.scrapExchange.CreateScrap(r.Context(), body)
if err != nil {
httputils.ReportError(w, err, "Failed to store scrap", http.StatusInternalServerError)
return
}
writeJSON(w, id)
}
// getType returns the Type specified in the URL, or false if the type was
// invalid or not present.
//
// This function will also report the error on the http.ResponseWriter.
func (a *Api) getType(w http.ResponseWriter, r *http.Request) (scrap.Type, bool) {
t := scrap.ToType(chi.URLParam(r, typeVar))
if t == scrap.UnknownType {
httputils.ReportError(w, errUnknownType, "Unknown type.", http.StatusBadRequest)
return scrap.UnknownType, false
}
return t, true
}
// hashOrNameAndType extracts the hashOrName and Type from the URL, returning true if the Type was valid.
// If false is returned then an error has already been reported on the http.ResponseWriter.
func (a *Api) hashOrNameAndType(w http.ResponseWriter, r *http.Request) (string, scrap.Type, bool) {
hashOrName := chi.URLParam(r, hashOrNameVar)
t, ok := a.getType(w, r)
return hashOrName, t, ok
}
// nameAndType extracts the name and Type from the URL, returning true if the Type was valid.
// If false is returned then an error has already been reported on the http.ResponseWriter.
func (a *Api) nameAndType(w http.ResponseWriter, r *http.Request) (string, scrap.Type, bool) {
name := chi.URLParam(r, nameVar)
t, ok := a.getType(w, r)
return name, t, ok
}
// scrapGetHandler implements the REST API, see AddHandlers.
func (a *Api) scrapGetHandler(w http.ResponseWriter, r *http.Request) {
metrics2.GetCounter(scrapsGetCallMetric).Inc(1)
hashOrName, t, ok := a.hashOrNameAndType(w, r)
if !ok {
return
}
scrapBody, err := a.scrapExchange.LoadScrap(r.Context(), t, hashOrName)
if err != nil {
httputils.ReportError(w, err, "Failed to load scrap.", http.StatusBadRequest)
return
}
writeJSON(w, scrapBody)
}
// scrapDeleteHandler implements the REST API, see AddHandlers.
func (a *Api) scrapDeleteHandler(w http.ResponseWriter, r *http.Request) {
metrics2.GetCounter(scrapsDeleteCallMetric).Inc(1)
hashOrName, t, ok := a.hashOrNameAndType(w, r)
if !ok {
return
}
err := a.scrapExchange.DeleteScrap(r.Context(), t, hashOrName)
if err != nil {
httputils.ReportError(w, err, "Failed to delete scrap.", http.StatusBadRequest)
return
}
}
// rawGetHandler implements the REST API, see AddHandlers.
func (a *Api) rawGetHandler(w http.ResponseWriter, r *http.Request) {
metrics2.GetCounter(rawGetCallMetric).Inc(1)
hashOrName, t, ok := a.hashOrNameAndType(w, r)
if !ok {
return
}
scrapBody, err := a.scrapExchange.LoadScrap(r.Context(), t, hashOrName)
if err != nil {
httputils.ReportError(w, err, "Failed to load scrap.", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", scrap.MimeTypes[t])
if _, err := w.Write([]byte(scrapBody.Body)); err != nil {
sklog.Errorf("Failed to write result: %s", err)
}
}
// templateGetHandler implements the REST API, see AddHandlers.
func (a *Api) templateGetHandler(w http.ResponseWriter, r *http.Request) {
metrics2.GetCounter(templateGetCallMetric).Inc(1)
hashOrName := chi.URLParam(r, hashOrNameVar)
t, ok := a.getType(w, r)
if !ok {
return
}
l := scrap.ToLang(chi.URLParam(r, langVar))
if l == scrap.UnknownLang {
httputils.ReportError(w, errUnknownLang, "Unknown language.", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/plain")
if err := a.scrapExchange.Expand(r.Context(), t, hashOrName, l, w); err != nil {
httputils.ReportError(w, err, "Failed to expand scrap.", http.StatusBadRequest)
return
}
}
// namePutHandler implements the REST API, see AddHandlers.
func (a *Api) namePutHandler(w http.ResponseWriter, r *http.Request) {
metrics2.GetCounter(namesPutCallMetric).Inc(1)
name, t, ok := a.nameAndType(w, r)
if !ok {
return
}
var nameBody scrap.Name
if err := json.NewDecoder(r.Body).Decode(&nameBody); err != nil {
httputils.ReportError(w, err, "Failed to decode Name", http.StatusBadRequest)
return
}
if err := a.scrapExchange.PutName(r.Context(), t, name, nameBody); err != nil {
httputils.ReportError(w, err, "Failed to write name.", http.StatusInternalServerError)
return
}
}
// nameGetHandler implements the REST API, see AddHandlers.
func (a *Api) nameGetHandler(w http.ResponseWriter, r *http.Request) {
metrics2.GetCounter(namesGetCallMetric).Inc(1)
name, t, ok := a.nameAndType(w, r)
if !ok {
return
}
nameBody, err := a.scrapExchange.GetName(r.Context(), t, name)
if err != nil {
httputils.ReportError(w, err, "Failed to retrieve Name.", http.StatusInternalServerError)
return
}
writeJSON(w, nameBody)
}
// nameDeleteHandler implements the REST API, see AddHandlers.
func (a *Api) nameDeleteHandler(w http.ResponseWriter, r *http.Request) {
metrics2.GetCounter(namesDeleteCallMetric).Inc(1)
name, t, ok := a.nameAndType(w, r)
if !ok {
return
}
err := a.scrapExchange.DeleteName(r.Context(), t, name)
if err != nil {
httputils.ReportError(w, err, "Failed to delete Name.", http.StatusInternalServerError)
return
}
}
// namesListHandler implements the REST API, see AddHandlers.
func (a *Api) namesListHandler(w http.ResponseWriter, r *http.Request) {
metrics2.GetCounter(namesListCallMetric).Inc(1)
t, ok := a.getType(w, r)
if !ok {
return
}
names, err := a.scrapExchange.ListNames(r.Context(), t)
if err != nil {
httputils.ReportError(w, err, "Failed to load Names.", http.StatusInternalServerError)
return
}
writeJSON(w, names)
}