@@ -19,35 +19,25 @@ package schema
19
19
import (
20
20
// Enable support for embedded static resources
21
21
_ "embed"
22
+ "errors"
22
23
"fmt"
24
+ "slices"
23
25
"strings"
24
26
"time"
25
27
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"
27
32
)
28
33
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 {
39
35
value , ok := input .(string )
40
36
if ! ok {
41
- return false
37
+ return fmt . Errorf ( "expected string" )
42
38
}
43
39
_ , 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
51
41
}
52
42
53
43
// Schema is the compose-spec JSON schema
@@ -57,108 +47,88 @@ var Schema string
57
47
58
48
// Validate uses the jsonschema to validate the configuration
59
49
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 ))
64
52
if err != nil {
65
53
return err
66
54
}
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 )}
70
68
}
71
-
72
- return nil
73
- }
74
-
75
- func toError (result * gojsonschema.Result ) error {
76
- err := getMostSpecificError (result .Errors ())
77
69
return err
78
70
}
79
71
80
- const (
81
- jsonschemaOneOf = "number_one_of"
82
- jsonschemaAnyOf = "number_any_of"
83
- )
72
+ type validationError struct {
73
+ err * jsonschema.ValidationError
74
+ }
84
75
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 ())
96
86
}
97
- return err .parent . Description ( )
87
+ return fmt . Sprintf ( "%s %s" , path , e . err .ErrorKind . LocalizedString ( p ) )
98
88
}
99
89
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 ]
105
97
}
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"
114
98
}
115
- if definition == "array" {
116
- return "list"
117
- }
118
- return definition
119
- }
120
99
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
+ }
125
103
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
+ )
129
110
}
130
111
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
149
116
}
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
156
121
}
157
- default :
158
- return validationError {parent : errors [mostSpecificError ]}
159
122
}
123
+ return mostSpecificError
160
124
}
161
125
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 )
164
134
}
0 commit comments