Skip to content

Commit 1553395

Browse files
ndeloofglours
authored andcommitted
adopt xeipuuv/gojsonschema
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
1 parent a6c896c commit 1553395

File tree

5 files changed

+164
-198
lines changed

5 files changed

+164
-198
lines changed

go.mod

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ require (
1010
github.com/google/go-cmp v0.5.9
1111
github.com/mattn/go-shellwords v1.0.12
1212
github.com/opencontainers/go-digest v1.0.0
13+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
1314
github.com/sirupsen/logrus v1.9.0
1415
github.com/stretchr/testify v1.8.4
15-
github.com/xeipuuv/gojsonschema v1.2.0
1616
github.com/xhit/go-str2duration/v2 v2.1.0
1717
golang.org/x/sync v0.3.0
1818
gopkg.in/yaml.v3 v3.0.1
@@ -22,7 +22,6 @@ require (
2222
require (
2323
github.com/davecgh/go-spew v1.1.1 // indirect
2424
github.com/pmezard/go-difflib v1.0.0 // indirect
25-
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
26-
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
27-
golang.org/x/sys v0.1.0 // indirect
25+
golang.org/x/sys v0.5.0 // indirect
26+
golang.org/x/text v0.14.0 // indirect
2827
)

go.sum

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
33
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
44
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
55
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
6+
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
7+
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
68
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
79
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
810
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -18,19 +20,16 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
1820
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
1921
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2022
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
23+
github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis=
24+
github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4=
25+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
26+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
2127
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
2228
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
2329
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
24-
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
2530
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
2631
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
2732
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
28-
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
29-
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
30-
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
31-
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
32-
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
33-
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
3433
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
3534
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
3635
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -50,10 +49,12 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
5049
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
5150
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
5251
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
53-
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
54-
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
52+
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
53+
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
5554
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
5655
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
56+
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
57+
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
5758
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
5859
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
5960
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=

loader/loader_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -640,7 +640,7 @@ services:
640640
environment:
641641
FOO: ["1"]
642642
`)
643-
assert.ErrorContains(t, err, "services.dict-env.environment.FOO must be a string, number, boolean or null")
643+
assert.ErrorContains(t, err, "services.dict-env.environment.FOO must be a boolean, null, number or string")
644644
}
645645

646646
func TestInvalidEnvironmentObject(t *testing.T) {
@@ -1054,7 +1054,7 @@ func TestInvalidResource(t *testing.T) {
10541054
impossible:
10551055
x: 1
10561056
`)
1057-
assert.ErrorContains(t, err, "Additional property impossible is not allowed")
1057+
assert.ErrorContains(t, err, "additional properties 'impossible' not allowed")
10581058
}
10591059

10601060
func TestInvalidExternalAndDriverCombination(t *testing.T) {
@@ -1196,7 +1196,7 @@ services:
11961196
foo:
11971197
bar: zot
11981198
`)
1199-
assert.ErrorContains(t, err, "services.tmpfs.volumes.0 Additional property foo is not allowed")
1199+
assert.ErrorContains(t, err, "services.tmpfs.volumes.0 additional properties 'foo' not allowed")
12001200
}
12011201

12021202
func TestLoadBindMountSourceMustNotBeEmpty(t *testing.T) {
@@ -1350,7 +1350,7 @@ services:
13501350
tmpfs:
13511351
size: -1
13521352
`)
1353-
assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size Must be greater than or equal to 0")
1353+
assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size must be greater than or equal to 0")
13541354
}
13551355

13561356
func TestLoadTmpfsVolumeSizeMustBeInteger(t *testing.T) {
@@ -2264,7 +2264,7 @@ services:
22642264
- driver: nvidia
22652265
count: 2
22662266
`)
2267-
assert.ErrorContains(t, err, `capabilities is required`)
2267+
assert.ErrorContains(t, err, "missing property 'capabilities'")
22682268
}
22692269

22702270
func TestServiceGpus(t *testing.T) {
@@ -3132,7 +3132,7 @@ services:
31323132
`, nil), func(options *Options) {
31333133
options.ResolvePaths = false
31343134
})
3135-
assert.ErrorContains(t, err, "validating filename0.yml: services.frontend.develop.watch.0 action is required")
3135+
assert.ErrorContains(t, err, "services.frontend.develop.watch.0 missing property 'action'")
31363136
}
31373137

31383138
func TestBadServiceConfig(t *testing.T) {

schema/schema.go

Lines changed: 71 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -19,35 +19,25 @@ package schema
1919
import (
2020
// Enable support for embedded static resources
2121
_ "embed"
22+
"errors"
2223
"fmt"
24+
"slices"
2325
"strings"
2426
"time"
2527

26-
"github.com/xeipuuv/gojsonschema"
28+
"github.com/santhosh-tekuri/jsonschema/v6"
29+
"github.com/santhosh-tekuri/jsonschema/v6/kind"
30+
"golang.org/x/text/language"
31+
"golang.org/x/text/message"
2732
)
2833

29-
type portsFormatChecker struct{}
30-
31-
func (checker portsFormatChecker) IsFormat(_ interface{}) bool {
32-
// TODO: implement this
33-
return true
34-
}
35-
36-
type durationFormatChecker struct{}
37-
38-
func (checker durationFormatChecker) IsFormat(input interface{}) bool {
34+
func durationFormatChecker(input any) error {
3935
value, ok := input.(string)
4036
if !ok {
41-
return false
37+
return fmt.Errorf("expected string")
4238
}
4339
_, err := time.ParseDuration(value)
44-
return err == nil
45-
}
46-
47-
func init() {
48-
gojsonschema.FormatCheckers.Add("expose", portsFormatChecker{})
49-
gojsonschema.FormatCheckers.Add("ports", portsFormatChecker{})
50-
gojsonschema.FormatCheckers.Add("duration", durationFormatChecker{})
40+
return err
5141
}
5242

5343
// Schema is the compose-spec JSON schema
@@ -57,108 +47,88 @@ var Schema string
5747

5848
// Validate uses the jsonschema to validate the configuration
5949
func Validate(config map[string]interface{}) error {
60-
schemaLoader := gojsonschema.NewStringLoader(Schema)
61-
dataLoader := gojsonschema.NewGoLoader(config)
62-
63-
result, err := gojsonschema.Validate(schemaLoader, dataLoader)
50+
compiler := jsonschema.NewCompiler()
51+
json, err := jsonschema.UnmarshalJSON(strings.NewReader(Schema))
6452
if err != nil {
6553
return err
6654
}
67-
68-
if !result.Valid() {
69-
return toError(result)
55+
err = compiler.AddResource("compose-spec.json", json)
56+
if err != nil {
57+
return err
58+
}
59+
compiler.RegisterFormat(&jsonschema.Format{
60+
Name: "duration",
61+
Validate: durationFormatChecker,
62+
})
63+
schema := compiler.MustCompile("compose-spec.json")
64+
err = schema.Validate(config)
65+
var verr *jsonschema.ValidationError
66+
if ok := errors.As(err, &verr); ok {
67+
return validationError{getMostSpecificError(verr)}
7068
}
71-
72-
return nil
73-
}
74-
75-
func toError(result *gojsonschema.Result) error {
76-
err := getMostSpecificError(result.Errors())
7769
return err
7870
}
7971

80-
const (
81-
jsonschemaOneOf = "number_one_of"
82-
jsonschemaAnyOf = "number_any_of"
83-
)
72+
type validationError struct {
73+
err *jsonschema.ValidationError
74+
}
8475

85-
func getDescription(err validationError) string {
86-
switch err.parent.Type() {
87-
case "invalid_type":
88-
if expectedType, ok := err.parent.Details()["expected"].(string); ok {
89-
return fmt.Sprintf("must be a %s", humanReadableType(expectedType))
90-
}
91-
case jsonschemaOneOf, jsonschemaAnyOf:
92-
if err.child == nil {
93-
return err.parent.Description()
94-
}
95-
return err.child.Description()
76+
func (e validationError) Error() string {
77+
path := strings.Join(e.err.InstanceLocation, ".")
78+
p := message.NewPrinter(language.English)
79+
switch k := e.err.ErrorKind.(type) {
80+
case *kind.Type:
81+
return fmt.Sprintf("%s must be a %s", path, humanReadableType(k.Want...))
82+
case *kind.Minimum:
83+
return fmt.Sprintf("%s must be greater than or equal to %s", path, k.Want.Num())
84+
case *kind.Maximum:
85+
return fmt.Sprintf("%s must be less than or equal to %s", path, k.Want.Num())
9686
}
97-
return err.parent.Description()
87+
return fmt.Sprintf("%s %s", path, e.err.ErrorKind.LocalizedString(p))
9888
}
9989

100-
func humanReadableType(definition string) string {
101-
if definition[0:1] == "[" {
102-
allTypes := strings.Split(definition[1:len(definition)-1], ",")
103-
for i, t := range allTypes {
104-
allTypes[i] = humanReadableType(t)
90+
func humanReadableType(want ...string) string {
91+
if len(want) == 1 {
92+
switch want[0] {
93+
case "object":
94+
return "mapping"
95+
default:
96+
return want[0]
10597
}
106-
return fmt.Sprintf(
107-
"%s or %s",
108-
strings.Join(allTypes[0:len(allTypes)-1], ", "),
109-
allTypes[len(allTypes)-1],
110-
)
111-
}
112-
if definition == "object" {
113-
return "mapping"
11498
}
115-
if definition == "array" {
116-
return "list"
117-
}
118-
return definition
119-
}
12099

121-
type validationError struct {
122-
parent gojsonschema.ResultError
123-
child gojsonschema.ResultError
124-
}
100+
for i, s := range want {
101+
want[i] = humanReadableType(s)
102+
}
125103

126-
func (err validationError) Error() string {
127-
description := getDescription(err)
128-
return fmt.Sprintf("%s %s", err.parent.Field(), description)
104+
slices.Sort(want)
105+
return fmt.Sprintf(
106+
"%s or %s",
107+
strings.Join(want[0:len(want)-1], ", "),
108+
want[len(want)-1],
109+
)
129110
}
130111

131-
func getMostSpecificError(errors []gojsonschema.ResultError) validationError {
132-
mostSpecificError := 0
133-
for i, err := range errors {
134-
if specificity(err) > specificity(errors[mostSpecificError]) {
135-
mostSpecificError = i
136-
continue
137-
}
138-
139-
if specificity(err) == specificity(errors[mostSpecificError]) {
140-
// Invalid type errors win in a tie-breaker for most specific field name
141-
if err.Type() == "invalid_type" && errors[mostSpecificError].Type() != "invalid_type" {
142-
mostSpecificError = i
143-
}
144-
}
145-
}
146-
147-
if mostSpecificError+1 == len(errors) {
148-
return validationError{parent: errors[mostSpecificError]}
112+
func getMostSpecificError(err *jsonschema.ValidationError) *jsonschema.ValidationError {
113+
var mostSpecificError *jsonschema.ValidationError
114+
if len(err.Causes) == 0 {
115+
return err
149116
}
150-
151-
switch errors[mostSpecificError].Type() {
152-
case "number_one_of", "number_any_of":
153-
return validationError{
154-
parent: errors[mostSpecificError],
155-
child: errors[mostSpecificError+1],
117+
for _, cause := range err.Causes {
118+
cause = getMostSpecificError(cause)
119+
if specificity(cause) > specificity(mostSpecificError) {
120+
mostSpecificError = cause
156121
}
157-
default:
158-
return validationError{parent: errors[mostSpecificError]}
159122
}
123+
return mostSpecificError
160124
}
161125

162-
func specificity(err gojsonschema.ResultError) int {
163-
return len(strings.Split(err.Field(), "."))
126+
func specificity(err *jsonschema.ValidationError) int {
127+
if err == nil {
128+
return -1
129+
}
130+
if _, ok := err.ErrorKind.(*kind.AdditionalProperties); ok {
131+
return len(err.InstanceLocation) + 1
132+
}
133+
return len(err.InstanceLocation)
164134
}

0 commit comments

Comments
 (0)