feat: go greenlight api wip

This commit is contained in:
2026-03-11 15:50:38 +01:00
parent 2ce943f94e
commit abf2db2798
19 changed files with 469 additions and 0 deletions
@@ -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)
}
BIN
View File
Binary file not shown.
View File
+59
View File
@@ -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)
}
}
+104
View File
@@ -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
}
+61
View File
@@ -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)
}
+55
View File
@@ -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)
}
}
+26
View File
@@ -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
}
+5
View File
@@ -0,0 +1,5 @@
module greenlight.debuggingjon.dev
go 1.25.0
require github.com/julienschmidt/httprouter v1.3.0 // indirect
+2
View File
@@ -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
}