github.com/kunnos/engine@v1.13.1/cli/compose/schema/schema.go (about) 1 package schema 2 3 //go:generate go-bindata -pkg schema -nometadata 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 := Asset(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 description := getDescription(err) 82 return fmt.Errorf("%s %s", err.Field(), description) 83 } 84 85 func getDescription(err gojsonschema.ResultError) string { 86 if err.Type() == "invalid_type" { 87 if expectedType, ok := err.Details()["expected"].(string); ok { 88 return fmt.Sprintf("must be a %s", humanReadableType(expectedType)) 89 } 90 } 91 92 return err.Description() 93 } 94 95 func humanReadableType(definition string) string { 96 if definition[0:1] == "[" { 97 allTypes := strings.Split(definition[1:len(definition)-1], ",") 98 for i, t := range allTypes { 99 allTypes[i] = humanReadableType(t) 100 } 101 return fmt.Sprintf( 102 "%s or %s", 103 strings.Join(allTypes[0:len(allTypes)-1], ", "), 104 allTypes[len(allTypes)-1], 105 ) 106 } 107 if definition == "object" { 108 return "mapping" 109 } 110 if definition == "array" { 111 return "list" 112 } 113 return definition 114 } 115 116 func getMostSpecificError(errors []gojsonschema.ResultError) gojsonschema.ResultError { 117 var mostSpecificError gojsonschema.ResultError 118 119 for _, err := range errors { 120 if mostSpecificError == nil { 121 mostSpecificError = err 122 } else if specificity(err) > specificity(mostSpecificError) { 123 mostSpecificError = err 124 } else if specificity(err) == specificity(mostSpecificError) { 125 // Invalid type errors win in a tie-breaker for most specific field name 126 if err.Type() == "invalid_type" && mostSpecificError.Type() != "invalid_type" { 127 mostSpecificError = err 128 } 129 } 130 } 131 132 return mostSpecificError 133 } 134 135 func specificity(err gojsonschema.ResultError) int { 136 return len(strings.Split(err.Field(), ".")) 137 }