Skip to content

Commit ca5609b

Browse files
authored
chore: keys of keys walks up the tree (#15)
* chore: more interaction with keys * chore: fix comment * chore: enforce
1 parent fa7ab3f commit ca5609b

12 files changed

+119
-123
lines changed

cmd/wasm/functions.go

+18-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"github.com/speakeasy-api/jsonpath/pkg/jsonpath"
99
"github.com/speakeasy-api/jsonpath/pkg/jsonpath/config"
10+
"github.com/speakeasy-api/jsonpath/pkg/jsonpath/token"
1011
"github.com/speakeasy-api/jsonpath/pkg/overlay"
1112
"gopkg.in/yaml.v3"
1213
"reflect"
@@ -32,6 +33,7 @@ func CalculateOverlay(originalYAML, targetYAML, existingOverlay string) (string,
3233
if err != nil {
3334
return "", fmt.Errorf("failed to parse overlay schema in CalculateOverlay: %w", err)
3435
}
36+
existingOverlayDocument.JSONPathVersion = "rfc9535" // force this in the playground.
3537
// now modify the original using the existing overlay
3638
err = existingOverlayDocument.ApplyTo(&orig)
3739
if err != nil {
@@ -108,10 +110,22 @@ func ApplyOverlay(originalYAML, overlayYAML string) (string, error) {
108110
if err != nil {
109111
return "", fmt.Errorf("failed to parse overlay schema in ApplyOverlay: %w", err)
110112
}
111-
113+
err = overlay.Validate()
114+
if err != nil {
115+
return "", fmt.Errorf("failed to validate overlay schema in ApplyOverlay: %w", err)
116+
}
117+
hasFilterExpression := false
112118
// check to see if we have an overlay with an error, or a partial overlay: i.e. any overlay actions are missing an update or remove
113119
for i, action := range overlay.Actions {
120+
tokenized := token.NewTokenizer(action.Target, config.WithPropertyNameExtension()).Tokenize()
121+
for _, tok := range tokenized {
122+
if tok.Token == token.FILTER {
123+
hasFilterExpression = true
124+
break
125+
}
126+
}
114127
parsed, pathErr := jsonpath.NewPath(action.Target, config.WithPropertyNameExtension())
128+
115129
var node *yaml.Node
116130
if pathErr != nil {
117131
node, err = lookupOverlayActionTargetNode(overlayYAML, i)
@@ -132,6 +146,9 @@ func ApplyOverlay(originalYAML, overlayYAML string) (string, error) {
132146
return applyOverlayJSONPathIncomplete(result, node)
133147
}
134148
}
149+
if hasFilterExpression && overlay.JSONPathVersion != "rfc9535" {
150+
return "", fmt.Errorf("invalid overlay schema: must have `x-speakeasy-jsonpath: rfc9535`")
151+
}
135152

136153
err = overlay.ApplyTo(&orig)
137154
if err != nil {

pkg/jsonpath/yaml_query.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ func (s innerSegment) Query(idx index, value *yaml.Node, root *yaml.Node) []*yam
103103
// we just want to return the values
104104
for i, child := range value.Content {
105105
if i%2 == 1 {
106+
idx.setPropertyKey(value.Content[i-1], value)
106107
idx.setPropertyKey(child, value.Content[i-1])
107108
result = append(result, child)
108109
}
@@ -123,6 +124,7 @@ func (s innerSegment) Query(idx index, value *yaml.Node, root *yaml.Node) []*yam
123124
val := value.Content[i+1]
124125

125126
if key.Value == s.dotName {
127+
idx.setPropertyKey(key, value)
126128
idx.setPropertyKey(val, key)
127129
result = append(result, val)
128130
break
@@ -156,8 +158,9 @@ func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.No
156158
key = child.Value
157159
continue
158160
}
159-
if key == s.name {
160-
idx.setPropertyKey(child, value.Content[i])
161+
if key == s.name && i%2 == 1 {
162+
idx.setPropertyKey(value.Content[i], value.Content[i-1])
163+
idx.setPropertyKey(value.Content[i-1], value)
161164
return []*yaml.Node{child}
162165
}
163166
}
@@ -181,6 +184,7 @@ func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.No
181184
var result []*yaml.Node
182185
for i, child := range value.Content {
183186
if i%2 == 1 {
187+
idx.setPropertyKey(value.Content[i-1], value)
184188
idx.setPropertyKey(child, value.Content[i-1])
185189
result = append(result, child)
186190
}
@@ -223,6 +227,7 @@ func (s selector) Query(idx index, value *yaml.Node, root *yaml.Node) []*yaml.No
223227
switch value.Kind {
224228
case yaml.MappingNode:
225229
for i := 1; i < len(value.Content); i += 2 {
230+
idx.setPropertyKey(value.Content[i-1], value)
226231
idx.setPropertyKey(value.Content[i], value.Content[i-1])
227232
if s.filter.Matches(idx, value.Content[i], root) {
228233
result = append(result, value.Content[i])

pkg/jsonpath/yaml_query_test.go

+24-2
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ store:
107107
parser := newParserPrivate(tokenizer, tokenizer.Tokenize())
108108
err = parser.parse()
109109
if err != nil {
110-
t.Errorf("Error parsing JSON ast: %v", err)
110+
t.Errorf("Error parsing JSON Path: %v", err)
111111
return
112112
}
113113

@@ -233,6 +233,28 @@ deeply:
233233
`,
234234
expected: []string{"key1", "key2", "key3", "key4"},
235235
},
236+
{
237+
name: "Custom x-my-ignore extension filter",
238+
input: "$.paths[?@[\"x-my-ignore\"][?@ == \"match\"]].found",
239+
yaml: `
240+
openapi: 3.1.0
241+
info:
242+
title: Test
243+
version: 0.1.0
244+
summary: Test Summary
245+
description: |-
246+
Some test description.
247+
About our test document.
248+
paths:
249+
/anything/ignored:
250+
x-my-ignore: [match, not_matched]
251+
found: true
252+
/anything/not-ignored:
253+
x-my-ignore: [not_matched]
254+
found: false
255+
`,
256+
expected: []string{"true"},
257+
},
236258
}
237259

238260
for _, test := range tests {
@@ -248,7 +270,7 @@ deeply:
248270
parser := newParserPrivate(tokenizer, tokenizer.Tokenize(), config.WithPropertyNameExtension())
249271
err = parser.parse()
250272
if err != nil {
251-
t.Errorf("Error parsing JSON ast: %v", err)
273+
t.Errorf("Error parsing JSON Path: %v", err)
252274
return
253275
}
254276

pkg/overlay/apply.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,13 @@ func removeNode(idx parentIndex, node *yaml.Node) {
5959
if child == node {
6060
switch parent.Kind {
6161
case yaml.MappingNode:
62-
// we have to delete the key too
63-
parent.Content = append(parent.Content[:i-1], parent.Content[i+1:]...)
62+
if i%2 == 1 {
63+
// if we select a value, we should delete the key too
64+
parent.Content = append(parent.Content[:i-1], parent.Content[i+1:]...)
65+
} else {
66+
// if we select a key, we should delete the value
67+
parent.Content = append(parent.Content[:i], parent.Content[i+1:]...)
68+
}
6469
return
6570
case yaml.SequenceNode:
6671
parent.Content = append(parent.Content[:i], parent.Content[i+1:]...)

pkg/overlay/schema.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ type Extensions map[string]any
1111

1212
// Overlay is the top-level configuration for an OpenAPI overlay.
1313
type Overlay struct {
14-
Extensions `yaml:"-,inline"`
14+
Extensions Extensions `yaml:",inline"`
1515

16-
// Version is the version of the overlay configuration. As the RFC was never
17-
// really ratifies, this value does not mean much.
16+
// Version is the version of the overlay configuration.
1817
Version string `yaml:"overlay"`
1918

19+
// JSONPathVersion should be set to rfc9535, and is used for backwards compatability purposes
20+
JSONPathVersion string `yaml:"x-speakeasy-jsonpath,omitempty"`
21+
2022
// Info describes the metadata for the overlay.
2123
Info Info `yaml:"info"`
2224

pkg/overlay/testdata/openapi-overlayed.yaml

+1-32
Original file line numberDiff line numberDiff line change
@@ -106,38 +106,7 @@ paths:
106106
/drinks:
107107
x-speakeasy-note:
108108
"$ref": "./removeNote.yaml"
109-
/drink/{name}: #TODO: this should be by product code and we should have search by name
110-
get:
111-
operationId: getDrink
112-
summary: Get a drink.
113-
description: Get a drink by name, if authenticated this will include stock levels and product codes otherwise it will only include public information.
114-
tags:
115-
- drinks
116-
parameters:
117-
- name: name
118-
in: path
119-
required: true
120-
schema:
121-
type: string
122-
- x-parameter-extension: foo
123-
name: test
124-
description: Test parameter
125-
in: query
126-
schema:
127-
type: string
128-
responses:
129-
"200":
130-
description: Test response
131-
content:
132-
application/json:
133-
schema:
134-
$ref: "#/components/schemas/Drink"
135-
type: string
136-
x-response-extension: foo
137-
"5XX":
138-
$ref: "#/components/responses/APIError"
139-
default:
140-
$ref: "#/components/responses/UnknownError"
109+
/drink/{name}: {}
141110
/ingredients:
142111
get:
143112
operationId: listIngredients

pkg/overlay/testdata/openapi.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ paths:
131131
default:
132132
$ref: "#/components/responses/UnknownError"
133133

134-
/drink/{name}: #TODO: this should be by product code and we should have search by name
134+
/drink/{name}:
135135
get:
136136
operationId: getDrink
137137
summary: Get a drink.

pkg/overlay/testdata/overlay-generated.yaml

+2-16
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,5 @@ actions:
1818
"$ref": "./removeNote.yaml"
1919
- target: $["paths"]["/drinks"]["get"]
2020
remove: true
21-
- target: $["paths"]["/drink/{name}"]["get"]["parameters"]
22-
update:
23-
- x-parameter-extension: foo
24-
name: test
25-
description: Test parameter
26-
in: query
27-
schema:
28-
type: string
29-
- target: $["paths"]["/drink/{name}"]["get"]["responses"]["200"]["description"]
30-
update: Test response
31-
- target: $["paths"]["/drink/{name}"]["get"]["responses"]["200"]["content"]["application/json"]["schema"]
32-
update:
33-
type: string
34-
- target: $["paths"]["/drink/{name}"]["get"]["responses"]["200"]
35-
update:
36-
x-response-extension: foo
21+
- target: $["paths"]["/drink/{name}"]["get"]
22+
remove: true

pkg/overlay/testdata/overlay.yaml

+3-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ actions:
2626
- target: $.paths["/drinks"].get
2727
description: Test remove
2828
remove: true
29-
x-action-extension: bar
29+
- target: $.paths["/drink/{name}"].get~
30+
description: Test removing a key -- should delete the node too
31+
remove: true
3032
- target: $.paths["/drinks"]
3133
update:
3234
x-speakeasy-note:

web/src/Playground.tsx

+50-63
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,54 @@ function Playground() {
8585
[],
8686
);
8787

88+
const onChangeOverlay = useCallback(
89+
async (value: string | undefined, _: editor.IModelContentChangedEvent) => {
90+
try {
91+
setChangedLoading(true);
92+
result.current = value || "";
93+
const response = await ApplyOverlay(
94+
original.current,
95+
result.current,
96+
true,
97+
);
98+
if (response.type == "success") {
99+
setApplyOverlayMode("original+overlay");
100+
changed.current = response.result || "";
101+
setError("");
102+
setOverlayMarkers([]);
103+
const info = await GetInfo(changed.current, false);
104+
tryHandlePageTitle(JSON.parse(info));
105+
} else if (response.type == "incomplete") {
106+
setApplyOverlayMode("jsonpathexplorer");
107+
changed.current = response.result || "";
108+
setError("");
109+
setOverlayMarkers([]);
110+
} else if (response.type == "error") {
111+
setApplyOverlayMode("jsonpathexplorer");
112+
setOverlayMarkers([
113+
{
114+
startLineNumber: response.line,
115+
endLineNumber: response.line,
116+
startColumn: response.col,
117+
endColumn: response.col + 1000, // end of line
118+
message: response.error,
119+
severity: MarkerSeverity.Error, // Use MarkerSeverity from Monaco
120+
},
121+
]);
122+
}
123+
} catch (e: unknown) {
124+
if (e instanceof Error) {
125+
setError(e.message);
126+
}
127+
} finally {
128+
setChangedLoading(false);
129+
}
130+
},
131+
[],
132+
);
133+
134+
const onChangeOverlayDebounced = useDebounceCallback(onChangeOverlay, 500);
135+
88136
const getShareUrl = useCallback(async () => {
89137
try {
90138
setShareUrlLoading(true);
@@ -146,20 +194,7 @@ function Playground() {
146194
original.current = decompressed.original;
147195
result.current = decompressed.result;
148196

149-
const changedNew = await ApplyOverlay(
150-
original.current,
151-
result.current,
152-
false,
153-
);
154-
if (changedNew.type == "success") {
155-
const info = await GetInfo(original.current, false);
156-
const parsedInfo = JSON.parse(info);
157-
tryHandlePageTitle(parsedInfo);
158-
posthog.capture("overlay.speakeasy.com:load-shared", {
159-
openapi: parsedInfo,
160-
});
161-
changed.current = changedNew.result;
162-
}
197+
await onChangeOverlay(result.current, {} as any);
163198
} catch (error: any) {
164199
console.error("invalid share url:", error.message);
165200
}
@@ -241,54 +276,6 @@ function Playground() {
241276

242277
const onChangeBDebounced = useDebounceCallback(onChangeB, 500);
243278

244-
const onChangeC = useCallback(
245-
async (value: string | undefined, _: editor.IModelContentChangedEvent) => {
246-
try {
247-
setChangedLoading(true);
248-
result.current = value || "";
249-
const response = await ApplyOverlay(
250-
original.current,
251-
result.current,
252-
true,
253-
);
254-
if (response.type == "success") {
255-
setApplyOverlayMode("original+overlay");
256-
changed.current = response.result || "";
257-
setError("");
258-
setOverlayMarkers([]);
259-
const info = await GetInfo(changed.current, false);
260-
tryHandlePageTitle(JSON.parse(info));
261-
} else if (response.type == "incomplete") {
262-
setApplyOverlayMode("jsonpathexplorer");
263-
changed.current = response.result || "";
264-
setError("");
265-
setOverlayMarkers([]);
266-
} else if (response.type == "error") {
267-
setApplyOverlayMode("jsonpathexplorer");
268-
setOverlayMarkers([
269-
{
270-
startLineNumber: response.line,
271-
endLineNumber: response.line,
272-
startColumn: response.col,
273-
endColumn: response.col + 1000, // end of line
274-
message: response.error,
275-
severity: MarkerSeverity.Error, // Use MarkerSeverity from Monaco
276-
},
277-
]);
278-
}
279-
} catch (e: unknown) {
280-
if (e instanceof Error) {
281-
setError(e.message);
282-
}
283-
} finally {
284-
setChangedLoading(false);
285-
}
286-
},
287-
[],
288-
);
289-
290-
const onChangeCDebounced = useDebounceCallback(onChangeC, 500);
291-
292279
const ref = useRef<ImperativePanelGroupHandle>(null);
293280

294281
const maxLayout = useCallback((index: number) => {
@@ -460,7 +447,7 @@ function Playground() {
460447
<Editor
461448
readonly={false}
462449
value={result.current}
463-
onChange={onChangeCDebounced}
450+
onChange={onChangeOverlayDebounced}
464451
loading={resultLoading}
465452
markers={overlayMarkers}
466453
title={"Overlay"}

web/src/assets/wasm/lib.wasm

55.4 KB
Binary file not shown.

0 commit comments

Comments
 (0)