@@ -19,35 +19,25 @@ package schema
1919import (
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,89 @@ var Schema string
5747
5848// Validate uses the jsonschema to validate the configuration
5949func 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- )
11198 }
112- if definition == "object" {
113- return "mapping"
114- }
115- if definition == "array" {
116- return "list"
99+
100+ for i , s := range want {
101+ want [i ] = humanReadableType (s )
117102 }
118- return definition
119- }
120103
121- type validationError struct {
122- parent gojsonschema.ResultError
123- child gojsonschema.ResultError
124- }
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+ )
125110
126- func (err validationError ) Error () string {
127- description := getDescription (err )
128- return fmt .Sprintf ("%s %s" , err .parent .Field (), description )
129111}
130112
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 ]}
113+ func getMostSpecificError (err * jsonschema.ValidationError ) * jsonschema.ValidationError {
114+ var mostSpecificError * jsonschema.ValidationError
115+ if len (err .Causes ) == 0 {
116+ return err
149117 }
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 ],
118+ for _ , cause := range err .Causes {
119+ cause = getMostSpecificError (cause )
120+ if specificity (cause ) > specificity (mostSpecificError ) {
121+ mostSpecificError = cause
156122 }
157- default :
158- return validationError {parent : errors [mostSpecificError ]}
159123 }
124+ return mostSpecificError
160125}
161126
162- func specificity (err gojsonschema.ResultError ) int {
163- return len (strings .Split (err .Field (), "." ))
127+ func specificity (err * jsonschema.ValidationError ) int {
128+ if err == nil {
129+ return - 1
130+ }
131+ if _ , ok := err .ErrorKind .(* kind.AdditionalProperties ); ok {
132+ return len (err .InstanceLocation ) + 1
133+ }
134+ return len (err .InstanceLocation )
164135}
0 commit comments