Skip to content

Commit f5f537b

Browse files
authored
feat(fxjsonapi): Provided module (#28)
* feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module * feat(fxjsonapi): Provided module
1 parent 033aec4 commit f5f537b

23 files changed

+2318
-0
lines changed

.github/workflows/coverage.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ jobs:
1717
module:
1818
- "fxgcppubsub"
1919
- "fxgomysqlserver"
20+
- "fxjsonapi"
2021
- "fxslack"
2122
- "fxredis"
2223
steps:

.github/workflows/fxjsonapi-ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: "fxjsonapi-ci"
2+
3+
on:
4+
push:
5+
branches:
6+
- "feat**"
7+
- "fix**"
8+
- "hotfix**"
9+
- "chore**"
10+
paths:
11+
- "fxjsonapi/**.go"
12+
- "fxjsonapi/go.mod"
13+
- "fxjsonapi/go.sum"
14+
pull_request:
15+
types:
16+
- opened
17+
- synchronize
18+
- reopened
19+
branches:
20+
- main
21+
paths:
22+
- "fxjsonapi/**.go"
23+
- "fxjsonapi/go.mod"
24+
- "fxjsonapi/go.sum"
25+
26+
jobs:
27+
ci:
28+
uses: ./.github/workflows/common-ci.yml
29+
secrets: inherit
30+
with:
31+
module: "fxjsonapi"

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
|------------------------------------|--------------------------------------------------------------------------|
1313
| [fxgcppubsub](fxgcppubsub) | Module for [GCP Pub/Sub](https://cloud.google.com/pubsub) |
1414
| [fxgomysqlserver](fxgomysqlserver) | Module for [Go Mysql Server](https://github.com/dolthub/go-mysql-server) |
15+
| [fxjsonapi](fxjsonapi) | Module for [JSON API](https://github.com/google/jsonapi) |
1516
| [fxslack](fxslack) | Module for [Slack](https://api.slack.com/) |
1617
| [fxredis](fxredis) | Module for [Redis](https://redis.io/docs/connect/clients/go/) |
1718

fxjsonapi/.golangci.yml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
run:
2+
timeout: 5m
3+
concurrency: 8
4+
5+
linters:
6+
enable:
7+
- asasalint
8+
- asciicheck
9+
- bidichk
10+
- bodyclose
11+
- containedctx
12+
- contextcheck
13+
- cyclop
14+
- decorder
15+
- dogsled
16+
- durationcheck
17+
- errcheck
18+
- errchkjson
19+
- errname
20+
- errorlint
21+
- exhaustive
22+
- forbidigo
23+
- forcetypeassert
24+
- gocognit
25+
- goconst
26+
- gocritic
27+
- gocyclo
28+
- godot
29+
- godox
30+
- gofmt
31+
- goheader
32+
- gomoddirectives
33+
- gomodguard
34+
- goprintffuncname
35+
- gosec
36+
- gosimple
37+
- govet
38+
- grouper
39+
- importas
40+
- ineffassign
41+
- interfacebloat
42+
- loggercheck
43+
- maintidx
44+
- makezero
45+
- misspell
46+
- nestif
47+
- nilerr
48+
- nilnil
49+
- nlreturn
50+
- nolintlint
51+
- nosprintfhostport
52+
- prealloc
53+
- predeclared
54+
- promlinter
55+
- reassign
56+
- staticcheck
57+
- tenv
58+
- thelper
59+
- tparallel
60+
- typecheck
61+
- unconvert
62+
- unparam
63+
- unused
64+
- usestdlibvars
65+
- whitespace

fxjsonapi/README.md

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
# Yokai JSON API Module
2+
3+
[![ci](https://github.com/ankorstore/yokai-contrib/actions/workflows/fxjsonapi-ci.yml/badge.svg)](https://github.com/ankorstore/yokai-contrib/actions/workflows/fxjsonapi-ci.yml)
4+
[![go report](https://goreportcard.com/badge/github.com/ankorstore/yokai-contrib/fxjsonapi)](https://goreportcard.com/report/github.com/ankorstore/yokai-contrib/fxjsonapi)
5+
[![codecov](https://codecov.io/gh/ankorstore/yokai-contrib/graph/badge.svg?token=ghUBlFsjhR&flag=fxjsonapi)](https://app.codecov.io/gh/ankorstore/yokai-contrib/tree/main/fxjsonapi)
6+
[![Deps](https://img.shields.io/badge/osi-deps-blue)](https://deps.dev/go/github.com%2Fankorstore%2Fyokai-contrib%2Ffxjsonapi)
7+
[![PkgGoDev](https://pkg.go.dev/badge/github.com/ankorstore/yokai-contrib/fxjsonapi)](https://pkg.go.dev/github.com/ankorstore/yokai-contrib/fxjsonapi)
8+
9+
> [Yokai](https://github.com/ankorstore/yokai) module for [JSON API](https://jsonapi.org/), based on [google/jsonapi](https://github.com/google/jsonapi).
10+
11+
<!-- TOC -->
12+
* [Overview](#overview)
13+
* [Installation](#installation)
14+
* [Configuration](#configuration)
15+
* [Processing](#processing)
16+
* [Request processing](#request-processing)
17+
* [Response processing](#response-processing)
18+
* [Error handling](#error-handling)
19+
* [Testing](#testing)
20+
<!-- TOC -->
21+
22+
## Overview
23+
24+
This module provides to your [Yokai](https://github.com/ankorstore/yokai) application a [Processor](processor.go), that you can `inject` in your HTTP handlers to process JSON API requests and responses.
25+
26+
It also provides automatic [error handling](error.go), compliant with the [JSON API specifications](https://jsonapi.org/).
27+
28+
## Installation
29+
30+
Install the module:
31+
32+
```shell
33+
go get github.com/ankorstore/yokai-contrib/fxjsonapi
34+
```
35+
36+
Then activate it in your application bootstrapper:
37+
38+
```go
39+
// internal/bootstrap.go
40+
package internal
41+
42+
import (
43+
"github.com/ankorstore/yokai-contrib/fxjsonapi"
44+
"github.com/ankorstore/yokai/fxcore"
45+
)
46+
47+
var Bootstrapper = fxcore.NewBootstrapper().WithOptions(
48+
// load modules
49+
fxjsonapi.FxJSONAPIModule,
50+
// ...
51+
)
52+
```
53+
54+
## Configuration
55+
56+
Configuration reference:
57+
58+
```yaml
59+
# ./configs/config.yaml
60+
modules:
61+
jsonapi:
62+
log:
63+
enabled: true # to automatically log JSON API processing, disabled by default
64+
trace:
65+
enabled: true # to automatically trace JSON API processing, disabled by default
66+
```
67+
68+
## Processing
69+
70+
### Request processing
71+
72+
You can use the provided [Processor](processor.go) to automatically process a JSON API request:
73+
74+
```go
75+
package handler
76+
77+
import (
78+
"net/http"
79+
80+
"github.com/ankorstore/yokai-contrib/fxjsonapi"
81+
"github.com/google/jsonapi"
82+
"github.com/labstack/echo/v4"
83+
)
84+
85+
type Foo struct {
86+
ID int `jsonapi:"primary,foo"`
87+
Name string `jsonapi:"attr,name"`
88+
Bar *Bar `jsonapi:"relation,bar"`
89+
}
90+
91+
func (f Foo) JSONAPIMeta() *jsonapi.Meta {
92+
return &jsonapi.Meta{
93+
"some": "foo meta",
94+
}
95+
}
96+
97+
type Bar struct {
98+
ID int `jsonapi:"primary,bar"`
99+
Name string `jsonapi:"attr,name"`
100+
}
101+
102+
func (b Bar) JSONAPIMeta() *jsonapi.Meta {
103+
return &jsonapi.Meta{
104+
"some": "bar meta",
105+
}
106+
}
107+
108+
type JSONAPIHandler struct {
109+
processor fxjsonapi.Processor
110+
}
111+
112+
func NewJSONAPIHandler(processor fxjsonapi.Processor) *JSONAPIHandler {
113+
return &JSONAPIHandler{
114+
processor: processor,
115+
}
116+
}
117+
118+
func (h *JSONAPIHandler) Handle() echo.HandlerFunc {
119+
return func(c echo.Context) error {
120+
foo := Foo{}
121+
122+
// unmarshall JSON API request payload in foo
123+
err := h.processor.ProcessRequest(
124+
// echo context
125+
c,
126+
// pointer to the struct to unmarshall
127+
&foo,
128+
// optionally override module config for logging
129+
fxjsonapi.WithLog(true),
130+
// optionally override module config for tracing
131+
fxjsonapi.WithTrace(true),
132+
)
133+
if err != nil {
134+
return err
135+
}
136+
137+
return c.JSON(http.StatusOK, foo)
138+
}
139+
}
140+
```
141+
142+
Notes about `ProcessRequest()`:
143+
144+
- if the request payload does not respect the [JSON API specifications](https://jsonapi.org/), a `400` error will be automatically returned
145+
- if the request `Content-Type` header is not `application/vnd.api+json`, a `415` error will be automatically returned
146+
147+
### Response processing
148+
149+
You can use the provided [Processor](processor.go) to automatically process a JSON API response:
150+
151+
```go
152+
package handler
153+
154+
import (
155+
"net/http"
156+
157+
"github.com/ankorstore/yokai-contrib/fxjsonapi"
158+
"github.com/ankorstore/yokai-contrib/fxjsonapi/testdata/model"
159+
"github.com/labstack/echo/v4"
160+
)
161+
162+
type Foo struct {
163+
ID int `jsonapi:"primary,foo"`
164+
Name string `jsonapi:"attr,name"`
165+
Bar *Bar `jsonapi:"relation,bar"`
166+
}
167+
168+
func (f Foo) JSONAPIMeta() *jsonapi.Meta {
169+
return &jsonapi.Meta{
170+
"some": "foo meta",
171+
}
172+
}
173+
174+
type Bar struct {
175+
ID int `jsonapi:"primary,bar"`
176+
Name string `jsonapi:"attr,name"`
177+
}
178+
179+
func (b Bar) JSONAPIMeta() *jsonapi.Meta {
180+
return &jsonapi.Meta{
181+
"some": "bar meta",
182+
}
183+
}
184+
185+
type JSONAPIHandler struct {
186+
processor fxjsonapi.Processor
187+
}
188+
189+
func NewJSONAPIHandler(processor fxjsonapi.Processor) *JSONAPIHandler {
190+
return &JSONAPIHandler{
191+
processor: processor,
192+
}
193+
}
194+
195+
func (h *JSONAPIHandler) Handle() echo.HandlerFunc {
196+
return func(c echo.Context) error {
197+
foo := Foo{
198+
ID: 123,
199+
Name: "foo",
200+
Bar: &Bar{
201+
ID: 456,
202+
Name: "bar",
203+
},
204+
}
205+
206+
return h.processor.ProcessResponse(
207+
// echo context
208+
c,
209+
// HTTP status code
210+
http.StatusOK,
211+
// pointer to the struct to marshall
212+
&foo,
213+
// optionally pass metadata to the JSON API response
214+
fxjsonapi.WithMetadata(map[string]interface{}{
215+
"some": "response meta",
216+
}),
217+
// optionally remove the included from the JSON API response (enabled by default)
218+
fxjsonapi.WithIncluded(false),
219+
// optionally override module config for logging
220+
fxjsonapi.WithLog(true),
221+
// optionally override module config for tracing
222+
fxjsonapi.WithTrace(true),
223+
)
224+
}
225+
}
226+
```
227+
228+
Notes about `ProcessResponse()`:
229+
230+
- you can pass a `pointer` or a `slice of pointers` to marshall as JSON API
231+
- `application/vnd.api+json` will be automatically added to the response `Content-Type` header
232+
233+
## Error handling
234+
235+
This module automatically enables the [ErrorHandler](error.go), to convert errors bubbling up in JSON API format.
236+
237+
It handles:
238+
239+
- [JSON API](https://github.com/google/jsonapi/blob/master/errors.go) errors: automatically sets the `status code of the error`
240+
- [validation](https://ankorstore.github.io/yokai/modules/fxvalidator/) errors: automatically sets a `400` status code
241+
- [HTTP](https://echo.labstack.com/docs/error-handling) errors: automatically sets the `status code of the error`
242+
- or any generic error: automatically sets a `500` status code
243+
244+
You can optionally `obfuscate` the errors details (ex: for production) in the [HTTP server configuration](https://ankorstore.github.io/yokai/modules/fxhttpserver/#configuration):
245+
246+
```yaml
247+
# ./configs/config.yaml
248+
modules:
249+
http:
250+
server:
251+
errors:
252+
obfuscate: true # disabled by default
253+
```
254+
255+
## Testing
256+
257+
This module provides a [ProcessorMock](fxjsonapitest/mock.go) for mocking [Processor](processor.go), see [usage example](fxjsonapitest/mock_test.go).

0 commit comments

Comments
 (0)