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