Skip to content

test: add goccy/go-yaml tests and improve YAML marshal/unmarshal #104

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ go get github.com/sv-tools/openapi
* `Validator.ValidateDataAsJSON()` method validates the data by converting it into `map[string]any` type first using `json.Marshal` and `json.Unmarshal`.
**WARNING**: the function is slow due to double conversion.
* Use OpenAPI `v3.1.1` by default.
* Added support for [goccy/yaml](https://github.com/goccy/go-yaml) library.

## Features

Expand Down
43 changes: 22 additions & 21 deletions bool_or_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,15 @@ package openapi

import (
"encoding/json"

"gopkg.in/yaml.v3"
)

// BoolOrSchema handles Boolean or Schema type.
//
// It MUST be used as a pointer,
// otherwise the `false` can be omitted by json or yaml encoders in case of `omitempty` tag is set.
type BoolOrSchema struct {
Schema *RefOrSpec[Schema]
Allowed bool
}

// UnmarshalJSON implements json.Unmarshaler interface.
func (o *BoolOrSchema) UnmarshalJSON(data []byte) error {
if json.Unmarshal(data, &o.Allowed) == nil {
o.Schema = nil
return nil
}
if err := json.Unmarshal(data, &o.Schema); err != nil {
return err
}
o.Allowed = true
return nil
Schema *RefOrSpec[Schema] `json:"-" yaml:"-"`
Allowed bool `json:"-" yaml:"-"`
}

// MarshalJSON implements json.Marshaler interface.
Expand All @@ -39,13 +24,13 @@ func (o *BoolOrSchema) MarshalJSON() ([]byte, error) {
return json.Marshal(&v)
}

// UnmarshalYAML implements yaml.Unmarshaler interface.
func (o *BoolOrSchema) UnmarshalYAML(node *yaml.Node) error {
if node.Decode(&o.Allowed) == nil {
// UnmarshalJSON implements json.Unmarshaler interface.
func (o *BoolOrSchema) UnmarshalJSON(data []byte) error {
if json.Unmarshal(data, &o.Allowed) == nil {
o.Schema = nil
return nil
}
if err := node.Decode(&o.Schema); err != nil {
if err := json.Unmarshal(data, &o.Schema); err != nil {
return err
}
o.Allowed = true
Expand All @@ -64,6 +49,22 @@ func (o *BoolOrSchema) MarshalYAML() (any, error) {
return v, nil
}

// UnmarshalYAML implements yaml.obsoleteUnmarshaler and goyaml.InterfaceUnmarshaler interfaces.
func (o *BoolOrSchema) UnmarshalYAML(unmarshal func(any) error) error {
if unmarshal(&o.Allowed) == nil {
o.Schema = nil
return nil
}
if o.Schema == nil {
o.Schema = &RefOrSpec[Schema]{}
}
if err := unmarshal(o.Schema); err != nil {
return err
}
o.Allowed = true
return nil
}

func (o *BoolOrSchema) validateSpec(path string, validator *Validator) []*validationError {
var errs []*validationError
if o.Schema != nil {
Expand Down
25 changes: 21 additions & 4 deletions bool_or_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import (
"encoding/json"
"testing"

goyaml "github.com/goccy/go-yaml"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"

"github.com/sv-tools/openapi"
)

type testAD struct {
type TestAD struct {
AP *openapi.BoolOrSchema `json:"ap,omitempty" yaml:"ap,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
}
Expand Down Expand Up @@ -52,7 +53,7 @@ func TestAdditionalPropertiesJSON(t *testing.T) {
} {
t.Run(tt.name, func(t *testing.T) {
t.Run("json", func(t *testing.T) {
var v testAD
var v TestAD
require.NoError(t, json.Unmarshal([]byte(tt.data), &v))
require.Equal(t, "foo", v.Name)
if tt.nilAP {
Expand All @@ -67,8 +68,8 @@ func TestAdditionalPropertiesJSON(t *testing.T) {
require.JSONEq(t, tt.data, string(newJson))
})

t.Run("yaml", func(t *testing.T) {
var v testAD
t.Run("yaml.v3", func(t *testing.T) {
var v TestAD
require.NoError(t, yaml.Unmarshal([]byte(tt.data), &v))
require.Equal(t, "foo", v.Name)
if tt.nilAP {
Expand All @@ -82,6 +83,22 @@ func TestAdditionalPropertiesJSON(t *testing.T) {
require.NoError(t, err)
require.YAMLEq(t, tt.data, string(newYaml))
})

t.Run("goccy/go-yaml", func(t *testing.T) {
var v TestAD
require.NoError(t, goyaml.Unmarshal([]byte(tt.data), &v))
require.Equal(t, "foo", v.Name)
if tt.nilAP {
require.Nil(t, v.AP)
} else {
require.NotNil(t, v.AP)
require.Equal(t, tt.allowed, v.AP.Allowed)
require.Equal(t, tt.nilSchema, v.AP.Schema == nil)
}
newYaml, err := goyaml.Marshal(&v)
require.NoError(t, err)
require.YAMLEq(t, tt.data, string(newYaml))
})
})
}
}
10 changes: 4 additions & 6 deletions callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package openapi

import (
"encoding/json"

"gopkg.in/yaml.v3"
)

// Callback is a map of possible out-of band callbacks related to the parent operation.
Expand All @@ -30,7 +28,7 @@ import (
// '200':
// description: callback successfully processed
type Callback struct {
Paths map[string]*RefOrSpec[Extendable[PathItem]]
Paths map[string]*RefOrSpec[Extendable[PathItem]] `json:"-" yaml:"-"`
}

// MarshalJSON implements json.Marshaler interface.
Expand All @@ -48,9 +46,9 @@ func (o *Callback) MarshalYAML() (any, error) {
return o.Paths, nil
}

// UnmarshalYAML implements yaml.Unmarshaler interface.
func (o *Callback) UnmarshalYAML(node *yaml.Node) error {
return node.Decode(&o.Paths)
// UnmarshalYAML implements yaml.obsoleteUnmarshaler and goyaml.InterfaceUnmarshaler interfaces.
func (o *Callback) UnmarshalYAML(unmarshal func(any) error) error {
return unmarshal(&o.Paths)
}

func (o *Callback) validateSpec(location string, validator *Validator) []*validationError {
Expand Down
58 changes: 58 additions & 0 deletions callback_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package openapi_test

import (
"encoding/json"
"testing"

goyaml "github.com/goccy/go-yaml"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"

"github.com/sv-tools/openapi"
)

func TestCallback_Marshal_Unmarshal(t *testing.T) {
for _, tt := range []struct {
name string
data string
expected string
}{
{
name: "spec",
data: `{"example.com": {"get": {"summary": "foo"}}}`,
},
} {
t.Run(tt.name, func(t *testing.T) {
t.Run("json", func(t *testing.T) {
var v openapi.Callback
require.NoError(t, json.Unmarshal([]byte(tt.data), &v))
data, err := json.Marshal(&v)
require.NoError(t, err)
if tt.expected == "" {
tt.expected = tt.data
}
require.JSONEq(t, tt.expected, string(data))
})
t.Run("yaml.v3", func(t *testing.T) {
var v openapi.Callback
require.NoError(t, yaml.Unmarshal([]byte(tt.data), &v))
data, err := yaml.Marshal(&v)
require.NoError(t, err)
if tt.expected == "" {
tt.expected = tt.data
}
require.YAMLEq(t, tt.expected, string(data))
})
t.Run("goccy/go-yaml", func(t *testing.T) {
var v openapi.Callback
require.NoError(t, goyaml.Unmarshal([]byte(tt.data), &v))
data, err := goyaml.Marshal(&v)
require.NoError(t, err)
if tt.expected == "" {
tt.expected = tt.data
}
require.YAMLEq(t, tt.expected, string(data))
})
})
}
}
73 changes: 39 additions & 34 deletions extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import (
"errors"
"fmt"
"strings"

"gopkg.in/yaml.v3"
)

const ExtensionPrefix = "x-"
Expand Down Expand Up @@ -69,20 +67,30 @@ func (o *Extendable[T]) GetExt(name string) any {

// MarshalJSON implements json.Marshaler interface.
func (o *Extendable[T]) MarshalJSON() ([]byte, error) {
var raw map[string]json.RawMessage
exts, err := json.Marshal(&o.Extensions)
if err != nil {
return nil, fmt.Errorf("%T.Extensions: %w", o.Spec, err)
if o == nil {
return nil, nil
}
if err := json.Unmarshal(exts, &raw); err != nil {
return nil, fmt.Errorf("%T(raw extensions): %w", o.Spec, err)
var raw map[string]json.RawMessage
if len(o.Extensions) > 0 {
exts, err := json.Marshal(&o.Extensions)
if err != nil {
return nil, fmt.Errorf("%T.Extensions: %w", o.Spec, err)
}
if err := json.Unmarshal(exts, &raw); err != nil {
return nil, fmt.Errorf("%T(raw extensions): %w", o.Spec, err)
}
}
fields, err := json.Marshal(&o.Spec)
if err != nil {
return nil, fmt.Errorf("%T: %w", o.Spec, err)
if o.Spec != nil {
fields, err := json.Marshal(o.Spec)
if err != nil {
return nil, fmt.Errorf("%T: %w", o.Spec, err)
}
if err := json.Unmarshal(fields, &raw); err != nil {
return nil, fmt.Errorf("%T(raw fields): %w", o.Spec, err)
}
}
if err := json.Unmarshal(fields, &raw); err != nil {
return nil, fmt.Errorf("%T(raw fields): %w", o.Spec, err)
if len(raw) == 0 {
return nil, nil
}
data, err := json.Marshal(&raw)
if err != nil {
Expand All @@ -97,14 +105,19 @@ func (o *Extendable[T]) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &raw); err != nil {
return fmt.Errorf("%T: %w", o.Spec, err)
}
o.Extensions = make(map[string]any)
exts := make(map[string]any)
for name, value := range raw {
if strings.HasPrefix(name, ExtensionPrefix) {
var v any
if err := json.Unmarshal(value, &v); err != nil {
return fmt.Errorf("%T.Extensions.%s: %w", o.Spec, name, err)
}
o.Extensions[name] = v
exts[name] = v
}
}
if len(exts) > 0 {
o.Extensions = exts
for name := range exts {
delete(raw, name)
}
}
Expand All @@ -121,28 +134,21 @@ func (o *Extendable[T]) UnmarshalJSON(data []byte) error {

// MarshalYAML implements yaml.Marshaler interface.
func (o *Extendable[T]) MarshalYAML() (any, error) {
var raw map[string]any
exts, err := yaml.Marshal(&o.Extensions)
if err != nil {
return nil, fmt.Errorf("%T.Extensions: %w", o.Spec, err)
}
if err := yaml.Unmarshal(exts, &raw); err != nil {
return nil, fmt.Errorf("%T(raw extensions): %w", o.Spec, err)
}
fields, err := yaml.Marshal(&o.Spec)
data, err := json.Marshal(o)
if err != nil {
return nil, fmt.Errorf("%T: %w", o.Spec, err)
}
if err := yaml.Unmarshal(fields, &raw); err != nil {
return nil, fmt.Errorf("%T(raw fields): %w", o.Spec, err)
var raw map[string]any
if err := json.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("%T(raw): %w", o.Spec, err)
}
return raw, nil
}

// UnmarshalYAML implements yaml.Unmarshaler interface.
func (o *Extendable[T]) UnmarshalYAML(node *yaml.Node) error {
// UnmarshalYAML implements yaml.obsoleteUnmarshaler and goyaml.InterfaceUnmarshaler interfaces.
func (o *Extendable[T]) UnmarshalYAML(unmarshal func(any) error) error {
var raw map[string]any
if err := node.Decode(&raw); err != nil {
if err := unmarshal(&raw); err != nil {
return fmt.Errorf("%T: %w", o.Spec, err)
}
o.Extensions = make(map[string]any)
Expand All @@ -152,14 +158,13 @@ func (o *Extendable[T]) UnmarshalYAML(node *yaml.Node) error {
delete(raw, name)
}
}
fields, err := yaml.Marshal(&raw)
if err != nil {
return fmt.Errorf("%T(raw): %w", o.Spec, err)
if o.Spec == nil {
o.Spec = new(T)
}
if err := yaml.Unmarshal(fields, &o.Spec); err != nil {
if err := unmarshal(o.Spec); err != nil {
o.Spec = nil
return fmt.Errorf("%T: %w", o.Spec, err)
}

return nil
}

Expand Down
Loading
Loading