schema.go 3.9 KB

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