Skip to content

Latest commit

 

History

History

fxjsonapi

Yokai JSON API Module

ci go report codecov Deps PkgGoDev

Yokai module for JSON API, based on google/jsonapi.

Overview

This module provides to your Yokai application a Processor, that you can inject in your HTTP handlers to process JSON API requests and responses.

It also provides automatic error handling, compliant with the JSON API specifications.

Installation

Install the module:

go get github.com/ankorstore/yokai-contrib/fxjsonapi

Then activate it in your application bootstrapper:

// internal/bootstrap.go
package internal

import (
	"github.com/ankorstore/yokai-contrib/fxjsonapi"
	"github.com/ankorstore/yokai/fxcore"
)

var Bootstrapper = fxcore.NewBootstrapper().WithOptions(
	// load modules
	fxjsonapi.FxJSONAPIModule,
	// ...
)

Configuration

Configuration reference:

# ./configs/config.yaml
modules:
  jsonapi:
    log:
      enabled: true # to automatically log JSON API processing, disabled by default
    trace:
      enabled: true # to automatically trace JSON API processing, disabled by default

Processing

JSON API request & response processing are driven by the jsonapi tag that you can set on your structs for un/marshalling operations.

You can find more information about this in the underlying google/jsonapi library documentation.

Request processing

You can use the provided Processor to automatically process a JSON API request:

package handler

import (
	"net/http"

	"github.com/ankorstore/yokai-contrib/fxjsonapi"
	"github.com/google/jsonapi"
	"github.com/labstack/echo/v4"
)

type Foo struct {
	ID   int    `jsonapi:"primary,foo"`
	Name string `jsonapi:"attr,name"`
	Bar  *Bar   `jsonapi:"relation,bar"`
}

func (f Foo) JSONAPIMeta() *jsonapi.Meta {
	return &jsonapi.Meta{
		"some": "foo meta",
	}
}

type Bar struct {
	ID   int    `jsonapi:"primary,bar"`
	Name string `jsonapi:"attr,name"`
}

func (b Bar) JSONAPIMeta() *jsonapi.Meta {
	return &jsonapi.Meta{
		"some": "bar meta",
	}
}

type JSONAPIHandler struct {
	processor fxjsonapi.Processor
}

func NewJSONAPIHandler(processor fxjsonapi.Processor) *JSONAPIHandler {
	return &JSONAPIHandler{
		processor: processor,
	}
}

func (h *JSONAPIHandler) Handle() echo.HandlerFunc {
	return func(c echo.Context) error {
		foo := Foo{}

		// unmarshall JSON API request payload in foo
		err := h.processor.ProcessRequest(
			// echo context
			c,
			// pointer to the struct to unmarshall
			&foo,
			// optionally override module config for logging
			fxjsonapi.WithLog(true),
			// optionally override module config for tracing
			fxjsonapi.WithTrace(true),
			)
		if err != nil {
			return err
		}

		return c.JSON(http.StatusOK, foo)
	}
}

Notes about ProcessRequest():

  • if the request payload does not respect the JSON API specifications, a 400 error will be automatically returned
  • if the request Content-Type header is not application/vnd.api+json, a 415 error will be automatically returned

Response processing

You can use the provided Processor to automatically process a JSON API response:

package handler

import (
	"net/http"

	"github.com/ankorstore/yokai-contrib/fxjsonapi"
	"github.com/ankorstore/yokai-contrib/fxjsonapi/testdata/model"
	"github.com/labstack/echo/v4"
)

type Foo struct {
	ID   int    `jsonapi:"primary,foo"`
	Name string `jsonapi:"attr,name"`
	Bar  *Bar   `jsonapi:"relation,bar"`
}

func (f Foo) JSONAPIMeta() *jsonapi.Meta {
	return &jsonapi.Meta{
		"some": "foo meta",
	}
}

type Bar struct {
	ID   int    `jsonapi:"primary,bar"`
	Name string `jsonapi:"attr,name"`
}

func (b Bar) JSONAPIMeta() *jsonapi.Meta {
	return &jsonapi.Meta{
		"some": "bar meta",
	}
}

type JSONAPIHandler struct {
	processor fxjsonapi.Processor
}

func NewJSONAPIHandler(processor fxjsonapi.Processor) *JSONAPIHandler {
	return &JSONAPIHandler{
		processor: processor,
	}
}

func (h *JSONAPIHandler) Handle() echo.HandlerFunc {
	return func(c echo.Context) error {
		foo := Foo{
			ID:   123,
			Name: "foo",
			Bar: &Bar{
				ID:   456,
				Name: "bar",
			},
		}

		return h.processor.ProcessResponse(
			// echo context
			c,
			// HTTP status code
			http.StatusOK,
			// pointer to the struct to marshall
			&foo,
			// optionally pass metadata to the JSON API response
			fxjsonapi.WithMetadata(map[string]interface{}{
				"some": "response meta",
			}),
			// optionally remove the included from the JSON API response (enabled by default)
			fxjsonapi.WithIncluded(false),
			// optionally override module config for logging
			fxjsonapi.WithLog(true),
			// optionally override module config for tracing
			fxjsonapi.WithTrace(true),
		)
	}
}

Notes about ProcessResponse():

  • you can pass a pointer or a slice of pointers to marshall as JSON API
  • application/vnd.api+json will be automatically added to the response Content-Type header

Error handling

This module automatically enables the ErrorHandler, to convert errors bubbling up in JSON API format.

It handles:

  • JSON API errors: automatically sets the status code of the error
  • validation errors: automatically sets a 400 status code
  • HTTP errors: automatically sets the status code of the error
  • or any generic error: automatically sets a 500 status code

You can optionally obfuscate the errors details (ex: for production) in the HTTP server configuration:

# ./configs/config.yaml
modules:
  http:
    server:
      errors:
        obfuscate: true # disabled by default

Testing

This module provides a ProcessorMock for mocking Processor, see usage example.