Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions datamodel/high/overlay/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,27 @@ import (
"github.com/pb33f/libopenapi/orderedmap"
)

// Action represents a high-level Overlay Action Object.
// https://spec.openapis.org/overlay/v1.1.0#action-object
// Action represents an Overlay action or reusable action reference.
// Ref identifies a reusable action; the other fields provide local overrides.
// https://spec.openapis.org/overlay/v1.2.0#action-object
type Action struct {
Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
Target string `json:"target,omitempty" yaml:"target,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Update *yaml.Node `json:"update,omitempty" yaml:"update,omitempty"`
Remove bool `json:"remove,omitempty" yaml:"remove,omitempty"`
Copy string `json:"copy,omitempty" yaml:"copy,omitempty"`
Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"`
low *low.Action
removeSet bool
}

// NewAction creates a new high-level Action instance from a low-level one.
func NewAction(action *low.Action) *Action {
a := new(Action)
a.low = action
a.Ref = action.Ref.Value
a.removeSet = action.Remove.KeyNode != nil
if !action.Target.IsEmpty() {
a.Target = action.Target.Value
}
Expand Down Expand Up @@ -63,23 +68,32 @@ func (a *Action) Render() ([]byte, error) {
// MarshalYAML creates a ready to render YAML representation of the Action object.
func (a *Action) MarshalYAML() (any, error) {
m := orderedmap.New[string, any]()
if a.Target != "" {
if a.Ref != "" || (a.low != nil && a.low.Ref.KeyNode != nil) {
m.Set(low.RefLabel, a.Ref)
}
if a.Target != "" || (a.low != nil && a.low.Target.KeyNode != nil) {
m.Set(low.TargetLabel, a.Target)
}
if a.Description != "" {
if a.Description != "" || (a.low != nil && a.low.Description.KeyNode != nil) {
m.Set(low.DescriptionLabel, a.Description)
}
if a.Copy != "" {
if a.Copy != "" || (a.low != nil && a.low.Copy.KeyNode != nil) {
m.Set(low.CopyLabel, a.Copy)
}
if a.Update != nil {
m.Set(low.UpdateLabel, a.Update)
}
if a.Remove {
if a.Remove || a.removeSet {
m.Set(low.RemoveLabel, a.Remove)
}
for pair := a.Extensions.First(); pair != nil; pair = pair.Next() {
m.Set(pair.Key(), pair.Value())
}
return m, nil
}

// SetRemove sets remove and preserves an explicit false override when rendering or resolving a reusable action.
func (a *Action) SetRemove(remove bool) { a.Remove = remove; a.removeSet = true }

// HasRemove reports whether remove is explicitly set, including a false override.
func (a *Action) HasRemove() bool { return a.Remove || a.removeSet }
60 changes: 60 additions & 0 deletions datamodel/high/overlay/components.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2022-2025 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT

package overlay

import (
"github.com/pb33f/go-yaml"
"github.com/pb33f/libopenapi/datamodel/high"
low "github.com/pb33f/libopenapi/datamodel/low/overlay"
"github.com/pb33f/libopenapi/orderedmap"
)

// Components represents a high-level Overlay Components Object.
// https://spec.openapis.org/overlay/v1.2.0#components-object
type Components struct {
Actions *orderedmap.Map[string, *ReusableAction] `json:"actions,omitempty" yaml:"actions,omitempty"`
Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"`
low *low.Components
}

// NewComponents creates a new high-level Components instance from a low-level one.
func NewComponents(info *low.Components) *Components {
i := new(Components)
i.low = info
if info.Actions.Value != nil {
i.Actions = orderedmap.New[string, *ReusableAction]()
for pair := info.Actions.Value.First(); pair != nil; pair = pair.Next() {
i.Actions.Set(pair.Key().Value, NewReusableAction(pair.Value().Value))
}
}
i.Extensions = high.ExtractExtensions(info.Extensions)
return i
}

// GoLow returns the low-level Components instance used to create the high-level one.
func (i *Components) GoLow() *low.Components {
return i.low
}

// GoLowUntyped returns the low-level Components instance with no type.
func (i *Components) GoLowUntyped() any {
return i.low
}

// Render returns a YAML representation of the Components object as a byte slice.
func (i *Components) Render() ([]byte, error) {
return yaml.Marshal(i)
}

// MarshalYAML creates a ready to render YAML representation of the Components object.
func (i *Components) MarshalYAML() (any, error) {
m := orderedmap.New[string, any]()
if i.Actions != nil {
m.Set(low.ActionsLabel, i.Actions)
}
for pair := i.Extensions.First(); pair != nil; pair = pair.Next() {
m.Set(pair.Key(), pair.Value())
}
return m, nil
}
6 changes: 3 additions & 3 deletions datamodel/high/overlay/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ func (i *Info) Render() ([]byte, error) {
// MarshalYAML creates a ready to render YAML representation of the Info object.
func (i *Info) MarshalYAML() (any, error) {
m := orderedmap.New[string, any]()
if i.Title != "" {
if i.Title != "" || i.low == nil || !i.low.Title.IsEmpty() {
m.Set("title", i.Title)
}
if i.Version != "" {
if i.Version != "" || i.low == nil || !i.low.Version.IsEmpty() {
m.Set("version", i.Version)
}
if i.Description != "" {
if i.Description != "" || (i.low != nil && !i.low.Description.IsEmpty()) {
m.Set("description", i.Description)
}
for pair := i.Extensions.First(); pair != nil; pair = pair.Next() {
Expand Down
16 changes: 14 additions & 2 deletions datamodel/high/overlay/overlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import (
)

// Overlay represents a high-level OpenAPI Overlay document.
// https://spec.openapis.org/overlay/v1.0.0
// https://spec.openapis.org/overlay/v1.2.0
type Overlay struct {
Self string `json:"$self,omitempty" yaml:"$self,omitempty"`
Components *Components `json:"components,omitempty" yaml:"components,omitempty"`
Overlay string `json:"overlay,omitempty" yaml:"overlay,omitempty"`
Info *Info `json:"info,omitempty" yaml:"info,omitempty"`
Extends string `json:"extends,omitempty" yaml:"extends,omitempty"`
Expand All @@ -25,6 +27,10 @@ type Overlay struct {
func NewOverlay(overlay *low.Overlay) *Overlay {
o := new(Overlay)
o.low = overlay
o.Self = overlay.Self.Value
if overlay.Components.Value != nil {
o.Components = NewComponents(overlay.Components.Value)
}
if !overlay.Overlay.IsEmpty() {
o.Overlay = overlay.Overlay.Value
}
Expand Down Expand Up @@ -66,10 +72,16 @@ func (o *Overlay) MarshalYAML() (interface{}, error) {
if o.Overlay != "" {
m.Set("overlay", o.Overlay)
}
if o.Self != "" || (o.low != nil && o.low.Self.KeyNode != nil) {
m.Set(low.SelfLabel, o.Self)
}
if o.Components != nil {
m.Set(low.ComponentsLabel, o.Components)
}
if o.Info != nil {
m.Set("info", o.Info)
}
if o.Extends != "" {
if o.Extends != "" || (o.low != nil && o.low.Extends.KeyNode != nil) {
m.Set("extends", o.Extends)
}
if len(o.Actions) > 0 {
Expand Down
120 changes: 120 additions & 0 deletions datamodel/high/overlay/overlay12_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright 2022-2026 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT

package overlay

import (
"context"
"github.com/pb33f/go-yaml"
"github.com/pb33f/libopenapi/datamodel/low"
lowoverlay "github.com/pb33f/libopenapi/datamodel/low/overlay"
"github.com/pb33f/testify/assert"
"github.com/pb33f/testify/require"
"testing"
)

func parseOverlay12(t *testing.T, text string) *Overlay {
t.Helper()
var node yaml.Node
require.NoError(t, yaml.Unmarshal([]byte(text), &node))
var model lowoverlay.Overlay
require.NoError(t, low.BuildModel(node.Content[0], &model))
require.NoError(t, model.Build(context.Background(), nil, node.Content[0], nil))
return NewOverlay(&model)
}

func TestOverlay12RoundTrip(t *testing.T) {
text := `overlay: 1.2.0
$self: https://example.com/overlay.yaml
extends: api.yaml
info: {title: Shared, version: '1'}
components:
x-source: docs
actions:
shared:
description: Reusable description
x-owner: docs
fields:
description: Field description
remove: true
copy: $.source
update: null
noop: {}
actions:
- $ref: '#/components/actions/shared'
target: $.info
description: ''
copy: ''
remove: false
`
ov := parseOverlay12(t, text)
c := ov.Components
assert.Equal(t, c.GoLow(), c.GoLowUntyped())
reusable, ok := c.Actions.Get("shared")
require.True(t, ok)
assert.Equal(t, reusable.GoLow(), reusable.GoLowUntyped())
componentBytes, err := c.Render()
require.NoError(t, err)
assert.Contains(t, string(componentBytes), "x-source: docs")
actionBytes, err := reusable.Render()
require.NoError(t, err)
assert.Contains(t, string(actionBytes), "x-owner: docs")
raw, err := ov.Render()
require.NoError(t, err)
assert.YAMLEq(t, text, string(raw))
again := parseOverlay12(t, string(raw))
assert.Equal(t, ov.GoLow().Hash(), again.GoLow().Hash())
assert.True(t, again.Actions[0].HasRemove())
assert.False(t, again.Actions[0].Remove)
ov.Actions[0].SetRemove(true)
assert.True(t, ov.Actions[0].Remove)
ov.Actions[0].SetRemove(false)
raw, err = ov.Actions[0].Render()
require.NoError(t, err)
assert.Contains(t, string(raw), "remove: false")
empty := parseOverlay12(t, "components: {}")
raw, err = empty.Components.Render()
require.NoError(t, err)
assert.YAMLEq(t, "{}", string(raw))
}

func TestOverlay12ExplicitEmptyURIs(t *testing.T) {
for _, text := range []string{
"$self: https://example.com/overlay.yaml\nextends: ''",
"$self: ''\nextends: ''",
} {
ov := parseOverlay12(t, text)
got, err := ov.ResolveExtends("https://example.com/overlay.yaml")
require.NoError(t, err)
assert.Equal(t, "https://example.com/overlay.yaml", got)
raw, err := ov.Render()
require.NoError(t, err)
assert.YAMLEq(t, text, string(raw))
}
absent := parseOverlay12(t, "$self: https://example.com/overlay.yaml")
got, err := absent.ResolveExtends("")
require.NoError(t, err)
assert.Empty(t, got)
}

func TestOverlay12EmptyReferenceAndTargetPreserved(t *testing.T) {
text := "actions: [{$ref: '', target: ''}]"
ov := parseOverlay12(t, text)
raw, err := ov.Render()
require.NoError(t, err)
assert.YAMLEq(t, text, string(raw))
}

func TestOverlay12EmptyInfoStrings(t *testing.T) {
for _, text := range []string{"info: {title: '', version: '1'}", "info: {title: Example, version: '', description: ''}"} {
ov := parseOverlay12(t, text)
rendered, err := ov.Render()
require.NoError(t, err)
assert.YAMLEq(t, text, string(rendered))
assert.Equal(t, ov.GoLow().Hash(), parseOverlay12(t, string(rendered)).GoLow().Hash())
}
info := &Info{}
raw, err := info.Render()
require.NoError(t, err)
assert.YAMLEq(t, "title: ''\nversion: ''", string(raw))
}
62 changes: 62 additions & 0 deletions datamodel/high/overlay/reusable_action.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2022-2025 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT

package overlay

import (
"github.com/pb33f/go-yaml"
"github.com/pb33f/libopenapi/datamodel/high"
low "github.com/pb33f/libopenapi/datamodel/low/overlay"
"github.com/pb33f/libopenapi/orderedmap"
)

// ReusableAction represents a high-level Overlay ReusableAction Object.
// https://spec.openapis.org/overlay/v1.2.0#reusable-action-object
type ReusableAction struct {
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Fields *Action `json:"fields,omitempty" yaml:"fields,omitempty"`
Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"`
low *low.ReusableAction
}

// NewReusableAction creates a new high-level ReusableAction instance from a low-level one.
func NewReusableAction(info *low.ReusableAction) *ReusableAction {
i := new(ReusableAction)
i.low = info
i.Description = info.Description.Value
if info.Fields.Value != nil {
i.Fields = NewAction(info.Fields.Value)
}
i.Extensions = high.ExtractExtensions(info.Extensions)
return i
}

// GoLow returns the low-level ReusableAction instance used to create the high-level one.
func (i *ReusableAction) GoLow() *low.ReusableAction {
return i.low
}

// GoLowUntyped returns the low-level ReusableAction instance with no type.
func (i *ReusableAction) GoLowUntyped() any {
return i.low
}

// Render returns a YAML representation of the ReusableAction object as a byte slice.
func (i *ReusableAction) Render() ([]byte, error) {
return yaml.Marshal(i)
}

// MarshalYAML creates a ready to render YAML representation of the ReusableAction object.
func (i *ReusableAction) MarshalYAML() (any, error) {
m := orderedmap.New[string, any]()
if i.Description != "" {
m.Set(low.DescriptionLabel, i.Description)
}
if i.Fields != nil {
m.Set(low.FieldsLabel, i.Fields)
}
for pair := i.Extensions.First(); pair != nil; pair = pair.Next() {
m.Set(pair.Key(), pair.Value())
}
return m, nil
}
Loading
Loading