From 59c295db1d2b010a25551b44d214079505903b32 Mon Sep 17 00:00:00 2001 From: Eric Myhre Date: Sun, 29 Mar 2020 19:01:34 +0200 Subject: [PATCH] Beginning of struct gen; much ado about Maybes. I *think* the notes on Maybes are now oscillating increasingly closely around a consistent centroid. But getting here has been a one heck of a noodle-scratcher. --- schema/gen/go/HACKME_wip.md | 28 +++ schema/gen/go/adjunctCfg.go | 4 +- schema/gen/go/genMinima.go | 28 +++ schema/gen/go/genString.go | 13 +- schema/gen/go/genStruct.go | 288 ++++++++++++++++++++++++++++++ schema/gen/go/genStructReprMap.go | 31 ++++ schema/gen/go/generators.go | 3 + schema/gen/go/smoke_test.go | 17 ++ schema/gen/go/templateUtil.go | 12 ++ schema/maybe.go | 4 +- schema/typeMethods.go | 6 + 11 files changed, 424 insertions(+), 10 deletions(-) create mode 100644 schema/gen/go/genMinima.go create mode 100644 schema/gen/go/genStruct.go create mode 100644 schema/gen/go/genStructReprMap.go diff --git a/schema/gen/go/HACKME_wip.md b/schema/gen/go/HACKME_wip.md index 2937035..4350d33 100644 --- a/schema/gen/go/HACKME_wip.md +++ b/schema/gen/go/HACKME_wip.md @@ -65,9 +65,37 @@ underreviewed - may also just want to shield it entirely. - might be easier than having its enum component include an 'invalid' state which can kick you in the shins at runtime. - roughly the same decision we previously considered for other types (aside from the word for such an enum already existing here). + - if we make the zero state be 'absent' rather than introducing a new 'invalid' state, that makes this less bad. - can't think of significant arguments against this. returning pointers to maybes embedded in larger structures does seem to be the way to go anyway. - it's an adjunct parameter whether the value field in the MaybeT is a pointer; may vary for each T. - need to sometimes use pointers: for cycle-breaking! - want customizability of optimism (oversize-allocation versus alloc-count-amortization). - is it possible to want different Maybe implementation strategies in different areas? Perhaps, but hopefully vanishingly unlikely in practice. Do not want to support this; complexity add high. - thinking this may want to default to useptr=yes. less likely to generate end user surprise. can opt into noptr fastness when/where you know it works. + - there are a couple very predictable places where we *do* want useptr=no, also. + - strings! and other scalars. essentially *never* useful to have a ptr to those inside their MaybeT. +- MaybeT should be an alias of `*_T_Maybe`. + - Sure, sometimes we could get along fine by passing them around by value. + - But sometimes -- when they contain a large value and don't use a ptr internally -- we don't want to. + - And we don't want that to then embroil into *this* MaybeT requiring you to *handle* it externally as a ptr at the same time when some MaybeT2 requires the opposite. + - Coincidentally, removes a fair number of conditionalizations of '&' when returning fields. + - Not a factor in the decision, but a nice bonus. + - A bunch of remaining conditionals in templates also all consistently move to the end of their area, which is... sort of pleasing. + - This means we get to "shield" it entirely. + - Not a factor in the decision (perhaps surprisingly). But doesn't hurt. + - The zero values of the maybe type already didn't threaten to reveal zeros of the T, as long as the field was private and the zero state for maybes isn't 'Maybe_Value'. + - We could also just use an exported `*MaybeT`. But I see no reason to favor this. + - Since we've already resolved to embed MaybeT values (rather than bitpack their states together), there's no cost to this. + - When speciated methods return a `MaybeT`, it's an internal pointer to existing memory. + - When creating a populated maybe... + - If it's a useptr=yes, this might mean you get two allocs... except I think the outer one should optimize out: + - the conversion into maybe should be inlinable, and that should make the whole thing escape-analyzable to get rid of the maybe alloc? + - If it's a useptr=no, construction is probably going to involve a big ol' memcopy. + - This is unfortunate, but I can't think of a way around it short of doing the full dance of a builder. + - This probably won't be an issue in practice, if we're imagining useptr=no is only likely to be used on fairly small values. + - We can also just try to avoid the freestanding creation of maybes entirely. + - The ideal look and field of speciated builders has not been determined. SetAbsent, etc, methods are certainly in bounds. + - Structs and lists can certainly amortize these. + - Maps would turn tricky, except we're already happy with having an internal slice in those too. So, it's covered. + - Interestingly, maps also have the ability to easily store all of (null,absent,value{...}) in them, without extra space. + - But considering the thing about slices-in-maps, that may not be relevant. We want quick linear iterator-friendly reads of Maybe state too. diff --git a/schema/gen/go/adjunctCfg.go b/schema/gen/go/adjunctCfg.go index 7de5f3d..407f87f 100644 --- a/schema/gen/go/adjunctCfg.go +++ b/schema/gen/go/adjunctCfg.go @@ -7,7 +7,7 @@ import ( ) type AdjunctCfg struct { - typeSymbolOverrides map[schema.Type]string + typeSymbolOverrides map[schema.TypeName]string fieldSymbolLowerOverrides map[schema.StructField]string fieldSymbolUpperOverrides map[schema.StructField]string @@ -26,7 +26,7 @@ type AdjunctCfg struct { // etc. // (Most such augmentations are not configurable.) func (cfg *AdjunctCfg) TypeSymbol(t schema.Type) string { - if x, ok := cfg.typeSymbolOverrides[t]; ok { + if x, ok := cfg.typeSymbolOverrides[t.Name()]; ok { return x } return string(t.Name()) // presumed already upper diff --git a/schema/gen/go/genMinima.go b/schema/gen/go/genMinima.go new file mode 100644 index 0000000..be17402 --- /dev/null +++ b/schema/gen/go/genMinima.go @@ -0,0 +1,28 @@ +package gengo + +import ( + "fmt" + "io" + + wish "github.com/warpfork/go-wish" +) + +// EmitInternalEnums creates a file with enum types used internal. +// For example, the state machine values used in map and list builders. +func EmitInternalEnums(packageName string, w io.Writer) { + fmt.Fprint(w, wish.Dedent(` + package `+packageName+` + + // Code generated go-ipld-prime DO NOT EDIT. + + type maState uint8 + + const ( + maState_initial maState = iota + maState_midKey + maState_expectValue + maState_midValue + maState_finished + ) + `)) +} diff --git a/schema/gen/go/genString.go b/schema/gen/go/genString.go index 4939c8c..2102f88 100644 --- a/schema/gen/go/genString.go +++ b/schema/gen/go/genString.go @@ -50,13 +50,14 @@ func (g stringGenerator) EmitNativeBuilder(w io.Writer) { } func (g stringGenerator) EmitNativeMaybe(w io.Writer) { - // REVIEW: can this be extracted to the mixins package? it doesn't even vary for kind. - // REVIEW: what conventions and interfaces are required around Maybe types is very non-finalized. + // REVIEW: can this be extracted to the mixins package? it doesn't appear to vary for kind. + // We *do* somtimes need to vary this for whether or not 'v' is a pointer. For strings: it's not. For others? Depends. doTemplate(` - type Maybe{{ .Type | TypeSymbol }} struct { + type _{{ .Type | TypeSymbol }}__Maybe struct { m schema.Maybe - n {{ .Type | TypeSymbol }} + v {{ .Type | TypeSymbol }} } + type Maybe{{ .Type | TypeSymbol }} = *_{{ .Type | TypeSymbol }}__Maybe func (m Maybe{{ .Type | TypeSymbol }}) IsNull() bool { return m.m == schema.Maybe_Null @@ -71,7 +72,7 @@ func (g stringGenerator) EmitNativeMaybe(w io.Writer) { if !m.Exists() { panic("unbox of a maybe rejected") } - return m.n + return m.v } `, w, g.AdjCfg, g) } @@ -128,7 +129,7 @@ func (g stringGenerator) EmitNodeMethodAsString(w io.Writer) { func (g stringGenerator) EmitNodeMethodStyle(w io.Writer) { doTemplate(` func ({{ .Type | TypeSymbol }}) Style() ipld.NodeStyle { - return nil // TODO + return _{{ .Type | TypeSymbol }}__Style{} } `, w, g.AdjCfg, g) } diff --git a/schema/gen/go/genStruct.go b/schema/gen/go/genStruct.go new file mode 100644 index 0000000..5553d0e --- /dev/null +++ b/schema/gen/go/genStruct.go @@ -0,0 +1,288 @@ +package gengo + +import ( + "io" + + "github.com/ipld/go-ipld-prime/schema" + "github.com/ipld/go-ipld-prime/schema/gen/go/mixins" +) + +type structGenerator struct { + AdjCfg *AdjunctCfg + mixins.MapTraits + PkgName string + Type schema.TypeStruct +} + +// --- native content and specializations ---> + +func (g structGenerator) EmitNativeType(w io.Writer) { + doTemplate(` + type _{{ .Type | TypeSymbol }} struct { + {{- range $field := .Type.Fields}} + {{ $field | FieldSymbolLower }} _{{ $field.Type | TypeSymbol }}{{if $field.IsMaybe }}__Maybe{{end}} + {{- end}} + } + type {{ .Type | TypeSymbol }} = *_{{ .Type | TypeSymbol }} + `, w, g.AdjCfg, g) +} + +func (g structGenerator) EmitNativeAccessors(w io.Writer) { + doTemplate(` + {{- $type := .Type -}} {{- /* ranging modifies dot, unhelpfully */ -}} + {{- range $field := .Type.Fields }} + func (n _{{ $type | TypeSymbol }}) Field{{ $field | FieldSymbolUpper }}() {{ if $field.IsMaybe }}Maybe{{end}}{{ $field.Type | TypeSymbol }} { + return &n.{{ $field | FieldSymbolLower }} + } + {{- end}} + `, w, g.AdjCfg, g) +} + +func (g structGenerator) EmitNativeBuilder(w io.Writer) { + // Unclear what, if anything, goes here. +} + +func (g structGenerator) EmitNativeMaybe(w io.Writer) { + // TODO maybes need a lot of questions answered +} + +// --- type info ---> + +func (g structGenerator) EmitTypeConst(w io.Writer) { + doTemplate(` + // TODO EmitTypeConst + `, w, g.AdjCfg, g) +} + +// --- TypedNode interface satisfaction ---> + +func (g structGenerator) EmitTypedNodeMethodType(w io.Writer) { + doTemplate(` + func ({{ .Type | TypeSymbol }}) Type() schema.Type { + return nil /*TODO:typelit*/ + } + `, w, g.AdjCfg, g) +} + +func (g structGenerator) EmitTypedNodeMethodRepresentation(w io.Writer) { + // Perhaps surprisingly, the way to get the representation node pointer + // does not actually depend on what the representation strategy is. + // REVIEW: this appears to be standard even across kinds; can we extract it? + doTemplate(` + func (n {{ .Type | TypeSymbol }}) Representation() ipld.Node { + return (*_{{ .Type | TypeSymbol }}__Repr)(n) + } + `, w, g.AdjCfg, g) +} + +// --- Node interface satisfaction ---> + +func (g structGenerator) EmitNodeType(w io.Writer) { + // No additional types needed. Methods all attach to the native type. + // We do, however, want some constants for our fields; + // they'll make iterators able to work faster. So let's emit those. + doTemplate(` + var ( + {{- $type := .Type -}} {{- /* ranging modifies dot, unhelpfully */ -}} + {{- range $field := .Type.Fields }} + fieldName__{{ $type | TypeSymbol }}_{{ $field | FieldSymbolUpper }} = _String{"{{ $field.Name }}"} + {{- end }} + ) + `, w, g.AdjCfg, g) + +} + +func (g structGenerator) EmitNodeTypeAssertions(w io.Writer) { + doTemplate(` + var _ ipld.Node = ({{ .Type | TypeSymbol }})(&_{{ .Type | TypeSymbol }}{}) + var _ schema.TypedNode = ({{ .Type | TypeSymbol }})(&_{{ .Type | TypeSymbol }}{}) + `, w, g.AdjCfg, g) +} + +func (g structGenerator) EmitNodeMethodLookupString(w io.Writer) { + doTemplate(` + func (n {{ .Type | TypeSymbol }}) LookupString(key string) (ipld.Node, error) { + switch key { + {{- range $field := .Type.Fields }} + case "{{ $field.Name }}": + {{- if $field.IsOptional }} + if n.{{ $field | FieldSymbolLower }}.m == schema.Maybe_Absent { + return ipld.Undef, nil + } + {{- end}} + {{- if $field.IsNullable }} + if n.{{ $field | FieldSymbolLower }}.m == schema.Maybe_Null { + return ipld.Null, nil + } + {{- end}} + {{- if or $field.IsOptional $field.IsNullable }} + return n.{{ $field | FieldSymbolLower }}.v, nil + {{- else}} + return &n.{{ $field | FieldSymbolLower }}, nil + {{- end}} + {{- end}} + default: + return nil, schema.ErrNoSuchField{Type: nil /*TODO*/, FieldName: key} + } + } + `, w, g.AdjCfg, g) +} + +func (g structGenerator) EmitNodeMethodLookup(w io.Writer) { + doTemplate(` + func (n {{ .Type | TypeSymbol }}) Lookup(key ipld.Node) (ipld.Node, error) { + ks, err := key.AsString() + if err != nil { + return nil, ipld.ErrInvalidKey{"got " + key.ReprKind().String() + ", need string"} + } + return n.LookupString(ks) + } + `, w, g.AdjCfg, g) +} + +func (g structGenerator) EmitNodeMethodMapIterator(w io.Writer) { + // Note that the typed iterator will report absent fields. + // The representation iterator (if has one) however will skip those. + doTemplate(` + func (n {{ .Type | TypeSymbol }}) MapIterator() ipld.MapIterator { + return &_{{ .Type | TypeSymbol }}__MapItr{n, 0} + } + + type _{{ .Type | TypeSymbol }}__MapItr struct { + n {{ .Type | TypeSymbol }} + idx int + } + + func (itr *_{{ .Type | TypeSymbol }}__MapItr) Next() (k ipld.Node, v ipld.Node, _ error) { + if itr.idx >= {{ len .Type.Fields }} { + return nil, nil, ipld.ErrIteratorOverread{} + } + switch itr.idx { + {{- $type := .Type -}} {{- /* ranging modifies dot, unhelpfully */ -}} + {{- range $i, $field := .Type.Fields }} + case {{ $i }}: + k = &fieldName__{{ $type | TypeSymbol }}_{{ $field | FieldSymbolUpper }} + {{- if $field.IsOptional }} + if itr.n.{{ $field | FieldSymbolLower }}.m == schema.Maybe_Absent { + v = ipld.Undef + break + } + {{- end}} + {{- if $field.IsNullable }} + if itr.n.{{ $field | FieldSymbolLower }}.m == schema.Maybe_Null { + v = ipld.Null + break + } + {{- end}} + {{- if or $field.IsOptional $field.IsNullable }} + v = itr.n.{{ $field | FieldSymbolLower}}.v + {{- else}} + v = &itr.n.{{ $field | FieldSymbolLower}} + {{- end}} + {{- end}} + default: + panic("unreachable") + } + itr.idx++ + return + } + func (itr *_{{ .Type | TypeSymbol }}__MapItr) Done() bool { + return itr.idx >= {{ len .Type.Fields }} + } + + `, w, g.AdjCfg, g) +} + +func (g structGenerator) EmitNodeMethodLength(w io.Writer) { + doTemplate(` + func ({{ .Type | TypeSymbol }}) Length() int { + return {{ len .Type.Fields }} + } + `, w, g.AdjCfg, g) +} + +func (g structGenerator) EmitNodeMethodStyle(w io.Writer) { + // REVIEW: this appears to be standard even across kinds; can we extract it? + doTemplate(` + func ({{ .Type | TypeSymbol }}) Style() ipld.NodeStyle { + return _{{ .Type | TypeSymbol }}__Style{} + } + `, w, g.AdjCfg, g) +} + +func (g structGenerator) EmitNodeStyleType(w io.Writer) { + // REVIEW: this appears to be standard even across kinds; can we extract it? + doTemplate(` + type _{{ .Type | TypeSymbol }}__Style struct{} + + func (_{{ .Type | TypeSymbol }}__Style) NewBuilder() ipld.NodeBuilder { + var nb _{{ .Type | TypeSymbol }}__Builder + nb.Reset() + return &nb + } + `, w, g.AdjCfg, g) +} + +// --- NodeBuilder and NodeAssembler ---> + +func (g structGenerator) EmitNodeBuilder(w io.Writer) { + doTemplate(` + type _{{ .Type | TypeSymbol }}__Builder struct { + _{{ .Type | TypeSymbol }}__Assembler + } + + func (nb *_{{ .Type | TypeSymbol }}__Builder) Build() ipld.Node { + if nb.state != maState_finished { + panic("invalid state: assembler for {{ .PkgName }}.{{ .Type.Name }} must be 'finished' before Build can be called!") + } + return nb.w + } + func (nb *_{{ .Type | TypeSymbol }}__Builder) Reset() { + var w _{{ .Type | TypeSymbol }} + *nb = _{{ .Type | TypeSymbol }}__Builder{_{{ .Type | TypeSymbol }}__Assembler{w: &w, state: maState_initial}} + } + `, w, g.AdjCfg, g) +} + +func (g structGenerator) EmitNodeAssembler(w io.Writer) { + doTemplate(` + type _{{ .Type | TypeSymbol }}__Assembler struct { + w *_{{ .Type | TypeSymbol }} + state maState + } + + func (na *_{{ .Type | TypeSymbol }}__Assembler) BeginMap(sizeHint int) (ipld.MapAssembler, error) { + panic("todo structassembler beginmap") + } + func (_{{ .Type | TypeSymbol }}__Assembler) BeginList(sizeHint int) (ipld.ListAssembler, error) { + return mixins.MapAssembler{"{{ .PkgName }}.{{ .Type.Name }}"}.BeginList(0) + } + func (_{{ .Type | TypeSymbol }}__Assembler) AssignNull() error { + return mixins.MapAssembler{"{{ .PkgName }}.{{ .Type.Name }}"}.AssignNull() + } + func (_{{ .Type | TypeSymbol }}__Assembler) AssignBool(bool) error { + return mixins.MapAssembler{"{{ .PkgName }}.{{ .Type.Name }}"}.AssignBool(false) + } + func (_{{ .Type | TypeSymbol }}__Assembler) AssignInt(int) error { + return mixins.MapAssembler{"{{ .PkgName }}.{{ .Type.Name }}"}.AssignInt(0) + } + func (_{{ .Type | TypeSymbol }}__Assembler) AssignFloat(float64) error { + return mixins.MapAssembler{"{{ .PkgName }}.{{ .Type.Name }}"}.AssignFloat(0) + } + func (_{{ .Type | TypeSymbol }}__Assembler) AssignString(v string) error { + return mixins.MapAssembler{"{{ .PkgName }}.{{ .Type.Name }}"}.AssignString("") + } + func (_{{ .Type | TypeSymbol }}__Assembler) AssignBytes([]byte) error { + return mixins.MapAssembler{"{{ .PkgName }}.{{ .Type.Name }}"}.AssignBytes(nil) + } + func (_{{ .Type | TypeSymbol }}__Assembler) AssignLink(ipld.Link) error { + return mixins.MapAssembler{"{{ .PkgName }}.{{ .Type.Name }}"}.AssignLink(nil) + } + func (na *_{{ .Type | TypeSymbol }}__Assembler) AssignNode(v ipld.Node) error { + panic("todo structassembler assignNode") + } + func (_{{ .Type | TypeSymbol }}__Assembler) Style() ipld.NodeStyle { + return _{{ .Type | TypeSymbol }}__Style{} + } + `, w, g.AdjCfg, g) +} diff --git a/schema/gen/go/genStructReprMap.go b/schema/gen/go/genStructReprMap.go new file mode 100644 index 0000000..13c48e0 --- /dev/null +++ b/schema/gen/go/genStructReprMap.go @@ -0,0 +1,31 @@ +package gengo + +import ( + "github.com/ipld/go-ipld-prime/schema" + "github.com/ipld/go-ipld-prime/schema/gen/go/mixins" +) + +var _ TypeGenerator = &stringReprStringGenerator{} + +func NewStructReprMapGenerator(pkgName string, typ schema.TypeStruct, adjCfg *AdjunctCfg) TypeGenerator { + return structReprMapGenerator{ + structGenerator{ + adjCfg, + mixins.MapTraits{ + pkgName, + string(typ.Name()), + adjCfg.TypeSymbol(typ), + }, + pkgName, + typ, + }, + } +} + +type structReprMapGenerator struct { + structGenerator +} + +func (g structReprMapGenerator) GetRepresentationNodeGen() NodeGenerator { + return nil +} diff --git a/schema/gen/go/generators.go b/schema/gen/go/generators.go index 94726c5..0e1041f 100644 --- a/schema/gen/go/generators.go +++ b/schema/gen/go/generators.go @@ -87,6 +87,9 @@ func EmitEntireType(tg TypeGenerator, w io.Writer) { tg.EmitTypedNodeMethodRepresentation(w) rng := tg.GetRepresentationNodeGen() + if rng == nil { // FIXME: hack to save me from stubbing tons right now, remove when done + return + } EmitNode(rng, w) } diff --git a/schema/gen/go/smoke_test.go b/schema/gen/go/smoke_test.go index 98a7487..126acd3 100644 --- a/schema/gen/go/smoke_test.go +++ b/schema/gen/go/smoke_test.go @@ -21,9 +21,26 @@ func TestSmoke(t *testing.T) { pkgName := "whee" adjCfg := &AdjunctCfg{} + f = openOrPanic("_test/minima.go") + EmitInternalEnums(pkgName, f) + tString := schema.SpawnString("String") + tStroct := schema.SpawnStruct("Stroct", + []schema.StructField{ + schema.SpawnStructField("f1", tString, false, false), + schema.SpawnStructField("f2", tString, true, false), + schema.SpawnStructField("f3", tString, true, true), + schema.SpawnStructField("f4", tString, false, true), + }, + schema.StructRepresentation_Map{}, + ) + f = openOrPanic("_test/tString.go") EmitFileHeader(pkgName, f) EmitEntireType(NewStringReprStringGenerator(pkgName, tString, adjCfg), f) + + f = openOrPanic("_test/tStroct.go") + EmitFileHeader(pkgName, f) + EmitEntireType(NewStructReprMapGenerator(pkgName, tStroct, adjCfg), f) } diff --git a/schema/gen/go/templateUtil.go b/schema/gen/go/templateUtil.go index 622f627..3af8f91 100644 --- a/schema/gen/go/templateUtil.go +++ b/schema/gen/go/templateUtil.go @@ -5,6 +5,8 @@ import ( "text/template" wish "github.com/warpfork/go-wish" + + "github.com/ipld/go-ipld-prime/schema" ) func doTemplate(tmplstr string, w io.Writer, adjCfg *AdjunctCfg, data interface{}) { @@ -13,6 +15,16 @@ func doTemplate(tmplstr string, w io.Writer, adjCfg *AdjunctCfg, data interface{ "TypeSymbol": adjCfg.TypeSymbol, "FieldSymbolLower": adjCfg.FieldSymbolLower, "FieldSymbolUpper": adjCfg.FieldSymbolUpper, + "FieldTypeOrMaybe": func(f schema.StructField) string { + // Returns the symbol used for embedding the field's type, or, the MaybeT of that type. + // Shorthand for: + // `{{if or $field.IsOptional $field.IsNullable }}Maybe{{else}}_{{end}}{{ $field.Type | TypeSymbol }}` + // REVIEW: still not sure if this is gonna be worth it. Thought it would appear in more places; actually, is only one. + if f.IsOptional() || f.IsNullable() { + return "Maybe" + adjCfg.TypeSymbol(f.Type()) + } + return "_" + adjCfg.TypeSymbol(f.Type()) + }, }). Parse(wish.Dedent(tmplstr))) if err := tmpl.Execute(w, data); err != nil { diff --git a/schema/maybe.go b/schema/maybe.go index 670a6bc..bdc4693 100644 --- a/schema/maybe.go +++ b/schema/maybe.go @@ -3,7 +3,7 @@ package schema type Maybe uint8 const ( - Maybe_Value = Maybe(0) + Maybe_Absent = Maybe(0) Maybe_Null = Maybe(1) - Maybe_Absent = Maybe(2) + Maybe_Value = Maybe(2) ) diff --git a/schema/typeMethods.go b/schema/typeMethods.go index 18ab064..4984634 100644 --- a/schema/typeMethods.go +++ b/schema/typeMethods.go @@ -135,6 +135,12 @@ func (f StructField) IsOptional() bool { return f.optional } // or either, or neither. func (f StructField) IsNullable() bool { return f.nullable } +// 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.nullable || f.optional } + func (t TypeStruct) RepresentationStrategy() StructRepresentation { return t.representation } -- GitLab