feat: go greenlight api wip
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func ping(pings chan<- string, msg string) {
|
||||||
|
pings <- msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func pong(pings <-chan string, pongs chan<- string) {
|
||||||
|
msg := <-pings
|
||||||
|
pongs <- msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
pings := make(chan string, 1)
|
||||||
|
pongs := make(chan string, 1)
|
||||||
|
ping(pings, "passed message")
|
||||||
|
pong(pings, pongs)
|
||||||
|
fmt.Println(<-pongs)
|
||||||
|
}
|
||||||
Vendored
BIN
Binary file not shown.
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module greenlight.debuggingjon.dev
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require github.com/julienschmidt/httprouter v1.3.0 // indirect
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Secrets
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
info:
|
||||||
|
name: Create Movie
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: "{{URL}}{{VERSION}}/movies"
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"title": "Moana"
|
||||||
|
}
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
info:
|
||||||
|
name: Get Single Movie
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: "{{URL}}{{VERSION}}/movies/{{MOVIE_ID}}"
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
info:
|
||||||
|
name: Healthcheck
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: "{{URL}}{{VERSION}}/healthcheck"
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
name: Development
|
||||||
|
variables:
|
||||||
|
- name: URL
|
||||||
|
value: http://localhost:4000/
|
||||||
|
- name: VERSION
|
||||||
|
value: v1
|
||||||
|
- name: MOVIE_ID
|
||||||
|
value: "1"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
opencollection: 1.0.0
|
||||||
|
|
||||||
|
info:
|
||||||
|
name: greenlight-bruno
|
||||||
|
bundled: false
|
||||||
|
extensions:
|
||||||
|
bruno:
|
||||||
|
ignore:
|
||||||
|
- node_modules
|
||||||
|
- .git
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Movie struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
CreatedAt time.Time `json:"-"` // Use the - directive
|
||||||
|
Title string `json:"title"`
|
||||||
|
Year int32 `json:"year,omitempty"` // Add the omitempty directive
|
||||||
|
Runtime Runtime `json:"runtime,omitempty"` // Add the omitempty directive
|
||||||
|
Genres []string `json:"genres,omitempty"` // Add the omitempty directive
|
||||||
|
Version int32 `json:"version"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Declare a custom Runtime type, which has the underlying type int32 (the same as our
|
||||||
|
// Movie struct field).
|
||||||
|
type Runtime int32
|
||||||
|
|
||||||
|
// Implement a MarshalJSON() method on the Runtime type so that it satisfies the
|
||||||
|
// json.Marshaler interface. This should return the JSON-encoded value for the movie
|
||||||
|
// runtime (in our case, it will return a string in the format "<runtime> mins").
|
||||||
|
func (r Runtime) MarshalJSON() ([]byte, error) {
|
||||||
|
// Generate a string containing the movie runtime in the required format.
|
||||||
|
jsonValue := fmt.Sprintf("%d mins", r)
|
||||||
|
|
||||||
|
// Use the strconv.Quote() function on the string to wrap it in double quotes. It
|
||||||
|
// needs to be surrounded by double quotes in order to be a valid *JSON string*.
|
||||||
|
quotedJSONValue := strconv.Quote(jsonValue)
|
||||||
|
|
||||||
|
// Convert the quoted string value to a byte slice and return it.
|
||||||
|
return []byte(quotedJSONValue), nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user