package schema //go:generate go-bindata -pkg schema -nometadata data import ( "fmt" "strings" "time" "github.com/pkg/errors" "github.com/xeipuuv/gojsonschema" ) const ( defaultVersion = "1.0" versionField = "version" ) type portsFormatChecker struct{} func (checker portsFormatChecker) IsFormat(input string) bool { // TODO: implement this return true } type durationFormatChecker struct{} func (checker durationFormatChecker) IsFormat(input string) bool { _, err := time.ParseDuration(input) return err == nil } func init() { gojsonschema.FormatCheckers.Add("expose", portsFormatChecker{}) gojsonschema.FormatCheckers.Add("ports", portsFormatChecker{}) gojsonschema.FormatCheckers.Add("duration", durationFormatChecker{}) } // Version returns the version of the config, defaulting to version 1.0 func Version(config map[string]interface{}) string { version, ok := config[versionField] if !ok { return defaultVersion } return normalizeVersion(fmt.Sprintf("%v", version)) } func normalizeVersion(version string) string { switch version { case "3": return "3.0" default: return version } } // Validate uses the jsonschema to validate the configuration func Validate(config map[string]interface{}, version string) error { schemaData, err := Asset(fmt.Sprintf("data/config_schema_v%s.json", version)) if err != nil { return errors.Errorf("unsupported Compose file version: %s", version) } schemaLoader := gojsonschema.NewStringLoader(string(schemaData)) dataLoader := gojsonschema.NewGoLoader(config) result, err := gojsonschema.Validate(schemaLoader, dataLoader) if err != nil { return err } if !result.Valid() { return toError(result) } return nil } func toError(result *gojsonschema.Result) error { err := getMostSpecificError(result.Errors()) description := getDescription(err) return fmt.Errorf("%s %s", err.Field(), description) } func getDescription(err gojsonschema.ResultError) string { if err.Type() == "invalid_type" { if expectedType, ok := err.Details()["expected"].(string); ok { return fmt.Sprintf("must be a %s", humanReadableType(expectedType)) } } return err.Description() } func humanReadableType(definition string) string { if definition[0:1] == "[" { allTypes := strings.Split(definition[1:len(definition)-1], ",") for i, t := range allTypes { allTypes[i] = humanReadableType(t) } return fmt.Sprintf( "%s or %s", strings.Join(allTypes[0:len(allTypes)-1], ", "), allTypes[len(allTypes)-1], ) } if definition == "object" { return "mapping" } if definition == "array" { return "list" } return definition } func getMostSpecificError(errors []gojsonschema.ResultError) gojsonschema.ResultError { var mostSpecificError gojsonschema.ResultError for _, err := range errors { if mostSpecificError == nil { mostSpecificError = err } else if specificity(err) > specificity(mostSpecificError) { mostSpecificError = err } else if specificity(err) == specificity(mostSpecificError) { // Invalid type errors win in a tie-breaker for most specific field name if err.Type() == "invalid_type" && mostSpecificError.Type() != "invalid_type" { mostSpecificError = err } } } return mostSpecificError } func specificity(err gojsonschema.ResultError) int { return len(strings.Split(err.Field(), ".")) }