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  }