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