| // 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) |
| } |