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