Custom types, round 2

This checkin continues progress on marapongo/mu#9.  It's still not
complete, however we're getting there.  In particular, this includes:

* Rename of ComplexLiteral to SchemaLiteral, as it is used exclusively
  for schematized types.  Also includes a set of changes associated
  with this, like deep value conversion to `map[string]interface{}`.

* Binding of schema types included within a Stack.  This allows names in
  type references to be bound to those schema types during typechecking.
  This also includes binding schema properties, reusing all the existing
  property binding logic for stacks.  In this way, properties between
  stacks and custom schema types are one and the same, which is nice.

* Enforcement for custom schema constraints; this includes Pattern,
  MaxLength, MinLength, Maximum, and Minimum, as per the JSON Schema
  specification.
This commit is contained in:
joeduffy 2016-12-06 20:51:05 -08:00
parent 38ec8d99ed
commit 86219e781b
8 changed files with 392 additions and 212 deletions

View file

@ -164,8 +164,12 @@ func ToValue(l ast.Literal) interface{} {
util.FailMF("Unexpected map key type: %v", keyt)
return nil
}
case ast.ComplexLiteral:
return t.Value()
case ast.SchemaLiteral:
p := make(map[string]interface{})
for k, v := range t.Properties() {
p[k] = ToValue(v)
}
return p
default:
util.FailM("Unexpected literal type")
return nil

View file

@ -152,29 +152,28 @@ type Schemas struct {
type SchemaMap map[Name]*Schema
// Schema represents a complex schema type that extends Mu's type system and can be used by name.
// TODO: support the full set of JSON schema operators (like allOf, anyOf, etc.); to see the full list, refer to the
// spec: http://json-schema.org/latest/json-schema-validation.html.
// TODO: we deviate from the spec in a few areas; for example, we default to required and support optional. We should
// do an audit of all such places and decide whether it's worth deviating. If yes, we should clearly document.
// TODO[marapongo/mu#9]: support the full set of JSON schema operators (like allOf, anyOf, etc.); to see the full list,
// refer to the spec: http://json-schema.org/latest/json-schema-validation.html.
// TODO[marapongo/mu#9]: we deviate from the spec in a few areas; e.g., we default to required and support optional. We
// should do an audit of all such places and decide whether it's worth deviating. If yes, we must clearly document.
type Schema struct {
Node
Base Ref `json:"base,omitempty"` // the base type from which this derives.
Properties Properties `json:"properties,omitempty"` // all of the custom properties for object-based types.
// constraints for all types:
Enum []interface{} `json:"enum,omitempty"` // an optional enum of legal values.
BoundBase *Type `json:"-"` // base, optionally bound during analysis.
Properties Properties `json:"properties,omitempty"` // all of the custom properties for object types.
// constraints for string types:
Pattern string `json:"pattern,omitempty"` // an optional regex pattern for string types.
MaxLength float64 `json:"maxLength,omitempty"` // an optional max string length (in characters).
MinLength float64 `json:"minLength,omitempty"` // an optional min string length (in characters).
Pattern *string `json:"pattern,omitempty"` // an optional regex pattern for string types.
MaxLength *float64 `json:"maxLength,omitempty"` // an optional max string length (in characters).
MinLength *float64 `json:"minLength,omitempty"` // an optional min string length (in characters).
// constraints for numeric types:
Maximum float64 `json:"maximum,omitempty"` // an optional max value for numeric types.
ExclusiveMaximum float64 `json:"exclusiveMaximum,omitempty"` // an optional exclusive max value for numeric types.
Minimum float64 `json:"minimum,omitempty"` // an optional min value for numeric types.
ExclusiveMinimum float64 `json:"exclusiveMinimum,omitempty"` // an optional exclusive min value for numeric types.
Maximum *float64 `json:"maximum,omitempty"` // an optional max value for numeric types.
Minimum *float64 `json:"minimum,omitempty"` // an optional min value for numeric types.
// constraints for strings *and* number types:
Enum []interface{} `json:"enum,omitempty"` // an optional enum of legal values.
Name Name `json:"-"` // a friendly name; decorated post-parsing, since it is contextual.
Public bool `json:"-"` // true if this schema type is publicly exposed; also decorated post-parsing.
@ -280,11 +279,11 @@ type MapLiteral interface {
Values() []Literal
}
// ComplexLiteral is an AST node containing a literal value that is strongly typed, but too complex to represent
// SchemaLiteral is an AST node containing a literal value that is strongly typed, but too complex to represent
// structually in Go's type system. For these types, we resort to dynamic manipulation of the contents.
type ComplexLiteral interface {
type SchemaLiteral interface {
Literal
Value() interface{}
Properties() LiteralPropertyBag
}
// TODO[marapongo/mu#9]: extensible schema support.

View file

@ -282,19 +282,19 @@ func (l *mapLiteral) ValueType() *Type { return l.valType }
func (l *mapLiteral) Keys() []Literal { return l.keys }
func (l *mapLiteral) Values() []Literal { return l.vals }
// NewComplexLiteral allocates a fresh ComplexLiteral with the given contents.
func NewTypedLiteral(node *Node, typ *Type, val interface{}) ComplexLiteral {
return &complexLiteral{node, typ, val}
// NewSchemaLiteral allocates a fresh SchemaLiteral with the given contents.
func NewSchemaLiteral(node *Node, schema *Schema, props LiteralPropertyBag) SchemaLiteral {
return &schemaLiteral{node, schema, props}
}
type complexLiteral struct {
node *Node
typ *Type
val interface{}
type schemaLiteral struct {
node *Node
schema *Schema
props LiteralPropertyBag
}
var _ ComplexLiteral = &complexLiteral{} // ensure complexLiteral implements ComplexLiteral.
var _ SchemaLiteral = &schemaLiteral{} // ensure schemaLiteral implements SchemaLiteral.
func (l *complexLiteral) Node() *Node { return l.node }
func (l *complexLiteral) Type() *Type { return l.typ }
func (l *complexLiteral) Value() interface{} { return l.val }
func (l *schemaLiteral) Node() *Node { return l.node }
func (l *schemaLiteral) Type() *Type { return NewSchemaType(l.schema) }
func (l *schemaLiteral) Properties() LiteralPropertyBag { return l.props }

View file

@ -3,12 +3,16 @@
package compiler
import (
"fmt"
"reflect"
"regexp"
"strings"
"unicode/utf8"
"github.com/golang/glog"
"github.com/marapongo/mu/pkg/ast"
"github.com/marapongo/mu/pkg/ast/conv"
"github.com/marapongo/mu/pkg/compiler/core"
"github.com/marapongo/mu/pkg/diag"
"github.com/marapongo/mu/pkg/errors"
@ -275,10 +279,20 @@ func (p *binderPreparePhase) VisitSchemas(parent *ast.Stack, schemas *ast.Schema
func (p *binderPreparePhase) VisitSchema(pstack *ast.Stack, parent *ast.Schemas, name ast.Name,
public bool, schema *ast.Schema) {
// TODO[marapongo/mu#9]: implement this as part of extensible schema types.
// If the schema has an unresolved base type, add it as a bound dependency.
if schema.BoundBase != nil && schema.BoundBase.IsUnresolvedRef() {
p.registerDependency(pstack, *schema.BoundBase.Unref)
}
// Add this schema to the symbol table so that this stack can reference it.
sym := NewSchemaSymbol(schema.Name, schema)
if !p.b.RegisterSymbol(sym) {
p.Diag().Errorf(errors.ErrorSymbolAlreadyExists.At(pstack), sym.Name)
}
}
func (p *binderPreparePhase) VisitProperty(parent *ast.Stack, name string, prop *ast.Property) {
func (p *binderPreparePhase) VisitProperty(parent *ast.Stack, schema *ast.Schema, name string, prop *ast.Property) {
// For properties whose types represent stack types, register them as a dependency.
if prop.BoundType.IsUnresolvedRef() {
p.registerDependency(parent, *prop.BoundType.Unref)
@ -398,7 +412,6 @@ func (p *binderBindPhase) VisitStack(stack *ast.Stack) {
if stack.Base != "" {
// TODO[marapongo/mu#7]: we need to plumb construction properties for this stack.
stack.BoundBase = p.ensureStack(stack.Base, nil)
util.Assert(stack.BoundBase != nil)
}
// Non-abstract Stacks must declare at least one Service.
@ -412,10 +425,21 @@ func (p *binderBindPhase) VisitSchemas(parent *ast.Stack, schemas *ast.Schemas)
func (p *binderBindPhase) VisitSchema(pstack *ast.Stack, parent *ast.Schemas, name ast.Name,
public bool, schema *ast.Schema) {
// TODO[marapongo/mu#9]: implement this as part of extensible schema types.
// Ensure the base schema is available to us.
if schema.BoundBase != nil && schema.BoundBase.IsUnresolvedRef() {
ref := *schema.BoundBase.Unref
base := p.ensureType(ref)
// Check to ensure that the base is of one of the legal kinds.
if !base.IsPrimitive() && !base.IsSchema() {
p.Diag().Errorf(errors.ErrorSchemaTypeExpected, ref, base)
}
}
// TODO: ensure that schemas with constraints don't have illegal constraints (wrong type; regex won't parse; etc).
}
func (p *binderBindPhase) VisitProperty(parent *ast.Stack, name string, prop *ast.Property) {
func (p *binderBindPhase) VisitProperty(parent *ast.Stack, schema *ast.Schema, name string, prop *ast.Property) {
// For properties whose types represent unresolved names, we must bind them to a name now.
if prop.BoundType.IsUnresolvedRef() {
prop.BoundType = p.ensureType(*prop.BoundType.Unref)
@ -433,40 +457,40 @@ func (p *binderBindPhase) VisitService(pstack *ast.Stack, parent *ast.Services,
util.AssertMF(svc.Type != "",
"Expected all Services to have types in binding phase2; %v is missing one", svc.Name)
svc.BoundType = p.ensureStack(svc.Type, svc.Properties)
util.Assert(svc.BoundType != nil)
// A service cannot instantiate an abstract stack.
if svc.BoundType.Abstract {
if svc.BoundType != nil && svc.BoundType.Abstract {
p.Diag().Errorf(errors.ErrorCannotCreateAbstractStack.At(pstack), svc.Name, svc.BoundType.Name)
}
}
// ensureStack binds a ref to a symbol, possibly instantiating it, and returns a fully bound stack.
// ensureStack binds a ref to a stack symbol, possibly instantiating it if needed.
func (p *binderBindPhase) ensureStack(ref ast.Ref, props ast.PropertyBag) *ast.Stack {
ty := p.ensureType(ref)
util.Assert(ty != nil)
// There are two possibilities. The first is that a type resolves to an *ast.Stack. That's simple, we just fetch
// and return it. The second is that a type resolves to a *diag.Document. That's more complex, as we need to
// actually parse the stack from a document, supplying properties, etc., for template expansion.
if ty.IsStack() {
return ty.Stack
} else {
util.Assert(ty.IsUninstStack())
} else if ty.IsUninstStack() {
// We have the dependency's Mufile; now we must "instantiate it", by parsing it and returning the result. Note
// that this will be processed later on in semantic analysis, to ensure semantic problems are caught.
pa := NewParser(p.b.c)
stack := pa.ParseStack(ty.UninstStack.Doc, props)
if !pa.Diag().Success() {
// If we failed to parse the stack, there was something wrong with our dependency information. Bail out.
return nil
}
p.deps = append(p.deps, stack)
return stack
} else {
p.Diag().Errorf(errors.ErrorStackTypeExpected, ref, ty)
return nil
}
}
// ensureType looksType up a ref, either as a stack, document, or schema symbol, and returns it as-is.
// ensureStackType looks up a ref, either as a stack, document, or schema symbol, and returns it as-is.
func (p *binderBindPhase) ensureType(ref ast.Ref) *ast.Type {
nm := refToName(ref)
stack, exists := p.b.LookupStack(nm)
@ -511,7 +535,7 @@ func (p *binderValidatePhase) VisitDependency(parent *ast.Workspace, ref ast.Ref
func (p *binderValidatePhase) VisitStack(stack *ast.Stack) {
if stack.PropertyValues != nil {
// Bind property values.
stack.BoundPropertyValues = p.bindProperties(stack, stack, stack.PropertyValues)
stack.BoundPropertyValues = p.bindProperties(&stack.Node, stack.Properties, stack.PropertyValues)
}
if stack.Base != "" {
util.Assert(stack.BoundBase != nil)
@ -524,10 +548,9 @@ func (p *binderValidatePhase) VisitSchemas(parent *ast.Stack, schemas *ast.Schem
func (p *binderValidatePhase) VisitSchema(pstack *ast.Stack, parent *ast.Schemas, name ast.Name,
public bool, schema *ast.Schema) {
// TODO[marapongo/mu#9]: implement this as part of extensible schema types.
}
func (p *binderValidatePhase) VisitProperty(parent *ast.Stack, name string, prop *ast.Property) {
func (p *binderValidatePhase) VisitProperty(parent *ast.Stack, schema *ast.Schema, name string, prop *ast.Property) {
}
func (p *binderValidatePhase) VisitServices(parent *ast.Stack, svcs *ast.Services) {
@ -538,8 +561,7 @@ func (p *binderValidatePhase) VisitService(pstack *ast.Stack, parent *ast.Servic
util.Assert(svc.BoundType != nil)
if svc.BoundType.PropertyValues == nil {
// For some types, there aren't any property values (e.g., built-in types). For those, bind now.
// TODO: we could clean this up a bit by having primitive types work more like unconstructed types.
svc.BoundProperties = p.bindProperties(pstack, svc.BoundType, svc.Properties)
svc.BoundProperties = p.bindProperties(&pstack.Node, svc.BoundType.Properties, svc.Properties)
} else {
// For imported types, we should have property values, which already got bound in an earlier phase.
util.Assert(svc.BoundType.BoundPropertyValues != nil)
@ -550,17 +572,17 @@ func (p *binderValidatePhase) VisitService(pstack *ast.Stack, parent *ast.Servic
// bindProperties typechecks a set of unbounded properties against the target stack, and expands them into a bag
// of bound properties (with AST nodes rather than the naked parsed types).
func (p *binderValidatePhase) bindProperties(parent *ast.Stack, stack *ast.Stack,
props ast.PropertyBag) ast.LiteralPropertyBag {
func (p *binderValidatePhase) bindProperties(node *ast.Node, props ast.Properties,
vals ast.PropertyBag) ast.LiteralPropertyBag {
bound := make(ast.LiteralPropertyBag)
// First, enumerate all known properties on the stack. Ensure all required properties are present, expand default
// values for missing ones where applicable, and check that types are correct, converting them as appropriate.
for _, pname := range ast.StableProperties(stack.Properties) {
prop := stack.Properties[pname]
for _, pname := range ast.StableProperties(props) {
prop := props[pname]
// First see if a value has been supplied by the caller.
val, has := props[pname]
val, has := vals[pname]
if !has {
if prop.Default != nil {
// If the property has a default value, stick it in and process it normally.
@ -570,7 +592,7 @@ func (p *binderValidatePhase) bindProperties(parent *ast.Stack, stack *ast.Stack
continue
} else {
// If there's no value, no default, and it isn't optional, issue an error and move on.
p.Diag().Errorf(errors.ErrorMissingRequiredProperty.At(parent), pname, stack.Name)
p.Diag().Errorf(errors.ErrorMissingRequiredProperty.At(node), pname)
continue
}
}
@ -581,10 +603,10 @@ func (p *binderValidatePhase) bindProperties(parent *ast.Stack, stack *ast.Stack
}
}
for _, pname := range ast.StablePropertyBag(props) {
if _, ok := stack.Properties[pname]; !ok {
for _, pname := range ast.StablePropertyBag(vals) {
if _, ok := props[pname]; !ok {
// TODO: edit distance checking to help with suggesting a fix.
p.Diag().Errorf(errors.ErrorUnrecognizedProperty.At(parent), pname, stack.Name)
p.Diag().Errorf(errors.ErrorUnrecognizedProperty.At(node), pname)
}
}
@ -595,85 +617,13 @@ func (p *binderValidatePhase) bindProperties(parent *ast.Stack, stack *ast.Stack
func (p *binderValidatePhase) bindValue(node *ast.Node, val interface{}, ty *ast.Type) ast.Literal {
util.Assert(ty != nil)
if ty.IsDecors() {
// For decorated types, we need to recurse.
if ty.Decors.ElemType != nil {
if arr := reflect.ValueOf(val); arr.Kind() == reflect.Slice {
len := arr.Len()
lits := make([]ast.Literal, len)
err := false
for i := 0; i < len; i++ {
if lits[i] = p.bindValue(node, arr.Index(i), ty.Decors.ElemType); lits[i] == nil {
err = true
}
}
if !err {
return ast.NewArrayLiteral(node, ty.Decors.ElemType, lits)
}
}
} else {
util.Assert(ty.Decors.KeyType != nil)
util.Assert(ty.Decors.ValueType != nil)
// TODO: ensure that keytype is something we can actually use as a key (primitive).
if m := reflect.ValueOf(val); m.Kind() == reflect.Map {
mk := m.MapKeys()
keys := make([]ast.Literal, len(mk))
err := false
for i := 0; i < len(mk); i++ {
if keys[i] = p.bindValue(node, mk[i], ty.Decors.KeyType); keys[i] == nil {
err = true
}
}
vals := make([]ast.Literal, len(mk))
for i := 0; i < len(mk); i++ {
if vals[i] = p.bindValue(node, m.MapIndex(mk[i]), ty.Decors.ValueType); vals[i] == nil {
err = true
}
}
if !err {
return ast.NewMapLiteral(node, ty.Decors.KeyType, ty.Decors.ValueType, keys, vals)
}
}
}
return p.bindDecorsValue(node, val, ty.Decors)
} else if ty.IsPrimitive() {
// For primitive types, simply cast the target to the expected type.
switch *ty.Primitive {
case ast.PrimitiveTypeAny:
// Any is easy: just store it as-is.
// TODO(joe): eventually we'll need to do translation to canonicalize the contents.
return ast.NewAnyLiteral(node, val)
case ast.PrimitiveTypeString:
if s, ok := val.(string); ok {
return ast.NewStringLiteral(node, s)
}
case ast.PrimitiveTypeNumber:
if n, ok := val.(float64); ok {
return ast.NewNumberLiteral(node, n)
}
case ast.PrimitiveTypeBool:
if b, ok := val.(bool); ok {
return ast.NewBoolLiteral(node, b)
}
case ast.PrimitiveTypeService:
// Extract the name of the service reference as a string. Then bind it to an actual service in our symbol
// table, and store a strong reference to the result. This lets the backend connect the dots.
if s, ok := val.(string); ok {
if ref := p.bindServiceRef(node, s, nil); ref != nil {
return ast.NewServiceLiteral(node, ref)
}
}
}
return p.bindPrimitiveValue(node, val, *ty.Primitive)
} else if ty.IsStack() {
// Bind the capability ref for this stack type.
if s, ok := val.(string); ok {
if ref := p.bindServiceRef(node, s, ty); ref != nil {
return ast.NewServiceLiteral(node, ref)
}
}
return p.bindServiceValue(node, val, ty)
} else if ty.IsSchema() {
// TODO[marapongo/mu#9]: implement this as part of extensible schema types.
util.FailM("Custom schema types not yet supported")
return p.bindSchemaValue(node, val, ty.Schema)
} else if ty.IsUnresolvedRef() {
util.FailM("Expected all unresolved refs to be gone by this phase in binding")
}
@ -682,6 +632,203 @@ func (p *binderValidatePhase) bindValue(node *ast.Node, val interface{}, ty *ast
return nil
}
func (p *binderValidatePhase) bindDecorsValue(node *ast.Node, val interface{}, decors *ast.TypeDecors) ast.Literal {
// For decorated types, we need to recurse.
if decors.ElemType != nil {
if arr := reflect.ValueOf(val); arr.Kind() == reflect.Slice {
len := arr.Len()
lits := make([]ast.Literal, len)
err := false
for i := 0; i < len; i++ {
if lits[i] = p.bindValue(node, arr.Index(i), decors.ElemType); lits[i] == nil {
err = true
}
}
if !err {
return ast.NewArrayLiteral(node, decors.ElemType, lits)
}
}
} else {
util.Assert(decors.KeyType != nil)
util.Assert(decors.ValueType != nil)
// TODO: ensure that keytype is something we can actually use as a key (primitive).
if m := reflect.ValueOf(val); m.Kind() == reflect.Map {
mk := m.MapKeys()
keys := make([]ast.Literal, len(mk))
err := false
for i := 0; i < len(mk); i++ {
if keys[i] = p.bindValue(node, mk[i], decors.KeyType); keys[i] == nil {
err = true
}
}
vals := make([]ast.Literal, len(mk))
for i := 0; i < len(mk); i++ {
if vals[i] = p.bindValue(node, m.MapIndex(mk[i]), decors.ValueType); vals[i] == nil {
err = true
}
}
if !err {
return ast.NewMapLiteral(node, decors.KeyType, decors.ValueType, keys, vals)
}
}
}
return nil
}
func (p *binderValidatePhase) bindPrimitiveValue(node *ast.Node, val interface{}, prim ast.PrimitiveType) ast.Literal {
// For primitive types, simply cast the target to the expected type.
switch prim {
case ast.PrimitiveTypeAny:
// Any is easy: just store it as-is.
// TODO(joe): eventually we'll need to do translation to canonicalize the contents.
return ast.NewAnyLiteral(node, val)
case ast.PrimitiveTypeString:
if s, ok := val.(string); ok {
return ast.NewStringLiteral(node, s)
}
return nil
case ast.PrimitiveTypeNumber:
if n, ok := val.(float64); ok {
return ast.NewNumberLiteral(node, n)
}
return nil
case ast.PrimitiveTypeBool:
if b, ok := val.(bool); ok {
return ast.NewBoolLiteral(node, b)
}
return nil
case ast.PrimitiveTypeService:
// Extract the name of the service reference as a string. Then bind it to an actual service in our symbol
// table, and store a strong reference to the result. This lets the backend connect the dots.
return p.bindServiceValue(node, val, nil)
default:
util.FailMF("Unrecognized primitive type: %v", prim)
return nil
}
}
func (p *binderValidatePhase) bindServiceValue(node *ast.Node, val interface{}, expect *ast.Type) ast.Literal {
// Bind the capability ref for this stack type.
if s, ok := val.(string); ok {
if ref := p.bindServiceRef(node, s, expect); ref != nil {
return ast.NewServiceLiteral(node, ref)
}
}
return nil
}
func (p *binderValidatePhase) bindSchemaValue(node *ast.Node, val interface{}, schema *ast.Schema) ast.Literal {
// Bind the custom schema type. This is rather involved, but there are two primary cases:
// 1) A base type exists, plus an optional set of constraints on that base type (if it's a primitive).
// 2) A set of properties exist, meaning an entirely custom object. We must go recursive.
// TODO[marapongo/mu#9]: we may want to support mixing these (e.g., additive properties); for now, we won't.
if schema.BoundBase != nil {
// There is a base type. Bind it as-is, and then apply any additional constraints we have added.
util.Assert(schema.Properties == nil)
lit := p.bindValue(node, val, schema.BoundBase)
if lit != nil {
// The following constraints are valid only on strings:
if schema.Pattern != nil {
if s, ok := conv.ToString(lit); ok {
rex := regexp.MustCompile(*schema.Pattern)
if rex.FindString(s) != s {
p.Diag().Errorf(errors.ErrorSchemaConstraintUnmet, "pattern", schema.Pattern, s)
}
} else {
p.Diag().Errorf(errors.ErrorSchemaConstraintType, "maxLength", ast.PrimitiveTypeString, lit.Type())
}
}
if schema.MaxLength != nil {
if s, ok := conv.ToString(lit); ok {
c := utf8.RuneCountInString(s)
if float64(c) > *schema.MaxLength {
p.Diag().Errorf(errors.ErrorSchemaConstraintUnmet,
"maxLength", fmt.Sprintf("max %v", schema.MaxLength), c)
}
} else {
p.Diag().Errorf(errors.ErrorSchemaConstraintType, "maxLength", ast.PrimitiveTypeString, lit.Type())
}
}
if schema.MinLength != nil {
if s, ok := conv.ToString(lit); ok {
c := utf8.RuneCountInString(s)
if float64(c) < *schema.MinLength {
p.Diag().Errorf(errors.ErrorSchemaConstraintUnmet,
"minLength", fmt.Sprintf("min %v", schema.MinLength), c)
}
} else {
p.Diag().Errorf(errors.ErrorSchemaConstraintType, "minLength", ast.PrimitiveTypeString, lit.Type())
}
}
// The following constraints are valid only on numeric ypes:
if schema.Maximum != nil {
if n, ok := conv.ToNumber(lit); ok {
if n > *schema.Maximum {
p.Diag().Errorf(errors.ErrorSchemaConstraintUnmet,
"maximum", fmt.Sprintf("max %v", schema.Maximum), n)
}
} else {
p.Diag().Errorf(errors.ErrorSchemaConstraintType, "maximum", ast.PrimitiveTypeNumber, lit.Type())
}
}
if schema.Minimum != nil {
if n, ok := conv.ToNumber(lit); ok {
if n < *schema.Minimum {
p.Diag().Errorf(errors.ErrorSchemaConstraintUnmet,
"minimum", fmt.Sprintf("min %v", schema.Minimum), n)
}
} else {
p.Diag().Errorf(errors.ErrorSchemaConstraintType, "minimum", ast.PrimitiveTypeNumber, lit.Type())
}
}
// The following constraints are valid on strings *and* number types.
if len(schema.Enum) > 0 {
if s, ok := conv.ToString(lit); ok {
ok := false
for _, e := range schema.Enum {
if s == e.(string) {
ok = true
break
}
}
if !ok {
p.Diag().Errorf(errors.ErrorSchemaConstraintUnmet,
"enum", fmt.Sprintf("enum %v", schema.Enum), s)
}
} else if n, ok := conv.ToNumber(lit); ok {
ok := false
for _, e := range schema.Enum {
if n == e.(float64) {
ok = true
break
}
}
if !ok {
p.Diag().Errorf(errors.ErrorSchemaConstraintUnmet,
"enum", fmt.Sprintf("enum %v", schema.Enum), n)
}
} else {
p.Diag().Errorf(errors.ErrorSchemaConstraintType,
"enum", ast.PrimitiveTypeString+" or "+ast.PrimitiveTypeNumber, lit.Type())
}
}
}
} else if schema.Properties != nil {
// There are some properties. This is a custom type. Bind the properties as usual.
if props, ok := val.(ast.PropertyBag); ok {
bag := p.bindProperties(node, schema.Properties, props)
return ast.NewSchemaLiteral(node, schema, bag)
}
}
return nil
}
// bindServiceRef binds a string to a service reference, resulting in a ServiceRef. The reference is expected
// to be in the form "<service>[:<selector>]", where <service> is the name of a service that's currently in scope, and
// <selector> is an optional selector of a public service exported from that service.

View file

@ -168,9 +168,9 @@ func TestBadMissingProperties(t *testing.T) {
assert.Equal(t, len(reqs), sink.Errors(), "expected an error per property")
for i, req := range reqs {
assert.Equal(t,
fmt.Sprintf("%v: %v%v: %v: %v\n",
diag.DefaultSinkErrorPrefix, diag.DefaultSinkIDPrefix, d.ID, "Mu.yaml",
fmt.Sprintf(d.Message, req, "mutest/provider")),
fmt.Sprintf("%v: %v%v: %v\n",
diag.DefaultSinkErrorPrefix, diag.DefaultSinkIDPrefix, d.ID,
fmt.Sprintf(d.Message, req)),
sink.ErrorMsgs()[i])
}
}
@ -183,9 +183,9 @@ func TestBadUnrecognizedProperties(t *testing.T) {
assert.Equal(t, len(unks), sink.Errors(), "expected an error per property")
for i, unk := range unks {
assert.Equal(t,
fmt.Sprintf("%v: %v%v: %v: %v\n",
diag.DefaultSinkErrorPrefix, diag.DefaultSinkIDPrefix, d.ID, "../provider/Mu.yaml",
fmt.Sprintf(d.Message, unk, "mutest/provider")),
fmt.Sprintf("%v: %v%v: %v\n",
diag.DefaultSinkErrorPrefix, diag.DefaultSinkIDPrefix, d.ID,
fmt.Sprintf(d.Message, unk)),
sink.ErrorMsgs()[i])
}
}
@ -199,8 +199,8 @@ func TestBadPropertyTypes(t *testing.T) {
assert.Equal(t, len(exp), sink.Errors(), "expected an error per property")
for i, ty := range exp {
assert.Equal(t,
fmt.Sprintf("%v: %v%v: %v: %v\n",
diag.DefaultSinkErrorPrefix, diag.DefaultSinkIDPrefix, d.ID, "../provider/Mu.yaml",
fmt.Sprintf("%v: %v%v: %v\n",
diag.DefaultSinkErrorPrefix, diag.DefaultSinkIDPrefix, d.ID,
fmt.Sprintf(d.Message, ty, got[i])),
sink.ErrorMsgs()[i])
}

View file

@ -16,7 +16,7 @@ type Visitor interface {
VisitStack(stack *ast.Stack)
VisitSchemas(parent *ast.Stack, schmas *ast.Schemas)
VisitSchema(pstack *ast.Stack, parent *ast.Schemas, name ast.Name, public bool, schema *ast.Schema)
VisitProperty(parent *ast.Stack, name string, prop *ast.Property)
VisitProperty(parent *ast.Stack, schema *ast.Schema, name string, prop *ast.Property)
VisitServices(parent *ast.Stack, svcs *ast.Services)
VisitService(pstack *ast.Stack, parent *ast.Services, name ast.Name, public bool, svc *ast.Service)
}
@ -90,7 +90,7 @@ func (v *inOrderVisitor) VisitStack(stack *ast.Stack) {
v.VisitSchemas(stack, &stack.Schema)
for _, name := range ast.StableProperties(stack.Properties) {
v.VisitProperty(stack, name, stack.Properties[name])
v.VisitProperty(stack, nil, name, stack.Properties[name])
}
v.VisitServices(stack, &stack.Services)
@ -121,17 +121,22 @@ func (v *inOrderVisitor) VisitSchema(pstack *ast.Stack, parent *ast.Schemas, nam
if v.pre != nil {
v.pre.VisitSchema(pstack, parent, name, public, schema)
}
for _, name := range ast.StableProperties(schema.Properties) {
v.VisitProperty(pstack, schema, name, schema.Properties[name])
}
if v.post != nil {
v.post.VisitSchema(pstack, parent, name, public, schema)
}
}
func (v *inOrderVisitor) VisitProperty(parent *ast.Stack, name string, prop *ast.Property) {
func (v *inOrderVisitor) VisitProperty(parent *ast.Stack, schema *ast.Schema, name string, prop *ast.Property) {
if v.pre != nil {
v.pre.VisitProperty(parent, name, prop)
v.pre.VisitProperty(parent, schema, name, prop)
}
if v.post != nil {
v.post.VisitProperty(parent, name, prop)
v.post.VisitProperty(parent, schema, name, prop)
}
}

View file

@ -88,75 +88,19 @@ func (a *ptAnalyzer) VisitSchema(pstack *ast.Stack, parent *ast.Schemas, name as
// Decorate the AST with contextual information.
schema.Name = name
schema.Public = public
// If the schema has a base type listed, parse it to the best of our ability.
if schema.Base != "" {
schema.BoundBase = a.parseType(schema.Base)
}
}
func (a *ptAnalyzer) VisitProperty(parent *ast.Stack, name string, prop *ast.Property) {
func (a *ptAnalyzer) VisitProperty(parent *ast.Stack, schema *ast.Schema, name string, prop *ast.Property) {
// Decorate the AST with contextual information so subsequent passes can operate context-free.
prop.Name = name
// Parse the property type to the best of our ability at this phase in the compiler.
prop.BoundType = a.parsePropertyType(prop.Type)
}
// parsePropertyType produces an ast.Type. This will not have been bound yet, so for example, we won't know whether
// an arbitrary non-primitive reference name references a stack or a schema, however at least this is a start.
func (a *ptAnalyzer) parsePropertyType(ref ast.Ref) *ast.Type {
refs := string(ref)
mix := strings.Index(refs, ast.TypeDecorsMapPrefix)
if mix == 0 {
// If we have a map, find the separator, and then parse the key and value parts.
rest := refs[mix+len(ast.TypeDecorsMapPrefix):]
if sep := strings.Index(rest, ast.TypeDecorsMapSeparator); sep != -1 {
keyn := ast.Ref(rest[:sep])
valn := ast.Ref(rest[:sep+len(ast.TypeDecorsMapSeparator)])
keyt := a.parsePropertyType(keyn)
valt := a.parsePropertyType(valn)
if keyt != nil && valt != nil {
return ast.NewMapType(keyt, valt)
}
} else {
a.Diag().Errorf(errors.ErrorIllegalMapLikeSyntax, refs)
}
} else if aix := strings.Index(refs, ast.TypeDecorsArrayPrefix); aix != -1 {
if aix == 0 {
// If we have an array, peel off the front and keep going.
rest := refs[aix+len(ast.TypeDecorsArrayPrefix):]
if elem := a.parsePropertyType(ast.Ref(rest)); elem != nil {
return ast.NewArrayType(elem)
}
} else {
// The array part was in the wrong position. Issue an error. Maybe they did T[] instead of []T?
a.Diag().Errorf(errors.ErrorIllegalArrayLikeSyntax, refs)
}
} else if mix != -1 {
// The map part was in the wrong position. Issue an error.
a.Diag().Errorf(errors.ErrorIllegalMapLikeSyntax, refs)
} else {
// Otherwise, there are no decorators. Parse the result as either a primitive type or unresolved name.
switch ast.PrimitiveType(refs) {
case ast.PrimitiveTypeAny:
return ast.NewAnyType()
case ast.PrimitiveTypeString:
return ast.NewStringType()
case ast.PrimitiveTypeNumber:
return ast.NewNumberType()
case ast.PrimitiveTypeBool:
return ast.NewBoolType()
case ast.PrimitiveTypeService:
return ast.NewServiceType()
}
// If we didn't recognize anything thus far, it's a simple name. We don't yet know what it references --
// it could be a stack, schema, or even a completely bogus, missing name -- so just store it as it is.
if _, err := ref.Parse(); err != nil {
a.Diag().Errorf(errors.ErrorIllegalNameLikeSyntax, refs, err)
} else {
return ast.NewUnresolvedRefType(&ref)
}
}
return nil
prop.BoundType = a.parseType(prop.Type)
}
func (a *ptAnalyzer) VisitServices(parent *ast.Stack, svcs *ast.Services) {
@ -201,3 +145,64 @@ func (a *ptAnalyzer) untypedServiceToTyped(parent *ast.Stack, name ast.Name, pub
func (a *ptAnalyzer) VisitService(pstack *ast.Stack, parent *ast.Services, name ast.Name, public bool,
svc *ast.Service) {
}
// parseType produces an ast.Type. This will not have been bound yet, so for example, we won't know whether
// an arbitrary non-primitive reference name references a stack or a schema, however at least this is a start.
func (a *ptAnalyzer) parseType(ref ast.Ref) *ast.Type {
refs := string(ref)
mix := strings.Index(refs, ast.TypeDecorsMapPrefix)
if mix == 0 {
// If we have a map, find the separator, and then parse the key and value parts.
rest := refs[mix+len(ast.TypeDecorsMapPrefix):]
if sep := strings.Index(rest, ast.TypeDecorsMapSeparator); sep != -1 {
keyn := ast.Ref(rest[:sep])
valn := ast.Ref(rest[:sep+len(ast.TypeDecorsMapSeparator)])
keyt := a.parseType(keyn)
valt := a.parseType(valn)
if keyt != nil && valt != nil {
return ast.NewMapType(keyt, valt)
}
} else {
a.Diag().Errorf(errors.ErrorIllegalMapLikeSyntax, refs)
}
} else if aix := strings.Index(refs, ast.TypeDecorsArrayPrefix); aix != -1 {
if aix == 0 {
// If we have an array, peel off the front and keep going.
rest := refs[aix+len(ast.TypeDecorsArrayPrefix):]
if elem := a.parseType(ast.Ref(rest)); elem != nil {
return ast.NewArrayType(elem)
}
} else {
// The array part was in the wrong position. Issue an error. Maybe they did T[] instead of []T?
a.Diag().Errorf(errors.ErrorIllegalArrayLikeSyntax, refs)
}
} else if mix != -1 {
// The map part was in the wrong position. Issue an error.
a.Diag().Errorf(errors.ErrorIllegalMapLikeSyntax, refs)
} else {
// Otherwise, there are no decorators. Parse the result as either a primitive type or unresolved name.
switch ast.PrimitiveType(refs) {
case ast.PrimitiveTypeAny:
return ast.NewAnyType()
case ast.PrimitiveTypeString:
return ast.NewStringType()
case ast.PrimitiveTypeNumber:
return ast.NewNumberType()
case ast.PrimitiveTypeBool:
return ast.NewBoolType()
case ast.PrimitiveTypeService:
return ast.NewServiceType()
}
// If we didn't recognize anything thus far, it's a simple name. We don't yet know what it references --
// it could be a stack, schema, or even a completely bogus, missing name -- so just store it as it is.
if _, err := ref.Parse(); err != nil {
a.Diag().Errorf(errors.ErrorIllegalNameLikeSyntax, refs, err)
} else {
return ast.NewUnresolvedRefType(&ref)
}
}
return nil
}

View file

@ -39,12 +39,12 @@ var ErrorCannotCreateAbstractStack = &diag.Diag{
var ErrorMissingRequiredProperty = &diag.Diag{
ID: 506,
Message: "Missing required property '%v' on '%v'",
Message: "Missing required property '%v'",
}
var ErrorUnrecognizedProperty = &diag.Diag{
ID: 507,
Message: "Unrecognized property '%v' on '%v'",
Message: "Unrecognized property '%v'",
}
var ErrorIncorrectType = &diag.Diag{
@ -81,3 +81,23 @@ var ErrorNotAName = &diag.Diag{
ID: 514,
Message: "The string '%v' is not a valid name (expected: " + ast.NamePartRegexps + ")",
}
var ErrorStackTypeExpected = &diag.Diag{
ID: 515,
Message: "A stack type was expected here; '%v' did not resolve to a stack ('%v')",
}
var ErrorSchemaTypeExpected = &diag.Diag{
ID: 516,
Message: "A schema type was expected here; '%v' did not resolve to a schema ('%v')",
}
var ErrorSchemaConstraintUnmet = &diag.Diag{
ID: 517,
Message: "Schema constraint %v unmet; expected %v, got %v",
}
var ErrorSchemaConstraintType = &diag.Diag{
ID: 517,
Message: "Unexpected type conflict with constraint %v; expected %v, got %v",
}