Commit 4da73fb9 authored by Eric Myhre's avatar Eric Myhre

gengo: support for unions with stringprefix representation.

parent 80a4a975
......@@ -31,3 +31,10 @@ func SplitExact(s string, sep string, count int) ([]string, error) {
}
return ss, nil
}
// SplitN is an alias of strings.SplitN, which is only present here to
// make it usable in codegen packages without requiring conditional imports
// in the generation process.
func SplitN(s, sep string, n int) []string {
return strings.SplitN(s, sep, n)
}
......@@ -47,70 +47,71 @@ Legend:
- `?` - feature definition needed! (applies to many of the "native extras" rows -- often there's partial features, but also room for more.)
- ` ` - table is not finished, please refer to the code and help fix the table :)
| feature | accessors | builders |
|:-------------------------------|:---------:|:--------:|
| structs | ... | ... |
| ... type level | ✔ | ✔ |
| ... native extras | ? | ? |
| ... map representation | ✔ | ✔ |
| ... ... including optional | ✔ | ✔ |
| ... ... including renames | ✔ | ✔ |
| ... ... including implicits | ⚠ | ⚠ |
| ... tuple representation | ✔ | ✔ |
| ... ... including optional | ✔ | ✔ |
| ... ... including renames | - | - |
| ... ... including implicits | ⚠ | ⚠ |
| ... stringjoin representation | ✔ | ✔ |
| ... ... including optional | - | - |
| ... ... including renames | - | - |
| ... ... including implicits | - | - |
| ... stringpairs representation | ✘ | ✘ |
| ... ... including optional | | |
| ... ... including renames | | |
| ... ... including implicits | | |
| ... listpairs representation | ✘ | ✘ |
| ... ... including optional | | |
| ... ... including renames | | |
| ... ... including implicits | | |
| feature | accessors | builders |
|:-------------------------------|:---------:|:--------:|
| lists | ... | ... |
| ... type level | ✔ | ✔ |
| ... native extras | ? | ? |
| ... list representation | ✔ | ✔ |
| feature | accessors | builders |
|:-------------------------------|:---------:|:--------:|
| maps | ... | ... |
| ... type level | ✔ | ✔ |
| ... native extras | ? | ? |
| ... map representation | ✔ | ✔ |
| ... stringpairs representation | ✘ | ✘ |
| ... listpairs representation | ✘ | ✘ |
| feature | accessors | builders |
|:-------------------------------|:---------:|:--------:|
| unions | ... | ... |
| ... type level | ✔ | ✔ |
| ... keyed representation | ✔ | ✔ |
| ... envelope representation | ✘ | ✘ |
| ... kinded representation | ✔ | ✔ |
| ... inline representation | ✘ | ✘ |
| ... byteprefix representation | ✘ | ✘ |
| feature | accessors | builders |
|:-------------------------------|:---------:|:--------:|
| strings | ✔ | ✔ |
| bytes | ✔ | ✔ |
| ints | ✔ | ✔ |
| floats | ✔ | ✔ |
| bools | ✔ | ✔ |
| links | ✔ | ✔ |
| feature | accessors | builders |
|:-------------------------------|:---------:|:--------:|
| enums | ... | ... |
| ... type level | ✘ | ✘ |
| ... string representation | ✘ | ✘ |
| ... int representation | ✘ | ✘ |
| feature | accessors | builders |
|:---------------------------------|:---------:|:--------:|
| structs | ... | ... |
| ... type level | ✔ | ✔ |
| ... native extras | ? | ? |
| ... map representation | ✔ | ✔ |
| ... ... including optional | ✔ | ✔ |
| ... ... including renames | ✔ | ✔ |
| ... ... including implicits | ⚠ | ⚠ |
| ... tuple representation | ✔ | ✔ |
| ... ... including optional | ✔ | ✔ |
| ... ... including renames | - | - |
| ... ... including implicits | ⚠ | ⚠ |
| ... stringjoin representation | ✔ | ✔ |
| ... ... including optional | - | - |
| ... ... including renames | - | - |
| ... ... including implicits | - | - |
| ... stringpairs representation | ✘ | ✘ |
| ... ... including optional | | |
| ... ... including renames | | |
| ... ... including implicits | | |
| ... listpairs representation | ✘ | ✘ |
| ... ... including optional | | |
| ... ... including renames | | |
| ... ... including implicits | | |
| feature | accessors | builders |
|:---------------------------------|:---------:|:--------:|
| lists | ... | ... |
| ... type level | ✔ | ✔ |
| ... native extras | ? | ? |
| ... list representation | ✔ | ✔ |
| feature | accessors | builders |
|:---------------------------------|:---------:|:--------:|
| maps | ... | ... |
| ... type level | ✔ | ✔ |
| ... native extras | ? | ? |
| ... map representation | ✔ | ✔ |
| ... stringpairs representation | ✘ | ✘ |
| ... listpairs representation | ✘ | ✘ |
| feature | accessors | builders |
|:---------------------------------|:---------:|:--------:|
| unions | ... | ... |
| ... type level | ✔ | ✔ |
| ... keyed representation | ✔ | ✔ |
| ... envelope representation | ✘ | ✘ |
| ... kinded representation | ✔ | ✔ |
| ... inline representation | ✘ | ✘ |
| ... stringprefix representation | ✔ | ✔ |
| ... byteprefix representation | ✘ | ✘ |
| feature | accessors | builders |
|:---------------------------------|:---------:|:--------:|
| strings | ✔ | ✔ |
| bytes | ✔ | ✔ |
| ints | ✔ | ✔ |
| floats | ✔ | ✔ |
| bools | ✔ | ✔ |
| links | ✔ | ✔ |
| feature | accessors | builders |
|:---------------------------------|:---------:|:--------:|
| enums | ... | ... |
| ... type level | ✘ | ✘ |
| ... string representation | ✘ | ✘ |
| ... int representation | ✘ | ✘ |
......@@ -40,13 +40,17 @@ func (g unionGenerator) EmitNativeType(w io.Writer) {
//
// The interface *mostly* isn't used... except for in the return type of a speciated function which can be used to do golang-native type switches.
//
// The interface also includes a requirement for an errorless primitive access method (such as `String() string`)
// if our representation strategy is one that has that semantic (e.g., stringprefix repr does).
//
// A note about index: in all cases the index of a member type is used, we increment it by one, to avoid using zero.
// We do this because it's desirable to reserve the zero in the 'tag' field (if we generate one) as a sentinel value
// (see further comments in the EmitNodeAssemblerType function);
// and since we do it in that one case, it's just as well to do it uniformly.
doTemplate(`
{{- if Comments -}}
// {{ .Type | TypeSymbol }} matches the IPLD Schema type "{{ .Type.Name }}". It has {{ .Type.TypeKind }} type-kind, and may be interrogated like {{ .Kind }} kind.
// {{ .Type | TypeSymbol }} matches the IPLD Schema type "{{ .Type.Name }}".
// {{ .Type | TypeSymbol }} has {{ .Type.TypeKind }} typekind, which means its data model behaviors are that of a {{ .Kind }} kind.
{{- end}}
type {{ .Type | TypeSymbol }} = *_{{ .Type | TypeSymbol }}
type _{{ .Type | TypeSymbol }} struct {
......@@ -61,6 +65,9 @@ func (g unionGenerator) EmitNativeType(w io.Writer) {
}
type _{{ .Type | TypeSymbol }}__iface interface {
_{{ .Type | TypeSymbol }}__member()
{{- if (eq (.Type.RepresentationStrategy | printf "%T") "schema.UnionRepresentation_Stringprefix") }}
String() string
{{- end}}
}
{{- range $member := .Type.Members }}
......
package gengo
import (
"io"
"github.com/ipld/go-ipld-prime/schema"
"github.com/ipld/go-ipld-prime/schema/gen/go/mixins"
)
var _ TypeGenerator = &unionReprStringprefixGenerator{}
func NewUnionReprStringprefixGenerator(pkgName string, typ *schema.TypeUnion, adjCfg *AdjunctCfg) TypeGenerator {
return unionReprStringprefixGenerator{
unionGenerator{
adjCfg,
mixins.MapTraits{
PkgName: pkgName,
TypeName: string(typ.Name()),
TypeSymbol: adjCfg.TypeSymbol(typ),
},
pkgName,
typ,
},
}
}
type unionReprStringprefixGenerator struct {
unionGenerator
}
func (g unionReprStringprefixGenerator) GetRepresentationNodeGen() NodeGenerator {
return unionReprStringprefixReprGenerator{
g.AdjCfg,
mixins.StringTraits{
PkgName: g.PkgName,
TypeName: string(g.Type.Name()) + ".Repr",
TypeSymbol: "_" + g.AdjCfg.TypeSymbol(g.Type) + "__Repr",
},
g.PkgName,
g.Type,
}
}
type unionReprStringprefixReprGenerator struct {
AdjCfg *AdjunctCfg
mixins.StringTraits
PkgName string
Type *schema.TypeUnion
}
func (unionReprStringprefixReprGenerator) IsRepr() bool { return true } // hint used in some generalized templates.
func (g unionReprStringprefixReprGenerator) EmitNodeType(w io.Writer) {
// The type is structurally the same, but will have a different set of methods.
doTemplate(`
type _{{ .Type | TypeSymbol }}__Repr _{{ .Type | TypeSymbol }}
`, w, g.AdjCfg, g)
// We do also want some constants for our discriminant values;
// they'll make iterators able to work faster.
doTemplate(`
var (
{{- range $member := .Type.Members }}
memberName__{{ dot.Type | TypeSymbol }}_{{ $member.Name }}_serial = _String{"{{ $member | dot.Type.RepresentationStrategy.GetDiscriminant }}"}
{{- end }}
)
`, w, g.AdjCfg, g)
}
func (g unionReprStringprefixReprGenerator) EmitNodeTypeAssertions(w io.Writer) {
doTemplate(`
var _ ipld.Node = &_{{ .Type | TypeSymbol }}__Repr{}
`, w, g.AdjCfg, g)
}
func (g unionReprStringprefixReprGenerator) EmitNodeMethodAsString(w io.Writer) {
// See comment block in structReprStringjoinReprGenerator.EmitNodeMethodAsString for a lot of philosophizing about this.
doTemplate(`
func (n *_{{ .Type | TypeSymbol }}__Repr) AsString() (string, error) {
return n.String(), nil
}
func (n *_{{ .Type | TypeSymbol }}__Repr) String() string {
{{- if (eq (.AdjCfg.UnionMemlayout .Type) "embedAll") }}
switch n.tag {
{{- range $i, $member := .Type.Members }}
case {{ add $i 1 }}:
return memberName__{{ dot.Type | TypeSymbol }}_{{ $member.Name }}_serial.String() + "{{ dot.Type.RepresentationStrategy.GetDelim }}" + n.x{{ add $i 1 }}.String()
{{- end}}
{{- else if (eq (.AdjCfg.UnionMemlayout .Type) "interface") }}
switch n2 := n.x.(type) {
{{- range $member := .Type.Members }}
case {{ $member | TypeSymbol }}:
return memberName__{{ dot.Type | TypeSymbol }}_{{ $member.Name }}_serial.String() + "{{ dot.Type.RepresentationStrategy.GetDelim }}" + n2.String()
{{- end}}
{{- end}}
default:
panic("unreachable")
}
}
func (n {{ .Type | TypeSymbol }}) String() string {
return (*_{{ .Type | TypeSymbol }}__Repr)(n).String()
}
`, w, g.AdjCfg, g)
}
func (g unionReprStringprefixReprGenerator) EmitNodeMethodPrototype(w io.Writer) {
emitNodeMethodPrototype_typical(w, g.AdjCfg, g)
}
func (g unionReprStringprefixReprGenerator) EmitNodePrototypeType(w io.Writer) {
emitNodePrototypeType_typical(w, g.AdjCfg, g)
}
// --- NodeBuilder and NodeAssembler --->
func (g unionReprStringprefixReprGenerator) GetNodeBuilderGenerator() NodeBuilderGenerator {
return unionReprStringprefixReprBuilderGenerator{
g.AdjCfg,
mixins.StringAssemblerTraits{
PkgName: g.PkgName,
TypeName: g.TypeName,
AppliedPrefix: "_" + g.AdjCfg.TypeSymbol(g.Type) + "__Repr",
},
g.PkgName,
g.Type,
}
}
type unionReprStringprefixReprBuilderGenerator struct {
AdjCfg *AdjunctCfg
mixins.StringAssemblerTraits
PkgName string
Type *schema.TypeUnion
}
func (unionReprStringprefixReprBuilderGenerator) IsRepr() bool { return true } // hint used in some generalized templates.
func (g unionReprStringprefixReprBuilderGenerator) EmitNodeBuilderType(w io.Writer) {
emitEmitNodeBuilderType_typical(w, g.AdjCfg, g)
}
func (g unionReprStringprefixReprBuilderGenerator) EmitNodeBuilderMethods(w io.Writer) {
emitNodeBuilderMethods_typical(w, g.AdjCfg, g)
// Generate a single-step construction function -- this is easy to do for a scalar,
// and all representations of scalar kind can be expected to have a method like this.
// The function is attached to the NodePrototype for convenient namespacing;
// it needs no new memory, so it would be inappropriate to attach to the builder or assembler.
// The function is directly used internally by anything else that might involve recursive destructuring on the same scalar kind
// (for example, structs using stringjoin strategies that have one of this type as a field, etc).
// Since we're a representation of scalar kind, and can recurse,
// we ourselves presume this plain construction method must also exist for all our members.
// REVIEW: We could make an immut-safe verion of this and export it on the NodePrototype too, as `FromString(string)`.
doTemplate(`
func (_{{ .Type | TypeSymbol }}__ReprPrototype) fromString(w *_{{ .Type | TypeSymbol }}, v string) error {
ss := mixins.SplitN(v, "{{ .Type.RepresentationStrategy.GetDelim }}", 2)
if len(ss) != 2 {
return ipld.ErrUnmatchable{TypeName:"{{ .PkgName }}.{{ .Type.Name }}.Repr"}.Reasonf("expecting a stringprefix union but found no delimiter in the value")
}
switch ss[0] {
{{- range $i, $member := .Type.Members }}
case "{{ $member | dot.Type.RepresentationStrategy.GetDiscriminant }}":
{{- if (eq (dot.AdjCfg.UnionMemlayout dot.Type) "embedAll") }}
w.tag = {{ add $i 1 }}
if err := (_{{ $member | TypeSymbol }}__ReprPrototype{}).fromString(&w.x{{ add $i 1 }}, ss[1]); err != nil {
return ipld.ErrUnmatchable{TypeName:"{{ dot.PkgName }}.{{ dot.Type.Name }}.Repr", Reason: err}
}
return nil
{{- else if (eq (dot.AdjCfg.UnionMemlayout dot.Type) "interface") }}
var n2 _{{ $member | TypeSymbol }}
if err := (_{{ $member | TypeSymbol }}__ReprPrototype{}).fromString(&n2, ss[1]); err != nil {
return ipld.ErrUnmatchable{TypeName:"{{ dot.PkgName }}.{{ dot.Type.Name }}.Repr", Reason: err}
}
w.x = &n2
return nil
{{- end}}
{{- end}}
default:
return schema.ErrNoSuchField{Type: nil /*TODO*/, Field: ipld.PathSegmentOfString(ss[0])}
}
}
`, w, g.AdjCfg, g)
}
func (g unionReprStringprefixReprBuilderGenerator) EmitNodeAssemblerType(w io.Writer) {
doTemplate(`
type _{{ .Type | TypeSymbol }}__ReprAssembler struct {
w *_{{ .Type | TypeSymbol }}
m *schema.Maybe
}
func (na *_{{ .Type | TypeSymbol }}__ReprAssembler) reset() {}
`, w, g.AdjCfg, g)
}
func (g unionReprStringprefixReprBuilderGenerator) EmitNodeAssemblerMethodAssignNull(w io.Writer) {
emitNodeAssemblerMethodAssignNull_scalar(w, g.AdjCfg, g)
}
func (g unionReprStringprefixReprBuilderGenerator) EmitNodeAssemblerMethodAssignString(w io.Writer) {
// This method contains a branch to support MaybeUsesPtr because new memory may need to be allocated.
// This allocation only happens if the 'w' ptr is nil, which means we're being used on a Maybe;
// otherwise, the 'w' ptr should already be set, and we fill that memory location without allocating, as usual.
// TODO:DRY: this is identical to other string-repr-on-non-string-type.
doTemplate(`
func (na *_{{ .Type | TypeSymbol }}__ReprAssembler) AssignString(v string) error {
switch *na.m {
case schema.Maybe_Value, schema.Maybe_Null:
panic("invalid state: cannot assign into assembler that's already finished")
}
{{- if .Type | MaybeUsesPtr }}
if na.w == nil {
na.w = &_{{ .Type | TypeSymbol }}{}
}
{{- end}}
if err := (_{{ .Type | TypeSymbol }}__ReprPrototype{}).fromString(na.w, v); err != nil {
return err
}
*na.m = schema.Maybe_Value
return nil
}
`, w, g.AdjCfg, g)
}
func (g unionReprStringprefixReprBuilderGenerator) EmitNodeAssemblerMethodAssignNode(w io.Writer) {
// AssignNode goes through three phases:
// 1. is it null? Jump over to AssignNull (which may or may not reject it).
// 2. is it our own type? Handle specially -- we might be able to do efficient things.
// 3. is it the right kind to morph into us? Do so.
// TODO:DRY: this is identical to other string-repr-on-non-string-type.
doTemplate(`
func (na *_{{ .Type | TypeSymbol }}__ReprAssembler) AssignNode(v ipld.Node) error {
if v.IsNull() {
return na.AssignNull()
}
if v2, ok := v.(*_{{ .Type | TypeSymbol }}); ok {
switch *na.m {
case schema.Maybe_Value, schema.Maybe_Null:
panic("invalid state: cannot assign into assembler that's already finished")
}
{{- if .Type | MaybeUsesPtr }}
if na.w == nil {
na.w = v2
*na.m = schema.Maybe_Value
return nil
}
{{- end}}
*na.w = *v2
*na.m = schema.Maybe_Value
return nil
}
if v2, err := v.AsString(); err != nil {
return err
} else {
return na.AssignString(v2)
}
}
`, w, g.AdjCfg, g)
}
func (g unionReprStringprefixReprBuilderGenerator) EmitNodeAssemblerOtherBits(w io.Writer) {
// None for this.
}
......@@ -74,6 +74,8 @@ func Generate(pth string, pkgName string, ts schema.TypeSystem, adjCfg *AdjunctC
fn(NewUnionReprKeyedGenerator(pkgName, t2, adjCfg), f)
case schema.UnionRepresentation_Kinded:
fn(NewUnionReprKindedGenerator(pkgName, t2, adjCfg), f)
case schema.UnionRepresentation_Stringprefix:
fn(NewUnionReprStringprefixGenerator(pkgName, t2, adjCfg), f)
default:
panic("unrecognized union representation strategy")
}
......
package gengo
import (
"testing"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/schema"
)
func TestUnionStringprefix(t *testing.T) {
t.Parallel()
ts := schema.TypeSystem{}
ts.Init()
adjCfg := &AdjunctCfg{}
ts.Accumulate(schema.SpawnString("String"))
ts.Accumulate(schema.SpawnStruct("SmolStruct",
[]schema.StructField{
schema.SpawnStructField("a", "String", false, false),
schema.SpawnStructField("b", "String", false, false),
},
schema.SpawnStructRepresentationStringjoin(":"),
))
ts.Accumulate(schema.SpawnUnion("WheeUnion",
[]schema.TypeName{
"String",
"SmolStruct",
},
schema.SpawnUnionRepresentationStringprefix(
":",
map[string]schema.TypeName{
"simple": "String",
"complex": "SmolStruct",
},
),
))
// These are the same *type-level* as in TestUnionKeyedComplexChildren,
// but (of course) have very different representations.
specs := []testcase{
{
name: "InhabitantA",
typeJson: `{"String":"whee"}`,
reprJson: `"simple:whee"`,
typePoints: []testcasePoint{
{"", ipld.Kind_Map},
{"String", "whee"},
//{"SmolStruct", ipld.ErrNotExists{}}, // TODO: need better error typing from traversal package.
},
reprPoints: []testcasePoint{
{"", ipld.Kind_String},
{"", "simple:whee"},
},
},
{
name: "InhabitantB",
typeJson: `{"SmolStruct":{"a":"whee","b":"woo"}}`,
reprJson: `"complex:whee:woo"`,
typePoints: []testcasePoint{
{"", ipld.Kind_Map},
//{"String", ipld.ErrNotExists{}}, // TODO: need better error typing from traversal package.
{"SmolStruct", ipld.Kind_Map},
{"SmolStruct/a", "whee"},
{"SmolStruct/b", "woo"},
},
reprPoints: []testcasePoint{
{"", ipld.Kind_String},
{"", "complex:whee:woo"},
},
},
}
test := func(t *testing.T, getPrototypeByName func(string) ipld.NodePrototype) {
np := getPrototypeByName("WheeUnion")
nrp := getPrototypeByName("WheeUnion.Repr")
for _, tcase := range specs {
tcase.Test(t, np, nrp)
}
}
t.Run("union-using-embed", func(t *testing.T) {
adjCfg.CfgUnionMemlayout = map[schema.TypeName]string{"WheeUnion": "embedAll"}
prefix := "union-stringprefix-using-embed"
pkgName := "main"
genAndCompileAndTest(t, prefix, pkgName, ts, adjCfg, func(t *testing.T, getPrototypeByName func(string) ipld.NodePrototype) {
test(t, getPrototypeByName)
})
})
t.Run("union-using-interface", func(t *testing.T) {
adjCfg.CfgUnionMemlayout = map[schema.TypeName]string{"WheeUnion": "interface"}
prefix := "union-stringprefix-using-interface"
pkgName := "main"
genAndCompileAndTest(t, prefix, pkgName, ts, adjCfg, func(t *testing.T, getPrototypeByName func(string) ipld.NodePrototype) {
test(t, getPrototypeByName)
})
})
}
......@@ -107,6 +107,9 @@ func SpawnUnionRepresentationKeyed(table map[string]TypeName) UnionRepresentatio
func SpawnUnionRepresentationKinded(table map[ipld.Kind]TypeName) UnionRepresentation_Kinded {
return UnionRepresentation_Kinded{table}
}
func SpawnUnionRepresentationStringprefix(delim string, table map[string]TypeName) UnionRepresentation_Stringprefix {
return UnionRepresentation_Stringprefix{delim, table}
}
// The methods relating to TypeSystem are also mutation-heavy and placeholdery.
......
......@@ -165,10 +165,11 @@ type TypeUnion struct {
type UnionRepresentation interface{ _UnionRepresentation() }
func (UnionRepresentation_Keyed) _UnionRepresentation() {}
func (UnionRepresentation_Kinded) _UnionRepresentation() {}
func (UnionRepresentation_Envelope) _UnionRepresentation() {}
func (UnionRepresentation_Inline) _UnionRepresentation() {}
func (UnionRepresentation_Keyed) _UnionRepresentation() {}
func (UnionRepresentation_Kinded) _UnionRepresentation() {}
func (UnionRepresentation_Envelope) _UnionRepresentation() {}
func (UnionRepresentation_Inline) _UnionRepresentation() {}
func (UnionRepresentation_Stringprefix) _UnionRepresentation() {}
// A bunch of these tables in union representation might be easier to use if flipped;
// we almost always index into them by type (since that's what we have an ordered list of);
......@@ -190,6 +191,10 @@ type UnionRepresentation_Inline struct {
discriminantKey string
table map[string]TypeName // key is user-defined freetext
}
type UnionRepresentation_Stringprefix struct {
delim string
table map[string]TypeName // key is user-defined freetext
}
type TypeStruct struct {
typeBase
......
......@@ -148,6 +148,19 @@ func (r UnionRepresentation_Keyed) GetDiscriminant(t Type) string {
panic("that type isn't a member of this union")
}
func (r UnionRepresentation_Stringprefix) GetDelim() string {
return r.delim
}
func (r UnionRepresentation_Stringprefix) GetDiscriminant(t Type) string {
for d, t2 := range r.table {
if t2 == t.Name() {
return d
}
}
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) TypeName {
......
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