Skip to content

Commit caa3857

Browse files
committed
Addressed #725, node_builder now performs some more detailed checks using reflection when handling empty nodes that need to remain empty.
1 parent a946f44 commit caa3857

File tree

4 files changed

+153
-6
lines changed

4 files changed

+153
-6
lines changed

bundler/bundler_composer_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"github.com/pb33f/libopenapi"
1717
"github.com/pb33f/libopenapi/datamodel"
18+
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
1819
"github.com/pb33f/libopenapi/index"
1920
"github.com/stretchr/testify/assert"
2021
"github.com/stretchr/testify/require"
@@ -754,3 +755,104 @@ oneOf:
754755

755756
runtime.GC()
756757
}
758+
759+
const emptyDefaultServerSpec = `openapi: 3.0.0
760+
info:
761+
title: defaults
762+
version: 1.0.0
763+
servers:
764+
- url: https://{env}.example.com
765+
variables:
766+
env:
767+
default: ""
768+
description: environment host
769+
- url: https://{shard}.example.com
770+
variables:
771+
shard:
772+
description: shard id
773+
default: ""
774+
- url: https://{slot}.example.com
775+
variables:
776+
slot:
777+
default: ""
778+
paths: {}`
779+
780+
func TestBundleBytesComposed_PreservesEmptyServerVariableDefaults(t *testing.T) {
781+
spec := emptyDefaultServerSpec
782+
783+
tmp := t.TempDir()
784+
require.NoError(t, os.WriteFile(filepath.Join(tmp, "main.yaml"), []byte(spec), 0o644))
785+
786+
data, err := os.ReadFile(filepath.Join(tmp, "main.yaml"))
787+
require.NoError(t, err)
788+
789+
bundled, err := BundleBytesComposed(data, &datamodel.DocumentConfiguration{
790+
BasePath: tmp,
791+
}, nil)
792+
require.NoError(t, err)
793+
794+
doc, err := libopenapi.NewDocument(bundled)
795+
require.NoError(t, err)
796+
797+
model, err := doc.BuildV3Model()
798+
require.NoError(t, err)
799+
800+
var envVar, shardVar *v3.ServerVariable
801+
for _, srv := range model.Model.Servers {
802+
if srv == nil || srv.Variables == nil {
803+
continue
804+
}
805+
if candidate := srv.Variables.GetOrZero("env"); candidate != nil {
806+
envVar = candidate
807+
}
808+
if candidate := srv.Variables.GetOrZero("shard"); candidate != nil {
809+
shardVar = candidate
810+
}
811+
}
812+
813+
require.NotNil(t, envVar, "env variable must exist")
814+
assert.Equal(t, "", envVar.Default)
815+
assert.False(t, envVar.GoLow().Default.IsEmpty())
816+
assert.Equal(t, "environment host", envVar.Description)
817+
818+
require.NotNil(t, shardVar, "shard variable must exist")
819+
assert.Equal(t, "", shardVar.Default)
820+
assert.False(t, shardVar.GoLow().Default.IsEmpty())
821+
assert.Equal(t, "shard id", shardVar.Description)
822+
823+
slotVar := model.Model.Servers[2].Variables.GetOrZero("slot")
824+
require.NotNil(t, slotVar, "slot variable must exist")
825+
assert.Equal(t, "", slotVar.Default)
826+
assert.False(t, slotVar.GoLow().Default.IsEmpty())
827+
assert.Equal(t, "", slotVar.Description)
828+
}
829+
830+
func TestBundleBytes_PreservesEmptyServerVariableDefaults(t *testing.T) {
831+
spec := []byte(emptyDefaultServerSpec)
832+
833+
bundled, err := BundleBytes(spec, &datamodel.DocumentConfiguration{})
834+
require.NoError(t, err)
835+
836+
doc, err := libopenapi.NewDocument(bundled)
837+
require.NoError(t, err)
838+
839+
model, err := doc.BuildV3Model()
840+
require.NoError(t, err)
841+
842+
require.Len(t, model.Model.Servers, 3)
843+
844+
envVar := model.Model.Servers[0].Variables.GetOrZero("env")
845+
require.NotNil(t, envVar)
846+
assert.Equal(t, "", envVar.Default)
847+
assert.False(t, envVar.GoLow().Default.IsEmpty())
848+
849+
shardVar := model.Model.Servers[1].Variables.GetOrZero("shard")
850+
require.NotNil(t, shardVar)
851+
assert.Equal(t, "", shardVar.Default)
852+
assert.False(t, shardVar.GoLow().Default.IsEmpty())
853+
854+
slotVar := model.Model.Servers[2].Variables.GetOrZero("slot")
855+
require.NotNil(t, slotVar)
856+
assert.Equal(t, "", slotVar.Default)
857+
assert.False(t, slotVar.GoLow().Default.IsEmpty())
858+
}

datamodel/high/node_builder.go

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,31 @@ func (n *NodeBuilder) add(key string, i int) {
5959
return
6060
}
6161

62+
var (
63+
lowFieldValue reflect.Value
64+
lowFieldValid bool
65+
)
66+
67+
if n.Low != nil && !reflect.ValueOf(n.Low).IsZero() {
68+
low := reflect.ValueOf(n.Low)
69+
if low.Kind() == reflect.Ptr && !low.IsNil() {
70+
elem := low.Elem()
71+
if elem.IsValid() {
72+
field := elem.FieldByName(key)
73+
if field.IsValid() {
74+
lowFieldValue = field
75+
lowFieldValid = true
76+
}
77+
}
78+
} else if low.IsValid() {
79+
field := low.FieldByName(key)
80+
if field.IsValid() {
81+
lowFieldValue = field
82+
lowFieldValid = true
83+
}
84+
}
85+
}
86+
6287
// if the key is 'Extensions' then we need to extract the keys from the map
6388
// and add them to the node builder.
6489
if key == "Extensions" {
@@ -137,6 +162,27 @@ func (n *NodeBuilder) add(key string, i int) {
137162
}
138163
}
139164
}
165+
166+
if isZero && lowFieldValid {
167+
var lowInterface any
168+
if lowFieldValue.Kind() == reflect.Ptr {
169+
if !lowFieldValue.IsNil() {
170+
lowInterface = lowFieldValue.Elem().Interface()
171+
}
172+
} else {
173+
lowInterface = lowFieldValue.Interface()
174+
}
175+
if lowInterface != nil {
176+
if emptier, ok := lowInterface.(interface{ IsEmpty() bool }); ok && !emptier.IsEmpty() {
177+
isZero = false
178+
} else if nodeGetter, ok := lowInterface.(interface{ GetValueNode() *yaml.Node }); ok {
179+
if node := nodeGetter.GetValueNode(); node != nil {
180+
isZero = false
181+
}
182+
}
183+
}
184+
}
185+
140186
if !renderZeroFlag && isZero || omitEmptyFlag && isZero {
141187
return
142188
}
@@ -180,8 +226,7 @@ func (n *NodeBuilder) add(key string, i int) {
180226
// so skip and default to 0, which means a new entry to the spec.
181227
// this will place new content and the top of the rendered object.
182228
if n.Low != nil && !reflect.ValueOf(n.Low).IsZero() {
183-
lowFieldValue := reflect.ValueOf(n.Low).Elem().FieldByName(key)
184-
if lowFieldValue.IsValid() {
229+
if lowFieldValid {
185230
fLow := lowFieldValue.Interface()
186231
value = reflect.ValueOf(fLow)
187232

document_examples_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,5 +639,5 @@ func ExampleNewDocument_modifyAndReRender() {
639639
fmt.Printf("There were %d original paths. There are now %d paths in the document\n", originalPaths, newPaths)
640640
fmt.Printf("The new spec has %d bytes\n", len(rawBytes))
641641
// Output: There were 13 original paths. There are now 14 paths in the document
642-
// The new spec has 31213 bytes
642+
// The new spec has 31406 bytes
643643
}

document_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ func TestDocument_RenderAndReload_ChangeCheck_Stripe(t *testing.T) {
247247
// get flat list of changes.
248248
flatChanges := compReport.GetAllChanges()
249249

250-
// remove everything that is a description change (stripe has a lot of those from having 519 empty descriptions)
250+
// remove everything that is a description change
251251
var filtered []*model.Change
252252
for i := range flatChanges {
253253
if flatChanges[i].Property != "description" {
@@ -259,9 +259,9 @@ func TestDocument_RenderAndReload_ChangeCheck_Stripe(t *testing.T) {
259259
tc := compReport.TotalChanges()
260260
bc := compReport.TotalBreakingChanges()
261261
assert.Equal(t, 0, bc)
262-
assert.Equal(t, 819, tc)
262+
assert.Equal(t, 9, tc)
263263

264-
// there should be no other changes than the 519 descriptions.
264+
// there should be no other changes besides descriptions.
265265
assert.Equal(t, 0, len(filtered))
266266
done <- struct{}{}
267267
}()

0 commit comments

Comments
 (0)