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