diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index 09f1192a..2642861f 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -1228,11 +1228,22 @@ func URIMapCache(reader ReadFromURIFunc) ReadFromURIFunc documents. type Ref struct { - Ref string `json:"$ref" yaml:"$ref"` + Ref string `json:"$ref" yaml:"$ref"` + Extensions map[string]any `json:"-" yaml:"-"` } Ref is specified by OpenAPI/Swagger 3.0 standard. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object +func (x Ref) MarshalJSON() ([]byte, error) + MarshalJSON returns the JSON encoding of Ref. + +func (x Ref) MarshalYAML() (any, error) + MarshalYAML returns the YAML encoding of Ref. + +func (e *Ref) Validate(ctx context.Context, opts ...ValidationOption) error + Validate returns an error if Extensions does not comply with the OpenAPI + spec. + type RefNameResolver func(*T, ComponentRef) string RefNameResolver maps a component to an name that is used as it's internalized name. diff --git a/openapi3/ref.go b/openapi3/ref.go index 07060731..e77fcff6 100644 --- a/openapi3/ref.go +++ b/openapi3/ref.go @@ -1,9 +1,42 @@ package openapi3 +import ( + "context" + "encoding/json" +) + //go:generate go run refsgenerator.go // Ref is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object type Ref struct { - Ref string `json:"$ref" yaml:"$ref"` + Ref string `json:"$ref" yaml:"$ref"` + Extensions map[string]any `json:"-" yaml:"-"` +} + +// MarshalYAML returns the YAML encoding of Ref. +func (x Ref) MarshalYAML() (any, error) { + m := make(map[string]any, 1+len(x.Extensions)) + for k, v := range x.Extensions { + m[k] = v + } + if x := x.Ref; x != "" { + m["$ref"] = x + } + return m, nil +} + +// MarshalJSON returns the JSON encoding of Ref. +func (x Ref) MarshalJSON() ([]byte, error) { + y, err := x.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(y) +} + +// Validate returns an error if Extensions does not comply with the OpenAPI spec. +func (e *Ref) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + return validateExtensions(ctx, e.Extensions) } diff --git a/openapi3/refs.go b/openapi3/refs.go index d337b0e3..f1820552 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -53,7 +53,7 @@ func (x *CallbackRef) setRefPath(u *url.URL) { // MarshalYAML returns the YAML encoding of CallbackRef. func (x CallbackRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { - return &Ref{Ref: ref}, nil + return &Ref{Ref: ref, Extensions: x.Extensions}, nil } return x.Value.MarshalYAML() } @@ -189,7 +189,7 @@ func (x *ExampleRef) setRefPath(u *url.URL) { // MarshalYAML returns the YAML encoding of ExampleRef. func (x ExampleRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { - return &Ref{Ref: ref}, nil + return &Ref{Ref: ref, Extensions: x.Extensions}, nil } return x.Value.MarshalYAML() } @@ -325,7 +325,7 @@ func (x *HeaderRef) setRefPath(u *url.URL) { // MarshalYAML returns the YAML encoding of HeaderRef. func (x HeaderRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { - return &Ref{Ref: ref}, nil + return &Ref{Ref: ref, Extensions: x.Extensions}, nil } return x.Value.MarshalYAML() } @@ -461,7 +461,7 @@ func (x *LinkRef) setRefPath(u *url.URL) { // MarshalYAML returns the YAML encoding of LinkRef. func (x LinkRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { - return &Ref{Ref: ref}, nil + return &Ref{Ref: ref, Extensions: x.Extensions}, nil } return x.Value.MarshalYAML() } @@ -597,7 +597,7 @@ func (x *ParameterRef) setRefPath(u *url.URL) { // MarshalYAML returns the YAML encoding of ParameterRef. func (x ParameterRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { - return &Ref{Ref: ref}, nil + return &Ref{Ref: ref, Extensions: x.Extensions}, nil } return x.Value.MarshalYAML() } @@ -733,7 +733,7 @@ func (x *RequestBodyRef) setRefPath(u *url.URL) { // MarshalYAML returns the YAML encoding of RequestBodyRef. func (x RequestBodyRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { - return &Ref{Ref: ref}, nil + return &Ref{Ref: ref, Extensions: x.Extensions}, nil } return x.Value.MarshalYAML() } @@ -869,7 +869,7 @@ func (x *ResponseRef) setRefPath(u *url.URL) { // MarshalYAML returns the YAML encoding of ResponseRef. func (x ResponseRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { - return &Ref{Ref: ref}, nil + return &Ref{Ref: ref, Extensions: x.Extensions}, nil } return x.Value.MarshalYAML() } @@ -1005,7 +1005,7 @@ func (x *SchemaRef) setRefPath(u *url.URL) { // MarshalYAML returns the YAML encoding of SchemaRef. func (x SchemaRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { - return &Ref{Ref: ref}, nil + return &Ref{Ref: ref, Extensions: x.Extensions}, nil } return x.Value.MarshalYAML() } @@ -1141,7 +1141,7 @@ func (x *SecuritySchemeRef) setRefPath(u *url.URL) { // MarshalYAML returns the YAML encoding of SecuritySchemeRef. func (x SecuritySchemeRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { - return &Ref{Ref: ref}, nil + return &Ref{Ref: ref, Extensions: x.Extensions}, nil } return x.Value.MarshalYAML() } diff --git a/openapi3/refs.tmpl b/openapi3/refs.tmpl index a3f5bdab..0a443e84 100644 --- a/openapi3/refs.tmpl +++ b/openapi3/refs.tmpl @@ -53,7 +53,7 @@ func (x *{{ $type.Name }}Ref) setRefPath(u *url.URL) { // MarshalYAML returns the YAML encoding of {{ $type.Name }}Ref. func (x {{ $type.Name }}Ref) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { - return &Ref{Ref: ref}, nil + return &Ref{Ref: ref, Extensions: x.Extensions}, nil } return x.Value.MarshalYAML() } diff --git a/openapi3/refs_test.go b/openapi3/refs_test.go index b6de316f..89ad64e1 100644 --- a/openapi3/refs_test.go +++ b/openapi3/refs_test.go @@ -12,6 +12,7 @@ import ( func TestCallbackRef_Extensions(t *testing.T) { data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + expectMarshalJson := []byte(`{"$ref":"#/components/schemas/Pet","x-order":1}`) ref := CallbackRef{} err := json.Unmarshal(data, &ref) @@ -34,6 +35,12 @@ func TestCallbackRef_Extensions(t *testing.T) { err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + // Verify round trip JSON + // Compare as string to make error message more readable if different + outJson, err := ref.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, string(outJson), string(expectMarshalJson), "MarshalJSON output is not the same as input data") + // non-extension not json lookable _, err = ref.JSONLookup("something") assert.Error(t, err) @@ -51,6 +58,7 @@ func TestCallbackRef_Extensions(t *testing.T) { func TestExampleRef_Extensions(t *testing.T) { data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + expectMarshalJson := []byte(`{"$ref":"#/components/schemas/Pet","x-order":1}`) ref := ExampleRef{} err := json.Unmarshal(data, &ref) @@ -73,6 +81,12 @@ func TestExampleRef_Extensions(t *testing.T) { err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + // Verify round trip JSON + // Compare as string to make error message more readable if different + outJson, err := ref.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, string(outJson), string(expectMarshalJson), "MarshalJSON output is not the same as input data") + // non-extension not json lookable _, err = ref.JSONLookup("something") assert.Error(t, err) @@ -90,6 +104,7 @@ func TestExampleRef_Extensions(t *testing.T) { func TestHeaderRef_Extensions(t *testing.T) { data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + expectMarshalJson := []byte(`{"$ref":"#/components/schemas/Pet","x-order":1}`) ref := HeaderRef{} err := json.Unmarshal(data, &ref) @@ -112,6 +127,12 @@ func TestHeaderRef_Extensions(t *testing.T) { err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + // Verify round trip JSON + // Compare as string to make error message more readable if different + outJson, err := ref.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, string(outJson), string(expectMarshalJson), "MarshalJSON output is not the same as input data") + // non-extension not json lookable _, err = ref.JSONLookup("something") assert.Error(t, err) @@ -121,6 +142,7 @@ func TestHeaderRef_Extensions(t *testing.T) { func TestLinkRef_Extensions(t *testing.T) { data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + expectMarshalJson := []byte(`{"$ref":"#/components/schemas/Pet","x-order":1}`) ref := LinkRef{} err := json.Unmarshal(data, &ref) @@ -143,6 +165,12 @@ func TestLinkRef_Extensions(t *testing.T) { err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + // Verify round trip JSON + // Compare as string to make error message more readable if different + outJson, err := ref.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, string(outJson), string(expectMarshalJson), "MarshalJSON output is not the same as input data") + // non-extension not json lookable _, err = ref.JSONLookup("something") assert.Error(t, err) @@ -160,6 +188,7 @@ func TestLinkRef_Extensions(t *testing.T) { func TestParameterRef_Extensions(t *testing.T) { data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + expectMarshalJson := []byte(`{"$ref":"#/components/schemas/Pet","x-order":1}`) ref := ParameterRef{} err := json.Unmarshal(data, &ref) @@ -182,6 +211,12 @@ func TestParameterRef_Extensions(t *testing.T) { err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + // Verify round trip JSON + // Compare as string to make error message more readable if different + outJson, err := ref.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, string(outJson), string(expectMarshalJson), "MarshalJSON output is not the same as input data") + // non-extension not json lookable _, err = ref.JSONLookup("something") assert.Error(t, err) @@ -199,6 +234,7 @@ func TestParameterRef_Extensions(t *testing.T) { func TestRequestBodyRef_Extensions(t *testing.T) { data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + expectMarshalJson := []byte(`{"$ref":"#/components/schemas/Pet","x-order":1}`) ref := RequestBodyRef{} err := json.Unmarshal(data, &ref) @@ -221,6 +257,12 @@ func TestRequestBodyRef_Extensions(t *testing.T) { err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + // Verify round trip JSON + // Compare as string to make error message more readable if different + outJson, err := ref.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, string(outJson), string(expectMarshalJson), "MarshalJSON output is not the same as input data") + // non-extension not json lookable _, err = ref.JSONLookup("something") assert.Error(t, err) @@ -238,6 +280,7 @@ func TestRequestBodyRef_Extensions(t *testing.T) { func TestResponseRef_Extensions(t *testing.T) { data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + expectMarshalJson := []byte(`{"$ref":"#/components/schemas/Pet","x-order":1}`) ref := ResponseRef{} err := json.Unmarshal(data, &ref) @@ -260,6 +303,12 @@ func TestResponseRef_Extensions(t *testing.T) { err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + // Verify round trip JSON + // Compare as string to make error message more readable if different + outJson, err := ref.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, string(outJson), string(expectMarshalJson), "MarshalJSON output is not the same as input data") + // non-extension not json lookable _, err = ref.JSONLookup("something") assert.Error(t, err) @@ -277,6 +326,7 @@ func TestResponseRef_Extensions(t *testing.T) { func TestSchemaRef_Extensions(t *testing.T) { data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + expectMarshalJson := []byte(`{"$ref":"#/components/schemas/Pet","x-order":1}`) ref := SchemaRef{} err := json.Unmarshal(data, &ref) @@ -299,6 +349,12 @@ func TestSchemaRef_Extensions(t *testing.T) { err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + // Verify round trip JSON + // Compare as string to make error message more readable if different + outJson, err := ref.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, string(outJson), string(expectMarshalJson), "MarshalJSON output is not the same as input data") + // non-extension not json lookable _, err = ref.JSONLookup("something") assert.Error(t, err) @@ -316,6 +372,7 @@ func TestSchemaRef_Extensions(t *testing.T) { func TestSecuritySchemeRef_Extensions(t *testing.T) { data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + expectMarshalJson := []byte(`{"$ref":"#/components/schemas/Pet","x-order":1}`) ref := SecuritySchemeRef{} err := json.Unmarshal(data, &ref) @@ -338,6 +395,12 @@ func TestSecuritySchemeRef_Extensions(t *testing.T) { err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + // Verify round trip JSON + // Compare as string to make error message more readable if different + outJson, err := ref.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, string(outJson), string(expectMarshalJson), "MarshalJSON output is not the same as input data") + // non-extension not json lookable _, err = ref.JSONLookup("something") assert.Error(t, err) diff --git a/openapi3/refs_test.tmpl b/openapi3/refs_test.tmpl index 634fccf6..a11e1769 100644 --- a/openapi3/refs_test.tmpl +++ b/openapi3/refs_test.tmpl @@ -12,6 +12,7 @@ import ( {{ range $type := .Types }} func Test{{ $type.Name }}Ref_Extensions(t *testing.T) { data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + expectMarshalJson := []byte(`{"$ref":"#/components/schemas/Pet","x-order":1}`) ref := {{ $type.Name }}Ref{} err := json.Unmarshal(data, &ref) @@ -34,6 +35,12 @@ func Test{{ $type.Name }}Ref_Extensions(t *testing.T) { err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + // Verify round trip JSON + // Compare as string to make error message more readable if different + outJson, err := ref.MarshalJSON() + assert.NoError(t, err) + assert.Equal(t, string(outJson), string(expectMarshalJson), "MarshalJSON output is not the same as input data") + // non-extension not json lookable _, err = ref.JSONLookup("something") assert.Error(t, err)