github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/compose/schema/schema.go (about) 1 // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: 2 //go:build go1.19 3 4 package schema 5 6 import ( 7 "embed" 8 "fmt" 9 "strings" 10 "time" 11 12 "github.com/pkg/errors" 13 "github.com/xeipuuv/gojsonschema" 14 ) 15 16 const ( 17 defaultVersion = "3.12" 18 versionField = "version" 19 ) 20 21 type portsFormatChecker struct{} 22 23 func (checker portsFormatChecker) IsFormat(_ any) bool { 24 // TODO: implement this 25 return true 26 } 27 28 type durationFormatChecker struct{} 29 30 func (checker durationFormatChecker) IsFormat(input any) bool { 31 value, ok := input.(string) 32 if !ok { 33 return false 34 } 35 _, err := time.ParseDuration(value) 36 return err == nil 37 } 38 39 func init() { 40 gojsonschema.FormatCheckers.Add("expose", portsFormatChecker{}) 41 gojsonschema.FormatCheckers.Add("ports", portsFormatChecker{}) 42 gojsonschema.FormatCheckers.Add("duration", durationFormatChecker{}) 43 } 44 45 // Version returns the version of the config, defaulting to the latest "3.x" 46 // version (3.12). If only the major version "3" is specified, it is used as 47 // version "3.x" and returns the default version (latest 3.x). 48 func Version(config map[string]any) string { 49 version, ok := config[versionField] 50 if !ok { 51 return defaultVersion 52 } 53 return normalizeVersion(fmt.Sprintf("%v", version)) 54 } 55 56 func normalizeVersion(version string) string { 57 switch version { 58 case "", "3": 59 return defaultVersion 60 default: 61 return version 62 } 63 } 64 65 //go:embed data/config_schema_v*.json 66 var schemas embed.FS 67 68 // Validate uses the jsonschema to validate the configuration 69 func Validate(config map[string]any, version string) error { 70 version = normalizeVersion(version) 71 schemaData, err := schemas.ReadFile("data/config_schema_v" + version + ".json") 72 if err != nil { 73 return errors.Errorf("unsupported Compose file version: %s", version) 74 } 75 76 schemaLoader := gojsonschema.NewStringLoader(string(schemaData)) 77 dataLoader := gojsonschema.NewGoLoader(config) 78 79 result, err := gojsonschema.Validate(schemaLoader, dataLoader) 80 if err != nil { 81 return err 82 } 83 84 if !result.Valid() { 85 return toError(result) 86 } 87 88 return nil 89 } 90 91 func toError(result *gojsonschema.Result) error { 92 err := getMostSpecificError(result.Errors()) 93 return err 94 } 95 96 const ( 97 jsonschemaOneOf = "number_one_of" 98 jsonschemaAnyOf = "number_any_of" 99 ) 100 101 func getDescription(err validationError) string { 102 switch err.parent.Type() { 103 case "invalid_type": 104 if expectedType, ok := err.parent.Details()["expected"].(string); ok { 105 return fmt.Sprintf("must be a %s", humanReadableType(expectedType)) 106 } 107 case jsonschemaOneOf, jsonschemaAnyOf: 108 if err.child == nil { 109 return err.parent.Description() 110 } 111 return err.child.Description() 112 } 113 return err.parent.Description() 114 } 115 116 func humanReadableType(definition string) string { 117 if definition[0:1] == "[" { 118 allTypes := strings.Split(definition[1:len(definition)-1], ",") 119 for i, t := range allTypes { 120 allTypes[i] = humanReadableType(t) 121 } 122 return fmt.Sprintf( 123 "%s or %s", 124 strings.Join(allTypes[0:len(allTypes)-1], ", "), 125 allTypes[len(allTypes)-1], 126 ) 127 } 128 if definition == "object" { 129 return "mapping" 130 } 131 if definition == "array" { 132 return "list" 133 } 134 return definition 135 } 136 137 type validationError struct { 138 parent gojsonschema.ResultError 139 child gojsonschema.ResultError 140 } 141 142 func (err validationError) Error() string { 143 description := getDescription(err) 144 return fmt.Sprintf("%s %s", err.parent.Field(), description) 145 } 146 147 func getMostSpecificError(errs []gojsonschema.ResultError) validationError { 148 mostSpecificError := 0 149 for i, err := range errs { 150 if specificity(err) > specificity(errs[mostSpecificError]) { 151 mostSpecificError = i 152 continue 153 } 154 155 if specificity(err) == specificity(errs[mostSpecificError]) { 156 // Invalid type errors win in a tie-breaker for most specific field name 157 if err.Type() == "invalid_type" && errs[mostSpecificError].Type() != "invalid_type" { 158 mostSpecificError = i 159 } 160 } 161 } 162 163 if mostSpecificError+1 == len(errs) { 164 return validationError{parent: errs[mostSpecificError]} 165 } 166 167 switch errs[mostSpecificError].Type() { 168 case "number_one_of", "number_any_of": 169 return validationError{ 170 parent: errs[mostSpecificError], 171 child: errs[mostSpecificError+1], 172 } 173 default: 174 return validationError{parent: errs[mostSpecificError]} 175 } 176 } 177 178 func specificity(err gojsonschema.ResultError) int { 179 return len(strings.Split(err.Field(), ".")) 180 }