Skip to content

Commit

Permalink
Add example for dynamic schema and improve docs (#170)
Browse files Browse the repository at this point in the history
  • Loading branch information
vearutop committed Aug 24, 2023
1 parent 2242740 commit 5f85765
Show file tree
Hide file tree
Showing 19 changed files with 282 additions and 31 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ env:
GO111MODULE: "on"
CACHE_BENCHMARK: "off" # Enables benchmark result reuse between runs, may skew latency results.
RUN_BASE_BENCHMARK: "on" # Runs benchmark for PR base in case benchmark result is missing.
GO_VERSION: 1.20.x
GO_VERSION: 1.21.x
jobs:
bench:
runs-on: ubuntu-latest
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ jobs:
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.20.x
go-version: 1.21.x
- uses: actions/checkout@v2
- name: golangci-lint
uses: golangci/golangci-lint-action@v3.4.0
uses: golangci/golangci-lint-action@v3.7.0
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.51.2
version: v1.54.1

# Optional: working directory, useful for monorepos
# working-directory: somedir
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/gorelease.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ concurrency:
cancel-in-progress: true

env:
GO_VERSION: 1.20.x
GO_VERSION: 1.21.x
jobs:
gorelease:
runs-on: ubuntu-latest
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/test-unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ concurrency:
env:
GO111MODULE: "on"
RUN_BASE_COVERAGE: "on" # Runs test for PR base in case base test coverage is missing.
COV_GO_VERSION: 1.20.x # Version of Go to collect coverage
COV_GO_VERSION: 1.21.x # Version of Go to collect coverage
TARGET_DELTA_COV: 90 # Target coverage of changed lines, in percents
jobs:
test:
strategy:
matrix:
go-version: [ 1.13.x, 1.19.x, 1.20.x ]
go-version: [ 1.13.x, 1.20.x, 1.21.x ]
runs-on: ubuntu-latest
steps:
- name: Install Go stable
Expand Down Expand Up @@ -88,14 +88,14 @@ jobs:
id: annotate
if: matrix.go-version == env.COV_GO_VERSION && github.event.pull_request.base.sha != ''
run: |
curl -sLO https://github.com/vearutop/gocovdiff/releases/download/v1.3.6/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz
curl -sLO https://github.com/vearutop/gocovdiff/releases/download/v1.4.0/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz && rm linux_amd64.tar.gz
gocovdiff_hash=$(git hash-object ./gocovdiff)
[ "$gocovdiff_hash" == "8e507e0d671d4d6dfb3612309b72b163492f28eb" ] || (echo "::error::unexpected hash for gocovdiff, possible tampering: $gocovdiff_hash" && exit 1)
[ "$gocovdiff_hash" == "f191b45548bb65ec2c7d88909679a57116ff1ba1" ] || (echo "::error::unexpected hash for gocovdiff, possible tampering: $gocovdiff_hash" && exit 1)
git fetch origin master ${{ github.event.pull_request.base.sha }}
REP=$(./gocovdiff -cov unit.coverprofile -gha-annotations gha-unit.txt -delta-cov-file delta-cov-unit.txt -target-delta-cov ${TARGET_DELTA_COV})
REP=$(./gocovdiff -mod github.com/$GITHUB_REPOSITORY -cov unit.coverprofile -gha-annotations gha-unit.txt -delta-cov-file delta-cov-unit.txt -target-delta-cov ${TARGET_DELTA_COV})
echo "${REP}"
cat gha-unit.txt
DIFF=$(test -e unit-base.txt && ./gocovdiff -func-cov unit.txt -func-base-cov unit-base.txt || echo "Missing base coverage file")
DIFF=$(test -e unit-base.txt && ./gocovdiff -mod github.com/$GITHUB_REPOSITORY -func-cov unit.txt -func-base-cov unit-base.txt || echo "Missing base coverage file")
TOTAL=$(cat delta-cov-unit.txt)
echo "rep<<EOF" >> $GITHUB_OUTPUT && echo "$REP" >> $GITHUB_OUTPUT && echo "EOF" >> $GITHUB_OUTPUT
echo "diff<<EOF" >> $GITHUB_OUTPUT && echo "$DIFF" >> $GITHUB_OUTPUT && echo "EOF" >> $GITHUB_OUTPUT
Expand Down
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ linters:
- deadcode
- testableexamples
- dupword
- depguard
- tagalign

issues:
exclude-use-default: false
Expand Down
48 changes: 48 additions & 0 deletions ADVANCED.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,51 @@ r.Method(http.MethodGet, "/docs/openapi.json", apiSchema)
r.Mount("/docs", v3cdn.NewHandler(apiSchema.Reflector().Spec.Info.Title,
"/docs/openapi.json", "/docs"))
```

## Request Control

Input type may implement methods to customize request handling.

```go
// LoadFromHTTPRequest takes full control of request decoding and validating and prevents automated request decoding.
LoadFromHTTPRequest(r *http.Request) error
```

```go
// SetRequest captures current *http.Request, but does not prevent automated request decoding.
// You can embed request.EmbeddedSetter into your structure to get access to the request.
SetRequest(r *http.Request)
```

## Response Control

Output type may implement methods to customize response handling.

```go
// NoContent controls whether status 204 should be used in response to current request.
NoContent() bool
```

```go
// SetupResponseHeader gives access to response headers of current request.
SetupResponseHeader(h http.Header)
```

```go
// ETag returns the Etag response header value for the current request.
ETag() string
```

```go
// SetWriter takes full control of http.ResponseWriter and prevents automated response handling.
SetWriter(w io.Writer)
```

```go
// HTTPStatus declares successful HTTP status for all requests.
HTTPStatus() int

// ExpectedHTTPStatuses declares additional HTTP statuses for all requests.
// Only used for documentation purposes.
ExpectedHTTPStatuses() []int
```
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
GOLANGCI_LINT_VERSION := "v1.51.1" # Optional configuration to pinpoint golangci-lint version.
#GOLANGCI_LINT_VERSION := "v1.54.1" # Optional configuration to pinpoint golangci-lint version.

# The head of Makefile determines location of dev-go to include standard targets.
GO ?= go
Expand Down
33 changes: 33 additions & 0 deletions _examples/advanced/_testdata/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,38 @@
"security":[{"User":[]}]
}
},
"/dynamic-schema":{
"get":{
"summary":"Dynamic Request Schema",
"description":"This use case demonstrates documentation of types that are only known at runtime.",
"operationId":"_examples/advanced.dynamicSchema",
"parameters":[
{"name":"bar","in":"query","schema":{"type":"string"}},
{"name":"type","in":"query","schema":{"type":"string"}},
{"name":"foo","in":"header","schema":{"enum":["123","456","789"],"type":"integer"}}
],
"responses":{
"200":{
"description":"OK",
"headers":{"foo":{"style":"simple","schema":{"enum":["123","456","789"],"type":"integer"}}},
"content":{"application/dummy+json":{"schema":{"$ref":"#/components/schemas/AdvancedDynamicOutput"}}}
},
"400":{
"description":"Bad Request",
"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/AdvancedCustomErr"}}}
},
"409":{
"description":"Conflict",
"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/AdvancedCustomErr"}}}
},
"412":{
"description":"Precondition Failed",
"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/AdvancedCustomErr"}}}
}
},
"x-forbid-unknown-query":true
}
},
"/error-response":{
"get":{
"summary":"Declare Expected Errors",
Expand Down Expand Up @@ -460,6 +492,7 @@
"schemas":{
"AdvancedAnotherErr":{"type":"object","properties":{"foo":{"type":"integer"}}},
"AdvancedCustomErr":{"type":"object","properties":{"details":{"type":"object","additionalProperties":{}},"msg":{"type":"string"}}},
"AdvancedDynamicOutput":{"type":"object","properties":{"bar":{"type":"string"},"status":{"type":"string"}}},
"AdvancedGzipPassThroughStruct":{
"type":"object",
"properties":{"id":{"type":"integer"},"text":{"type":"array","items":{"type":"string"},"nullable":true}}
Expand Down
94 changes: 94 additions & 0 deletions _examples/advanced/dynamic_schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package main

import (
"context"
"encoding/json"
"errors"
"net/http"

"github.com/bool64/ctxd"
"github.com/swaggest/jsonschema-go"
"github.com/swaggest/rest/request"
"github.com/swaggest/usecase"
"github.com/swaggest/usecase/status"
)

type dynamicInput struct {
jsonschema.Struct
request.EmbeddedSetter

// Type is a static field example.
Type string `query:"type"`
}

type dynamicOutput struct {
// Embedded jsonschema.Struct exposes dynamic fields for documentation.
jsonschema.Struct

jsonFields map[string]interface{}
headerFields map[string]string

// Status is a static field example.
Status string `json:"status"`
}

func (o dynamicOutput) SetupResponseHeader(h http.Header) {
for k, v := range o.headerFields {
h.Set(k, v)
}
}

func (o dynamicOutput) MarshalJSON() ([]byte, error) {
if o.jsonFields == nil {
o.jsonFields = map[string]interface{}{}
}

o.jsonFields["status"] = o.Status

return json.Marshal(o.jsonFields)
}

func dynamicSchema() usecase.Interactor {
dynIn := dynamicInput{}
dynIn.Struct.Fields = []jsonschema.Field{
{Name: "Foo", Value: 123, Tag: `header:"foo" enum:"123,456,789"`},
{Name: "Bar", Value: "abc", Tag: `query:"bar"`},
}

dynOut := dynamicOutput{}
dynOut.Struct.Fields = []jsonschema.Field{
{Name: "Foo", Value: 123, Tag: `header:"foo" enum:"123,456,789"`},
{Name: "Bar", Value: "abc", Tag: `json:"bar"`},
}

u := usecase.NewIOI(dynIn, dynOut, func(ctx context.Context, input, output interface{}) (err error) {
var (
in = input.(dynamicInput)
out = output.(*dynamicOutput)
)

switch in.Type {
case "ok":
out.Status = "ok"
out.jsonFields = map[string]interface{}{
"bar": in.Request().URL.Query().Get("bar"),
}
out.headerFields = map[string]string{
"foo": in.Request().Header.Get("foo"),
}
case "invalid_argument":
return status.Wrap(errors.New("bad value for foo"), status.InvalidArgument)
case "conflict":
return status.Wrap(ctxd.NewError(ctx, "conflict", "foo", "bar"),
status.AlreadyExists)
}

return nil
})

u.SetTitle("Dynamic Request Schema")
u.SetDescription("This use case demonstrates documentation of types that are only known at runtime.")
u.SetExpectedErrors(status.InvalidArgument, status.FailedPrecondition, status.AlreadyExists)

return u
}
24 changes: 24 additions & 0 deletions _examples/advanced/dynamic_schema_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package main

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/swaggest/assertjson"
)

func Test_dynamicOutput(t *testing.T) {
r := NewRouter()

req := httptest.NewRequest(http.MethodGet, "/dynamic-schema?bar=ccc&type=ok", nil)
req.Header.Set("foo", "456")

rw := httptest.NewRecorder()
r.ServeHTTP(rw, req)

assertjson.Equal(t, []byte(`{"bar": "ccc","status": "ok"}`), rw.Body.Bytes())
assert.Equal(t, http.StatusOK, rw.Code)
assert.Equal(t, "456", rw.Header().Get("foo"))
}
1 change: 1 addition & 0 deletions _examples/advanced/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ func NewRouter() http.Handler {
s.Head("/gzip-pass-through", directGzip())

s.Get("/error-response", errorResponse())
s.Get("/dynamic-schema", dynamicSchema())

// Security middlewares.
// - sessMW is the actual request-level processor,
Expand Down
4 changes: 2 additions & 2 deletions _examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ replace github.com/swaggest/rest => ../

require (
github.com/bool64/ctxd v1.2.1
github.com/bool64/dev v0.2.29
github.com/bool64/dev v0.2.31
github.com/bool64/httpmock v0.1.13
github.com/bool64/httptestbench v0.1.4
github.com/gin-gonic/gin v1.9.1
Expand All @@ -16,7 +16,7 @@ require (
github.com/rs/cors v1.9.0
github.com/stretchr/testify v1.8.4
github.com/swaggest/assertjson v1.9.0
github.com/swaggest/jsonschema-go v0.3.57
github.com/swaggest/jsonschema-go v0.3.58
github.com/swaggest/openapi-go v0.2.39
github.com/swaggest/rest v0.0.0-00010101000000-000000000000
github.com/swaggest/swgui v1.7.2
Expand Down
8 changes: 4 additions & 4 deletions _examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ github.com/bool64/ctxd v1.2.1 h1:hARFteq0zdn4bwfmxLhak3fXFuvtJVKDH2X29VV/2ls=
github.com/bool64/ctxd v1.2.1/go.mod h1:ZG6QkeGVLTiUl2mxPpyHmFhDzFZCyocr9hluBV3LYuc=
github.com/bool64/dev v0.2.5/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zRWU=
github.com/bool64/dev v0.2.25/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bool64/dev v0.2.29 h1:x+syGyh+0eWtOzQ1ItvLzOGIWyNWnyjXpHIcpF2HvL4=
github.com/bool64/dev v0.2.29/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bool64/dev v0.2.31 h1:OS57EqYaYe2M/2bw9uhDCIFiZZwywKFS/4qMLN6JUmQ=
github.com/bool64/dev v0.2.31/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bool64/httpmock v0.1.13 h1:3QpRXQ5kwHLW8xnVT8+Ug7VS6RerhdEFV+RWYC61aVo=
github.com/bool64/httpmock v0.1.13/go.mod h1:YMTLaypQ3o5DAx78eA/kDRSLec0f+42sLMDmHdmeY+E=
github.com/bool64/httptestbench v0.1.4 h1:35f9RwWqcnQRXM+sA+GUhWVGSa6XEFlKpNSH9oYzOjI=
Expand Down Expand Up @@ -104,8 +104,8 @@ github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7
github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
github.com/swaggest/form/v5 v5.1.1 h1:ct6/rOQBGrqWUQ0FUv3vW5sHvTUb31AwTUWj947N6cY=
github.com/swaggest/form/v5 v5.1.1/go.mod h1:X1hraaoONee20PMnGNLQpO32f9zbQ0Czfm7iZThuEKg=
github.com/swaggest/jsonschema-go v0.3.57 h1:n6D/2K9557Yqn/NohXoszmjuN0Lp5n0DyHlVLRZXbOM=
github.com/swaggest/jsonschema-go v0.3.57/go.mod h1:5WFFGBBte5JAWAV8gDpNRJ/tlQnb1AHDdf/ghgsVUik=
github.com/swaggest/jsonschema-go v0.3.58 h1:OPixN4HW9H3FTh9BSomH2i0bdJi3V646TfSihzt9QBc=
github.com/swaggest/jsonschema-go v0.3.58/go.mod h1:5WFFGBBte5JAWAV8gDpNRJ/tlQnb1AHDdf/ghgsVUik=
github.com/swaggest/openapi-go v0.2.39 h1:GfICsAAFnQuyxfywsGyCbPqDKeMXxots4N/9j6+qSCk=
github.com/swaggest/openapi-go v0.2.39/go.mod h1:g+AfRIkPCHdhqfW8zOD1Sk3PwLhxpWW8SNWHXrmA08c=
github.com/swaggest/refl v1.2.1 h1:1meX9NaXjM5lmb4kk4RP3OZsXFRke9B1EHAP/pCEKO0=
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/swaggest/rest
go 1.17

require (
github.com/bool64/dev v0.2.29
github.com/bool64/dev v0.2.31
github.com/bool64/httpmock v0.1.13
github.com/bool64/shared v0.1.5
github.com/cespare/xxhash/v2 v2.2.0
Expand All @@ -13,7 +13,7 @@ require (
github.com/stretchr/testify v1.8.2
github.com/swaggest/assertjson v1.9.0
github.com/swaggest/form/v5 v5.1.1
github.com/swaggest/jsonschema-go v0.3.57
github.com/swaggest/jsonschema-go v0.3.58
github.com/swaggest/openapi-go v0.2.39
github.com/swaggest/refl v1.2.1
github.com/swaggest/usecase v1.2.1
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ github.com/bool64/dev v0.2.17/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8
github.com/bool64/dev v0.2.24/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bool64/dev v0.2.25/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bool64/dev v0.2.27/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bool64/dev v0.2.29 h1:x+syGyh+0eWtOzQ1ItvLzOGIWyNWnyjXpHIcpF2HvL4=
github.com/bool64/dev v0.2.29/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bool64/dev v0.2.31 h1:OS57EqYaYe2M/2bw9uhDCIFiZZwywKFS/4qMLN6JUmQ=
github.com/bool64/dev v0.2.31/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bool64/httpmock v0.1.13 h1:3QpRXQ5kwHLW8xnVT8+Ug7VS6RerhdEFV+RWYC61aVo=
github.com/bool64/httpmock v0.1.13/go.mod h1:YMTLaypQ3o5DAx78eA/kDRSLec0f+42sLMDmHdmeY+E=
github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
Expand Down Expand Up @@ -76,8 +77,9 @@ github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7
github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
github.com/swaggest/form/v5 v5.1.1 h1:ct6/rOQBGrqWUQ0FUv3vW5sHvTUb31AwTUWj947N6cY=
github.com/swaggest/form/v5 v5.1.1/go.mod h1:X1hraaoONee20PMnGNLQpO32f9zbQ0Czfm7iZThuEKg=
github.com/swaggest/jsonschema-go v0.3.57 h1:n6D/2K9557Yqn/NohXoszmjuN0Lp5n0DyHlVLRZXbOM=
github.com/swaggest/jsonschema-go v0.3.57/go.mod h1:5WFFGBBte5JAWAV8gDpNRJ/tlQnb1AHDdf/ghgsVUik=
github.com/swaggest/jsonschema-go v0.3.58 h1:OPixN4HW9H3FTh9BSomH2i0bdJi3V646TfSihzt9QBc=
github.com/swaggest/jsonschema-go v0.3.58/go.mod h1:5WFFGBBte5JAWAV8gDpNRJ/tlQnb1AHDdf/ghgsVUik=
github.com/swaggest/openapi-go v0.2.39 h1:GfICsAAFnQuyxfywsGyCbPqDKeMXxots4N/9j6+qSCk=
github.com/swaggest/openapi-go v0.2.39/go.mod h1:g+AfRIkPCHdhqfW8zOD1Sk3PwLhxpWW8SNWHXrmA08c=
github.com/swaggest/refl v1.2.0/go.mod h1:CkC6g7h1PW33KprTuYRSw8UUOslRUt4lF3oe7tTIgNU=
Expand Down
Loading

0 comments on commit 5f85765

Please sign in to comment.