Skip to content

Commit 885ea5c

Browse files
chore: fix inconsistent handling of embedded maps and validation of 3.0 schemas to be forward compatible
1 parent 34d01b0 commit 885ea5c

File tree

16 files changed

+105
-55
lines changed

16 files changed

+105
-55
lines changed

extensions/extensions.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@ func (e *Extensions) Init() {
5656
e.Map = sequencedmap.New[string, Extension]()
5757
}
5858

59+
// Len returns the number of elements in the extensions map. nil safe.
60+
func (e *Extensions) Len() int {
61+
if e == nil || e.Map == nil {
62+
return 0
63+
}
64+
65+
return e.Map.Len()
66+
}
67+
5968
func (e *Extensions) SetCore(core any) {
6069
c, ok := core.(*sequencedmap.Map[string, marshaller.Node[*yaml.Node]])
6170
if !ok {

hashing/hashing_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func TestHash(t *testing.T) {
134134
{
135135
name: "model with embedded map",
136136
v: &tests.TestEmbeddedMapWithFieldsHighModel{
137-
Map: *sequencedmap.New(sequencedmap.NewElem("hello", &tests.TestPrimitiveHighModel{
137+
Map: sequencedmap.New(sequencedmap.NewElem("hello", &tests.TestPrimitiveHighModel{
138138
StringField: "world",
139139
})),
140140
NameField: "some name",

jsonpointer/map_index_key_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ func TestEmbeddedMapModel_Success(t *testing.T) {
184184
embeddedMap.Set("data", "some data")
185185

186186
model := &tests.TestEmbeddedMapHighModel{
187-
Map: *embeddedMap,
187+
Map: embeddedMap,
188188
}
189189

190190
tests := []struct {

jsonpointer/models_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ func TestNavigateModel_EmbeddedMap(t *testing.T) {
169169
t.Parallel()
170170
// Create a simple embedded map model
171171
embeddedMap := &tests.TestEmbeddedMapHighModel{}
172-
embeddedMap.Map = *sequencedmap.New[string, string]()
172+
embeddedMap.Map = sequencedmap.New[string, string]()
173173
embeddedMap.Set("key1", "value1")
174174
embeddedMap.Set("key2", "value2")
175175

@@ -199,7 +199,7 @@ func TestNavigateModel_EmbeddedMap(t *testing.T) {
199199
embeddedMapWithFields := &tests.TestEmbeddedMapWithFieldsHighModel{
200200
NameField: "test name",
201201
}
202-
embeddedMapWithFields.Map = *sequencedmap.New[string, *tests.TestPrimitiveHighModel]()
202+
embeddedMapWithFields.Map = sequencedmap.New[string, *tests.TestPrimitiveHighModel]()
203203
embeddedMapWithFields.Set("model1", nestedModel1)
204204
embeddedMapWithFields.Set("model2", nestedModel2)
205205

@@ -230,7 +230,7 @@ func TestNavigateModel_EmbeddedMap(t *testing.T) {
230230
t.Run("EmbeddedMapNotFound", func(t *testing.T) {
231231
t.Parallel()
232232
embeddedMap := &tests.TestEmbeddedMapHighModel{}
233-
embeddedMap.Map = *sequencedmap.New[string, string]()
233+
embeddedMap.Map = sequencedmap.New[string, string]()
234234
embeddedMap.Set("existing", "value")
235235

236236
// Test navigating to non-existent key in embedded map
@@ -248,7 +248,7 @@ func TestNavigateModel_EmbeddedMapEscapedKeys(t *testing.T) {
248248
// Create a test that mimics OpenAPI paths structure
249249
// This reproduces the issue with escaped JSON pointer paths like /paths/~1users~1{userId}
250250
embeddedMap := &tests.TestEmbeddedMapHighModel{}
251-
embeddedMap.Map = *sequencedmap.New[string, string]()
251+
embeddedMap.Map = sequencedmap.New[string, string]()
252252

253253
// Set keys that contain special characters like OpenAPI paths
254254
embeddedMap.Set("/users/{userId}", "path-item-1")
@@ -298,7 +298,7 @@ func TestNavigateModel_EmbeddedMapComparison_PointerVsValue(t *testing.T) {
298298
t.Parallel()
299299
// Create a model with value embedded map
300300
model := &tests.TestEmbeddedMapHighModel{}
301-
model.Map = *sequencedmap.New[string, string]()
301+
model.Map = sequencedmap.New[string, string]()
302302
model.Set("valKey1", "value value1")
303303
model.Set("valKey2", "value value2")
304304

@@ -320,7 +320,7 @@ func TestNavigateModel_EmbeddedMapComparison_PointerVsValue(t *testing.T) {
320320
ptrModel.Set("sharedKey", "shared value")
321321

322322
valueModel := &tests.TestEmbeddedMapHighModel{}
323-
valueModel.Map = *sequencedmap.New[string, string]()
323+
valueModel.Map = sequencedmap.New[string, string]()
324324
valueModel.Set("sharedKey", "shared value")
325325

326326
// Both should navigate to the same result
@@ -353,7 +353,7 @@ func TestNavigateModel_EmbeddedMapComparison_PointerVsValue(t *testing.T) {
353353
valueModel := &tests.TestEmbeddedMapWithFieldsHighModel{
354354
NameField: "value test name",
355355
}
356-
valueModel.Map = *sequencedmap.New[string, *tests.TestPrimitiveHighModel]()
356+
valueModel.Map = sequencedmap.New[string, *tests.TestPrimitiveHighModel]()
357357
valueModel.Set("nested", nestedModel)
358358

359359
// Test navigating to regular fields

jsonschema/oas3/schema30.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@
129129
"type": "string"
130130
},
131131
"default": {},
132+
"const": {
133+
"description": "Speakeasy addition to 3.0 schema to allow forward compatibility with OpenAPI 3.1"
134+
},
132135
"nullable": {
133136
"type": "boolean",
134137
"default": false

marshaller/empty_map_marshal_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func TestMarshal_TestEmbeddedMapModel_Empty_Success(t *testing.T) {
3333
name: "initialized empty embedded map should render as empty object",
3434
setup: func() *tests.TestEmbeddedMapHighModel {
3535
model := &tests.TestEmbeddedMapHighModel{}
36-
model.Map = *sequencedmap.New[string, string]()
36+
model.Map = sequencedmap.New[string, string]()
3737
return model
3838
},
3939
expected: "{}\n",
@@ -42,7 +42,7 @@ func TestMarshal_TestEmbeddedMapModel_Empty_Success(t *testing.T) {
4242
name: "embedded map with content should render normally",
4343
setup: func() *tests.TestEmbeddedMapHighModel {
4444
model := &tests.TestEmbeddedMapHighModel{}
45-
model.Map = *sequencedmap.New[string, string]()
45+
model.Map = sequencedmap.New[string, string]()
4646
model.Set("key1", "value1")
4747
return model
4848
},
@@ -87,7 +87,7 @@ func TestMarshal_TestEmbeddedMapWithFieldsModel_Empty_Success(t *testing.T) {
8787
name: "initialized empty embedded map with fields should render fields only",
8888
setup: func() *tests.TestEmbeddedMapWithFieldsHighModel {
8989
model := &tests.TestEmbeddedMapWithFieldsHighModel{}
90-
model.Map = *sequencedmap.New[string, *tests.TestPrimitiveHighModel]()
90+
model.Map = sequencedmap.New[string, *tests.TestPrimitiveHighModel]()
9191
model.NameField = "test name"
9292
return model
9393
},
@@ -97,7 +97,7 @@ func TestMarshal_TestEmbeddedMapWithFieldsModel_Empty_Success(t *testing.T) {
9797
name: "embedded map with content and fields should render both",
9898
setup: func() *tests.TestEmbeddedMapWithFieldsHighModel {
9999
model := &tests.TestEmbeddedMapWithFieldsHighModel{}
100-
model.Map = *sequencedmap.New[string, *tests.TestPrimitiveHighModel]()
100+
model.Map = sequencedmap.New[string, *tests.TestPrimitiveHighModel]()
101101
model.NameField = "test name"
102102
model.Set("key1", &tests.TestPrimitiveHighModel{
103103
StringField: "value1",

marshaller/sequencedmap.go

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -122,21 +122,24 @@ func populateSequencedMap(source any, target interfaces.SequencedMapInterface, p
122122
var sm interfaces.SequencedMapInterface
123123
var ok bool
124124

125-
// Handle both pointer and non-pointer cases
126-
switch {
127-
case sourceValue.Kind() == reflect.Ptr:
128-
// Source is already a pointer
129-
sm, ok = source.(interfaces.SequencedMapInterface)
130-
case sourceValue.CanAddr():
131-
// Source is addressable, get a pointer to it
125+
// Handle pointer embeds: dereference until we get to the actual map
126+
for sourceValue.Kind() == reflect.Ptr {
127+
if sourceValue.IsNil() {
128+
return nil
129+
}
130+
sourceValue = sourceValue.Elem()
131+
}
132+
133+
// Now try to get the SequencedMapInterface
134+
if sourceValue.CanAddr() {
132135
sm, ok = sourceValue.Addr().Interface().(interfaces.SequencedMapInterface)
133-
default:
134-
// Source is neither a pointer nor addressable
135-
return fmt.Errorf("expected source to be addressable or a pointer to SequencedMap, got %s", sourceValue.Type())
136+
} else {
137+
// Try direct interface conversion as fallback
138+
sm, ok = sourceValue.Interface().(interfaces.SequencedMapInterface)
136139
}
137140

138141
if !ok {
139-
return fmt.Errorf("expected source to be SequencedMap, got %s", sourceValue.Type())
142+
return fmt.Errorf("expected source to be SequencedMap, got %s", reflect.TypeOf(source))
140143
}
141144

142145
target.Init()

marshaller/syncing_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ func TestSync_EmbeddedMapWithFields_Success(t *testing.T) {
327327
}
328328

329329
// Initialize the embedded map
330-
highModel.Map = *sequencedmap.New[string, *tests.TestPrimitiveHighModel]()
330+
highModel.Map = sequencedmap.New[string, *tests.TestPrimitiveHighModel]()
331331
highModel.Set("syncKey1", dynamicVal1)
332332
highModel.Set("syncKey2", dynamicVal2)
333333

@@ -389,7 +389,7 @@ func TestSync_EmbeddedMap_Success(t *testing.T) {
389389
highModel := tests.TestEmbeddedMapHighModel{}
390390

391391
// Initialize the embedded map
392-
highModel.Map = *sequencedmap.New[string, string]()
392+
highModel.Map = sequencedmap.New[string, string]()
393393
highModel.Set("syncKey1", "synced value1")
394394
highModel.Set("syncKey2", "synced value2")
395395
highModel.Set("syncKey3", "synced value3")
@@ -704,7 +704,7 @@ func TestSync_TypeConversionModel_Success(t *testing.T) {
704704
}
705705

706706
// Initialize the embedded map with HTTPMethod keys
707-
highModel.Map = *sequencedmap.New[tests.HTTPMethod, *tests.TestPrimitiveHighModel]()
707+
highModel.Map = sequencedmap.New[tests.HTTPMethod, *tests.TestPrimitiveHighModel]()
708708
highModel.Set(tests.HTTPMethodPost, postOp)
709709
highModel.Set(tests.HTTPMethodGet, getOp)
710710
highModel.Set(tests.HTTPMethodPut, putOp)
@@ -1097,7 +1097,7 @@ func TestSync_EmbeddedMapComparison_PointerVsValue_Success(t *testing.T) {
10971097
t.Parallel()
10981098
// Test value embedded map
10991099
valueModel := tests.TestEmbeddedMapHighModel{}
1100-
valueModel.Map = *sequencedmap.New[string, string]()
1100+
valueModel.Map = sequencedmap.New[string, string]()
11011101
valueModel.Set("key1", "val_value1")
11021102
valueModel.Set("key2", "val_value2")
11031103

@@ -1122,7 +1122,7 @@ func TestSync_EmbeddedMapComparison_PointerVsValue_Success(t *testing.T) {
11221122
ptrModel.Set("shared_key", "shared_value")
11231123

11241124
valueModel := tests.TestEmbeddedMapHighModel{}
1125-
valueModel.Map = *sequencedmap.New[string, string]()
1125+
valueModel.Map = sequencedmap.New[string, string]()
11261126
valueModel.Set("shared_key", "shared_value")
11271127

11281128
// Sync both models

marshaller/tests/core/models.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,13 @@ type TestComplexModel struct {
5959
type TestEmbeddedMapModel struct {
6060
marshaller.CoreModel `model:"testEmbeddedMapModel"`
6161

62-
sequencedmap.Map[string, marshaller.Node[string]]
62+
*sequencedmap.Map[string, marshaller.Node[string]]
6363
}
6464

6565
// TestEmbeddedMapWithFieldsModel covers embedded sequenced map with additional fields
6666
type TestEmbeddedMapWithFieldsModel struct {
6767
marshaller.CoreModel `model:"testEmbeddedMapWithFieldsModel"`
68-
sequencedmap.Map[string, marshaller.Node[*TestPrimitiveModel]]
68+
*sequencedmap.Map[string, marshaller.Node[*TestPrimitiveModel]]
6969

7070
NameField marshaller.Node[string] `key:"name"`
7171
Extensions core.Extensions `key:"extensions"`
@@ -74,7 +74,7 @@ type TestEmbeddedMapWithFieldsModel struct {
7474
// TestEmbeddedMapWithExtensionsModel covers embedded sequenced map with extensions only
7575
type TestEmbeddedMapWithExtensionsModel struct {
7676
marshaller.CoreModel `model:"testEmbeddedMapWithExtensionsModel"`
77-
sequencedmap.Map[string, marshaller.Node[string]]
77+
*sequencedmap.Map[string, marshaller.Node[string]]
7878

7979
Extensions core.Extensions `key:"extensions"`
8080
}
@@ -179,7 +179,7 @@ type TestRequiredNilableModel struct {
179179
type TestTypeConversionCoreModel struct {
180180
marshaller.CoreModel `model:"testTypeConversionCoreModel"`
181181

182-
sequencedmap.Map[string, marshaller.Node[*TestPrimitiveModel]]
182+
*sequencedmap.Map[string, marshaller.Node[*TestPrimitiveModel]]
183183
HTTPMethodField marshaller.Node[*string] `key:"httpMethodField"`
184184
Extensions core.Extensions `key:"extensions"`
185185
}

marshaller/tests/models.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ type TestComplexHighModel struct {
4444

4545
type TestEmbeddedMapHighModel struct {
4646
marshaller.Model[core.TestEmbeddedMapModel]
47-
sequencedmap.Map[string, string]
47+
*sequencedmap.Map[string, string]
4848
}
4949

5050
type TestEmbeddedMapWithFieldsHighModel struct {
5151
marshaller.Model[core.TestEmbeddedMapWithFieldsModel]
52-
sequencedmap.Map[string, *TestPrimitiveHighModel]
52+
*sequencedmap.Map[string, *TestPrimitiveHighModel]
5353
NameField string
5454
Extensions *extensions.Extensions
5555
}
@@ -108,7 +108,7 @@ const (
108108
// This reproduces the issue where high-level model expects HTTPMethod keys but core provides string keys
109109
type TestTypeConversionHighModel struct {
110110
marshaller.Model[core.TestTypeConversionCoreModel]
111-
sequencedmap.Map[HTTPMethod, *TestPrimitiveHighModel]
111+
*sequencedmap.Map[HTTPMethod, *TestPrimitiveHighModel]
112112
HTTPMethodField *HTTPMethod
113113
Extensions *extensions.Extensions
114114
}

0 commit comments

Comments
 (0)