Skip to content

Commit 1dfde02

Browse files
Merge branch 'main' into blake/spec-3.2
2 parents 627ba02 + 526f2c7 commit 1dfde02

19 files changed

+999
-19
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515

1616
<p align="center">
1717
<p align="center">
18-
<img width="200px" alt="OpenAPI" src="https://github.com/user-attachments/assets/555a0899-5719-42ee-b4b1-ece8d1d812ea">
18+
<img width="200px" alt="OpenAPI" src="https://github.com/user-attachments/assets/b9fa9c14-1c6f-4d8b-910f-15e5f962bab6">
19+
1920
</p>
2021
<h1 align="center"><b>OpenAPI</b></h1>
2122
<p align="center">A set of packages and tools for working with <a href="https://www.speakeasy.com/openapi">OpenAPI Specification documents</a>. <br /> Used directly in Speakeasy's product to power our <a href="https://www.speakeasy.com/product/sdk-generation">SDK Generation</a> and <a href="https://www.speakeasy.com/product/gram">Gram</a> products.

cmd/openapi/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
github.com/charmbracelet/bubbles v0.21.0
77
github.com/charmbracelet/bubbletea v1.3.10
88
github.com/charmbracelet/lipgloss v1.1.0
9-
github.com/speakeasy-api/openapi v1.11.3-0.20251208054628-3d74e4534686
9+
github.com/speakeasy-api/openapi v1.11.4-0.20251208232812-c8a601825ff5
1010
github.com/spf13/cobra v1.10.1
1111
github.com/stretchr/testify v1.11.1
1212
gopkg.in/yaml.v3 v3.0.1

cmd/openapi/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
7575
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
7676
github.com/speakeasy-api/jsonpath v0.6.2 h1:Mys71yd6u8kuowNCR0gCVPlVAHCmKtoGXYoAtcEbqXQ=
7777
github.com/speakeasy-api/jsonpath v0.6.2/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw=
78-
github.com/speakeasy-api/openapi v1.11.3-0.20251208054628-3d74e4534686 h1:6i29DwrR+lBf6jxvl0UDsgMORmmLEyNFJ8nHcFtDEoY=
79-
github.com/speakeasy-api/openapi v1.11.3-0.20251208054628-3d74e4534686/go.mod h1:ITV3em4IFe1Hd4gX5Peq9TE7+Rfd/WIHZE/aqxNgihg=
78+
github.com/speakeasy-api/openapi v1.11.4-0.20251208232812-c8a601825ff5 h1:RQzb/8G4zpMhAp/qLiz1Y6FMByUW8AG+FZUedWVcrTM=
79+
github.com/speakeasy-api/openapi v1.11.4-0.20251208232812-c8a601825ff5/go.mod h1:ITV3em4IFe1Hd4gX5Peq9TE7+Rfd/WIHZE/aqxNgihg=
8080
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
8181
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
8282
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=

openapi/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<p align="center">
22
<p align="center">
3-
<img width="200px" alt="OpenAPI" src="https://github.com/user-attachments/assets/555a0899-5719-42ee-b4b1-ece8d1d812ea">
3+
<img width="200px" alt="OpenAPI" src="https://github.com/user-attachments/assets/b9fa9c14-1c6f-4d8b-910f-15e5f962bab6">
44
</p>
55
<h1 align="center"><b>OpenAPI Parser</b></h1>
66
<p align="center">An API for working with <a href="https://spec.openapis.org/oas/v3.1.1">OpenAPI documents</a> including: read, walk, create, mutate, validate, and upgrade

overlay/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,16 @@
2525
2626
## Features
2727

28-
- **OpenAPI Overlay Specification Compliance**: Full implementation of the [OpenAPI Overlay Specification](https://github.com/OAI/Overlay-Specification/blob/3f398c6/versions/1.0.0.md) (2023-10-12)
28+
- **OpenAPI Overlay Specification Compliance**: Full implementation of the [OpenAPI Overlay Specification](https://github.com/OAI/Overlay-Specification/blob/3f398c6/versions/1.0.0.md) (2023-10-12) and [version 1.1.0](https://github.com/OAI/Overlay-Specification/blob/e2c3cec/versions/1.1.0-dev.md)
2929
- **JSONPath Target Selection**: Uses JSONPath expressions to select nodes for modification
30-
- **Remove and Update Actions**: Support for both remove actions (pruning nodes) and update actions (merging values)
30+
- **Remove, Update, and Copy Actions**: Support for remove actions (pruning nodes), update actions (merging values), and copy actions (duplicating or moving nodes)
3131
- **Flexible Input/Output**: Works with both YAML and JSON formats
3232
- **Batch Operations**: Apply multiple modifications to large numbers of nodes in a single operation
3333
- **YAML v1.2 Support**: Uses [gopkg.in/yaml.v3](https://pkg.go.dev/gopkg.in/yaml.v3) for YAML v1.2 parsing (superset of JSON)
3434

3535
## About OpenAPI Overlays
3636

37-
This specification defines a means of editing an OpenAPI Specification file by applying a list of actions. Each action is either a remove action that prunes nodes or an update that merges a value into nodes. The nodes impacted are selected by a target expression which uses JSONPath.
37+
This specification defines a means of editing an OpenAPI Specification file by applying a list of actions. Each action is either a remove action that prunes nodes or an update that merges a value into nodes. The nodes impacted are selected by a target expression which uses JSONPath. This implementation also supports [version 1.1.0](https://github.com/OAI/Overlay-Specification/blob/e2c3cec/versions/1.1.0-dev.md) which adds a `copy` action for duplicating or moving nodes within the document.
3838

3939
The specification itself says very little about the input file to be modified or the output file. The presumed intention is that the input and output be an OpenAPI Specification, but that is not required.
4040

overlay/apply.go

Lines changed: 112 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,17 @@ import (
1212
// ApplyTo will take an overlay and apply its changes to the given YAML
1313
// document.
1414
func (o *Overlay) ApplyTo(root *yaml.Node) error {
15+
// Priority is: remove > update > copy
16+
// Copy has no impact if remove is true or update contains a value
1517
for _, action := range o.Actions {
1618
var err error
17-
if action.Remove {
19+
switch {
20+
case action.Remove:
1821
err = o.applyRemoveAction(root, action, nil)
19-
} else {
22+
case !action.Update.IsZero():
2023
err = o.applyUpdateAction(root, action, &[]string{})
24+
case action.Copy != "":
25+
err = o.applyCopyAction(root, action, &[]string{})
2126
}
2227

2328
if err != nil {
@@ -32,6 +37,9 @@ func (o *Overlay) ApplyToStrict(root *yaml.Node) ([]string, error) {
3237
multiError := []string{}
3338
warnings := []string{}
3439
hasFilterExpression := false
40+
41+
// Priority is: remove > update > copy
42+
// Copy has no impact if remove is true or update contains a value
3543
for i, action := range o.Actions {
3644
tokens := token.NewTokenizer(action.Target, config.WithPropertyNameExtension()).Tokenize()
3745
for _, tok := range tokens {
@@ -45,19 +53,27 @@ func (o *Overlay) ApplyToStrict(root *yaml.Node) ([]string, error) {
4553
if err != nil {
4654
multiError = append(multiError, err.Error())
4755
}
48-
if action.Remove {
56+
57+
// Determine action type based on priority: remove > update > copy
58+
actionType := "unknown"
59+
switch {
60+
case action.Remove:
61+
actionType = "remove"
4962
err = o.applyRemoveAction(root, action, &actionWarnings)
50-
if err != nil {
51-
multiError = append(multiError, err.Error())
52-
}
53-
} else {
63+
case !action.Update.IsZero():
64+
actionType = "update"
5465
err = o.applyUpdateAction(root, action, &actionWarnings)
55-
if err != nil {
56-
multiError = append(multiError, err.Error())
57-
}
66+
case action.Copy != "":
67+
actionType = "copy"
68+
err = o.applyCopyAction(root, action, &actionWarnings)
69+
default:
70+
err = fmt.Errorf("unknown action type: %v", action)
71+
}
72+
if err != nil {
73+
return nil, err
5874
}
5975
for _, warning := range actionWarnings {
60-
warnings = append(warnings, fmt.Sprintf("update action (%v / %v) target=%s: %s", i+1, len(o.Actions), action.Target, warning))
76+
warnings = append(warnings, fmt.Sprintf("%s action (%v / %v) target=%s: %s", actionType, i+1, len(o.Actions), action.Target, warning))
6177
}
6278
}
6379

@@ -87,6 +103,24 @@ func (o *Overlay) validateSelectorHasAtLeastOneTarget(root *yaml.Node, action Ac
87103
return fmt.Errorf("selector %q did not match any targets", action.Target)
88104
}
89105

106+
// For copy actions, validate the source path (only if copy will actually be applied)
107+
// Copy has no impact if remove is true or update contains a value
108+
if action.Copy != "" && !action.Remove && action.Update.IsZero() {
109+
sourcePath, err := o.NewPath(action.Copy, nil)
110+
if err != nil {
111+
return err
112+
}
113+
114+
sourceNodes := sourcePath.Query(root)
115+
if len(sourceNodes) == 0 {
116+
return fmt.Errorf("copy source selector %q did not match any nodes", action.Copy)
117+
}
118+
119+
if len(sourceNodes) > 1 {
120+
return fmt.Errorf("copy source selector %q matched multiple nodes (%d), expected exactly one", action.Copy, len(sourceNodes))
121+
}
122+
}
123+
90124
return nil
91125
}
92126

@@ -189,6 +223,13 @@ func mergeNode(node *yaml.Node, merge *yaml.Node) bool {
189223
// node.
190224
func mergeMappingNode(node *yaml.Node, merge *yaml.Node) bool {
191225
anyChange := false
226+
227+
// If the target is an empty flow-style mapping and we're merging content,
228+
// convert to block style for better readability
229+
if len(node.Content) == 0 && node.Style == yaml.FlowStyle && len(merge.Content) > 0 {
230+
node.Style = 0 // Reset to default (block) style
231+
}
232+
192233
NextKey:
193234
for i := 0; i < len(merge.Content); i += 2 {
194235
mergeKey := merge.Content[i].Value
@@ -236,3 +277,63 @@ func clone(node *yaml.Node) *yaml.Node {
236277
}
237278
return newNode
238279
}
280+
281+
// applyCopyAction applies a copy action to the document
282+
// This is a stub implementation for the copy feature from Overlay Specification v1.1.0
283+
func (o *Overlay) applyCopyAction(root *yaml.Node, action Action, warnings *[]string) error {
284+
if action.Target == "" {
285+
return nil
286+
}
287+
288+
if action.Copy == "" {
289+
return nil
290+
}
291+
292+
// Parse the source path
293+
sourcePath, err := o.NewPath(action.Copy, warnings)
294+
if err != nil {
295+
return fmt.Errorf("invalid copy source path %q: %w", action.Copy, err)
296+
}
297+
298+
// Query the source nodes
299+
sourceNodes := sourcePath.Query(root)
300+
if len(sourceNodes) == 0 {
301+
// Source not found - in non-strict mode this is silently ignored
302+
// In strict mode, this will be caught by validateSelectorHasAtLeastOneTarget
303+
if warnings != nil {
304+
*warnings = append(*warnings, fmt.Sprintf("copy source %q not found", action.Copy))
305+
}
306+
return nil
307+
}
308+
309+
if len(sourceNodes) > 1 {
310+
return fmt.Errorf("copy source path %q matched multiple nodes (%d), expected exactly one", action.Copy, len(sourceNodes))
311+
}
312+
313+
sourceNode := sourceNodes[0]
314+
315+
// Parse the target path
316+
targetPath, err := o.NewPath(action.Target, warnings)
317+
if err != nil {
318+
return fmt.Errorf("invalid target path %q: %w", action.Target, err)
319+
}
320+
321+
// Query the target nodes
322+
targetNodes := targetPath.Query(root)
323+
324+
// Copy the source node to each target
325+
didMakeChange := false
326+
for _, targetNode := range targetNodes {
327+
// Clone the source node to avoid reference issues
328+
copiedNode := clone(sourceNode)
329+
330+
// Merge the copied node into the target
331+
didMakeChange = mergeNode(targetNode, copiedNode) || didMakeChange
332+
}
333+
334+
if !didMakeChange && warnings != nil {
335+
*warnings = append(*warnings, "does nothing")
336+
}
337+
338+
return nil
339+
}

overlay/apply_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,21 @@ func cloneNode(node *yaml.Node) *yaml.Node {
270270
return clone
271271
}
272272

273+
func TestApplyTo_CopyVersionToHeader(t *testing.T) {
274+
t.Parallel()
275+
276+
node, err := loader.LoadSpecification("testdata/openapi-version-header.yaml")
277+
require.NoError(t, err)
278+
279+
o, err := loader.LoadOverlay("testdata/overlay-version-header.yaml")
280+
require.NoError(t, err)
281+
282+
err = o.ApplyTo(node)
283+
require.NoError(t, err)
284+
285+
NodeMatchesFile(t, node, "testdata/openapi-version-header-expected.yaml")
286+
}
287+
273288
func TestApplyToOld(t *testing.T) {
274289
t.Parallel()
275290

0 commit comments

Comments
 (0)