Skip to content

Commit 2869aa7

Browse files
authored
feat: rfc9535 compatability (#2)
* chore: hackweek * chore: tinker * chore: cleanup * chore: add the jsonpath-compliance-test-suite * chore: implement more stuff * chore: parse filter expressions * chore: add functions into tokenizer * chore: parse filter expressions * chore: evaluate * chore: basic evaluation * chore: make more private * chore: work through compliance suite * continuing to tinker * chore: support basic unions * chore: basic eval * chore: standard filters * chore: filter matches * chore: more conformance * chore: more conformance * chore: more conformance * chore: slice sanitization * chore: conformance * chore: fix slices * chore: type validation * chore: fixup functions * chore: pass conformance test * chore: clean up tests after conformance test * chore: remove unnecessary comment * chore: bridge, service workers * chore: commentary * chore: allow superceding messages * chore: setup defaults * chore: add wasm to git
1 parent 69a55f2 commit 2869aa7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+7337
-663
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.idea
2-
dist
2+
dist
3+
*.iml

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "jsonpath-compliance-test-suite"]
2+
path = jsonpath-compliance-test-suite
3+
url = [email protected]:jsonpath-standard/jsonpath-compliance-test-suite

Makefile

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,18 @@ web/src/assets/wasm/lib.wasm: $(SOURCE)
77
mkdir -p dist
88
rm -f dist/*
99
cp "$(shell go env GOROOT)/misc/wasm/wasm_exec.js" web/src/assets/wasm/wasm_exec.js
10-
GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o ./web/src/assets/wasm/lib.wasm cmd/wasm/main.go
10+
GOOS=js GOARCH=wasm go build -o ./web/src/assets/wasm/lib.wasm cmd/wasm/functions.go
11+
12+
.PHONY: tinygo web/src/assets/wasm/lib.tinygo.wasm web/src/assets/wasm/lib.wasm
13+
tinygo:
14+
brew tap tinygo-org/tools
15+
brew install tinygo
16+
17+
18+
web/src/assets/wasm/lib.tinygo.wasm: $(SOURCE)
19+
echo "Not working: requires regex which is not supported by tinygo"
20+
exit 1
21+
mkdir -p dist
22+
rm -f dist/*
23+
cp "${shell brew --prefix tinygo}/targets/wasm_exec.js" web/src/assets/wasm/wasm_exec.js
24+
GOOS=js GOARCH=wasm tinygo build -target=wasm -o ./web/src/assets/wasm/lib.tinygo.wasm cmd/wasm/functions.go

cmd/wasm/functions.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//go:build js && wasm
2+
3+
package main
4+
5+
import (
6+
"fmt"
7+
"github.com/speakeasy-api/jsonpath/pkg/overlay"
8+
"gopkg.in/yaml.v3"
9+
"syscall/js"
10+
)
11+
12+
func CalculateOverlay(originalYAML, targetYAML string) (string, error) {
13+
var orig yaml.Node
14+
err := yaml.Unmarshal([]byte(originalYAML), &orig)
15+
if err != nil {
16+
return "", fmt.Errorf("failed to parse source schema: %w", err)
17+
}
18+
var target yaml.Node
19+
err = yaml.Unmarshal([]byte(targetYAML), &target)
20+
if err != nil {
21+
return "", fmt.Errorf("failed to parse target schema: %w", err)
22+
}
23+
24+
overlay, err := overlay.Compare("example overlay", &orig, target)
25+
if err != nil {
26+
return "", fmt.Errorf("failed to compare schemas: %w", err)
27+
}
28+
out, err := yaml.Marshal(overlay)
29+
if err != nil {
30+
return "", fmt.Errorf("failed to marshal schema: %w", err)
31+
}
32+
33+
return string(out), nil
34+
}
35+
36+
func ApplyOverlay(originalYAML, overlayYAML string) (string, error) {
37+
var orig yaml.Node
38+
err := yaml.Unmarshal([]byte(originalYAML), &orig)
39+
if err != nil {
40+
return "", fmt.Errorf("failed to parse original schema: %w", err)
41+
}
42+
43+
var overlay overlay.Overlay
44+
err = yaml.Unmarshal([]byte(overlayYAML), &overlay)
45+
if err != nil {
46+
return "", fmt.Errorf("failed to parse overlay schema: %w", err)
47+
}
48+
49+
err = overlay.ApplyTo(&orig)
50+
if err != nil {
51+
return "", fmt.Errorf("failed to apply overlay: %w", err)
52+
}
53+
54+
// Unwrap the document node if it exists and has only one content node
55+
if orig.Kind == yaml.DocumentNode && len(orig.Content) == 1 {
56+
orig = *orig.Content[0]
57+
}
58+
59+
out, err := yaml.Marshal(&orig)
60+
if err != nil {
61+
return "", fmt.Errorf("failed to marshal result: %w", err)
62+
}
63+
64+
return string(out), nil
65+
}
66+
67+
func promisify(fn func(args []js.Value) (string, error)) js.Func {
68+
return js.FuncOf(func(this js.Value, args []js.Value) any {
69+
// Handler for the Promise
70+
handler := js.FuncOf(func(this js.Value, promiseArgs []js.Value) interface{} {
71+
resolve := promiseArgs[0]
72+
reject := promiseArgs[1]
73+
74+
// Run this code asynchronously
75+
go func() {
76+
result, err := fn(args)
77+
if err != nil {
78+
errorConstructor := js.Global().Get("Error")
79+
errorObject := errorConstructor.New(err.Error())
80+
reject.Invoke(errorObject)
81+
return
82+
}
83+
84+
resolve.Invoke(result)
85+
}()
86+
87+
// The handler of a Promise doesn't return any value
88+
return nil
89+
})
90+
91+
// Create and return the Promise object
92+
promiseConstructor := js.Global().Get("Promise")
93+
return promiseConstructor.New(handler)
94+
})
95+
}
96+
97+
func main() {
98+
js.Global().Set("CalculateOverlay", promisify(func(args []js.Value) (string, error) {
99+
if len(args) != 2 {
100+
return "", fmt.Errorf("CalculateOverlay: expected 2 args, got %v", len(args))
101+
}
102+
103+
return CalculateOverlay(args[0].String(), args[1].String())
104+
}))
105+
106+
js.Global().Set("ApplyOverlay", promisify(func(args []js.Value) (string, error) {
107+
if len(args) != 2 {
108+
return "", fmt.Errorf("ApplyOverlay: expected 2 args, got %v", len(args))
109+
}
110+
111+
return ApplyOverlay(args[0].String(), args[1].String())
112+
}))
113+
114+
<-make(chan bool)
115+
}

cmd/wasm/main.go

Lines changed: 0 additions & 45 deletions
This file was deleted.

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ go 1.21.0
55
toolchain go1.22.2
66

77
require (
8+
github.com/pmezard/go-difflib v1.0.0
89
github.com/speakeasy-api/openapi-overlay v0.6.0
910
github.com/stretchr/testify v1.9.0
1011
github.com/vmware-labs/yaml-jsonpath v0.3.2
1112
gopkg.in/yaml.v3 v3.0.1
13+
sigs.k8s.io/yaml v1.4.0
1214
)
1315

1416
require (
1517
github.com/davecgh/go-spew v1.1.1 // indirect
1618
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
17-
github.com/pmezard/go-difflib v1.0.0 // indirect
1819
)

go.sum

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
2323
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
2424
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
2525
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
26-
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
2726
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
27+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
28+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
2829
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
2930
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
3031
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -132,3 +133,5 @@ gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C
132133
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
133134
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
134135
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
136+
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
137+
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

integration_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package jsonpath_test
2+
3+
import (
4+
"encoding/json"
5+
"github.com/pmezard/go-difflib/difflib"
6+
"github.com/speakeasy-api/jsonpath/pkg/jsonpath"
7+
"github.com/stretchr/testify/require"
8+
"gopkg.in/yaml.v3"
9+
"os"
10+
"slices"
11+
"strings"
12+
"testing"
13+
)
14+
15+
type FullTestSuite struct {
16+
Description string `json:"description"`
17+
Tests []Test `json:"tests"`
18+
}
19+
type Test struct {
20+
Name string `json:"name"`
21+
Selector string `json:"selector"`
22+
Document interface{} `json:"document"`
23+
Result []interface{} `json:"result"`
24+
Results [][]interface{} `json:"results"`
25+
InvalidSelector bool `json:"invalid_selector"`
26+
Tags []string `json:"tags"`
27+
}
28+
29+
func TestJSONPathComplianceTestSuite(t *testing.T) {
30+
// Read the test suite JSON file
31+
file, err := os.ReadFile("./jsonpath-compliance-test-suite/cts.json")
32+
require.NoError(t, err, "Failed to read test suite file")
33+
// alter the file to delete any unicode tests: these break the yaml library we use..
34+
var testSuite FullTestSuite
35+
json.Unmarshal(file, &testSuite)
36+
for i := 0; i < len(testSuite.Tests); i++ {
37+
// if Tags contains "unicode", delete it
38+
shouldDelete := slices.Contains(testSuite.Tests[i].Tags, "unicode")
39+
// delete new line / some unicode tests -- these break the yaml parser
40+
shouldDelete = shouldDelete || strings.Contains(testSuite.Tests[i].Name, "line feed")
41+
shouldDelete = shouldDelete || strings.Contains(testSuite.Tests[i].Name, "carriage return")
42+
shouldDelete = shouldDelete || strings.Contains(testSuite.Tests[i].Name, "u2028")
43+
shouldDelete = shouldDelete || strings.Contains(testSuite.Tests[i].Name, "u2029")
44+
if shouldDelete {
45+
testSuite.Tests = append(testSuite.Tests[:i], testSuite.Tests[i+1:]...)
46+
i--
47+
}
48+
}
49+
50+
// Run each test case as a subtest
51+
for _, test := range testSuite.Tests {
52+
t.Run(test.Name, func(t *testing.T) {
53+
// Test case for a valid selector
54+
jp, err := jsonpath.NewPath(test.Selector)
55+
if test.InvalidSelector {
56+
require.Error(t, err, "Expected an error for invalid selector, but got none")
57+
return
58+
} else {
59+
require.NoError(t, err, "Failed to parse JSONPath selector")
60+
}
61+
// interface{} to yaml.Node
62+
toYAML := func(i interface{}) *yaml.Node {
63+
o, err := yaml.Marshal(i)
64+
require.NoError(t, err, "Failed to marshal interface to yaml")
65+
n := new(yaml.Node)
66+
err = yaml.Unmarshal(o, n)
67+
require.NoError(t, err, "Failed to unmarshal yaml to yaml.Node")
68+
// unwrap the document node
69+
if n.Kind == yaml.DocumentNode && len(n.Content) == 1 {
70+
n = n.Content[0]
71+
}
72+
return n
73+
}
74+
75+
result := jp.Query(toYAML(test.Document))
76+
77+
if test.Results != nil {
78+
expectedResults := make([][]*yaml.Node, 0)
79+
for _, expectedResult := range test.Results {
80+
expected := make([]*yaml.Node, 0)
81+
for _, expectedResult := range expectedResult {
82+
expected = append(expected, toYAML(expectedResult))
83+
}
84+
expectedResults = append(expectedResults, expected)
85+
}
86+
87+
// Test case with multiple possible results
88+
var found bool
89+
for i, _ := range test.Results {
90+
if match, msg := compareResults(result, expectedResults[i]); match {
91+
found = true
92+
break
93+
} else {
94+
t.Log(msg)
95+
}
96+
}
97+
if !found {
98+
t.Errorf("Unexpected result. Got: %v, Want one of: %v", result, test.Results)
99+
}
100+
} else {
101+
expectedResult := make([]*yaml.Node, 0)
102+
for _, res := range test.Result {
103+
expectedResult = append(expectedResult, toYAML(res))
104+
}
105+
// Test case with a single expected result
106+
if match, msg := compareResults(result, expectedResult); !match {
107+
t.Error(msg)
108+
}
109+
}
110+
})
111+
}
112+
}
113+
114+
func compareResults(actual, expected []*yaml.Node) (bool, string) {
115+
actualStr, err := yaml.Marshal(actual)
116+
if err != nil {
117+
return false, "Failed to serialize actual result: " + err.Error()
118+
}
119+
120+
expectedStr, err := yaml.Marshal(expected)
121+
if err != nil {
122+
return false, "Failed to serialize expected result: " + err.Error()
123+
}
124+
125+
if string(actualStr) == string(expectedStr) {
126+
return true, ""
127+
}
128+
129+
// Use a differ library to generate a nice diff string
130+
// You can use a package like github.com/pmezard/go-difflib/difflib
131+
diff := difflib.UnifiedDiff{
132+
A: difflib.SplitLines(string(expectedStr)),
133+
B: difflib.SplitLines(string(actualStr)),
134+
FromFile: "Expected",
135+
ToFile: "Actual",
136+
Context: 3,
137+
}
138+
diffStr, err := difflib.GetUnifiedDiffString(diff)
139+
if err != nil {
140+
return false, "Failed to generate diff: " + err.Error()
141+
}
142+
143+
return false, diffStr
144+
}

jsonpath-compliance-test-suite

0 commit comments

Comments
 (0)