jsonapi

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 18, 2022 License: MIT Imports: 9 Imported by: 0

README

jsonapi

A toolkit for building consistent json apis with ease.

Features:

  • Custom request validation (we provide an adapter for github.com/xeipuuv/gojsonschema)
  • Automatic parameter injection (we provide an adapter for github.com/gorilla/mux)

Check usage for examples.

Install

go get -u github.com/mnavarrocarter/jsonapi

Usage

You can wrap simple functions into a JsonHandler using jsonapi.Wrap(). This JsonHandler implements http.Handler so you can use it as you would use any handler.

package main

import (
	"context"
	"errors"
	"fmt"
	"github.com/mnavarrocarter/jsonapi"
	"net/http"
)

type GreetCmd struct {
	Name        string `json:"name"`
	ShouldPanic bool   `json:"shouldPanic"`
}

func Greet(_ context.Context, cmd *GreetCmd) (map[string]string, error) {
	if cmd.ShouldPanic {
		panic("something unexpected has happened")
	}

	if cmd.Name == "" {
		return nil, errors.New("you must provide a name")
	}

	return map[string]string{
		"message": fmt.Sprintf("Hello %s", cmd.Name),
	}, nil
}

func main() {
	handler := jsonapi.Wrap(Greet)

	err := http.ListenAndServe(":8000", handler)
	if err != nil {
		panic(err)
	}
}

Then you can invoke the handler and see the magic in action!

POST http://localhost:8000
Content-Type: application/json

{
  "name": "Matias"
}

---

HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 25 Jul 2022 12:07:48 GMT
Content-Length: 27

{
  "message": "Hello Matias"
}

The json handler takes care of serializing the json and put it into the right struct, and then passing it into your function in the correct order. It can also inject any type that implements context.Context.

The handler is smart enough to check that it needs a body and if none is present will report back to the user:

GET http://localhost:8000

---

HTTP/1.1 400 Bad Request
Content-Type: application/json
Date: Mon, 25 Jul 2022 14:10:21 GMT
Content-Length: 81

{
  "status": 400,
  "kind": "Invalid Request",
  "details": "Request body cannot be empty"
}
Validation

You can instruct the handler to validate payloads by passing a json schema. This gives you valid structs in your handlers. The error reporting of the schema validation is consistent.

package main

import (
	"context"
	"errors"
	"fmt"
	"github.com/mnavarrocarter/jsonapi"
	"net/http"
	"strings"
)

type GreetCmd struct {
	Name        string `json:"name"`
	ShouldPanic bool   `json:"should_panic"`
}

func Greet(_ context.Context, cmd *GreetCmd) (map[string]string, error) {
	if cmd.ShouldPanic {
		panic("something unexpected has happened")
	}

	if cmd.Name == "" {
		return nil, errors.New("you must provide a name")
	}

	return map[string]string{
		"message": fmt.Sprintf("Hello %s", cmd.Name),
	}, nil
}

var schema = `{
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "minLength": 2
        },
        "should_panic": {
            "type": "boolean"
        }
    }
}`

func main() {
	handler := jsonapi.Wrap(Greet, jsonapi.WithSchema(strings.NewReader(schema)))

	err := http.ListenAndServe(":8000", handler)
	if err != nil {
		panic(err)
	}
}

You can make your own validation logic by implementing jsonapi.RequestValidator.

Error Handling

Errors are handled properly by the handler's error handler.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var Defaults = &defaults{}

Defaults implements all the interfaces of this package in a default way.

You can use this to compose custom behaviour on top.

View Source
var ErrArgumentResolution = errors.New("argument resolution error")
View Source
var ErrArgumentUnsupported = errors.New("argument resolution unsupported")
View Source
var ErrEmptyBody = errors.New("request body is empty")
View Source
var ErrValidation = errors.New("validation error")
View Source
var MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
	HandleError(w, req, &apiError{
		code: http.StatusMethodNotAllowed,
		msg:  fmt.Sprintf("Method not allowed for %s %s", req.Method, req.URL.Path),
	})
})
View Source
var NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
	HandleError(w, req, &apiError{
		code: http.StatusNotFound,
		msg:  fmt.Sprintf("No handler found for %s %s", req.Method, req.URL.Path),
	})
})
View Source
var VarFunc = func(r *http.Request) map[string]string {
	return map[string]string{}
}

Functions

This section is empty.

Types

type ArgumentResolver

type ArgumentResolver interface {
	// Resolve resolves an argument using the request information and the argument's type and position.
	// It must return a valid reflect.Value
	//
	// ErrArgumentUnsupported is returned when the type could not be resolved
	// ErrArgumentResolution is returned when something unexpected happens while resolving
	// ErrEmptyBody is returned when the body EOFs
	Resolve(req *http.Request, t reflect.Type, pos int) (reflect.Value, error)
}

An ArgumentResolver resolves argument types from a function in the context of an HTTP Request

For every argument in a function, it receives the current http.Request, the reflect.Type of the argument and the position on the argument on the function signature.

Go does not have a notion of argument names, so the position is crucial to implement custom resolving logic.

type Coder

type Coder interface {
	Code() int
}

Coder yields the status code for an error

type ErrorHandlerFunc

type ErrorHandlerFunc = func(w http.ResponseWriter, req *http.Request, err error)
var HandleError ErrorHandlerFunc = handleError

type ErrorItem

type ErrorItem struct {
	Field string      `json:"field"`
	Value interface{} `json:"value"`
	Msg   string      `json:"msg"`
}

type JsonHandler

type JsonHandler struct {
	RequestValidator RequestValidator // The validator for the request
	ArgumentResolver ArgumentResolver // The argument resolver to be used
	SkipPanic        bool             // Whether to skip panics or not
	// contains filtered or unexported fields
}

A JsonHandler wraps a function in JsonHandler implements http.Handler. This handler is capable of resolving arguments at request time and injecting them into the function. See ArgumentResolver.

For instance, the default maker will inject the context included on the request, and also any json body into a struct.

func Wrap

func Wrap(fn any, opts ...OptsFn) *JsonHandler

Wrap makes a JsonHandler using the Defaults

See JsonHandler for documentation on how this handler works.

Also, see Defaults to study the default implementations of the different components.

func (*JsonHandler) ServeHTTP

func (h *JsonHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)

type OptsFn

type OptsFn func(h *JsonHandler)

func WithSchema

func WithSchema(schema io.Reader) OptsFn

func WithVar

func WithVar(key string, pos int) OptsFn

type RequestValidator

type RequestValidator interface {
	// Validate validates the request
	//
	// If the body of the request is read, then the validator needs to restore
	// the request body so future handlers still have access to it.
	//
	// Validate MUST return ErrValidation error when an error running the validation occurs.
	//
	// If there are actual validation errors, then a slice of ErrorItem should be returned
	Validate(req *http.Request) ([]*ErrorItem, error)
}

type ResponseSenderFunc

type ResponseSenderFunc = func(w http.ResponseWriter, req *http.Request, v any)
var SendResponse ResponseSenderFunc = sendResponse

type Wrapper

type Wrapper interface {
	Unwrap() error
}

Wrapper mimics the unwrap functionality in go errors package

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL