Skip to content

Commit

Permalink
Add support for OpenAPI deep object decoding (#164)
Browse files Browse the repository at this point in the history
  • Loading branch information
vearutop committed Jul 24, 2023
1 parent 020a738 commit 18f55a4
Show file tree
Hide file tree
Showing 20 changed files with 363 additions and 95 deletions.
35 changes: 29 additions & 6 deletions _examples/advanced-generic/_testdata/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -308,15 +308,15 @@
},
{
"name":"identity","in":"query","description":"JSON value in query",
"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedJSONPayload"}}}
"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryAdvancedJSONPayload"}}}
},
{
"name":"in-path","in":"path","description":"Simple scalar value in path","required":true,
"schema":{"type":"string","description":"Simple scalar value in path"}
},
{
"name":"in_cookie","in":"cookie","description":"UUID in cookie.",
"schema":{"$ref":"#/components/schemas/UuidUUID"}
"schema":{"$ref":"#/components/schemas/CookieUuidUUID"}
},
{
"name":"X-Header","in":"header","description":"Simple scalar value in header.",
Expand Down Expand Up @@ -488,6 +488,14 @@
{
"name":"in_query","in":"query","description":"Object value in query.","style":"deepObject","explode":true,
"schema":{"type":"object","additionalProperties":{"type":"number"},"description":"Object value in query."}
},
{
"name":"json_filter","in":"query","description":"JSON object value in query.",
"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryAdvancedJsonFilter"}}}
},
{
"name":"deep_object_filter","in":"query","description":"Deep object value in query params.",
"style":"deepObject","explode":true,"schema":{"$ref":"#/components/schemas/QueryAdvancedDeepObjectFilter"}
}
],
"responses":{
Expand Down Expand Up @@ -622,6 +630,10 @@
"schemas":{
"AdvancedAnotherErr":{"type":"object","properties":{"foo":{"type":"integer"}}},
"AdvancedCustomErr":{"type":"object","properties":{"details":{"type":"object","additionalProperties":{}},"msg":{"type":"string"}}},
"AdvancedDeepObjectFilter":{
"type":"object",
"properties":{"bar":{"minLength":3,"type":"string"},"baz":{"minLength":3,"type":"string","nullable":true}}
},
"AdvancedGzipPassThroughStruct":{
"type":"object",
"properties":{"id":{"type":"integer"},"text":{"type":"array","items":{"type":"string"},"nullable":true}}
Expand Down Expand Up @@ -682,10 +694,10 @@
"additionalProperties":false
},
"AdvancedJSONMapPayload":{"type":"object","additionalProperties":{"type":"number"}},
"AdvancedJSONPayload":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}},"additionalProperties":false},
"AdvancedJSONPayloadType2":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}},"additionalProperties":false},
"AdvancedJSONPayloadType3":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}},"additionalProperties":false},
"AdvancedJSONSlicePayload":{"type":"array","items":{"type":"integer"},"nullable":true},
"AdvancedJsonFilter":{"type":"object","properties":{"foo":{"maxLength":5,"type":"string"}}},
"AdvancedJsonMapReq":{"type":"object","additionalProperties":{"type":"number"}},
"AdvancedJsonOutput":{
"type":"object",
Expand All @@ -711,7 +723,11 @@
"AdvancedOutputPortType3":{"type":"object","properties":{"data":{"type":"object","properties":{"value":{"type":"string"}}}}},
"AdvancedOutputQueryObject":{
"type":"object",
"properties":{"inQuery":{"type":"object","additionalProperties":{"type":"number"},"nullable":true}}
"properties":{
"deepObjectFilter":{"$ref":"#/components/schemas/AdvancedDeepObjectFilter"},
"inQuery":{"type":"object","additionalProperties":{"type":"number"},"nullable":true},
"jsonFilter":{"$ref":"#/components/schemas/AdvancedJsonFilter"}
}
},
"AdvancedOutputType2":{"type":"object","properties":{"path":{"type":"string"},"query":{"type":"integer"},"text":{"type":"string"}}},
"AdvancedOutputType3":{"type":"object","properties":{"path":{"type":"string"},"query":{"type":"integer"},"text":{"type":"string"}}},
Expand Down Expand Up @@ -744,6 +760,7 @@
"name":{"minLength":3,"type":"string"}
}
},
"CookieUuidUUID":{"type":"string","format":"uuid","example":"248df4b7-aa70-47b8-a036-33ac447e668d"},
"FormDataAdvancedForm":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}},"additionalProperties":false},
"FormDataAdvancedInputPort":{
"required":["val2"],"type":"object",
Expand Down Expand Up @@ -776,6 +793,13 @@
},
"FormDataMultipartFile":{"type":"string","format":"binary","nullable":true},
"FormDataMultipartFileHeader":{"type":"string","format":"binary","nullable":true},
"QueryAdvancedDeepObjectFilter":{
"type":"object",
"properties":{"bar":{"minLength":3,"type":"string"},"baz":{"minLength":3,"type":"string","nullable":true}},
"additionalProperties":false
},
"QueryAdvancedJSONPayload":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}},"additionalProperties":false},
"QueryAdvancedJsonFilter":{"type":"object","properties":{"foo":{"maxLength":5,"type":"string"}},"additionalProperties":false},
"RestErrResponse":{
"type":"object",
"properties":{
Expand All @@ -785,8 +809,7 @@
"status":{"type":"string","description":"Status text."}
}
},
"TextprotoMIMEHeader":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}},
"UuidUUID":{"type":"string","format":"uuid","example":"248df4b7-aa70-47b8-a036-33ac447e668d"}
"TextprotoMIMEHeader":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}
},
"securitySchemes":{"User":{"type":"apiKey","name":"sessid","in":"cookie"}}
}
Expand Down
19 changes: 17 additions & 2 deletions _examples/advanced-generic/query_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,31 @@ import (
)

func queryObject() usecase.Interactor {
type jsonFilter struct {
Foo string `json:"foo" maxLength:"5"`
}

type deepObjectFilter struct {
Bar string `json:"bar" query:"bar" minLength:"3"`
Baz *string `json:"baz,omitempty" query:"baz" minLength:"3"`
}

type inputQueryObject struct {
Query map[int]float64 `query:"in_query" description:"Object value in query."`
Query map[int]float64 `query:"in_query" description:"Object value in query."`
JSONFilter jsonFilter `query:"json_filter" description:"JSON object value in query."`
DeepObjectFilter deepObjectFilter `query:"deep_object_filter" description:"Deep object value in query params."`
}

type outputQueryObject struct {
Query map[int]float64 `json:"inQuery"`
Query map[int]float64 `json:"inQuery"`
JSONFilter jsonFilter `json:"jsonFilter"`
DeepObjectFilter deepObjectFilter `json:"deepObjectFilter"`
}

u := usecase.NewInteractor(func(ctx context.Context, in inputQueryObject, out *outputQueryObject) (err error) {
out.Query = in.Query
out.JSONFilter = in.JSONFilter
out.DeepObjectFilter = in.DeepObjectFilter

return nil
})
Expand Down
82 changes: 82 additions & 0 deletions _examples/advanced-generic/query_object_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package main

import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"

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

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

srv := httptest.NewServer(r)
defer srv.Close()

for _, tc := range []struct {
name string
url string
code int
resp string
}{
{
name: "validation_failed_deep_object",
url: `/query-object?in_query[1]=0&in_query[2]=0&in_query[3]=0&json_filter={"foo":"strin"}&deep_object_filter[bar]=sd`,
code: http.StatusBadRequest,
resp: `{
"msg":"invalid argument: validation failed",
"details":{"query:deep_object_filter":["#/bar: length must be \u003e= 3, but got 2"]}
}`,
},
{
name: "validation_failed_deep_object_2",
url: `/query-object?in_query[1]=0&in_query[2]=0&in_query[3]=0&json_filter={"foo":"strin"}&deep_object_filter[bar]=asd&deep_object_filter[baz]=sd`,
code: http.StatusBadRequest,
resp: `{
"msg":"invalid argument: validation failed",
"details":{"query:deep_object_filter":["#/baz: length must be \u003e= 3, but got 2"]}
}`,
},
{
name: "validation_failed_json",
url: `/query-object?in_query[1]=0&in_query[2]=0&in_query[3]=0&json_filter={"foo":"string"}&deep_object_filter[bar]=asd`,
code: http.StatusBadRequest,
resp: `{
"msg":"invalid argument: validation failed",
"details":{"query:json_filter":["#/foo: length must be \u003c= 5, but got 6"]}
}`,
},
{
name: "ok",
url: `/query-object?in_query[1]=0&in_query[2]=0&in_query[3]=0&json_filter={"foo":"strin"}&deep_object_filter[bar]=asd`,
code: http.StatusOK,
resp: `{
"inQuery":{"1":0,"2":0,"3":0},"jsonFilter":{"foo":"strin"},
"deepObjectFilter":{"bar":"asd"}
}`,
},
} {
t.Run(tc.name, func(t *testing.T) {
req, err := http.NewRequest(
http.MethodGet,
srv.URL+tc.url,
nil,
)
require.NoError(t, err)

resp, err := http.DefaultTransport.RoundTrip(req)
require.NoError(t, err)

body, err := io.ReadAll(resp.Body)
assert.NoError(t, err)
assert.NoError(t, resp.Body.Close())
assertjson.EqMarshal(t, tc.resp, json.RawMessage(body))
assert.Equal(t, tc.code, resp.StatusCode)
})
}
}
2 changes: 1 addition & 1 deletion _examples/advanced-generic/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func NewRouter() http.Handler {
return usecase.Interact(func(ctx context.Context, input, output interface{}) error {
err := next.Interact(ctx, input, output)
if err != nil && err != rest.HTTPCodeAsError(http.StatusNotModified) {
log.Printf("usecase %s request (%v) failed: %v\n", name, input, err)
log.Printf("usecase %s request (%+v) failed: %v\n", name, input, err)
}

return err
Expand Down
10 changes: 5 additions & 5 deletions _examples/advanced/_testdata/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -242,15 +242,15 @@
},
{
"name":"identity","in":"query","description":"JSON value in query",
"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdvancedJSONPayload"}}}
"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryAdvancedJSONPayload"}}}
},
{
"name":"in-path","in":"path","description":"Simple scalar value in path","required":true,
"schema":{"type":"string","description":"Simple scalar value in path"}
},
{
"name":"in_cookie","in":"cookie","description":"UUID in cookie.",
"schema":{"$ref":"#/components/schemas/UuidUUID"}
"schema":{"$ref":"#/components/schemas/CookieUuidUUID"}
},
{
"name":"X-Header","in":"header","description":"Simple scalar value in header.",
Expand Down Expand Up @@ -518,7 +518,6 @@
"additionalProperties":false
},
"AdvancedJSONMapPayload":{"type":"object","additionalProperties":{"type":"number"}},
"AdvancedJSONPayload":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}},"additionalProperties":false},
"AdvancedJSONPayloadType2":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}},"additionalProperties":false},
"AdvancedJSONSlicePayload":{"type":"array","items":{"type":"integer"},"nullable":true},
"AdvancedJsonMapReq":{"type":"object","additionalProperties":{"type":"number"},"nullable":true},
Expand Down Expand Up @@ -569,6 +568,7 @@
"name":{"minLength":3,"type":"string"}
}
},
"CookieUuidUUID":{"type":"string","format":"uuid","example":"248df4b7-aa70-47b8-a036-33ac447e668d"},
"FormDataAdvancedInputPort":{
"required":["val2"],"type":"object",
"properties":{"val2":{"minimum":3,"type":"integer","description":"Simple scalar value with sample validation."}},
Expand Down Expand Up @@ -600,6 +600,7 @@
},
"FormDataMultipartFile":{"type":"string","format":"binary","nullable":true},
"FormDataMultipartFileHeader":{"type":"string","format":"binary","nullable":true},
"QueryAdvancedJSONPayload":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"}},"additionalProperties":false},
"RestErrResponse":{
"type":"object",
"properties":{
Expand All @@ -609,8 +610,7 @@
"status":{"type":"string","description":"Status text."}
}
},
"TextprotoMIMEHeader":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}},
"UuidUUID":{"type":"string","format":"uuid","example":"248df4b7-aa70-47b8-a036-33ac447e668d"}
"TextprotoMIMEHeader":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}
},
"securitySchemes":{"User":{"type":"apiKey","name":"sessid","in":"cookie"}}
}
Expand Down
2 changes: 1 addition & 1 deletion _examples/advanced/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"github.com/swaggest/rest/response"
"github.com/swaggest/rest/response/gzip"
"github.com/swaggest/rest/web"
swgui "github.com/swaggest/swgui/v4emb"
swgui "github.com/swaggest/swgui/v5emb"
)

func NewRouter() http.Handler {
Expand Down
2 changes: 1 addition & 1 deletion _examples/basic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (

"github.com/swaggest/rest/response/gzip"
"github.com/swaggest/rest/web"
swgui "github.com/swaggest/swgui/v4emb"
swgui "github.com/swaggest/swgui/v5emb"
"github.com/swaggest/usecase"
"github.com/swaggest/usecase/status"
)
Expand Down
2 changes: 1 addition & 1 deletion _examples/generic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

"github.com/swaggest/rest/response/gzip"
"github.com/swaggest/rest/web"
swgui "github.com/swaggest/swgui/v4emb"
swgui "github.com/swaggest/swgui/v5emb"
"github.com/swaggest/usecase"
"github.com/swaggest/usecase/status"
)
Expand Down
18 changes: 9 additions & 9 deletions _examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ replace github.com/swaggest/rest => ../

require (
github.com/bool64/ctxd v1.2.1
github.com/bool64/dev v0.2.28
github.com/bool64/dev v0.2.29
github.com/bool64/httpmock v0.1.13
github.com/bool64/httptestbench v0.1.4
github.com/go-chi/chi/v5 v5.0.8
github.com/go-chi/chi/v5 v5.0.10
github.com/kelseyhightower/envconfig v1.4.0
github.com/rs/cors v1.9.0
github.com/stretchr/testify v1.8.2
github.com/swaggest/assertjson v1.8.1
github.com/stretchr/testify v1.8.4
github.com/swaggest/assertjson v1.9.0
github.com/swaggest/jsonschema-go v0.3.52
github.com/swaggest/openapi-go v0.2.30
github.com/swaggest/swgui v1.6.2
github.com/swaggest/openapi-go v0.2.33
github.com/swaggest/swgui v1.6.4
github.com/swaggest/usecase v1.2.1
github.com/valyala/fasthttp v1.46.0
)
Expand All @@ -32,13 +32,13 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/iancoleman/orderedmap v0.2.0 // indirect
github.com/iancoleman/orderedmap v0.3.0 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/santhosh-tekuri/jsonschema/v3 v3.1.0 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/swaggest/form/v5 v5.0.4 // indirect
github.com/swaggest/refl v1.1.0 // indirect
github.com/swaggest/form/v5 v5.1.1 // indirect
github.com/swaggest/refl v1.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vearutop/dynhist-go v1.1.0 // indirect
github.com/vearutop/statigz v1.3.0 // indirect
Expand Down
Loading

0 comments on commit 18f55a4

Please sign in to comment.