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
14 changes: 1 addition & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,6 @@ like our _very kind_ sponsors:

---

`libopenapi` is kinda new, so our list of notable projects that depend on `libopenapi` is small (let me know if you'd like to add your project)

- [github.com/daveshanley/vacuum](https://github.com/daveshanley/vacuum) - "The world's fastest and most scalable OpenAPI/Swagger linter/quality tool"
- [github.com/pb33f/openapi-changes](https://github.com/pb33f/openapi-changes) - "The world's **sexiest** OpenAPI breaking changes detector"
- [github.com/pb33f/wiretap](https://github.com/pb33f/wiretap) - "The world's **coolest** OpenAPI compliance analysis tool"
- [github.com/danielgtaylor/restish](https://github.com/danielgtaylor/restish) - "Restish is a CLI for interacting with REST-ish HTTP APIs"
- [github.com/speakeasy-api/speakeasy](https://github.com/speakeasy-api/speakeasy) - "Speakeasy CLI makes validating OpenAPI docs and generating idiomatic SDKs easy!"
- [github.com/apicat/apicat](https://github.com/apicat/apicat) - "AI-powered API development tool"
- [github.com/mattermost/mattermost](https://github.com/mattermost/mattermost) - "Software development lifecycle platform"
- [github.com/gopher-fleece/gleece](https://github.com/gopher-fleece/gleece) - "Building and documenting REST APIs through code-first development"
- Your project here?
---

## Come chat with us

Need help? Have a question? Want to share your work? [Join our discord](https://discord.gg/x7VACVuEGP) and
Expand Down Expand Up @@ -90,6 +77,7 @@ See all the documentation at https://pb33f.io/libopenapi/
- [Circular References](https://pb33f.io/libopenapi/circular-references/)
- [Bundling Specs](https://pb33f.io/libopenapi/bundling/)
- [What Changed / Diff Engine](https://pb33f.io/libopenapi/what-changed/)
- [Overlays](https://pb33f.io/libopenapi/overlays/)
- [FAQ](https://pb33f.io/libopenapi/faq/)
- [About libopenapi](https://pb33f.io/libopenapi/about/)
---
Expand Down
78 changes: 78 additions & 0 deletions datamodel/high/overlay/action.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2022-2025 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT

package overlay

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

// Action represents a high-level Overlay Action Object.
// https://spec.openapis.org/overlay/v1.0.0#action-object
type Action struct {
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"`
Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"`
low *low.Action
}

// 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
if !action.Target.IsEmpty() {
a.Target = action.Target.Value
}
if !action.Description.IsEmpty() {
a.Description = action.Description.Value
}
if !action.Update.IsEmpty() {
a.Update = action.Update.Value
}
if !action.Remove.IsEmpty() {
a.Remove = action.Remove.Value
}
a.Extensions = high.ExtractExtensions(action.Extensions)
return a
}

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

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

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

// MarshalYAML creates a ready to render YAML representation of the Action object.
func (a *Action) MarshalYAML() (interface{}, error) {
m := orderedmap.New[string, any]()
if a.Target != "" {
m.Set("target", a.Target)
}
if a.Description != "" {
m.Set("description", a.Description)
}
if a.Update != nil {
m.Set("update", a.Update)
}
if a.Remove {
m.Set("remove", a.Remove)
}
for pair := a.Extensions.First(); pair != nil; pair = pair.Next() {
m.Set(pair.Key(), pair.Value())
}
return m, nil
}
162 changes: 162 additions & 0 deletions datamodel/high/overlay/action_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright 2022-2025 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT

package overlay

import (
"context"
"testing"

"github.com/pb33f/libopenapi/datamodel/low"
lowoverlay "github.com/pb33f/libopenapi/datamodel/low/overlay"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.yaml.in/yaml/v4"
)

func TestNewAction_Update(t *testing.T) {
yml := `target: $.info.title
description: Update the title
update: New Title`

var node yaml.Node
err := yaml.Unmarshal([]byte(yml), &node)
require.NoError(t, err)

var lowAction lowoverlay.Action
err = low.BuildModel(node.Content[0], &lowAction)
require.NoError(t, err)
err = lowAction.Build(context.Background(), nil, node.Content[0], nil)
require.NoError(t, err)

highAction := NewAction(&lowAction)

assert.Equal(t, "$.info.title", highAction.Target)
assert.Equal(t, "Update the title", highAction.Description)
assert.NotNil(t, highAction.Update)
assert.Equal(t, "New Title", highAction.Update.Value)
assert.False(t, highAction.Remove)
}

func TestNewAction_Remove(t *testing.T) {
yml := `target: $.info.description
remove: true`

var node yaml.Node
err := yaml.Unmarshal([]byte(yml), &node)
require.NoError(t, err)

var lowAction lowoverlay.Action
err = low.BuildModel(node.Content[0], &lowAction)
require.NoError(t, err)
err = lowAction.Build(context.Background(), nil, node.Content[0], nil)
require.NoError(t, err)

highAction := NewAction(&lowAction)

assert.Equal(t, "$.info.description", highAction.Target)
assert.True(t, highAction.Remove)
}

func TestNewAction_WithExtensions(t *testing.T) {
yml := `target: $.paths
x-priority: high`

var node yaml.Node
err := yaml.Unmarshal([]byte(yml), &node)
require.NoError(t, err)

var lowAction lowoverlay.Action
err = low.BuildModel(node.Content[0], &lowAction)
require.NoError(t, err)
err = lowAction.Build(context.Background(), nil, node.Content[0], nil)
require.NoError(t, err)

highAction := NewAction(&lowAction)

assert.NotNil(t, highAction.Extensions)
assert.Equal(t, 1, highAction.Extensions.Len())
}

func TestAction_GoLow(t *testing.T) {
yml := `target: $.info`

var node yaml.Node
_ = yaml.Unmarshal([]byte(yml), &node)

var lowAction lowoverlay.Action
_ = low.BuildModel(node.Content[0], &lowAction)
_ = lowAction.Build(context.Background(), nil, node.Content[0], nil)

highAction := NewAction(&lowAction)

assert.Equal(t, &lowAction, highAction.GoLow())
assert.Equal(t, &lowAction, highAction.GoLowUntyped())
}

func TestAction_Render(t *testing.T) {
yml := `target: $.info
update:
title: Test`

var node yaml.Node
_ = yaml.Unmarshal([]byte(yml), &node)

var lowAction lowoverlay.Action
_ = low.BuildModel(node.Content[0], &lowAction)
_ = lowAction.Build(context.Background(), nil, node.Content[0], nil)

highAction := NewAction(&lowAction)

rendered, err := highAction.Render()
require.NoError(t, err)
assert.Contains(t, string(rendered), "target: $.info")
}

func TestAction_MarshalYAML(t *testing.T) {
yml := `target: $.info
description: Update info
update:
title: Test
x-custom: value`

var node yaml.Node
_ = yaml.Unmarshal([]byte(yml), &node)

var lowAction lowoverlay.Action
_ = low.BuildModel(node.Content[0], &lowAction)
_ = lowAction.Build(context.Background(), nil, node.Content[0], nil)

highAction := NewAction(&lowAction)

result, err := highAction.MarshalYAML()
require.NoError(t, err)
assert.NotNil(t, result)
}

func TestAction_MarshalYAML_Remove(t *testing.T) {
yml := `target: $.info
remove: true`

var node yaml.Node
_ = yaml.Unmarshal([]byte(yml), &node)

var lowAction lowoverlay.Action
_ = low.BuildModel(node.Content[0], &lowAction)
_ = lowAction.Build(context.Background(), nil, node.Content[0], nil)

highAction := NewAction(&lowAction)

result, err := highAction.MarshalYAML()
require.NoError(t, err)
assert.NotNil(t, result)
}

func TestAction_MarshalYAML_Empty(t *testing.T) {
var lowAction lowoverlay.Action
highAction := NewAction(&lowAction)

result, err := highAction.MarshalYAML()
require.NoError(t, err)
assert.NotNil(t, result)
}
64 changes: 64 additions & 0 deletions datamodel/high/overlay/info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2022-2025 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT

package overlay

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

// Info represents a high-level Overlay Info Object.
// https://spec.openapis.org/overlay/v1.0.0#info-object
type Info struct {
Title string `json:"title,omitempty" yaml:"title,omitempty"`
Version string `json:"version,omitempty" yaml:"version,omitempty"`
Extensions *orderedmap.Map[string, *yaml.Node] `json:"-" yaml:"-"`
low *low.Info
}

// NewInfo creates a new high-level Info instance from a low-level one.
func NewInfo(info *low.Info) *Info {
i := new(Info)
i.low = info
if !info.Title.IsEmpty() {
i.Title = info.Title.Value
}
if !info.Version.IsEmpty() {
i.Version = info.Version.Value
}
i.Extensions = high.ExtractExtensions(info.Extensions)
return i
}

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

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

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

// MarshalYAML creates a ready to render YAML representation of the Info object.
func (i *Info) MarshalYAML() (interface{}, error) {
m := orderedmap.New[string, any]()
if i.Title != "" {
m.Set("title", i.Title)
}
if i.Version != "" {
m.Set("version", i.Version)
}
for pair := i.Extensions.First(); pair != nil; pair = pair.Next() {
m.Set(pair.Key(), pair.Value())
}
return m, nil
}
Loading