feat: go greenlight api wip
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// The logError() method is a generic helper for logging an error message. “Later in the
|
||||
// book we'll upgrade this to use structured logging, and record additional information
|
||||
// about the request including the HTTP method and URL.
|
||||
func (app *application) logError(r *http.Request, err error) {
|
||||
app.logger.Println(err)
|
||||
}
|
||||
|
||||
// The errorResponse() method is a generic helper for sending JSON-formatted error
|
||||
// messages to the client with a given status code. Note that we're using an interface{}
|
||||
// type for the message parameter, rather than just a string type, as this gives us
|
||||
// more flexibility over the values that we can include in the response.
|
||||
func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message interface{}) {
|
||||
env := envelope{"error": message}
|
||||
|
||||
// Write the response using the writeJSON() helper. If this happens to return an
|
||||
// error then log it, and fall back to sending the client an empty response with a
|
||||
// 500 Internal Server Error status code.
|
||||
err := app.writeJSON(w, status, env, nil)
|
||||
if err != nil {
|
||||
app.logError(r, err)
|
||||
w.WriteHeader(500)
|
||||
}
|
||||
}
|
||||
|
||||
// The serverErrorResponse() method will be used when our application encounters an
|
||||
// unexpected problem at runtime. It logs the detailed error message, then uses the
|
||||
// errorResponse() helper to send a 500 Internal Server Error status code and JSON
|
||||
// response (containing a generic error message) to the client.
|
||||
func (app *application) serverErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||
app.logError(r, err)
|
||||
|
||||
message := "the server encountered a problem and could not process your request"
|
||||
app.errorResponse(w, r, http.StatusInternalServerError, message)
|
||||
}
|
||||
|
||||
// The notFoundResponse() method will be used to send a 404 Not Found status code and
|
||||
// JSON response to the client.
|
||||
func (app *application) notFoundResponse(w http.ResponseWriter, r *http.Request) {
|
||||
message := "the requested resource could not be found"
|
||||
app.errorResponse(w, r, http.StatusNotFound, message)
|
||||
}
|
||||
|
||||
// The methodNotAllowedResponse() method will be used to send a 405 Method Not Allowed
|
||||
// status code and JSON response to the client.
|
||||
func (app *application) methodNotAllowedResponse(w http.ResponseWriter, r *http.Request) {
|
||||
message := fmt.Sprintf("the %s method is not supported for this resource", r.Method)
|
||||
app.errorResponse(w, r, http.StatusMethodNotAllowed, message)
|
||||
}
|
||||
|
||||
func (app *application) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||
app.errorResponse(w, r, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (app *application) healthcheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
env := envelope{
|
||||
"status": "available",
|
||||
"environment": app.config.env,
|
||||
"version": version,
|
||||
}
|
||||
|
||||
err := app.writeJSON(w, http.StatusOK, env, nil)
|
||||
if err != nil {
|
||||
// Use the new serverErrorResponse() helper.
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
// Define an envelope type.
|
||||
type envelope map[string]any
|
||||
|
||||
// Retrieve the "id" URL parameter from the current request context, then convert it to
|
||||
// an integer and return it. If the operation isn't successful, return 0 and an error.
|
||||
func (app *application) readIDParam(r *http.Request) (int64, error) {
|
||||
params := httprouter.ParamsFromContext(r.Context())
|
||||
|
||||
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
|
||||
if err != nil || id < 1 {
|
||||
return 0, errors.New("invalid id parameter")
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Change the data parameter to have the type envelope instead of interface{}.
|
||||
func (app *application) writeJSON(w http.ResponseWriter, status int, data envelope, headers http.Header) error {
|
||||
js, err := json.MarshalIndent(data, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
js = append(js, '\n')
|
||||
|
||||
maps.Copy(w.Header(), headers)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
w.Write(js)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *application) readJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {
|
||||
// Decode the request body into the target destination.
|
||||
err := json.NewDecoder(r.Body).Decode(dst)
|
||||
if err != nil {
|
||||
// If there is an error during decoding, start the triage...
|
||||
var syntaxError *json.SyntaxError
|
||||
var unmarshalTypeError *json.UnmarshalTypeError
|
||||
var invalidUnmarshalError *json.InvalidUnmarshalError
|
||||
|
||||
switch {
|
||||
// Use the errors.As() function to check whether the error has the type
|
||||
|
||||
// *json.SyntaxError. If it does, then return a plain-english error message
|
||||
// which includes the location of the problem.
|
||||
case errors.As(err, &syntaxError):
|
||||
return fmt.Errorf("body contains badly-formed JSON (at character %d)", syntaxError.Offset)
|
||||
|
||||
// In some circumstances Decode() may also return an io.ErrUnexpectedEOF error
|
||||
// for syntax errors in the JSON. So we check for this using errors.Is() and
|
||||
// return a generic error message. There is an open issue regarding this at
|
||||
// https://github.com/golang/go/issues/25956.
|
||||
case errors.Is(err, io.ErrUnexpectedEOF):
|
||||
return errors.New("body contains badly-formed JSON")
|
||||
|
||||
// Likewise, catch any *json.UnmarshalTypeError errors. These occur when the
|
||||
// JSON value is the wrong type for the target destination. If the error relates
|
||||
// to a specific field, then we include that in our error message to make it
|
||||
// easier for the client to debug.
|
||||
case errors.As(err, &unmarshalTypeError):
|
||||
if unmarshalTypeError.Field != "" {
|
||||
return fmt.Errorf("body contains incorrect JSON type for field %q", unmarshalTypeError.Field)
|
||||
}
|
||||
return fmt.Errorf("body contains incorrect JSON type (at character %d)", unmarshalTypeError.Offset)
|
||||
|
||||
// An io.EOF error will be returned by Decode() if the request body is empty. We
|
||||
// check for this with errors.Is() and return a plain-english error message
|
||||
// instead.
|
||||
case errors.Is(err, io.EOF):
|
||||
return errors.New("body must not be empty")
|
||||
|
||||
// A json.InvalidUnmarshalError error will be returned if we pass a non-nil
|
||||
// pointer to Decode(). We catch this and panic, rather than returning an error
|
||||
// to our handler. At the end of this chapter we'll talk about panicking
|
||||
// versus returning errors, and discuss why it's an appropriate thing to do in
|
||||
// this specific situation.
|
||||
case errors.As(err, &invalidUnmarshalError):
|
||||
panic(err)
|
||||
|
||||
// For anything else, return the error message as-is.
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Declare a string containing the application version number. Later in the book we'll
|
||||
// generate this automatically at build time, but for now we'll just store the version
|
||||
// number as a hard-coded global constant.
|
||||
const version = "1.0.0"
|
||||
|
||||
// Define a config struct to hold all the configuration settings for our application.
|
||||
// For now, the only configuration settings will be the network port that we want the
|
||||
// server to listen on, and the name of the current operating environment for the
|
||||
// application (development, staging, production, etc.). We will read in these
|
||||
// configuration settings from command-line flags when the application starts.
|
||||
type config struct {
|
||||
port int
|
||||
env string
|
||||
}
|
||||
|
||||
// Define an application struct to hold the dependencies for our HTTP handlers, helpers,
|
||||
// and middleware. At the moment this only contains a copy of the config struct and a
|
||||
// logger, but it will grow to include a lot more as our build progresses.
|
||||
type application struct {
|
||||
config config
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func main() {
|
||||
var cfg config
|
||||
|
||||
flag.IntVar(&cfg.port, "port", 4000, "API server port")
|
||||
flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)")
|
||||
flag.Parse()
|
||||
|
||||
logger := log.New(os.Stdout, "", log.Ldate|log.Ltime)
|
||||
|
||||
app := &application{
|
||||
config: cfg,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Use the httprouter instance returned by app.routes() as the server handler.
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", cfg.port),
|
||||
Handler: app.routes(),
|
||||
IdleTimeout: time.Minute,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
logger.Printf("starting %s server on %s", cfg.env, srv.Addr)
|
||||
err := srv.ListenAndServe()
|
||||
logger.Fatal(err)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"greenlight.debuggingjon.dev/internal/data"
|
||||
)
|
||||
|
||||
func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var input struct {
|
||||
Title string `json:"title"`
|
||||
Year int32 `json:"year"`
|
||||
Runtime int32 `json:"runtime"`
|
||||
Genres []string `json:"genres"`
|
||||
}
|
||||
|
||||
// Use the new readJSON() helper to decode the request body into the input struct.
|
||||
// If this returns an error we send the client the error message along with a 400
|
||||
// Bad Request status code, just like before.
|
||||
err := app.readJSON(w, r, &input)
|
||||
if err != nil {
|
||||
// Use the new badRequestResponse() helper.
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%+v\n", input)
|
||||
}
|
||||
|
||||
func (app *application) showMovieHandler(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := app.readIDParam(r)
|
||||
if err != nil {
|
||||
// Use the new notFoundResponse() helper.
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
movie := data.Movie{
|
||||
ID: id,
|
||||
CreatedAt: time.Now(),
|
||||
Title: "Casablanca",
|
||||
Runtime: 102,
|
||||
Genres: []string{"drama", "romance", "war"}, Version: 1,
|
||||
}
|
||||
|
||||
// Create an envelope{"movie": movie} instance and pass it to writeJSON(), instead
|
||||
// of passing the plain movie struct.
|
||||
err = app.writeJSON(w, http.StatusOK, envelope{"movie": movie}, nil)
|
||||
if err != nil {
|
||||
// Use the new serverErrorResponse() helper.
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
func (app *application) routes() *httprouter.Router {
|
||||
router := httprouter.New()
|
||||
|
||||
// Convert the notFoundResponse() helper to a http.Handler using the
|
||||
// http.HandlerFunc() adapter, and then set it as the custom error handler for 404
|
||||
// Not Found responses.
|
||||
router.NotFound = http.HandlerFunc(app.notFoundResponse)
|
||||
|
||||
// Likewise, convert the methodNotAllowedResponse() helper to a http.Handler and set
|
||||
// it as the custom error handler for 405 Method Not Allowed responses.
|
||||
router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse)
|
||||
|
||||
router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler)
|
||||
router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler)
|
||||
router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.showMovieHandler)
|
||||
|
||||
return router
|
||||
}
|
||||
Reference in New Issue
Block a user