Skip to content

Commit 5105b6a

Browse files
committed
Added ChangeIsReferenced interface
allows us to know if a change came from a referenced object.
1 parent 30f67cd commit 5105b6a

File tree

3 files changed

+184
-1
lines changed

3 files changed

+184
-1
lines changed

what-changed/model/change_types.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ type ChangeContext struct {
4545
NewColumn *int `json:"newColumn,omitempty" yaml:"newColumn,omitempty"`
4646
}
4747

48+
type ChangeIsReferenced interface {
49+
GetChangeReference() string
50+
SetChangeReference(ref string)
51+
}
52+
4853
// HasChanged determines if the line and column numbers of the original and new values have changed.
4954
//
5055
// It's worth noting that there is no guarantee to the positions of anything in either left or right, so
@@ -98,6 +103,9 @@ type Change struct {
98103

99104
// Path represents the path to the object that was changed (not used in the current implementation).
100105
Path string `json:"path,omitempty"`
106+
107+
// Reference is populated when the change is related to a $ref change.
108+
Reference string `json:"reference,omitempty"`
101109
}
102110

103111
// MarshalJSON is a custom JSON marshaller for the Change object.
@@ -145,9 +153,18 @@ func (c *Change) MarshalJSON() ([]byte, error) {
145153
// PropertyChanges holds a slice of Change pointers
146154
type PropertyChanges struct {
147155
RenderPropertiesOnly bool `json:"-" yaml:"-"`
156+
ChangeReference string `json:"changeReference,omitempty""`
148157
Changes []*Change `json:"changes,omitempty" yaml:"changes,omitempty"`
149158
}
150159

160+
func (p *PropertyChanges) SetChangeReference(ref string) {
161+
p.ChangeReference = ref
162+
}
163+
164+
func (p *PropertyChanges) GetChangeReference() string {
165+
return p.ChangeReference
166+
}
167+
151168
// TotalChanges returns the total number of property changes made.
152169
func (p *PropertyChanges) TotalChanges() int {
153170
if p == nil {

what-changed/model/comparison_functions.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ package model
55

66
import (
77
"fmt"
8-
"github.com/pb33f/libopenapi/datamodel/low/base"
98
"reflect"
109
"strings"
1110
"sync"
1211

12+
"github.com/pb33f/libopenapi/datamodel/low/base"
13+
1314
"github.com/pb33f/libopenapi/orderedmap"
1415
"github.com/pb33f/libopenapi/utils"
1516

@@ -24,6 +25,16 @@ const (
2425

2526
var changeMutex sync.Mutex
2627

28+
// SetReferenceIfExists checks if a low-level value has a reference and sets it on the change object
29+
// if the change object implements the ChangeIsReferenced interface.
30+
func SetReferenceIfExists[T any](value *low.ValueReference[T], changeObj any) {
31+
if value != nil && value.IsReference() {
32+
if refChange, ok := changeObj.(ChangeIsReferenced); ok {
33+
refChange.SetChangeReference(value.GetReference())
34+
}
35+
}
36+
}
37+
2738
func checkLocation(ctx *ChangeContext, hs base.HasIndex) bool {
2839
if !reflect.ValueOf(hs).IsNil() {
2940
idx := hs.GetIndex()
@@ -341,6 +352,9 @@ func CheckMapForChangesWithComp[T any, R any](expLeft, expRight *orderedmap.Map[
341352
// https://github.com/pb33f/libopenapi/issues/61
342353
if !reflect.ValueOf(&ch).Elem().IsZero() {
343354
expChanges[k] = ch
355+
var cr any = ch
356+
pVal := p[k]
357+
SetReferenceIfExists(&pVal, cr)
344358
}
345359
chLock.Unlock()
346360
}

what-changed/model/comparison_functions_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,3 +275,155 @@ func Test_checkLocation_sameIdx(t *testing.T) {
275275
assert.False(t, checkLocation(&ChangeContext{DocumentLocation: ""}, testHasIndex))
276276

277277
}
278+
279+
// TestSetReferenceIfExists tests the SetReferenceIfExists function
280+
func TestSetReferenceIfExists(t *testing.T) {
281+
tests := []struct {
282+
name string
283+
setupValue func() *low.ValueReference[string]
284+
setupChangeObj func() any
285+
expectRef string
286+
}{
287+
{
288+
name: "sets reference when value has reference and object implements interface",
289+
setupValue: func() *low.ValueReference[string] {
290+
val := low.ValueReference[string]{
291+
Value: "test",
292+
}
293+
val.SetReference("#/components/headers/TestHeader", nil)
294+
return &val
295+
},
296+
setupChangeObj: func() any {
297+
return &PropertyChanges{}
298+
},
299+
expectRef: "#/components/headers/TestHeader",
300+
},
301+
{
302+
name: "does not set reference when value has no reference",
303+
setupValue: func() *low.ValueReference[string] {
304+
val := low.ValueReference[string]{
305+
Value: "test",
306+
}
307+
return &val
308+
},
309+
setupChangeObj: func() any {
310+
return &PropertyChanges{}
311+
},
312+
expectRef: "",
313+
},
314+
{
315+
name: "handles nil value without panic",
316+
setupValue: func() *low.ValueReference[string] { return nil },
317+
setupChangeObj: func() any {
318+
return &PropertyChanges{}
319+
},
320+
expectRef: "",
321+
},
322+
{
323+
name: "handles non-ChangeIsReferenced object without panic",
324+
setupValue: func() *low.ValueReference[string] {
325+
val := low.ValueReference[string]{
326+
Value: "test",
327+
}
328+
val.SetReference("#/components/headers/TestHeader", nil)
329+
return &val
330+
},
331+
setupChangeObj: func() any {
332+
// Return something that doesn't implement ChangeIsReferenced
333+
return &struct{}{}
334+
},
335+
expectRef: "",
336+
},
337+
{
338+
name: "sets reference for remote reference",
339+
setupValue: func() *low.ValueReference[string] {
340+
val := low.ValueReference[string]{
341+
Value: "test",
342+
}
343+
val.SetReference("./schemas.yaml#/components/schemas/TestSchema", nil)
344+
return &val
345+
},
346+
setupChangeObj: func() any {
347+
return &PropertyChanges{}
348+
},
349+
expectRef: "./schemas.yaml#/components/schemas/TestSchema",
350+
},
351+
}
352+
353+
for _, tt := range tests {
354+
t.Run(tt.name, func(t *testing.T) {
355+
value := tt.setupValue()
356+
changeObj := tt.setupChangeObj()
357+
358+
// Call the function
359+
SetReferenceIfExists(value, changeObj)
360+
361+
// Check the result if it implements ChangeIsReferenced
362+
if refObj, ok := changeObj.(ChangeIsReferenced); ok {
363+
assert.Equal(t, tt.expectRef, refObj.GetChangeReference())
364+
}
365+
})
366+
}
367+
}
368+
369+
// TestSetReferenceIfExists_Integration tests the integration with actual comparison scenarios
370+
func TestSetReferenceIfExists_Integration(t *testing.T) {
371+
// Create a referenced value
372+
valueNode := &yaml.Node{
373+
Kind: yaml.ScalarNode,
374+
Value: "test-value",
375+
}
376+
377+
refValue := low.ValueReference[string]{
378+
Value: "test-value",
379+
ValueNode: valueNode,
380+
}
381+
refValue.SetReference("#/components/parameters/TestParam", nil)
382+
383+
// Test with HeaderChanges (which embeds PropertyChanges)
384+
t.Run("HeaderChanges with reference", func(t *testing.T) {
385+
headerChanges := &HeaderChanges{
386+
PropertyChanges: &PropertyChanges{},
387+
}
388+
389+
SetReferenceIfExists(&refValue, headerChanges)
390+
assert.Equal(t, "#/components/parameters/TestParam", headerChanges.GetChangeReference())
391+
})
392+
393+
// Test with SchemaChanges
394+
t.Run("SchemaChanges with reference", func(t *testing.T) {
395+
schemaChanges := &SchemaChanges{
396+
PropertyChanges: &PropertyChanges{},
397+
}
398+
399+
SetReferenceIfExists(&refValue, schemaChanges)
400+
assert.Equal(t, "#/components/parameters/TestParam", schemaChanges.GetChangeReference())
401+
})
402+
403+
// Test that it preserves existing references
404+
t.Run("preserves existing reference", func(t *testing.T) {
405+
changes := &PropertyChanges{
406+
ChangeReference: "#/existing/reference",
407+
}
408+
409+
// Create a value without reference
410+
nonRefValue := low.ValueReference[string]{
411+
Value: "test",
412+
}
413+
414+
SetReferenceIfExists(&nonRefValue, changes)
415+
// Should still have the existing reference
416+
assert.Equal(t, "#/existing/reference", changes.GetChangeReference())
417+
})
418+
419+
// Test overwriting reference
420+
t.Run("overwrites reference when new reference exists", func(t *testing.T) {
421+
changes := &PropertyChanges{
422+
ChangeReference: "#/old/reference",
423+
}
424+
425+
SetReferenceIfExists(&refValue, changes)
426+
// Should have the new reference
427+
assert.Equal(t, "#/components/parameters/TestParam", changes.GetChangeReference())
428+
})
429+
}

0 commit comments

Comments
 (0)