Commit eed03b4c authored by Eric Myhre's avatar Eric Myhre

schema compiler: migrate compile tests, and drop schema2 (everything good is now migrated).

Extracted a few last comment hunks that were useful and that's
the end of it.

The migrated tests only mostly pass (but, remember, we've been several
commits in a row already where we're making checkpoints to keep this
refactor managable, and CI is not full green across the board for
some of them).  I've updated some of the error text assertions,
but there's other breakage as well.  One is just other todos.
Another is... It may be time to switch test libraries.  What go-wish
(and transitively, go-cmp) is doing with `[]error` values is...
not helpful.
parent 6b5a471f
......@@ -128,6 +128,12 @@ But for all those drawbacks: it works.
(This whole section has a double duty: it also serves as a nice list
of cool features you get for free when using our codegen.)
There is one nice bonus to this approach
(which is shared with any of the others that involve a compiler indirection):
downstream users of the library can depend on the `schema` package
without necessarily gaining a transitive dependency on the `schema/dmt` package
(which may be desirable to avoid because it's a rather large package, due to its codegen'd contents).
### two packages, compiler is with dmt
Doesn't really fly for the same reasons as three packages.
......
......@@ -62,6 +62,16 @@ func validate(ts *TypeSystem, typ Type, errs *[]error) {
}
}
// The rules table contains all the logical validations that apply to a schema during compilation.
// Some forms of validation of the data are already done by nature of the schema-schema; others will require more work here.
// In general: rules which stretch across multiple types (especially, if they're graph properties) can't be implemented in schemas alone, and so end up here.
// The most common example is that any type that has some kind of recursion (maps, lists, structs, unions, links with target type info)
// will need to do a lookup to see if the referenced types were defined elsewhere in the schema document.
// Some kinds of types involve other more specific checks,
// such as maps verifying that their keys are stringable (which is a rule we enforce for reasons relating to pathing),
// and unions verifying that all their discriminant tables are complete (which is a rule that's necessary for sanity!),
// and etc.
//
// To validate a type:
// - first get the slice of rules that apply to its typekind
// - then, for each rule:
......@@ -75,6 +85,7 @@ func validate(ts *TypeSystem, typ Type, errs *[]error) {
// The table-like design here hopefully will make the semantics defined within
// easier to port to other implementations in other languages.
var rules = map[TypeKind][]rule{
// FUTURE: after adding unit types, we'll need most recursives to additionally do some checks for correct composition of nullability.
TypeKind_Map: []rule{
{"map declaration's key type must be defined",
alwaysApplies,
......
......@@ -12,6 +12,7 @@ import (
func (schdmt Schema) Compile() (*schema.TypeSystem, []error) {
c := &schema.Compiler{}
c.Init()
typesdmt := schdmt.FieldTypes()
for itr := typesdmt.Iterator(); !itr.Done(); {
tn, t := itr.Next()
......
package schema
package schemadmt
import (
"fmt"
......@@ -9,10 +9,10 @@ import (
. "github.com/warpfork/go-wish"
"github.com/ipld/go-ipld-prime/codec/dagjson"
schemadmt "github.com/ipld/go-ipld-prime/schema/dmt"
"github.com/ipld/go-ipld-prime/schema"
)
func TestBuildTypeSystem(t *testing.T) {
func TestCompile(t *testing.T) {
// NOTE: several of these fixtures will need updating when support for implicits is completed.
t.Run("SimpleHappyPath", func(t *testing.T) {
ts := testParse(t,
......@@ -26,8 +26,8 @@ func TestBuildTypeSystem(t *testing.T) {
nil,
nil,
)
Wish(t, ts.types["Woop"], ShouldBeSameTypeAs, &TypeString{})
Wish(t, ts.types["Woop"].TypeKind(), ShouldEqual, TypeKind_String)
Wish(t, ts.GetType("Woop"), ShouldBeSameTypeAs, &schema.TypeString{})
Wish(t, ts.GetType("Woop").TypeKind(), ShouldEqual, schema.TypeKind_String)
})
t.Run("MissingTypeInList", func(t *testing.T) {
testParse(t,
......@@ -46,7 +46,7 @@ func TestBuildTypeSystem(t *testing.T) {
}`,
nil,
[]error{
fmt.Errorf("type SomeList refers to missing type Bork as value type"),
fmt.Errorf(`type SomeList is invalid: list declaration's value type must be defined: missing type "Bork"`),
},
)
})
......@@ -68,8 +68,9 @@ func TestBuildTypeSystem(t *testing.T) {
}`,
nil,
[]error{
fmt.Errorf("type SomeMap refers to missing type Bork as key type"),
fmt.Errorf("type SomeMap refers to missing type Spork as value type"),
fmt.Errorf(`type SomeMap is invalid: map declaration's key type must be defined: missing type "Bork"`),
// REVIEW: this is a case where the short-circuit exiting during rule evaluation blocks an easy win:
//fmt.Errorf(`type SomeMap is invalid: map declaration's value type must be defined: missing type "Spork"`),
},
)
})
......@@ -95,9 +96,9 @@ func TestBuildTypeSystem(t *testing.T) {
nil,
nil,
)
Wish(t, ts.types["SomeMap"], ShouldBeSameTypeAs, &TypeMap{})
Wish(t, ts.types["SomeMap"].TypeKind(), ShouldEqual, TypeKind_Map)
Wish(t, ts.types["SomeMap"].(*TypeMap).KeyType().Name().String(), ShouldEqual, "String")
Wish(t, ts.GetType("SomeMap"), ShouldBeSameTypeAs, &schema.TypeMap{})
Wish(t, ts.GetType("SomeMap").TypeKind(), ShouldEqual, schema.TypeKind_Map)
Wish(t, ts.GetType("SomeMap").(*schema.TypeMap).KeyType().Name().String(), ShouldEqual, "String")
})
t.Run("ComplexValidMapKeyType", func(t *testing.T) {
ts := testParse(t,
......@@ -142,9 +143,9 @@ func TestBuildTypeSystem(t *testing.T) {
nil,
nil,
)
Wish(t, ts.types["SomeMap"], ShouldBeSameTypeAs, &TypeMap{})
Wish(t, ts.types["SomeMap"].TypeKind(), ShouldEqual, TypeKind_Map)
Wish(t, ts.types["SomeMap"].(*TypeMap).KeyType().Name().String(), ShouldEqual, "StringyStruct")
Wish(t, ts.GetType("SomeMap"), ShouldBeSameTypeAs, &schema.TypeMap{})
Wish(t, ts.GetType("SomeMap").TypeKind(), ShouldEqual, schema.TypeKind_Map)
Wish(t, ts.GetType("SomeMap").(*schema.TypeMap).KeyType().Name().String(), ShouldEqual, "StringyStruct")
})
t.Run("InvalidMapKeyType", func(t *testing.T) {
testParse(t,
......@@ -192,22 +193,22 @@ func TestBuildTypeSystem(t *testing.T) {
})
}
func testParse(t *testing.T, schemajson string, expectParseErr error, expectTypesystemError []error) *TypeSystem {
func testParse(t *testing.T, schemajson string, expectParseErr error, expectTypesystemError []error) *schema.TypeSystem {
t.Helper()
dmt, parseErr := parseSchema(schemajson)
Wish(t, parseErr, ShouldEqual, expectParseErr)
if parseErr != nil {
return nil
}
ts, typesystemErr := BuildTypeSystem(dmt)
Wish(t, typesystemErr, ShouldEqual, expectTypesystemError)
ts, typesystemErrs := dmt.Compile()
Require(t, typesystemErrs, ShouldEqual, expectTypesystemError)
return ts
}
func parseSchema(schemajson string) (schemadmt.Schema, error) {
nb := schemadmt.Type.Schema__Repr.NewBuilder()
func parseSchema(schemajson string) (Schema, error) {
nb := Type.Schema__Repr.NewBuilder()
if err := dagjson.Unmarshal(nb, json.NewDecoder(strings.NewReader(schemajson))); err != nil {
return nil, err
}
return nb.Build().(schemadmt.Schema), nil
return nb.Build().(Schema), nil
}
package schema
import (
ipld "github.com/ipld/go-ipld-prime"
)
// TypeKind is an enum of kind in the IPLD Schema system.
//
// Note that schema.TypeKind is distinct from ipld.Kind!
// Schema kinds include concepts such as "struct" and "enum", which are
// concepts only introduced by the Schema layer, and not present in the
// Data Model layer.
type TypeKind uint8
const (
TypeKind_Invalid TypeKind = 0
TypeKind_Map TypeKind = '{'
TypeKind_List TypeKind = '['
TypeKind_Unit TypeKind = '1'
TypeKind_Bool TypeKind = 'b'
TypeKind_Int TypeKind = 'i'
TypeKind_Float TypeKind = 'f'
TypeKind_String TypeKind = 's'
TypeKind_Bytes TypeKind = 'x'
TypeKind_Link TypeKind = '/'
TypeKind_Struct TypeKind = '$'
TypeKind_Union TypeKind = '^'
TypeKind_Enum TypeKind = '%'
// FUTURE: TypeKind_Any = '?'?
)
func (k TypeKind) String() string {
switch k {
case TypeKind_Invalid:
return "Invalid"
case TypeKind_Map:
return "Map"
case TypeKind_List:
return "List"
case TypeKind_Unit:
return "Unit"
case TypeKind_Bool:
return "Bool"
case TypeKind_Int:
return "Int"
case TypeKind_Float:
return "Float"
case TypeKind_String:
return "String"
case TypeKind_Bytes:
return "Bytes"
case TypeKind_Link:
return "Link"
case TypeKind_Struct:
return "Struct"
case TypeKind_Union:
return "Union"
case TypeKind_Enum:
return "Enum"
default:
panic("invalid enumeration value!")
}
}
// ActsLike returns a constant from the ipld.Kind enum describing what
// this schema.TypeKind acts like at the Data Model layer.
//
// Things with similar names are generally conserved
// (e.g. "map" acts like "map");
// concepts added by the schema layer have to be mapped onto something
// (e.g. "struct" acts like "map").
//
// Note that this mapping describes how a typed Node will *act*, programmatically;
// it does not necessarily describe how it will be *serialized*
// (for example, a struct will always act like a map, even if it has a tuple
// representation strategy and thus becomes a list when serialized).
func (k TypeKind) ActsLike() ipld.Kind {
switch k {
case TypeKind_Invalid:
return ipld.Kind_Invalid
case TypeKind_Map:
return ipld.Kind_Map
case TypeKind_List:
return ipld.Kind_List
case TypeKind_Unit:
return ipld.Kind_Bool // maps to 'true'.
case TypeKind_Bool:
return ipld.Kind_Bool
case TypeKind_Int:
return ipld.Kind_Int
case TypeKind_Float:
return ipld.Kind_Float
case TypeKind_String:
return ipld.Kind_String
case TypeKind_Bytes:
return ipld.Kind_Bytes
case TypeKind_Link:
return ipld.Kind_Link
case TypeKind_Struct:
return ipld.Kind_Map // clear enough: fields are keys.
case TypeKind_Union:
return ipld.Kind_Map // REVIEW: unions are tricky.
case TypeKind_Enum:
return ipld.Kind_String // 'AsString' is the one clear thing to define.
default:
panic("invalid enumeration value!")
}
}
package schema
import (
ipld "github.com/ipld/go-ipld-prime"
)
// typesystem.Type is an union interface; each of the `Type*` concrete types
// in this package are one of its members.
//
// Specifically,
//
// TypeBool
// TypeString
// TypeBytes
// TypeInt
// TypeFloat
// TypeMap
// TypeList
// TypeLink
// TypeUnion
// TypeStruct
// TypeEnum
//
// are all of the kinds of Type.
//
// This is a closed union; you can switch upon the above members without
// including a default case. The membership is closed by the unexported
// '_Type' method; you may use the BurntSushi/go-sumtype tool to check
// your switches for completeness.
//
// Many interesting properties of each Type are only defined for that specific
// type, so it's typical to use a type switch to handle each type of Type.
// (Your humble author is truly sorry for the word-mash that results from
// attempting to describe the types that describe the typesystem.Type.)
//
// For example, to inspect the kind of fields in a struct: you might
// cast a `Type` interface into `TypeStruct`, and then the `Fields()` on
// that `TypeStruct` can be inspected. (`Fields()` isn't defined for any
// other kind of Type.)
type Type interface {
// Unexported marker method to force the union closed.
_Type()
// Returns a pointer to the TypeSystem this Type is a member of.
TypeSystem() *TypeSystem
// Returns the string name of the Type. This name is unique within the
// universe this type is a member of, *unless* this type is Anonymous,
// in which case a string describing the type will still be returned, but
// that string will not be required to be unique.
Name() TypeName
// Returns the TypeKind of this Type.
//
// The returned value is a 1:1 association with which of the concrete
// "schema.Type*" structs this interface can be cast to.
//
// Note that a schema.TypeKind is a different enum than ipld.Kind;
// and furthermore, there's no strict relationship between them.
// schema.TypedNode values can be described by *two* distinct Kinds:
// one which describes how the Node itself will act,
// and another which describes how the Node presents for serialization.
// For some combinations of Type and representation strategy, one or both
// of the Kinds can be determined statically; but not always:
// it can sometimes be necessary to inspect the value quite concretely
// (e.g., `schema.TypedNode{}.Representation().Kind()`) in order to find
// out exactly how a node will be serialized! This is because some types
// can vary in representation kind based on their value (specifically,
// kinded-representation unions have this property).
TypeKind() TypeKind
// RepresentationBehavior returns a description of how the representation
// of this type will behave in terms of the IPLD Data Model.
// This property varies based on the representation strategy of a type.
//
// In one case, the representation behavior cannot be known statically,
// and varies based on the data: kinded unions have this trait.
//
// This property is used by kinded unions, which require that their members
// all have distinct representation behavior.
// (It follows that a kinded union cannot have another kinded union as a member.)
//
// You may also be interested in a related property that might have been called "TypeBehavior".
// However, this method doesn't exist, because it's a deterministic property of `TypeKind()`!
// You can use `TypeKind.ActsLike()` to get type-level behavioral information.
RepresentationBehavior() ipld.Kind
}
package schema
import (
"github.com/ipld/go-ipld-prime"
schemadmt "github.com/ipld/go-ipld-prime/schema/dmt"
)
type TypeBool struct {
name TypeName
dmt schemadmt.TypeBool
ts *TypeSystem
}
// -- schema.Type interface satisfaction -->
var _ Type = (*TypeBool)(nil)
func (t *TypeBool) _Type() {}
func (t *TypeBool) TypeSystem() *TypeSystem {
return t.ts
}
func (TypeBool) TypeKind() TypeKind {
return TypeKind_Bool
}
func (t *TypeBool) Name() TypeName {
return t.name
}
func (t TypeBool) RepresentationBehavior() ipld.Kind {
return ipld.Kind_Bool
}
package schema
import (
"github.com/ipld/go-ipld-prime"
schemadmt "github.com/ipld/go-ipld-prime/schema/dmt"
)
type TypeBytes struct {
name TypeName
dmt schemadmt.TypeBytes
ts *TypeSystem
}
// -- schema.Type interface satisfaction -->
var _ Type = (*TypeBytes)(nil)
func (t *TypeBytes) _Type() {}
func (t *TypeBytes) TypeSystem() *TypeSystem {
return t.ts
}
func (TypeBytes) TypeKind() TypeKind {
return TypeKind_Bytes
}
func (t *TypeBytes) Name() TypeName {
return t.name
}
func (t TypeBytes) RepresentationBehavior() ipld.Kind {
return ipld.Kind_Bytes
}
package schema
import (
"github.com/ipld/go-ipld-prime"
schemadmt "github.com/ipld/go-ipld-prime/schema/dmt"
)
type TypeEnum struct {
name TypeName
dmt schemadmt.TypeEnum
ts *TypeSystem
}
type EnumRepresentation interface{ _EnumRepresentation() }
func (EnumRepresentation_String) _EnumRepresentation() {}
func (EnumRepresentation_Int) _EnumRepresentation() {}
type EnumRepresentation_String struct {
dmt schemadmt.EnumRepresentation_String
}
type EnumRepresentation_Int struct {
dmt schemadmt.EnumRepresentation_Int
}
// -- schema.Type interface satisfaction -->
var _ Type = (*TypeEnum)(nil)
func (t *TypeEnum) _Type() {}
func (t *TypeEnum) TypeSystem() *TypeSystem {
return t.ts
}
func (TypeEnum) TypeKind() TypeKind {
return TypeKind_Struct
}
func (t *TypeEnum) Name() TypeName {
return t.name
}
func (t TypeEnum) RepresentationBehavior() ipld.Kind {
switch t.dmt.FieldRepresentation().AsInterface().(type) {
case schemadmt.EnumRepresentation_String:
return ipld.Kind_String
case schemadmt.EnumRepresentation_Int:
return ipld.Kind_Int
default:
panic("unreachable")
}
}
// -- specific to TypeEnum -->
func (t *TypeEnum) RepresentationStrategy() EnumRepresentation {
switch x := t.dmt.FieldRepresentation().AsInterface().(type) {
case schemadmt.EnumRepresentation_String:
return EnumRepresentation_String{x}
case schemadmt.EnumRepresentation_Int:
return EnumRepresentation_Int{x}
default:
panic("unreachable")
}
}
package schema
import (
"github.com/ipld/go-ipld-prime"
schemadmt "github.com/ipld/go-ipld-prime/schema/dmt"
)
type TypeFloat struct {
name TypeName
dmt schemadmt.TypeFloat
ts *TypeSystem
}
// -- schema.Type interface satisfaction -->
var _ Type = (*TypeFloat)(nil)
func (t *TypeFloat) _Type() {}
func (t *TypeFloat) TypeSystem() *TypeSystem {
return t.ts
}
func (TypeFloat) TypeKind() TypeKind {
return TypeKind_Float
}
func (t *TypeFloat) Name() TypeName {
return t.name
}
func (t TypeFloat) RepresentationBehavior() ipld.Kind {
return ipld.Kind_Float
}
package schema
import (
"github.com/ipld/go-ipld-prime"
schemadmt "github.com/ipld/go-ipld-prime/schema/dmt"
)
type TypeInt struct {
name TypeName
dmt schemadmt.TypeInt
ts *TypeSystem
}
// -- schema.Type interface satisfaction -->
var _ Type = (*TypeInt)(nil)
func (t *TypeInt) _Type() {}
func (t *TypeInt) TypeSystem() *TypeSystem {
return t.ts
}
func (TypeInt) TypeKind() TypeKind {
return TypeKind_Int
}
func (t *TypeInt) Name() TypeName {
return t.name
}
func (t TypeInt) RepresentationBehavior() ipld.Kind {
return ipld.Kind_Int
}
package schema
import (
"github.com/ipld/go-ipld-prime"
schemadmt "github.com/ipld/go-ipld-prime/schema/dmt"
)
type TypeLink struct {
name TypeName
dmt schemadmt.TypeLink
ts *TypeSystem
}
// -- schema.Type interface satisfaction -->
var _ Type = (*TypeLink)(nil)
func (t *TypeLink) _Type() {}
func (t *TypeLink) TypeSystem() *TypeSystem {
return t.ts
}
func (TypeLink) TypeKind() TypeKind {
return TypeKind_Link
}
func (t *TypeLink) Name() TypeName {
return t.name
}
func (t TypeLink) RepresentationBehavior() ipld.Kind {
return ipld.Kind_Link
}
// -- specific to TypeLink -->
// HasReferencedType returns true if the link has a hint about the type it references.
func (t *TypeLink) HasReferencedType() bool {
return t.dmt.FieldExpectedType().Exists()
}
// ReferencedType returns the type which is expected for the node on the other side of the link.
// Nil is returned if there is no information about the expected type
// (which may be interpreted as "any").
func (t *TypeLink) ReferencedType() Type {
if !t.dmt.FieldExpectedType().Exists() {
return nil
}
return t.ts.types[t.dmt.FieldExpectedType().Must().TypeReference()]
}
package schema
import (
"github.com/ipld/go-ipld-prime"
schemadmt "github.com/ipld/go-ipld-prime/schema/dmt"
)
type TypeList struct {
name TypeName
dmt schemadmt.TypeList
ts *TypeSystem
}
// -- schema.Type interface satisfaction -->
var _ Type = (*TypeList)(nil)
func (t *TypeList) _Type() {}
func (t *TypeList) TypeSystem() *TypeSystem {
return t.ts
}
func (TypeList) TypeKind() TypeKind {
return TypeKind_Map
}
func (t *TypeList) Name() TypeName {
return t.name
}
func (t TypeList) RepresentationBehavior() ipld.Kind {
return ipld.Kind_Map
}
// -- specific to TypeList -->
// ValueType returns the Type of the map values.
func (t *TypeList) ValueType() Type {
return t.ts.types[t.dmt.FieldValueType().TypeReference()]
}
// ValueIsNullable returns a bool describing if the map values are permitted
// to be null.
func (t *TypeList) ValueIsNullable() bool {
return t.dmt.FieldValueNullable().Bool()
}
package schema
import (
"github.com/ipld/go-ipld-prime"
schemadmt "github.com/ipld/go-ipld-prime/schema/dmt"
)
type TypeMap struct {
name TypeName
dmt schemadmt.TypeMap
ts *TypeSystem
}
// -- schema.Type interface satisfaction -->
var _ Type = (*TypeMap)(nil)
func (t *TypeMap) _Type() {}
func (t *TypeMap) TypeSystem() *TypeSystem {
return t.ts
}
func (TypeMap) TypeKind() TypeKind {
return TypeKind_Map
}
func (t *TypeMap) Name() TypeName {
return t.name
}
func (t TypeMap) RepresentationBehavior() ipld.Kind {
return ipld.Kind_Map
}
// -- specific to TypeMap -->
// KeyType returns the Type of the map keys.
//
// Note that map keys will must always be some type which is representable as a
// string in the IPLD Data Model (e.g. either TypeString or TypeEnum).
func (t *TypeMap) KeyType() Type {
return t.ts.types[t.dmt.FieldKeyType().TypeReference()]
}
// ValueType returns the Type of the map values.
func (t *TypeMap) ValueType() Type {
return t.ts.types[t.dmt.FieldValueType().TypeReference()]
}
// ValueIsNullable returns a bool describing if the map values are permitted
// to be null.
func (t *TypeMap) ValueIsNullable() bool {
return t.dmt.FieldValueNullable().Bool()
}
package schema
import (
"github.com/ipld/go-ipld-prime"
schemadmt "github.com/ipld/go-ipld-prime/schema/dmt"
)
type TypeString struct {
name TypeName
dmt schemadmt.TypeString
ts *TypeSystem
}
// -- schema.Type interface satisfaction -->
var _ Type = (*TypeString)(nil)
func (t *TypeString) _Type() {}
func (t *TypeString) TypeSystem() *TypeSystem {
return t.ts
}
func (TypeString) TypeKind() TypeKind {
return TypeKind_String
}
func (t *TypeString) Name() TypeName {
return t.name
}
func (t TypeString) RepresentationBehavior() ipld.Kind {
return ipld.Kind_String
}
package schema
import (
"fmt"
"github.com/ipld/go-ipld-prime"
schemadmt "github.com/ipld/go-ipld-prime/schema/dmt"
)
type TypeStruct struct {
name TypeName
dmt schemadmt.TypeStruct
ts *TypeSystem
}
type StructField struct {
parent *TypeStruct
name schemadmt.FieldName
dmt schemadmt.StructField
}
type StructRepresentation interface{ _StructRepresentation() }
func (StructRepresentation_Map) _StructRepresentation() {}
func (StructRepresentation_Tuple) _StructRepresentation() {}
func (StructRepresentation_Stringpairs) _StructRepresentation() {}
func (StructRepresentation_Stringjoin) _StructRepresentation() {}
func (StructRepresentation_Listpairs) _StructRepresentation() {}
type StructRepresentation_Map struct {
parent *TypeStruct // this one needs a pointer back up to figure out its defaults.
dmt schemadmt.StructRepresentation_Map
}
type StructRepresentation_Tuple struct {
dmt schemadmt.StructRepresentation_Tuple
}
type StructRepresentation_Stringpairs struct {
dmt schemadmt.StructRepresentation_Stringpairs
}
type StructRepresentation_Stringjoin struct {
dmt schemadmt.StructRepresentation_Stringjoin
}
type StructRepresentation_Listpairs struct {
dmt schemadmt.StructRepresentation_Listpairs
}
// -- schema.Type interface satisfaction -->
var _ Type = (*TypeStruct)(nil)
func (t *TypeStruct) _Type() {}
func (t *TypeStruct) TypeSystem() *TypeSystem {
return t.ts
}
func (TypeStruct) TypeKind() TypeKind {
return TypeKind_Struct
}
func (t *TypeStruct) Name() TypeName {
return t.name
}
func (t TypeStruct) RepresentationBehavior() ipld.Kind {
switch t.dmt.FieldRepresentation().AsInterface().(type) {
case schemadmt.StructRepresentation_Map:
return ipld.Kind_Map
case schemadmt.StructRepresentation_Tuple:
return ipld.Kind_List
case schemadmt.StructRepresentation_Stringpairs:
return ipld.Kind_String
case schemadmt.StructRepresentation_Stringjoin:
return ipld.Kind_String
case schemadmt.StructRepresentation_Listpairs:
return ipld.Kind_List
default:
panic("unreachable")
}
}
// -- specific to TypeStruct -->
// Fields returns a slice of descriptions of the object's fields.
func (t *TypeStruct) Fields() []StructField {
a := make([]StructField, 0, t.dmt.FieldFields().Length())
for itr := t.dmt.FieldFields().Iterator(); itr.Done(); {
k, v := itr.Next()
a = append(a, StructField{t, k, v})
}
return a
}
// Field looks up a StructField by name, or returns nil if no such field.
func (t *TypeStruct) Field(name string) *StructField {
fndmt, err := schemadmt.Type.FieldName.FromString(name)
if err != nil {
panic(fmt.Errorf("invalid fieldname: %w", err))
}
fdmt := t.dmt.FieldFields().Lookup(fndmt)
if fdmt == nil {
return nil
}
return &StructField{t, fndmt, fdmt}
}
// Parent returns the type information that this field describes a part of.
//
// While in many cases, you may know the parent already from context,
// there may still be situations where want to pass around a field and
// not need to continue passing down the parent type with it; this method
// helps your code be less redundant in such a situation.
// (You'll find this useful for looking up any rename directives, for example,
// when holding onto a field, since that requires looking up information from
// the representation strategy, which is a property of the type as a whole.)
func (f *StructField) Parent() *TypeStruct { return f.parent }
// Name returns the string name of this field. The name is the string that
// will be used as a map key if the structure this field is a member of is
// serialized as a map representation.
func (f *StructField) Name() string { return f.name.String() }
// Type returns the Type of this field's value. Note the field may
// also be unset if it is either Optional or Nullable.
func (f *StructField) Type() Type { return f.parent.ts.types[f.dmt.FieldType().TypeReference()] }
// IsOptional returns true if the field is allowed to be absent from the object.
// If IsOptional is false, the field may be absent from the serial representation
// of the object entirely.
//
// Note being optional is different than saying the value is permitted to be null!
// A field may be both nullable and optional simultaneously, or either, or neither.
func (f *StructField) IsOptional() bool { return f.dmt.FieldOptional().Bool() }
// IsNullable returns true if the field value is allowed to be null.
//
// If is Nullable is false, note that it's still possible that the field value
// will be absent if the field is Optional! Being nullable is unrelated to
// whether the field's presence is optional as a whole.
//
// Note that a field may be both nullable and optional simultaneously,
// or either, or neither.
func (f *StructField) IsNullable() bool { return f.dmt.FieldNullable().Bool() }
// IsMaybe returns true if the field value is allowed to be either null or absent.
//
// This is a simple "or" of the two properties,
// but this method is a shorthand that turns out useful often.
func (f *StructField) IsMaybe() bool { return f.IsNullable() || f.IsOptional() }
func (t *TypeStruct) RepresentationStrategy() StructRepresentation {
switch x := t.dmt.FieldRepresentation().AsInterface().(type) {
case schemadmt.StructRepresentation_Map:
return StructRepresentation_Map{t, x}
case schemadmt.StructRepresentation_Tuple:
return StructRepresentation_Tuple{x}
case schemadmt.StructRepresentation_Stringpairs:
return StructRepresentation_Stringpairs{x}
case schemadmt.StructRepresentation_Stringjoin:
return StructRepresentation_Stringjoin{x}
case schemadmt.StructRepresentation_Listpairs:
return StructRepresentation_Listpairs{x}
default:
panic("unreachable")
}
}
// GetFieldKey returns the string that should be the key when serializing this field.
// For some fields, it's the same as the field name; for others, a rename directive may provide a different value.
func (r StructRepresentation_Map) GetFieldKey(field StructField) string {
maybeOverrides := r.dmt.FieldFields()
if !maybeOverrides.Exists() {
return field.Name()
}
fieldInfo := maybeOverrides.Must().Lookup(field.name)
if fieldInfo == nil {
return field.Name()
}
maybeRename := fieldInfo.FieldRename()
if !maybeRename.Exists() {
return field.Name()
}
return maybeRename.Must().String()
}
func (r StructRepresentation_Stringjoin) GetJoinDelim() string {
return r.dmt.FieldJoin().String()
}
package schema
import (
"github.com/ipld/go-ipld-prime"
schemadmt "github.com/ipld/go-ipld-prime/schema/dmt"
)
type TypeUnion struct {
name TypeName
dmt schemadmt.TypeUnion
ts *TypeSystem
}
type UnionRepresentation interface{ _UnionRepresentation() }
func (UnionRepresentation_Keyed) _UnionRepresentation() {}
func (UnionRepresentation_Kinded) _UnionRepresentation() {}
func (UnionRepresentation_Envelope) _UnionRepresentation() {}
func (UnionRepresentation_Inline) _UnionRepresentation() {}
func (UnionRepresentation_StringPrefix) _UnionRepresentation() {}
func (UnionRepresentation_BytePrefix) _UnionRepresentation() {}
type UnionRepresentation_Keyed struct {
ts *TypeSystem
dmt schemadmt.UnionRepresentation_Keyed
}
type UnionRepresentation_Kinded struct {
ts *TypeSystem
dmt schemadmt.UnionRepresentation_Kinded
}
type UnionRepresentation_Envelope struct {
ts *TypeSystem
dmt schemadmt.UnionRepresentation_Envelope
}
type UnionRepresentation_Inline struct {
ts *TypeSystem
dmt schemadmt.UnionRepresentation_Inline
}
type UnionRepresentation_StringPrefix struct {
ts *TypeSystem
dmt schemadmt.UnionRepresentation_StringPrefix
}
type UnionRepresentation_BytePrefix struct {
ts *TypeSystem
dmt schemadmt.UnionRepresentation_BytePrefix
}
// -- schema.Type interface satisfaction -->
var _ Type = (*TypeUnion)(nil)
func (t *TypeUnion) _Type() {}
func (t *TypeUnion) TypeSystem() *TypeSystem {
return t.ts
}
func (TypeUnion) TypeKind() TypeKind {
return TypeKind_Union
}
func (t *TypeUnion) Name() TypeName {
return t.name
}
func (t TypeUnion) RepresentationBehavior() ipld.Kind {
switch t.dmt.FieldRepresentation().AsInterface().(type) {
case schemadmt.UnionRepresentation_Keyed:
return ipld.Kind_Map
case schemadmt.UnionRepresentation_Kinded:
return ipld.Kind_Invalid // you can't know with this one, until you see the value (and thus can see its inhabitant's behavior)!
case schemadmt.UnionRepresentation_Envelope:
return ipld.Kind_Map
case schemadmt.UnionRepresentation_Inline:
return ipld.Kind_Map
case schemadmt.UnionRepresentation_StringPrefix:
return ipld.Kind_String
case schemadmt.UnionRepresentation_BytePrefix:
return ipld.Kind_Bytes
default:
panic("unreachable")
}
}
// -- specific to TypeUnion -->
func (t *TypeUnion) RepresentationStrategy() UnionRepresentation {
switch x := t.dmt.FieldRepresentation().AsInterface().(type) {
case schemadmt.UnionRepresentation_Keyed:
return UnionRepresentation_Keyed{t.ts, x}
case schemadmt.UnionRepresentation_Kinded:
return UnionRepresentation_Kinded{t.ts, x}
case schemadmt.UnionRepresentation_Envelope:
return UnionRepresentation_Envelope{t.ts, x}
case schemadmt.UnionRepresentation_Inline:
return UnionRepresentation_Inline{t.ts, x}
case schemadmt.UnionRepresentation_StringPrefix:
return UnionRepresentation_StringPrefix{t.ts, x}
case schemadmt.UnionRepresentation_BytePrefix:
return UnionRepresentation_BytePrefix{t.ts, x}
default:
panic("unreachable")
}
}
// GetDiscriminantForType looks up the descriminant key for the given type.
// It panics if the given type is not a member of this union.
func (r UnionRepresentation_Keyed) GetDiscriminantForType(t Type) string {
if t.TypeSystem() != r.ts {
panic("that type isn't even from the same universe!")
}
for itr := r.dmt.Iterator(); !itr.Done(); {
k, v := itr.Next()
if v == t.Name() {
return k.String()
}
}
panic("that type isn't a member of this union")
}
// GetMember returns type info for the member matching the kind argument,
// or may return nil if that kind is not mapped to a member of this union.
func (r UnionRepresentation_Kinded) GetMember(k ipld.Kind) Type {
rkdmt, _ := schemadmt.Type.RepresentationKind.FromString(k.String()) // FUTURE: this is currently awkward because we used a string where we should use an enum; this can be fixed when codegen for enums is implemented.
tn := r.dmt.Lookup(rkdmt)
if tn == nil {
return nil
}
return r.ts.types[tn.TypeReference()]
}
package schema
import (
schemadmt "github.com/ipld/go-ipld-prime/schema/dmt"
)
type TypeName = schemadmt.TypeName
type TypeReference = schemadmt.TypeReference
package schema
import (
"fmt"
"github.com/ipld/go-ipld-prime"
schemadmt "github.com/ipld/go-ipld-prime/schema/dmt"
)
type TypeSystem struct {
// Mind the key type here: TypeReference, not TypeName.
// The key might be a computed anon "name" which is not actually a valid type name itself.
types map[TypeReference]Type
// TODO: we should probably have iterable orders ready, both for named types and all types.
// We can derive these afresh from the dmt every time, but we either should have an exported method for that, or even just compute it eagerly and cache it.
}
func BuildTypeSystem(schdmt schemadmt.Schema) (*TypeSystem, []error) {
ts := &TypeSystem{
types: make(map[TypeReference]Type, schdmt.FieldTypes().Length()),
}
var ee []error
// Iterate over all the types, creating the reified forms of them as we go.
// Some forms of validation of the data are already done by nature of the schema; others will require more work here.
// In general: rules which stretch across multiple types (especially, if they're graph properties) can't be implemented in schemas alone, and so end up here.
// Any type that has some kind of recursion (maps, lists, structs, unions, links with target type info) causes a lookup to see if the referenced types exist.
// FUTURE: we'll need most recursives to additionally do some checks for correct composition of nullability after the introduction of unit types.
// We manage to avoid the need for a two-pass system because we just give all reified types a pointer to the typesystem aggregate;
// this means each of them still just stores the type names, and if asked for another type pointer, looks it up on the fly (by which time it's available).
// Some kinds of types involve other more specific checks,
// such as maps verifying that their keys are stringable (which is a rule we enforce for reasons relating to pathing),
// and unions verifying that all their discriminant tables are complete (which is a rule that's necessary for sanity!),
// and etc.
// Only return the assembled TypeSystem value if we encountered no errors.
// If we encountered errors, the TypeSystem is partially constructed and many of its contents cannot uphold their contracts, so it's better not to expose it.
if ee == nil {
return ts, nil
}
return nil, 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