Skip to content

Commit

Permalink
Explicit implementations of un/marshalers
Browse files Browse the repository at this point in the history
Fixes getkin#513
Gets rid of ./jsoninfo package

Signed-off-by: Pierre Fenoll <[email protected]>
  • Loading branch information
fenollp committed Dec 22, 2022
1 parent 46e0df8 commit 3cdbfdd
Show file tree
Hide file tree
Showing 6 changed files with 344 additions and 17 deletions.
85 changes: 81 additions & 4 deletions openapi2/openapi2.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package openapi2

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"sort"
"strings"

"github.com/getkin/kin-openapi/jsoninfo"
"github.com/getkin/kin-openapi/openapi3"
Expand Down Expand Up @@ -223,7 +226,8 @@ func (parameter *Parameter) UnmarshalJSON(data []byte) error {
}

type Response struct {
openapi3.ExtensionProps
Extensions map[string]json.RawMessage `json:"-" yaml:"-"` // x-... fields

Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"`
Expand All @@ -233,12 +237,81 @@ type Response struct {

// MarshalJSON returns the JSON encoding of Response.
func (response *Response) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(response)
// return jsoninfo.MarshalStrictStruct(response)

var illegals []string
for k := range response.Extensions {
if !strings.HasPrefix(k, "x-") {
illegals = append(illegals, k)
}
}
if len(illegals) != 0 {
sort.Strings(illegals)
return nil, fmt.Errorf(`expected "x-" prefixes, got: %+v`, illegals) // move to Validate()
}

if ref := response.Ref; ref != "" {
return json.Marshal(struct {
Ref string `json:"$ref" yaml:"$ref"`
}{Ref: ref})
}

m := make(map[string]interface{}, 4+len(response.Extensions))
if x := response.Description; x != "" {
m["description"] = response.Description
}
if x := response.Schema; x != nil {
m["schema"] = x
}
if x := response.Headers; len(x) != 0 {
m["headers"] = x
}
if x := response.Examples; len(x) != 0 {
m["examples"] = x
}
for k, v := range response.Extensions {
m[k] = v
}
return json.Marshal(m)

}

// UnmarshalJSON sets Response to a copy of data.
func (response *Response) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, response)
// return jsoninfo.UnmarshalStrictStruct(data, response)

var refOnly struct {
Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
}
if err := json.Unmarshal(data, &refOnly); err == nil && refOnly.Ref != "" {
response.Ref = refOnly.Ref
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
return dec.Decode(&refOnly)
}

type ResponseBis Response
var x ResponseBis
if err := json.Unmarshal(data, &x); err != nil {
return err
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, "description")
delete(x.Extensions, "schema")
delete(x.Extensions, "headers")
delete(x.Extensions, "examples")
var unknowns []string
for k := range x.Extensions {
if !strings.HasPrefix(k, "x-") {
unknowns = append(unknowns, k)
}
}
if len(unknowns) != 0 {
sort.Strings(unknowns)
return fmt.Errorf("unknown fields: %+v", unknowns)
}
*response = Response(x)
return nil
}

type Header struct {
Expand All @@ -259,7 +332,11 @@ type SecurityRequirements []map[string][]string

type SecurityScheme struct {
openapi3.ExtensionProps
Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`

// https://github.com/OAI/OpenAPI-Specification/blob/cede0ea56b887e29d0f46627503648d16340078d/versions/2.0.md#response-object
// A simple object to allow referencing other definitions in the specification. It can be used to reference parameters and responses that are defined at the top level for reuse.
Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`

Description string `json:"description,omitempty" yaml:"description,omitempty"`
Type string `json:"type,omitempty" yaml:"type,omitempty"`
In string `json:"in,omitempty" yaml:"in,omitempty"`
Expand Down
31 changes: 25 additions & 6 deletions openapi2conv/openapi2_conv.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,14 +413,32 @@ func onlyOneReqBodyParam(bodies []*openapi3.RequestBodyRef, formDataSchemas map[
return nil, nil
}

func extensionPropsAsExtensions(eps openapi3.ExtensionProps) map[string]json.RawMessage {
es := eps.Extensions
m := make(map[string]json.RawMessage, len(es))
for k, v := range es {
m[k] = v.(json.RawMessage)
}
return m
}

func extensionsAsExtensionProps(m map[string]json.RawMessage) (eps openapi3.ExtensionProps) {
es := make(map[string]interface{}, len(m))
for k, v := range m {
es[k] = v
}
return openapi3.ExtensionProps{Extensions: es}
}

func ToV3Response(response *openapi2.Response, produces []string) (*openapi3.ResponseRef, error) {
if ref := response.Ref; ref != "" {
return &openapi3.ResponseRef{Ref: ToV3Ref(ref)}, nil
}
stripNonCustomExtensions(response.Extensions)
// stripNonCustomExtensions(response.Extensions)
result := &openapi3.Response{
Description: &response.Description,
ExtensionProps: response.ExtensionProps,
Description: &response.Description,
// Extensions: extensionPropsAsExtensions(response.ExtensionProps),
Extensions: response.Extensions,
}

// Default to "application/json" if "produces" is not specified.
Expand Down Expand Up @@ -1088,10 +1106,11 @@ func FromV3Response(ref *openapi3.ResponseRef, components *openapi3.Components)
if desc := response.Description; desc != nil {
description = *desc
}
stripNonCustomExtensions(response.Extensions)
// stripNonCustomExtensions(response.Extensions)
result := &openapi2.Response{
Description: description,
ExtensionProps: response.ExtensionProps,
Description: description,
// ExtensionProps: extensionsAsExtensionProps(response.Extensions),
Extensions: response.Extensions,
}
if content := response.Content; content != nil {
if ct := content["application/json"]; ct != nil {
Expand Down
132 changes: 132 additions & 0 deletions openapi3/issue513_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package openapi3

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"
)

func TestIssue513OKWithExtension(t *testing.T) {
spec := `
openapi: "3.0.3"
info:
title: 'My app'
version: 1.0.0
description: 'An API'
paths:
/v1/operation:
delete:
summary: Delete something
responses:
200:
description: Success
default:
description: '* **400** - Bad Request'
x-my-extension: {val: ue}
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Error:
type: object
description: An error response body.
properties:
message:
description: A detailed message describing the error.
type: string
`[1:]
sl := NewLoader()
doc, err := sl.LoadFromData([]byte(spec))
require.NoError(t, err)
err = doc.Validate(sl.Context)
require.NoError(t, err)
data, err := json.Marshal(doc)
require.NoError(t, err)
require.Contains(t, string(data), `x-my-extension`)
}

func TestIssue513KOHasExtraFieldSchema(t *testing.T) {
spec := `
openapi: "3.0.3"
info:
title: 'My app'
version: 1.0.0
description: 'An API'
paths:
/v1/operation:
delete:
summary: Delete something
responses:
200:
description: Success
default:
description: '* **400** - Bad Request'
x-my-extension: {val: ue}
# Notice here schema is invalid. It should instead be:
# content:
# application/json:
# schema:
# $ref: '#/components/schemas/Error'
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Error:
type: object
description: An error response body.
properties:
message:
description: A detailed message describing the error.
type: string
`[1:]
sl := NewLoader()
doc, err := sl.LoadFromData([]byte(spec))
require.NoError(t, err)
err = doc.Validate(sl.Context) // FIXME unmarshal or validation error
// TODO: merge unmarshal + validation ?
// but still allow Validate so one can modify value then validate without marshaling
require.Error(t, err)
}

func TestIssue513KOMixesRefAlongWithOtherFields(t *testing.T) {
spec := `
openapi: "3.0.3"
info:
title: 'My app'
version: 1.0.0
description: 'An API'
paths:
/v1/operation:
delete:
summary: Delete something
responses:
200:
description: Success
$ref: '#/components/responseBodies/SomeResponseBody'
components:
responseBodies:
SomeResponseBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
schemas:
Error:
type: object
description: An error response body.
properties:
message:
description: A detailed message describing the error.
type: string
`[1:]
sl := NewLoader()
doc, err := sl.LoadFromData([]byte(spec))
require.Error(t, err)
require.Nil(t, doc)
}
1 change: 1 addition & 0 deletions openapi3/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.UR
func unmarshal(data []byte, v interface{}) error {
// See https://github.com/getkin/kin-openapi/issues/680
if err := json.Unmarshal(data, v); err != nil {
// return yaml.UnmarshalStrict(data, v) TODO: investigate how yaml.v3 handles duplicate map keys
return yaml.Unmarshal(data, v)
}
return nil
Expand Down
39 changes: 37 additions & 2 deletions openapi3/refs.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package openapi3

import (
"bytes"
"context"
"encoding/json"

"github.com/go-openapi/jsonpointer"

Expand Down Expand Up @@ -233,12 +235,45 @@ func (value *ResponseRef) MarshalYAML() (interface{}, error) {

// MarshalJSON returns the JSON encoding of ResponseRef.
func (value *ResponseRef) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalRef(value.Ref, value.Value)
// return jsoninfo.MarshalRef(value.Ref, value.Value)

if ref := value.Ref; ref != "" {
return json.Marshal(struct {
Ref string `json:"$ref" yaml:"$ref"`
}{Ref: ref})
}
return json.Marshal(value.Value)
}

// // RefHasSiblings is returned when a $ref is defined along with other fields
// var RefHasSiblings refHasSiblings

// type refHasSiblings struct{}

// func (e refHasSiblings) Error() string {
// return "properties additional to $ref are ignored"
// }

// UnmarshalJSON sets ResponseRef to a copy of data.
func (value *ResponseRef) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value)
// return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value)

var refOnly struct {
Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
}
if err := json.Unmarshal(data, &refOnly); err == nil && refOnly.Ref != "" {
value.Ref = refOnly.Ref
dec := json.NewDecoder(bytes.NewReader(data))
dec.DisallowUnknownFields()
return dec.Decode(&refOnly)
// } else if strings.Contains(err.Error(), "json: unknown field") {
// // panic(err) // panic: json: unknown field "description"
// if true {
// panic(fmt.Sprintf(">>> refOnly:%#v err=%v", refOnly, err))
// }
// return RefHasSiblings
}
return json.Unmarshal(data, &value.Value)
}

// Validate returns an error if ResponseRef does not comply with the OpenAPI spec.
Expand Down
Loading

0 comments on commit 3cdbfdd

Please sign in to comment.