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