Skip to content

Commit 9442e5e

Browse files
committed
Adds full support for Overlays! #125
Overlays can now be built and applied to `libopenapi` documents.
1 parent e247a28 commit 9442e5e

21 files changed

Lines changed: 3501 additions & 0 deletions

datamodel/high/overlay/action.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2022-2025 Princess B33f Heavy Industries / Dave Shanley
2+
// SPDX-License-Identifier: MIT
3+
4+
package overlay
5+
6+
import (
7+
"github.com/pb33f/libopenapi/datamodel/high"
8+
low "github.com/pb33f/libopenapi/datamodel/low/overlay"
9+
"github.com/pb33f/libopenapi/orderedmap"
10+
"go.yaml.in/yaml/v4"
11+
)
12+
13+
// Action represents a high-level Overlay Action Object.
14+
// https://spec.openapis.org/overlay/v1.0.0#action-object
15+
type Action struct {
16+
Target string `json:"target,omitempty" yaml:"target,omitempty"`
17+
Description string `json:"description,omitempty" yaml:"description,omitempty"`
18+
Update *yaml.Node `json:"update,omitempty" yaml:"update,omitempty"`
19+
Remove bool `json:"remove,omitempty" yaml:"remove,omitempty"`
20+
Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"`
21+
low *low.Action
22+
}
23+
24+
// NewAction creates a new high-level Action instance from a low-level one.
25+
func NewAction(action *low.Action) *Action {
26+
a := new(Action)
27+
a.low = action
28+
if !action.Target.IsEmpty() {
29+
a.Target = action.Target.Value
30+
}
31+
if !action.Description.IsEmpty() {
32+
a.Description = action.Description.Value
33+
}
34+
if !action.Update.IsEmpty() {
35+
a.Update = action.Update.Value
36+
}
37+
if !action.Remove.IsEmpty() {
38+
a.Remove = action.Remove.Value
39+
}
40+
a.Extensions = high.ExtractExtensions(action.Extensions)
41+
return a
42+
}
43+
44+
// GoLow returns the low-level Action instance used to create the high-level one.
45+
func (a *Action) GoLow() *low.Action {
46+
return a.low
47+
}
48+
49+
// GoLowUntyped returns the low-level Action instance with no type.
50+
func (a *Action) GoLowUntyped() any {
51+
return a.low
52+
}
53+
54+
// Render returns a YAML representation of the Action object as a byte slice.
55+
func (a *Action) Render() ([]byte, error) {
56+
return yaml.Marshal(a)
57+
}
58+
59+
// MarshalYAML creates a ready to render YAML representation of the Action object.
60+
func (a *Action) MarshalYAML() (interface{}, error) {
61+
m := orderedmap.New[string, any]()
62+
if a.Target != "" {
63+
m.Set("target", a.Target)
64+
}
65+
if a.Description != "" {
66+
m.Set("description", a.Description)
67+
}
68+
if a.Update != nil {
69+
m.Set("update", a.Update)
70+
}
71+
if a.Remove {
72+
m.Set("remove", a.Remove)
73+
}
74+
for pair := a.Extensions.First(); pair != nil; pair = pair.Next() {
75+
m.Set(pair.Key(), pair.Value())
76+
}
77+
return m, nil
78+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Copyright 2022-2025 Princess B33f Heavy Industries / Dave Shanley
2+
// SPDX-License-Identifier: MIT
3+
4+
package overlay
5+
6+
import (
7+
"context"
8+
"testing"
9+
10+
"github.com/pb33f/libopenapi/datamodel/low"
11+
lowoverlay "github.com/pb33f/libopenapi/datamodel/low/overlay"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
"go.yaml.in/yaml/v4"
15+
)
16+
17+
func TestNewAction_Update(t *testing.T) {
18+
yml := `target: $.info.title
19+
description: Update the title
20+
update: New Title`
21+
22+
var node yaml.Node
23+
err := yaml.Unmarshal([]byte(yml), &node)
24+
require.NoError(t, err)
25+
26+
var lowAction lowoverlay.Action
27+
err = low.BuildModel(node.Content[0], &lowAction)
28+
require.NoError(t, err)
29+
err = lowAction.Build(context.Background(), nil, node.Content[0], nil)
30+
require.NoError(t, err)
31+
32+
highAction := NewAction(&lowAction)
33+
34+
assert.Equal(t, "$.info.title", highAction.Target)
35+
assert.Equal(t, "Update the title", highAction.Description)
36+
assert.NotNil(t, highAction.Update)
37+
assert.Equal(t, "New Title", highAction.Update.Value)
38+
assert.False(t, highAction.Remove)
39+
}
40+
41+
func TestNewAction_Remove(t *testing.T) {
42+
yml := `target: $.info.description
43+
remove: true`
44+
45+
var node yaml.Node
46+
err := yaml.Unmarshal([]byte(yml), &node)
47+
require.NoError(t, err)
48+
49+
var lowAction lowoverlay.Action
50+
err = low.BuildModel(node.Content[0], &lowAction)
51+
require.NoError(t, err)
52+
err = lowAction.Build(context.Background(), nil, node.Content[0], nil)
53+
require.NoError(t, err)
54+
55+
highAction := NewAction(&lowAction)
56+
57+
assert.Equal(t, "$.info.description", highAction.Target)
58+
assert.True(t, highAction.Remove)
59+
}
60+
61+
func TestNewAction_WithExtensions(t *testing.T) {
62+
yml := `target: $.paths
63+
x-priority: high`
64+
65+
var node yaml.Node
66+
err := yaml.Unmarshal([]byte(yml), &node)
67+
require.NoError(t, err)
68+
69+
var lowAction lowoverlay.Action
70+
err = low.BuildModel(node.Content[0], &lowAction)
71+
require.NoError(t, err)
72+
err = lowAction.Build(context.Background(), nil, node.Content[0], nil)
73+
require.NoError(t, err)
74+
75+
highAction := NewAction(&lowAction)
76+
77+
assert.NotNil(t, highAction.Extensions)
78+
assert.Equal(t, 1, highAction.Extensions.Len())
79+
}
80+
81+
func TestAction_GoLow(t *testing.T) {
82+
yml := `target: $.info`
83+
84+
var node yaml.Node
85+
_ = yaml.Unmarshal([]byte(yml), &node)
86+
87+
var lowAction lowoverlay.Action
88+
_ = low.BuildModel(node.Content[0], &lowAction)
89+
_ = lowAction.Build(context.Background(), nil, node.Content[0], nil)
90+
91+
highAction := NewAction(&lowAction)
92+
93+
assert.Equal(t, &lowAction, highAction.GoLow())
94+
assert.Equal(t, &lowAction, highAction.GoLowUntyped())
95+
}
96+
97+
func TestAction_Render(t *testing.T) {
98+
yml := `target: $.info
99+
update:
100+
title: Test`
101+
102+
var node yaml.Node
103+
_ = yaml.Unmarshal([]byte(yml), &node)
104+
105+
var lowAction lowoverlay.Action
106+
_ = low.BuildModel(node.Content[0], &lowAction)
107+
_ = lowAction.Build(context.Background(), nil, node.Content[0], nil)
108+
109+
highAction := NewAction(&lowAction)
110+
111+
rendered, err := highAction.Render()
112+
require.NoError(t, err)
113+
assert.Contains(t, string(rendered), "target: $.info")
114+
}
115+
116+
func TestAction_MarshalYAML(t *testing.T) {
117+
yml := `target: $.info
118+
description: Update info
119+
update:
120+
title: Test
121+
x-custom: value`
122+
123+
var node yaml.Node
124+
_ = yaml.Unmarshal([]byte(yml), &node)
125+
126+
var lowAction lowoverlay.Action
127+
_ = low.BuildModel(node.Content[0], &lowAction)
128+
_ = lowAction.Build(context.Background(), nil, node.Content[0], nil)
129+
130+
highAction := NewAction(&lowAction)
131+
132+
result, err := highAction.MarshalYAML()
133+
require.NoError(t, err)
134+
assert.NotNil(t, result)
135+
}
136+
137+
func TestAction_MarshalYAML_Remove(t *testing.T) {
138+
yml := `target: $.info
139+
remove: true`
140+
141+
var node yaml.Node
142+
_ = yaml.Unmarshal([]byte(yml), &node)
143+
144+
var lowAction lowoverlay.Action
145+
_ = low.BuildModel(node.Content[0], &lowAction)
146+
_ = lowAction.Build(context.Background(), nil, node.Content[0], nil)
147+
148+
highAction := NewAction(&lowAction)
149+
150+
result, err := highAction.MarshalYAML()
151+
require.NoError(t, err)
152+
assert.NotNil(t, result)
153+
}
154+
155+
func TestAction_MarshalYAML_Empty(t *testing.T) {
156+
var lowAction lowoverlay.Action
157+
highAction := NewAction(&lowAction)
158+
159+
result, err := highAction.MarshalYAML()
160+
require.NoError(t, err)
161+
assert.NotNil(t, result)
162+
}

datamodel/high/overlay/info.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2022-2025 Princess B33f Heavy Industries / Dave Shanley
2+
// SPDX-License-Identifier: MIT
3+
4+
package overlay
5+
6+
import (
7+
"github.com/pb33f/libopenapi/datamodel/high"
8+
low "github.com/pb33f/libopenapi/datamodel/low/overlay"
9+
"github.com/pb33f/libopenapi/orderedmap"
10+
"go.yaml.in/yaml/v4"
11+
)
12+
13+
// Info represents a high-level Overlay Info Object.
14+
// https://spec.openapis.org/overlay/v1.0.0#info-object
15+
type Info struct {
16+
Title string `json:"title,omitempty" yaml:"title,omitempty"`
17+
Version string `json:"version,omitempty" yaml:"version,omitempty"`
18+
Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"`
19+
low *low.Info
20+
}
21+
22+
// NewInfo creates a new high-level Info instance from a low-level one.
23+
func NewInfo(info *low.Info) *Info {
24+
i := new(Info)
25+
i.low = info
26+
if !info.Title.IsEmpty() {
27+
i.Title = info.Title.Value
28+
}
29+
if !info.Version.IsEmpty() {
30+
i.Version = info.Version.Value
31+
}
32+
i.Extensions = high.ExtractExtensions(info.Extensions)
33+
return i
34+
}
35+
36+
// GoLow returns the low-level Info instance used to create the high-level one.
37+
func (i *Info) GoLow() *low.Info {
38+
return i.low
39+
}
40+
41+
// GoLowUntyped returns the low-level Info instance with no type.
42+
func (i *Info) GoLowUntyped() any {
43+
return i.low
44+
}
45+
46+
// Render returns a YAML representation of the Info object as a byte slice.
47+
func (i *Info) Render() ([]byte, error) {
48+
return yaml.Marshal(i)
49+
}
50+
51+
// MarshalYAML creates a ready to render YAML representation of the Info object.
52+
func (i *Info) MarshalYAML() (interface{}, error) {
53+
m := orderedmap.New[string, any]()
54+
if i.Title != "" {
55+
m.Set("title", i.Title)
56+
}
57+
if i.Version != "" {
58+
m.Set("version", i.Version)
59+
}
60+
for pair := i.Extensions.First(); pair != nil; pair = pair.Next() {
61+
m.Set(pair.Key(), pair.Value())
62+
}
63+
return m, nil
64+
}

0 commit comments

Comments
 (0)