Skip to content

Commit 9db8e36

Browse files
committed
fix(jsonSchema): fix resolving $ref and $dynamicRef
1 parent 8e6c8a3 commit 9db8e36

8 files changed

Lines changed: 318 additions & 140 deletions

File tree

config/dynamic/asyncApi/parsing_test.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ package asyncApi_test
22

33
import (
44
"fmt"
5-
"github.com/stretchr/testify/require"
65
"mokapi/config/dynamic"
76
"mokapi/config/dynamic/asyncApi"
87
"mokapi/providers/asyncapi3"
98
"mokapi/schema/json/schema"
109
"net/url"
1110
"testing"
11+
12+
"github.com/stretchr/testify/require"
1213
)
1314

1415
type readFunc func(cfg *dynamic.Config) error
@@ -444,21 +445,19 @@ func TestSchema(t *testing.T) {
444445
t.Run("modify file reference direct", func(t *testing.T) {
445446
target := &schema.Schema{}
446447
message.Payload = &asyncapi3.SchemaRef{Value: &asyncapi3.MultiSchemaFormat{Schema: &schema.Schema{Ref: "foo.yml"}}}
447-
var fooConfig *dynamic.Config
448448
reader := &testReader{readFunc: func(file *dynamic.Config) error {
449-
file.Data = &schema.Schema{}
450-
fooConfig = file
449+
file.Data = target
451450
return nil
452451
}}
453452

454453
err := config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader)
455454
require.NoError(t, err)
456455

457456
// modify
458-
fooConfig.Data = target
459-
err = fooConfig.Data.(dynamic.Parser).Parse(fooConfig, reader)
457+
target = &schema.Schema{Description: "TARGET"}
458+
err = config.Parse(&dynamic.Config{Info: dynamic.ConfigInfo{Url: &url.URL{}}, Data: config}, reader)
460459

461460
require.NoError(t, err)
462-
require.Equal(t, target, message.Payload.Value.Schema.(*schema.Schema))
461+
require.Equal(t, "TARGET", message.Payload.Value.Schema.(*schema.Schema).Description)
463462
})
464463
}

config/dynamic/config.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type Config struct {
4747
Listeners Listeners
4848
Scope Scope
4949
SourceType SourceType
50+
resolving map[string]bool
5051
}
5152

5253
type Refs struct {
@@ -229,3 +230,19 @@ func (c *Config) CloseScope() {
229230
func (e Event) String() string {
230231
return EventText[e]
231232
}
233+
234+
func (c *Config) EnterRef(ref string) bool {
235+
if c.resolving == nil {
236+
c.resolving = map[string]bool{}
237+
}
238+
239+
if c.resolving[ref] {
240+
return false
241+
}
242+
c.resolving[ref] = true
243+
return true
244+
}
245+
246+
func (c *Config) LeaveRef(ref string) {
247+
delete(c.resolving, ref)
248+
}

config/dynamic/resolve.go

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ package dynamic
22

33
import (
44
"fmt"
5-
log "github.com/sirupsen/logrus"
65
"mokapi/sortedmap"
76
"net/url"
87
"path/filepath"
98
"reflect"
109
"strings"
10+
11+
log "github.com/sirupsen/logrus"
1112
)
1213

1314
type PathResolver interface {
@@ -22,18 +23,44 @@ func Resolve(ref string, element interface{}, config *Config, reader Reader) err
2223
var err error
2324

2425
fragment := ref[1:]
26+
isLocal := true
27+
parent := config
2528
if !strings.HasPrefix(ref, "#") {
2629
fragment, config, err = resolveResource(ref, element, config, reader)
2730
if err != nil {
2831
return fmt.Errorf("resolve reference '%v' failed: %w", ref, err)
2932
}
33+
isLocal = false
3034
}
3135

3236
err = resolveFragment(fragment, element, config, false)
3337

3438
if err != nil {
3539
return fmt.Errorf("resolve reference '%v' failed: %w", ref, err)
3640
}
41+
42+
// Parse the referenced schema again in the current context.
43+
// This ensures nested $ref and $dynamicRef are resolved relative
44+
// to the correct dynamic scope.
45+
// element is **struct
46+
p, ok := reflect.ValueOf(element).Elem().Interface().(Parser)
47+
if ok {
48+
if !isLocal {
49+
// set parent scope hierarchy
50+
config = &Config{Raw: config.Raw, Data: copyData(config.Data), Info: config.Info}
51+
config.Scope.SetParent(parent.Scope)
52+
}
53+
if !config.EnterRef(ref) {
54+
return nil
55+
}
56+
defer config.LeaveRef(ref)
57+
58+
err = p.Parse(config, reader)
59+
if err != nil {
60+
return fmt.Errorf("resolve reference '%v' failed: %w", ref, err)
61+
}
62+
}
63+
3764
return nil
3865
}
3966

@@ -239,15 +266,6 @@ func resolveResource(ref string, element interface{}, config *Config, reader Rea
239266
sub, err := reader.Read(removeFragment(u), data)
240267
if err == nil {
241268
AddRef(config, sub)
242-
if _, ok := sub.Data.(Parser); ok && len(sub.Raw) > 0 {
243-
// parse again with parent scope hierarchy
244-
sub = &Config{Raw: sub.Raw, Data: copyData(sub.Data), Info: sub.Info}
245-
sub.Scope.SetParent(config.Scope)
246-
err = Parse(sub, reader)
247-
if err != nil {
248-
return "", nil, err
249-
}
250-
}
251269
}
252270
return u.Fragment, sub, err
253271
}

providers/openapi/schema/schema.go

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -149,37 +149,47 @@ func (p *schemaParser) parse(s *Schema) error {
149149
}
150150
}
151151

152-
if err := p.parse(s.Items); err != nil {
153-
return err
152+
if !s.skipParse("items") {
153+
if err := p.parse(s.Items); err != nil {
154+
return err
155+
}
154156
}
155157

156-
if s.Properties != nil {
158+
if s.Properties != nil && !s.skipParse("properties") {
157159
for it := s.Properties.Iter(); it.Next(); {
158160
if err := p.parse(it.Value()); err != nil {
159161
return fmt.Errorf("parse schema '%v' failed: %w", it.Key(), err)
160162
}
161163
}
162164
}
163165

164-
if err := p.parse(s.AdditionalProperties); err != nil {
165-
return err
166+
if !s.skipParse("additionalProperties") {
167+
if err := p.parse(s.AdditionalProperties); err != nil {
168+
return err
169+
}
166170
}
167171

168-
for _, r := range s.AnyOf {
169-
if err := p.parse(r); err != nil {
170-
return err
172+
if !s.skipParse("anyOf") {
173+
for _, r := range s.AnyOf {
174+
if err := p.parse(r); err != nil {
175+
return err
176+
}
171177
}
172178
}
173179

174-
for _, r := range s.AllOf {
175-
if err := p.parse(r); err != nil {
176-
return err
180+
if !s.skipParse("allOf") {
181+
for _, r := range s.AllOf {
182+
if err := p.parse(r); err != nil {
183+
return err
184+
}
177185
}
178186
}
179187

180-
for _, r := range s.OneOf {
181-
if err := p.parse(r); err != nil {
182-
return err
188+
if !s.skipParse("oneOf") {
189+
for _, r := range s.OneOf {
190+
if err := p.parse(r); err != nil {
191+
return err
192+
}
183193
}
184194
}
185195

@@ -189,19 +199,6 @@ func (p *schemaParser) parse(s *Schema) error {
189199
return err
190200
}
191201

192-
scope := s.Ref
193-
if s.Sub.Id != "" {
194-
scope = s.Sub.Id
195-
}
196-
p.config.OpenScope(scope)
197-
defer p.config.CloseScope()
198-
199-
// Parse the referenced schema again in the current context.
200-
// This ensures nested $ref and $dynamicRef are resolved relative
201-
// to the correct dynamic scope.
202-
if err = p.parse(s.Sub); err != nil {
203-
return err
204-
}
205202
// Apply the resolved schema as an overlay onto the current schema.
206203
// The referenced schema is cloned to preserve the immutability of
207204
// the parsed schema graph. Dynamic references may resolve differently
@@ -428,3 +425,11 @@ func (s *Schema) UnmarshalYAML(node *yaml.Node) error {
428425
*s = Schema(a)
429426
return nil
430427
}
428+
429+
func (s *Schema) skipParse(name string) bool {
430+
if s.Ref != "" {
431+
_, ok := s.m[name]
432+
return !ok
433+
}
434+
return false
435+
}

providers/openapi/schema/schema_parse_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,36 @@ func TestJson_Structuring(t *testing.T) {
151151
require.Equal(t, "string", s.Properties.Get("first_name").Type.String())
152152
},
153153
},
154+
{
155+
name: "$ref using in referenced file",
156+
test: func(t *testing.T) {
157+
reader := &dynamictest.Reader{
158+
Data: map[string]*dynamic.Config{
159+
"/bar.json": {
160+
Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("/bar.json")),
161+
Raw: []byte(`{
162+
"$defs": { "items": { "type": "integer" } },
163+
"type": "array",
164+
"items": { "$ref": "#/$defs/items" }
165+
}`),
166+
},
167+
},
168+
}
169+
170+
foo := &dynamic.Config{
171+
Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("/foo.json")),
172+
Data: &schema.Schema{
173+
Ref: "/bar.json",
174+
},
175+
}
176+
177+
err := foo.Data.(*schema.Schema).Parse(foo, reader)
178+
require.NoError(t, err)
179+
180+
require.NoError(t, err)
181+
require.Equal(t, "integer", foo.Data.(*schema.Schema).Items.Type.String())
182+
},
183+
},
154184
{
155185
name: "recursion",
156186
test: func(t *testing.T) {
@@ -224,6 +254,40 @@ $ref: '#/$defs/a'
224254
require.Equal(t, "string", person.Data.(*schema.Schema).Items.Type.String())
225255
},
226256
},
257+
{
258+
name: "parsing twice with $refs",
259+
test: func(t *testing.T) {
260+
reader := &dynamictest.Reader{
261+
Data: map[string]*dynamic.Config{
262+
"https://example.com/schemas/foo": {
263+
Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("https://example.com/schemas/foo")),
264+
Raw: []byte(`{
265+
"$defs": { "items": { "type": "integer" } },
266+
"type": "array",
267+
"items": { "$ref": "#/$defs/items" },
268+
}`),
269+
},
270+
},
271+
}
272+
273+
person := &dynamic.Config{
274+
Info: dynamictest.NewConfigInfo(dynamictest.WithUrl("https://example.com/schemas/bar")),
275+
Data: &schema.Schema{
276+
Ref: "https://example.com/schemas/foo",
277+
},
278+
}
279+
280+
// 1. parsing
281+
err := person.Data.(*schema.Schema).Parse(person, reader)
282+
require.NoError(t, err)
283+
284+
// 2. parsing
285+
err = person.Data.(*schema.Schema).Parse(person, reader)
286+
287+
require.NoError(t, err)
288+
require.Equal(t, "integer", person.Data.(*schema.Schema).Items.Type.String())
289+
},
290+
},
227291
}
228292

229293
t.Parallel()

0 commit comments

Comments
 (0)