Skip to content

Commit

Permalink
Merge pull request #567 from tdakkota/feat/parse-extensions
Browse files Browse the repository at this point in the history
feat: parse extensions
  • Loading branch information
ernado authored Sep 14, 2022
2 parents cd7113d + a6622e8 commit 3dfa9ef
Show file tree
Hide file tree
Showing 20 changed files with 459 additions and 159 deletions.
30 changes: 20 additions & 10 deletions dsl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ const (
)

var (
_extensions = ogen.Extensions(nil)
_common = ogen.OpenAPICommon{
Extensions: _extensions,
}

// reusable query param
_queryParam = ogen.NewParameter().
InQuery().
Expand Down Expand Up @@ -103,8 +108,8 @@ func TestBuilder(t *testing.T) {
Version: "0.1.0",
},
Servers: []ogen.Server{
{"staging", "staging.api.com", nil},
{"production", "api.com", nil},
{"staging", "staging.api.com", nil, _extensions},
{"production", "api.com", nil, _extensions},
},
Paths: map[string]*ogen.PathItem{
pathWithID: {
Expand Down Expand Up @@ -212,6 +217,7 @@ func TestBuilder(t *testing.T) {
},
},
},
Extensions: _extensions,
}
// referenced path
path := ogen.NewPathItem().
Expand Down Expand Up @@ -328,18 +334,22 @@ func TestBuilder(t *testing.T) {
SetHead(ogen.NewOperation().SetOperationID("head").SetResponses(ogen.Responses{"resp": ogen.NewResponse()})).
SetPatch(ogen.NewOperation().SetOperationID("patch").AddParameters(ogen.NewParameter().InHeader().SetDeprecated(true))).
SetTrace(ogen.NewOperation().SetOperationID("trace")).
SetServers([]ogen.Server{{"url1", "desc1", nil}}).
SetServers([]ogen.Server{{"url1", "desc1", nil, nil}}).
AddServers(ogen.NewServer().SetDescription("desc2").SetURL("url2")).
SetParameters([]*ogen.Parameter{_queryParam.Parameter})
assert.Equal(t, &ogen.PathItem{
Put: &ogen.Operation{OperationID: "put", Tags: []string{"tag1", "tag2"}},
Delete: &ogen.Operation{OperationID: "delete", Summary: "summary"},
Options: &ogen.Operation{OperationID: "options", Parameters: []*ogen.Parameter{_cookieParam.Parameter}},
Head: &ogen.Operation{OperationID: "head", Responses: ogen.Responses{"resp": &ogen.Response{}}},
Patch: &ogen.Operation{OperationID: "patch", Parameters: []*ogen.Parameter{{In: "header", Deprecated: true}}},
Trace: &ogen.Operation{OperationID: "trace"},
Servers: []ogen.Server{{"url1", "desc1", nil}, {"url2", "desc2", nil}},
Put: &ogen.Operation{OperationID: "put", Tags: []string{"tag1", "tag2"}},
Delete: &ogen.Operation{OperationID: "delete", Summary: "summary"},
Options: &ogen.Operation{OperationID: "options", Parameters: []*ogen.Parameter{_cookieParam.Parameter}},
Head: &ogen.Operation{OperationID: "head", Responses: ogen.Responses{"resp": &ogen.Response{}}},
Patch: &ogen.Operation{OperationID: "patch", Parameters: []*ogen.Parameter{{In: "header", Deprecated: true}}},
Trace: &ogen.Operation{OperationID: "trace"},
Servers: []ogen.Server{
{"url1", "desc1", nil, nil},
{"url2", "desc2", nil, nil},
},
Parameters: []*ogen.Parameter{_queryParam.Parameter},
Common: _common,
}, pi)

mlt := uint64(1)
Expand Down
154 changes: 154 additions & 0 deletions jsonschema/extensions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package jsonschema

import (
"encoding/json"
"reflect"
"strings"

"github.com/go-faster/errors"
"github.com/go-faster/jx"
yaml "github.com/go-faster/yamlx"

"github.com/ogen-go/ogen/internal/location"
)

// Extensions map is "^x-" fields list.
type Extensions map[string]yaml.Node

func isExtensionKey(key string) bool {
return strings.HasPrefix(key, "x-")
}

// MarshalYAML implements yaml.Marshaler.
func (p Extensions) MarshalYAML() (any, error) {
content := make([]*yaml.Node, 0, len(p)*2)
for key, val := range p {
val := val
if !isExtensionKey(key) {
continue
}
content = append(content,
&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key},
&val,
)
}

return &yaml.Node{
Kind: yaml.MappingNode,
Content: content,
}, nil
}

// UnmarshalYAML implements yaml.Unmarshaler.
func (p *Extensions) UnmarshalYAML(node *yaml.Node) error {
if node.Kind != yaml.MappingNode {
return &yaml.UnmarshalError{
Node: node,
Type: reflect.TypeOf(p),
Err: errors.Errorf("cannot unmarshal %s into %T", node.ShortTag(), p),
}
}
m := *p
if m == nil {
m = make(Extensions, len(node.Content)/2)
*p = m
}
for i := 0; i < len(node.Content); i += 2 {
var (
keyNode = node.Content[i]
value = node.Content[i+1]
key string
)
if err := keyNode.Decode(&key); err != nil {
return err
}
// FIXME(tdakkota): use *yamlx.Node instead of yaml.Node
if isExtensionKey(key) && value != nil {
m[key] = *value
}
}
return nil
}

// MarshalJSON implements json.Marshaler.
func (p Extensions) MarshalJSON() ([]byte, error) {
e := &jx.Encoder{}

e.ObjStart()
for key, val := range p {
val := val
if !isExtensionKey(key) {
continue
}

e.FieldStart(key)
b, err := convertYAMLtoRawJSON(&val)
if err != nil {
return nil, errors.Wrap(err, "marshal")
}
e.Raw(b)
}
e.ObjEnd()
return e.Bytes(), nil
}

// UnmarshalJSON implements json.Unmarshaler.
func (p *Extensions) UnmarshalJSON(data []byte) error {
m := *p
if m == nil {
m = make(Extensions)
*p = m
}

d := jx.DecodeBytes(data)
return d.Obj(func(d *jx.Decoder, key string) error {
if !isExtensionKey(key) {
return d.Skip()
}

b, err := d.Raw()
if err != nil {
return err
}

value, err := convertJSONToRawYAML(json.RawMessage(b))
if err != nil {
return err
}
m[key] = *value

return nil
})
}

// OpenAPICommon is common fields for OpenAPI objects.
type OpenAPICommon struct {
Extensions
location.Locator
}

// MarshalYAML implements yaml.Marshaler.
func (p OpenAPICommon) MarshalYAML() (any, error) {
return p.Extensions.MarshalYAML()
}

// UnmarshalYAML implements yaml.Unmarshaler.
func (p *OpenAPICommon) UnmarshalYAML(node *yaml.Node) error {
if err := p.Extensions.UnmarshalYAML(node); err != nil {
return errors.Wrap(err, "unmarshal extensions")
}
if err := p.Locator.UnmarshalYAML(node); err != nil {
return errors.Wrap(err, "unmarshal locator")
}
return nil
}

// MarshalJSON implements json.Marshaler.
func (p OpenAPICommon) MarshalJSON() ([]byte, error) {
return p.Extensions.MarshalJSON()
}

// UnmarshalJSON implements json.Unmarshaler.
func (p *OpenAPICommon) UnmarshalJSON(data []byte) error {
return p.Extensions.UnmarshalJSON(data)
}
29 changes: 13 additions & 16 deletions jsonschema/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"regexp"

"github.com/go-faster/errors"
"github.com/go-faster/jx"

"github.com/ogen-go/ogen/internal/jsonpointer"
"github.com/ogen-go/ogen/internal/location"
Expand Down Expand Up @@ -57,7 +56,7 @@ func (p *Parser) Resolve(ref string, ctx *jsonpointer.ResolveCtx) (*Schema, erro
func (p *Parser) parse(schema *RawSchema, ctx *jsonpointer.ResolveCtx) (_ *Schema, rerr error) {
if schema != nil {
defer func() {
rerr = p.wrapLocation(ctx.LastLoc(), schema.Locator, rerr)
rerr = p.wrapLocation(ctx.LastLoc(), schema.Common.Locator, rerr)
}()
}
return p.parse1(schema, ctx, func(s *Schema) *Schema {
Expand All @@ -73,7 +72,7 @@ func (p *Parser) parse1(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hook fun

if schema != nil && s != nil {
if enum := schema.Enum; len(enum) > 0 {
loc := schema.Locator.Field("enum")
loc := schema.Common.Locator.Field("enum")
for i := range enum {
for j := range enum {
if i == j {
Expand Down Expand Up @@ -112,15 +111,13 @@ func (p *Parser) parse1(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hook fun
return nil
}(); err != nil {
err := errors.Wrap(err, "parse default")
return nil, p.wrapField("default", ctx.LastLoc(), schema.Locator, err)
return nil, p.wrapField("default", ctx.LastLoc(), schema.Common.Locator, err)
}
}
if a, ok := schema.XAnnotations["x-ogen-name"]; ok {
name, err := jx.DecodeBytes(a).Str()
if err != nil {
return nil, errors.Wrapf(err, "decode %q", a)
if a, ok := schema.Common.Extensions["x-ogen-name"]; ok {
if err := a.Decode(&s.XOgenName); err != nil {
return nil, errors.Wrap(err, "parse x-ogen-name")
}
s.XOgenName = name
}
}

Expand All @@ -132,7 +129,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
return nil, nil
}
wrapField := func(field string, err error) error {
return p.wrapField(field, ctx.LastLoc(), schema.Locator, err)
return p.wrapField(field, ctx.LastLoc(), schema.Common.Locator, err)
}

validateMinMax := func(prop string, min, max *uint64) (rerr error) {
Expand Down Expand Up @@ -180,7 +177,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
case len(schema.OneOf) > 0:
s := hook(&Schema{})

schemas, err := p.parseMany(schema.OneOf, schema.Locator, ctx)
schemas, err := p.parseMany(schema.OneOf, schema.Common.Locator, ctx)
if err != nil {
return nil, wrapField("oneOf", err)
}
Expand Down Expand Up @@ -208,7 +205,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
Pattern: schema.Pattern,
})

schemas, err := p.parseMany(schema.AnyOf, schema.Locator, ctx)
schemas, err := p.parseMany(schema.AnyOf, schema.Common.Locator, ctx)
if err != nil {
return nil, wrapField("anyOf", err)
}
Expand All @@ -218,7 +215,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
case len(schema.AllOf) > 0:
s := hook(&Schema{})

schemas, err := p.parseMany(schema.AllOf, schema.Locator, ctx)
schemas, err := p.parseMany(schema.AllOf, schema.Common.Locator, ctx)
if err != nil {
return nil, wrapField("allOf", err)
}
Expand Down Expand Up @@ -302,7 +299,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
}

if pp := schema.PatternProperties; len(pp) > 0 {
ppLoc := schema.Locator.Field("patternProperties")
ppLoc := schema.Common.Locator.Field("patternProperties")

patterns := make([]PatternProperty, len(pp))
for idx, prop := range pp {
Expand All @@ -326,7 +323,7 @@ func (p *Parser) parseSchema(schema *RawSchema, ctx *jsonpointer.ResolveCtx, hoo
s.PatternProperties = patterns
}

propsLoc := schema.Locator.Field("properties")
propsLoc := schema.Common.Locator.Field("properties")
for _, propSpec := range schema.Properties {
prop, err := p.parse(propSpec.Schema, ctx)
if err != nil {
Expand Down Expand Up @@ -489,6 +486,6 @@ func (p *Parser) extendInfo(schema *RawSchema, s *Schema) *Schema {
}
}

s.Locator = schema.Locator
s.Locator = schema.Common.Locator
return s
}
43 changes: 43 additions & 0 deletions jsonschema/parser_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package jsonschema

import (
"fmt"
"strconv"
"strings"
"testing"

"github.com/go-faster/errors"
yaml "github.com/go-faster/yamlx"
"github.com/stretchr/testify/require"

"github.com/ogen-go/ogen/internal/jsonpointer"
"github.com/ogen-go/ogen/internal/location"
)

type components map[string]*RawSchema
Expand Down Expand Up @@ -345,6 +348,46 @@ func TestSchemaReferencedArray(t *testing.T) {
require.Equal(t, expect, out)
}

func TestSchemaExtensions(t *testing.T) {
tests := []struct {
raw string
expect *Schema
expectErr bool
}{
{
`{"type": "string", "x-ogen-name": "foo"}`,
&Schema{
Type: String,
XOgenName: "foo",
},
false,
},
{`{"type": "string", "x-ogen-name": {}}`, nil, true},
}

for i, tt := range tests {
tt := tt
t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
a := require.New(t)

var raw RawSchema
a.NoError(yaml.Unmarshal([]byte(tt.raw), &raw))

out, err := NewParser(Settings{
Filename: "test.yaml",
}).Parse(&raw)
if tt.expectErr {
a.Error(err)
return
}
a.NoError(err)
// Zero locator to simplify comparison.
out.Locator = location.Locator{}
a.Equal(tt.expect, out)
})
}
}

func TestInvalidMultipleOf(t *testing.T) {
values := []int{0, -1, -10}
parser := NewParser(Settings{
Expand Down
Loading

0 comments on commit 3dfa9ef

Please sign in to comment.