Commit fe935c9c authored by Eric Myhre's avatar Eric Myhre

schema: beginning (re)implementation of validation rules.

Carving out hunks of the schema2 implementation of them (which still
hoped to use the dmt more directly) as I port them.

As comments in the diff state: I had a hope here that I could make
this relatively table-driven, which would increase legibility and
make it easier to port these checks to other implementations as well.
We'll... see how that goes; it's not easy to flatten.
parent 3da7e2ad
package schema
import "fmt"
type rule struct {
// text is the name of the rule and the start of the error message body if the rule is flunked.
text string
// predicate should return true if this rule will apply to this type at all.
// Usually it just checks the representation strategy.
// The typekind is already defacto checked by indexing into the rules map.
//
// The distinction between predicate and rule may be useful to build a more advanced diagnostic output.
//
// Note that since these functions are called on a type *during* its validation process,
// some data in the type isn't known to be valid yet,
// and as a result some helper functions may not be safe to use;
// therefore it's often necessary to access the raw fields directly.
predicate func(Type) bool
// rule is the actual rule body. If it's a non-nil return, the rule is flunked.
// The error is for freetext detail formatting; it will be wrapped by another error
// which is based on the rule's Text.
//
// Same caveats about the Type's validity apply as they did for the predicate func.
rule func(*TypeSystem, Type) error
}
type ErrInvalidTypeSpec struct {
Rule string
Type TypeReference
Detail error
}
// To validate a type:
// - first get the slice fo rules that apply to its typekind
// - then, for each rule:
// - check if it applies (by virtue of the predicate); skip if not
// - evaluate the rule and check if it errors
// - errors accumulate and do not cause halts
var rules = map[TypeKind][]rule{
TypeKind_Map: []rule{
{"map declaration's key type must be defined",
alwaysApplies,
func(ts *TypeSystem, t Type) error {
tRef := TypeReference(t.(*TypeMap).keyTypeRef)
if _, exists := ts.types[tRef]; !exists {
return fmt.Errorf("missing type %q", tRef)
}
return nil
},
},
{"map declaration's key type must be stringable",
alwaysApplies,
func(ts *TypeSystem, t Type) error {
tRef := TypeReference(t.(*TypeMap).keyTypeRef)
if hasStringRepresentation(ts.types[tRef]) {
return fmt.Errorf("type %q is not a string typekind nor representation with string kind", tRef)
}
return nil
},
},
{"map declaration's value type must be defined",
alwaysApplies,
func(ts *TypeSystem, t Type) error {
tRef := TypeReference(t.(*TypeMap).valueTypeRef)
if _, exists := ts.types[tRef]; !exists {
return fmt.Errorf("missing type %q", tRef)
}
return nil
},
},
},
TypeKind_List: []rule{
{"list declaration's value type must be defined",
alwaysApplies,
func(ts *TypeSystem, t Type) error {
tRef := TypeReference(t.(*TypeList).valueTypeRef)
if _, exists := ts.types[tRef]; !exists {
return fmt.Errorf("missing type %q", tRef)
}
return nil
},
},
},
TypeKind_Link: []rule{
{"link declaration's expected target type must be defined",
alwaysApplies,
func(ts *TypeSystem, t Type) error {
tRef := TypeReference(t.(*TypeLink).expectedTypeRef)
if _, exists := ts.types[tRef]; !exists {
return fmt.Errorf("missing type %q", tRef)
}
return nil
},
},
},
TypeKind_Struct: []rule{
{"struct declaration's field type must be defined",
alwaysApplies,
func(ts *TypeSystem, t Type) error {
for _, field := range t.(*TypeStruct).fields {
tRef := TypeReference(field.typeRef)
if _, exists := ts.types[tRef]; !exists {
return fmt.Errorf("missing type %q", tRef) // TODO: want this to return multiple errors. time for another `*[]error` accumulator? sigh.
}
}
return nil
},
},
},
TypeKind_Union: []rule{
{"union declaration's potential members must all be defined",
alwaysApplies,
func(ts *TypeSystem, t Type) error {
for _, member := range t.(*TypeUnion).members {
tRef := TypeReference(member)
if _, exists := ts.types[tRef]; !exists {
return fmt.Errorf("missing type %q", tRef) // TODO: want this to return multiple errors.
}
}
return nil
},
},
// TODO continue with more union rules... but... they're starting to get conditional on the passage of prior rules.
// Unsure how much effort it's worth to represent this in detail.
// - Should we have flunk of one rule cause subsequent rules to be skipped on that type?
// - Should we just re-do all the prerequisite checks, but return nil if those fail (since another rule should've already reported those)?
// - Should we re-do all the prerequisite checks, and return a special 'inapplicable' error code if those fail?
// - Should we build a terribly complicated prerequisite tracking system?
// - (Okay, maybe it's not that complicated; a tree would probably suffice?)
// My original aim with this design was to get as close as possible to something table-driven,
// in the hope this would make it easier to port the semantics to other languages.
// As this code gets fancier, that goal fades fast, so a solution that's KISS is probably preferrable.
},
}
var alwaysApplies = func(Type) bool { return true }
// hasStringRepresentation returns a bool for... well, what it says on the tin.
func hasStringRepresentation(t Type) bool {
// Note that some of these cases might get more complicated in the future.
// We haven't defined or implemented features like "type Foo int representation base10str" yet, but we could.
// This doesn't recursively check the sanity of types that claim to have string representation
// (e.g. that every field in a stringjoin struct is also stringable);
// the caller should do that (and the Compiler, which is the caller, does: on each type as it is looping over the whole set).
switch t2 := t.(type) {
case *TypeBool:
return false
case *TypeString:
return true
case *TypeBytes:
return false
case *TypeInt:
return false
case *TypeFloat:
return false
case *TypeMap:
switch t2.Representation().(type) {
case MapRepresentation_Map:
return false
case MapRepresentation_Stringpairs:
return true
case MapRepresentation_Listpairs:
return false
default:
panic("unreachable")
}
case *TypeList:
return false
case *TypeLink:
return false
case *TypeStruct:
switch t2.Representation().(type) {
case StructRepresentation_Map:
return false
case StructRepresentation_Tuple:
return false
case StructRepresentation_Stringpairs:
return true
case StructRepresentation_Stringjoin:
return true
case StructRepresentation_Listpairs:
return false
default:
panic("unreachable")
}
case *TypeUnion:
switch t2.Representation().(type) {
case UnionRepresentation_Keyed:
return false
case UnionRepresentation_Kinded:
return false
case UnionRepresentation_Envelope:
return false
case UnionRepresentation_Inline:
return false
case UnionRepresentation_Stringprefix:
return true
case UnionRepresentation_Byteprefix:
return false
default:
panic("unreachable")
}
case *TypeEnum:
switch t2.Representation().(type) {
case EnumRepresentation_String:
return true
case EnumRepresentation_Int:
return false
default:
panic("unreachable")
}
default:
panic("unreachable")
}
}
......@@ -37,81 +37,6 @@ func BuildTypeSystem(schdmt schemadmt.Schema) (*TypeSystem, []error) {
for itr := typesdmt.Iterator(); !itr.Done(); {
tn, t := itr.Next()
switch t2 := t.AsInterface().(type) {
case schemadmt.TypeBool:
ts.types[tn.TypeReference()] = &TypeBool{tn, t2, ts}
case schemadmt.TypeString:
ts.types[tn.TypeReference()] = &TypeString{tn, t2, ts}
case schemadmt.TypeBytes:
ts.types[tn.TypeReference()] = &TypeBytes{tn, t2, ts}
case schemadmt.TypeInt:
ts.types[tn.TypeReference()] = &TypeInt{tn, t2, ts}
case schemadmt.TypeFloat:
ts.types[tn.TypeReference()] = &TypeFloat{tn, t2, ts}
case schemadmt.TypeMap:
// Verify that:
// - the key type exists
// - the key type either is a string or has a representation strategy that makes it stringable
// - the value type exists
// - or, if the value is an inline defn, make that happen.
ktdmt := typesdmt.Lookup(t2.FieldKeyType())
if ktdmt == nil {
ee = append(ee, fmt.Errorf("type %s refers to missing type %s as key type", tn, t2.FieldKeyType()))
} else {
if !hasStringRepresentation(ktdmt) {
ee = append(ee, fmt.Errorf("type %s refers to type %s as key type, but it is not a valid key type because it is not stringable", tn, t2.FieldKeyType()))
}
}
switch vtndmt := t2.FieldValueType().AsInterface().(type) {
case schemadmt.TypeName:
vtdmt := typesdmt.Lookup(vtndmt)
if vtdmt == nil {
ee = append(ee, fmt.Errorf("type %s refers to missing type %s as value type", tn, vtndmt))
}
case schemadmt.TypeDefnInline:
panic("nyi") // TODO this needs to engage in anonymous type spawning.
}
ts.types[tn.TypeReference()] = &TypeMap{tn, t2, ts}
case schemadmt.TypeList:
// Verify that:
// - the value type exists
// - or, if the value is an inline defn, make that happen.
switch vtndmt := t2.FieldValueType().AsInterface().(type) {
case schemadmt.TypeName:
vtdmt := typesdmt.Lookup(vtndmt)
if vtdmt == nil {
ee = append(ee, fmt.Errorf("type %s refers to missing type %s as value type", tn, vtndmt))
}
case schemadmt.TypeDefnInline:
panic("nyi") // TODO this needs to engage in anonymous type spawning.
}
ts.types[tn.TypeReference()] = &TypeList{tn, t2, ts}
case schemadmt.TypeLink:
// Verify that:
// - if there's an expected type, that type exists.
if t2.FieldExpectedType().Exists() {
referencedTn := t2.FieldExpectedType().Must()
if typesdmt.Lookup(referencedTn) == nil {
ee = append(ee, fmt.Errorf("type %s refers to missing type %s as link reference type", tn, referencedTn))
}
}
ts.types[tn.TypeReference()] = &TypeLink{tn, t2, ts}
case schemadmt.TypeStruct:
// Verify that:
// - each field's type exists
// - or, if the field is an inline defn, make that happen.
for itr := t2.FieldFields().Iterator(); !itr.Done(); {
fndmt, ftddmt := itr.Next()
switch f2 := ftddmt.FieldType().AsInterface().(type) {
case schemadmt.TypeName:
vtdmt := typesdmt.Lookup(f2)
if vtdmt == nil {
ee = append(ee, fmt.Errorf("type %s refers to missing type %s in field %s", tn, f2, fndmt))
}
case schemadmt.TypeDefnInline:
panic("nyi") // TODO this needs to engage in anonymous type spawning.
}
}
ts.types[tn.TypeReference()] = &TypeStruct{tn, t2, ts}
case schemadmt.TypeUnion:
// Verify... oh boy. A lot of things; and each representation strategy has different constraints:
// - for everyone: that all referenced member types exist.
......@@ -325,88 +250,6 @@ func BuildTypeSystem(schdmt schemadmt.Schema) (*TypeSystem, []error) {
return nil, ee
}
// hasStringRepresentation returns a bool for... well, what it says on the tin.
// This question is easier to ask of fully reified data; this function works pre-reification, because we use it during that process.
func hasStringRepresentation(t schemadmt.TypeDefn) bool {
// Note that some of these cases might get more complicated in the future.
// We haven't defined or implemented features like "type Foo int representation base10str" yet, but we could.
// This doesn't recursively check the sanity of types that claim to have string representation
// (e.g. that every field in a stringjoin struct is also stringable);
// the caller should do that (and BuildTypeSystem, which is the caller, does: on each type as its looping over the whole set).
switch t2 := t.AsInterface().(type) {
case schemadmt.TypeBool:
return false
case schemadmt.TypeString:
return true
case schemadmt.TypeBytes:
return false
case schemadmt.TypeInt:
return false
case schemadmt.TypeFloat:
return false
case schemadmt.TypeMap:
switch t2.FieldRepresentation().AsInterface().(type) {
case schemadmt.MapRepresentation_Map:
return false
case schemadmt.MapRepresentation_Stringpairs:
return true
case schemadmt.MapRepresentation_Listpairs:
return false
default:
panic("unreachable")
}
case schemadmt.TypeList:
return false
case schemadmt.TypeLink:
return false
case schemadmt.TypeStruct:
switch t2.FieldRepresentation().AsInterface().(type) {
case schemadmt.StructRepresentation_Map:
return false
case schemadmt.StructRepresentation_Tuple:
return false
case schemadmt.StructRepresentation_Stringpairs:
return true
case schemadmt.StructRepresentation_Stringjoin:
return true
case schemadmt.StructRepresentation_Listpairs:
return false
default:
panic("unreachable")
}
case schemadmt.TypeUnion:
switch t2.FieldRepresentation().AsInterface().(type) {
case schemadmt.UnionRepresentation_Keyed:
return false
case schemadmt.UnionRepresentation_Kinded:
return false
case schemadmt.UnionRepresentation_Envelope:
return false
case schemadmt.UnionRepresentation_Inline:
return false
case schemadmt.UnionRepresentation_StringPrefix:
return true
case schemadmt.UnionRepresentation_BytePrefix:
return false
default:
panic("unreachable")
}
case schemadmt.TypeEnum:
switch t2.FieldRepresentation().AsInterface().(type) {
case schemadmt.EnumRepresentation_String:
return true
case schemadmt.EnumRepresentation_Int:
return false
default:
panic("unreachable")
}
case schemadmt.TypeCopy:
panic("no support for 'copy' types. I might want to reneg on whether these are even part of the schema dmt.")
default:
panic("unreachable")
}
}
// checkUnionDiscriminantInfo verifies that every member in the list
// appears exactly once as a value in the discriminants map, and nothing else appears in the map.
// Errors are appended to ee.
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment