github.com/olljanat/moby@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  }