diff --git a/cmd/api/command.go b/cmd/api/command.go index 744e2ca..002db60 100644 --- a/cmd/api/command.go +++ b/cmd/api/command.go @@ -1,8 +1,8 @@ package main import ( - "encoding/json" "errors" + "fmt" "net/http" "github.com/lyx0/nourybot/internal/data" @@ -37,30 +37,40 @@ func (app *application) showCommandHandler(w http.ResponseWriter, r *http.Reques } } -type envelope map[string]interface{} +func (app *application) createCommandHandler(w http.ResponseWriter, r *http.Request) { + var input struct { + Name string `json:"name"` + Text string `json:"text"` + Category string `json:"category"` + Level int `json:"level"` + } -func (app *application) writeJSON(w http.ResponseWriter, status int, data envelope, headers http.Header) error { - // Encode the data into JSON and return any errors if there were any. - // Use MarshalIndent instead of normal Marshal so it looks prettier on terminals. - js, err := json.MarshalIndent(data, "", "\t") + err := app.readJSON(w, r, &input) if err != nil { - return err + app.badRequestResponse(w, r, err) + return } - // Append a newline to make it prettier on terminals. - js = append(js, '\n') - - // Iterate over the header map and add each header to the - // http.ResponseWriter header map. - for key, value := range headers { - w.Header()[key] = value + command := &data.Command{ + Name: input.Name, + Text: input.Text, + Category: input.Category, + Level: input.Level, } - // Set `Content-Type` to `application/json` because go - // defaults to `text-plain; charset=utf8`. - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - w.Write(js) + err = app.Models.Commands.Insert(command) + if err != nil { + app.serverErrorResponse(w, r, err) + return + } - return nil + headers := make(http.Header) + headers.Set("Location", fmt.Sprintf("/v1/commands/%s", command.Name)) + + err = app.writeJSON(w, http.StatusCreated, envelope{"command": command}, headers) + if err != nil { + app.serverErrorResponse(w, r, err) + } } + +type envelope map[string]interface{} diff --git a/cmd/api/errors.go b/cmd/api/errors.go index 6c6fecd..8825b5c 100644 --- a/cmd/api/errors.go +++ b/cmd/api/errors.go @@ -29,3 +29,7 @@ func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, st // 500 Internal Server Error status code. fmt.Fprintf(w, "Error: %d", status) } + +func (app *application) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) { + app.errorResponse(w, r, http.StatusBadRequest, err.Error()) +} diff --git a/cmd/api/helpers.go b/cmd/api/helpers.go index 87a8a6f..00032cf 100644 --- a/cmd/api/helpers.go +++ b/cmd/api/helpers.go @@ -1,7 +1,12 @@ package main import ( + "encoding/json" + "errors" + "fmt" + "io" "net/http" + "strings" "github.com/julienschmidt/httprouter" ) @@ -20,6 +25,57 @@ import ( // return id, nil // } +func (app *application) readJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error { + // Limit the size of the requst body to 1MB. + maxBytes := 1_048_576 + r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes)) + + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + + err := dec.Decode(dst) + if err != nil { + var syntaxError *json.SyntaxError + var unmarshalTypeError *json.UnmarshalTypeError + var invalidUnmarshalError *json.InvalidUnmarshalError + + switch { + case errors.As(err, &syntaxError): + return fmt.Errorf("body contains malformed JSON (at character %d)", syntaxError.Offset) + + case errors.Is(err, io.ErrUnexpectedEOF): + return errors.New("body contains invalid JSON") + + case errors.As(err, &unmarshalTypeError): + if unmarshalTypeError.Field != "" { + return fmt.Errorf("body contains invalid JSON type for field %q", unmarshalTypeError.Field) + } + return fmt.Errorf("body contains invalid JSON type (at character %d)", unmarshalTypeError.Offset) + + case errors.Is(err, io.EOF): + return errors.New("body contains no data") + + case strings.HasPrefix(err.Error(), "json: unknown field "): + fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ") + return fmt.Errorf("body contains unknown key %s", fieldName) + + case err.Error() == "htto: request body too large": + return fmt.Errorf("body must not be larger than %d bytes", maxBytes) + + case errors.As(err, &invalidUnmarshalError): + app.Logger.Panic(err) + default: + return err + } + } + + err = dec.Decode(&struct{}{}) + if err != io.EOF { + return errors.New("body must only contain a single JSON value") + } + return nil +} + func (app *application) readCommandNameParam(r *http.Request) (string, error) { params := httprouter.ParamsFromContext(r.Context()) @@ -30,3 +86,29 @@ func (app *application) readCommandNameParam(r *http.Request) (string, error) { return name, nil } + +func (app *application) writeJSON(w http.ResponseWriter, status int, data envelope, headers http.Header) error { + // Encode the data into JSON and return any errors if there were any. + // Use MarshalIndent instead of normal Marshal so it looks prettier on terminals. + js, err := json.MarshalIndent(data, "", "\t") + if err != nil { + return err + } + + // Append a newline to make it prettier on terminals. + js = append(js, '\n') + + // Iterate over the header map and add each header to the + // http.ResponseWriter header map. + for key, value := range headers { + w.Header()[key] = value + } + + // Set `Content-Type` to `application/json` because go + // defaults to `text-plain; charset=utf8`. + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + w.Write(js) + + return nil +} diff --git a/cmd/api/routes.go b/cmd/api/routes.go index 780e3b9..f79dcd4 100644 --- a/cmd/api/routes.go +++ b/cmd/api/routes.go @@ -10,5 +10,6 @@ func (app *application) routes() *httprouter.Router { router := httprouter.New() router.HandlerFunc(http.MethodGet, "/v1/commands/:name", app.showCommandHandler) + router.HandlerFunc(http.MethodPost, "/v1/commands", app.createCommandHandler) return router }