github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cli/compose/schema/schema.go (about)

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