package props import ( "fmt" "github.com/dave/jennifer/jen" "github.com/go-fed/activity/tools/exp/codegen" "sync" ) // NonFunctionalPropertyGenerator produces Go code for properties that can have // more than one value. The resulting property is a type that is a list of // iterators; each iterator is a concrete struct type. The property can be // sorted and iterated over so individual elements can be inspected. type NonFunctionalPropertyGenerator struct { PropertyGenerator cacheOnce sync.Once cachedStruct *codegen.Struct cachedTypedef *codegen.Typedef } // NewNonFunctionalPropertyGenerator is a convenience constructor to create // NonFunctionalPropertyGenerators. func NewNonFunctionalPropertyGenerator(pkg string, name Identifier, kinds []Kind, hasNaturalLanguageMap bool) *NonFunctionalPropertyGenerator { return &NonFunctionalPropertyGenerator{ PropertyGenerator: PropertyGenerator{ Package: pkg, HasNaturalLanguageMap: hasNaturalLanguageMap, Name: name, Kinds: kinds, }, } } // Definitions produces the Go code definitions, which can generate their Go // implementations. The struct is the iterator for various values of the // property, which is defined by the type definition. func (p *NonFunctionalPropertyGenerator) Definitions() (*codegen.Struct, *codegen.Typedef) { p.cacheOnce.Do(func() { methods, funcs := p.serializationFuncs() methods = append(methods, p.funcs()...) property := codegen.NewTypedef( jen.Commentf("%s is the non-functional property %q. It is permitted to have one or more values, and of different value types.", p.StructName(), p.PropertyName()), p.StructName(), jen.Index().Id(p.iteratorTypeName().CamelName), methods, funcs) iterator := p.elementTypeGenerator().Definition() p.cachedStruct, p.cachedTypedef = iterator, property }) return p.cachedStruct, p.cachedTypedef } // iteratorTypeName determines the identifier to use for the iterator type. func (p *NonFunctionalPropertyGenerator) iteratorTypeName() Identifier { return Identifier{ LowerName: fmt.Sprintf("%sPropertyIterator", p.Name.LowerName), CamelName: fmt.Sprintf("%sPropertyIterator", p.Name.CamelName), } } // elementTypeGenerator produces a FunctionalPropertyGenerator for the iterator // type. func (p *NonFunctionalPropertyGenerator) elementTypeGenerator() *FunctionalPropertyGenerator { return &FunctionalPropertyGenerator{ PropertyGenerator: PropertyGenerator{ Package: p.PropertyGenerator.Package, Name: p.iteratorTypeName(), Kinds: p.Kinds, HasNaturalLanguageMap: p.PropertyGenerator.HasNaturalLanguageMap, asIterator: true, }, } } // funcs produces the methods needed for the NonFunctional property. func (p *NonFunctionalPropertyGenerator) funcs() []*codegen.Method { var methods []*codegen.Method less := jen.Empty() for i, kind := range p.Kinds { dict := jen.Dict{ jen.Id(p.memberName(i)): jen.Id("v"), } if !kind.Nilable { dict[jen.Id(p.hasMemberName(i))] = jen.True() } // Prepend Method prependMethodName := fmt.Sprintf("%s%s", prependMethod, p.kindCamelName(i)) methods = append(methods, codegen.NewCommentedPointerMethod( p.packageName(), prependMethodName, p.StructName(), []jen.Code{jen.Id("v").Id(kind.ConcreteKind)}, /*ret=*/ nil, []jen.Code{ jen.Op("*").Id(codegen.This()).Op("=").Append( jen.Index().Id(p.iteratorTypeName().CamelName).Values( jen.Values(dict), ), jen.Op("*").Id(codegen.This()).Op("..."), ), }, jen.Commentf("%s prepends a %s value to the front of a list of the property %q.", prependMethodName, kind.ConcreteKind, p.PropertyName()))) // Append Method appendMethodName := fmt.Sprintf("%s%s", appendMethod, p.kindCamelName(i)) methods = append(methods, codegen.NewCommentedPointerMethod( p.packageName(), appendMethodName, p.StructName(), []jen.Code{jen.Id("v").Id(kind.ConcreteKind)}, /*ret=*/ nil, []jen.Code{ jen.Op("*").Id(codegen.This()).Op("=").Append( jen.Op("*").Id(codegen.This()), jen.Id(p.iteratorTypeName().CamelName).Values( dict, ), ), }, jen.Commentf("%s appends a %s value to the back of a list of the property %q", appendMethodName, kind.ConcreteKind, p.PropertyName()))) // Less logic if i > 0 { less.Else() } less.If( jen.Id("idx1").Op("==").Lit(i), ).Block( jen.Id("lhs").Op(":=").Id(codegen.This()).Index(jen.Id("i")).Dot(p.getFnName(i)).Call(), jen.Id("rhs").Op(":=").Id(codegen.This()).Index(jen.Id("j")).Dot(p.getFnName(i)).Call(), jen.Return(kind.LessFn.Call( jen.Id("lhs"), jen.Id("rhs"), )), ) } // Remove Method methods = append(methods, codegen.NewCommentedPointerMethod( p.packageName(), removeMethod, p.StructName(), []jen.Code{jen.Id("idx").Int()}, /*ret=*/ nil, []jen.Code{ jen.Copy( jen.Parens( jen.Op("*").Id(codegen.This()), ).Index( jen.Id("idx"), jen.Empty(), ), jen.Parens( jen.Op("*").Id(codegen.This()), ).Index( jen.Id("idx").Op("+").Lit(1), jen.Empty(), ), ), jen.Parens( jen.Op("*").Id(codegen.This()), ).Index( jen.Len(jen.Op("*").Id(codegen.This())).Op("-").Lit(1), ).Op("=").Id(p.iteratorTypeName().CamelName).Values(), jen.Op("*").Id(codegen.This()).Op("=").Parens( jen.Op("*").Id(codegen.This()), ).Index( jen.Empty(), jen.Len(jen.Op("*").Id(codegen.This())).Op("-").Lit(1), ), }, jen.Commentf("%s deletes an element at the specified index from a list of the property %q, regardless of its type.", removeMethod, p.PropertyName()))) // Len Method methods = append(methods, codegen.NewCommentedValueMethod( p.packageName(), lenMethod, p.StructName(), /*params=*/ nil, []jen.Code{jen.Id("length").Int()}, []jen.Code{ jen.Return( jen.Len( jen.Id(codegen.This()), ), ), }, jen.Commentf("%s returns the number of values that exist for the %q property.", lenMethod, p.PropertyName()))) // Swap Method methods = append(methods, codegen.NewCommentedValueMethod( p.packageName(), swapMethod, p.StructName(), []jen.Code{ jen.Id("i"), jen.Id("j").Int(), }, /*ret=*/ nil, []jen.Code{ jen.List( jen.Id(codegen.This()).Index(jen.Id("i")), jen.Id(codegen.This()).Index(jen.Id("j")), ).Op("=").List( jen.Id(codegen.This()).Index(jen.Id("j")), jen.Id(codegen.This()).Index(jen.Id("i")), ), }, jen.Commentf("%s swaps the location of values at two indices for the %q property.", swapMethod, p.PropertyName()))) // Less Method methods = append(methods, codegen.NewCommentedValueMethod( p.packageName(), lessMethod, p.StructName(), []jen.Code{ jen.Id("i"), jen.Id("j").Int(), }, []jen.Code{jen.Bool()}, []jen.Code{ jen.Id("idx1").Op(":=").Id(codegen.This()).Dot(kindIndexMethod).Call(jen.Id("i")), jen.Id("idx2").Op(":=").Id(codegen.This()).Dot(kindIndexMethod).Call(jen.Id("j")), jen.If(jen.Id("idx1").Op("<").Id("idx2")).Block( jen.Return(jen.True()), ).Else().If(jen.Id("idx1").Op("==").Id("idx2")).Block( less, ), jen.Return(jen.False()), }, jen.Commentf("%s computes whether another property is less than this one. Mixing types results in a consistent but arbitrary ordering", lessMethod))) // Kind Method methods = append(methods, codegen.NewCommentedValueMethod( p.packageName(), kindIndexMethod, p.StructName(), []jen.Code{jen.Id("idx").Int()}, []jen.Code{jen.Int()}, []jen.Code{ jen.Return( jen.Id(codegen.This()).Index(jen.Id("idx")).Dot(kindIndexMethod).Call(), ), }, jen.Commentf("%s computes an arbitrary value for indexing this kind of value.", kindIndexMethod))) return methods } // serializationFuncs produces the Methods and Functions needed for a // NonFunctional property to be serialized and deserialized to and from an // encoding. func (p *NonFunctionalPropertyGenerator) serializationFuncs() ([]*codegen.Method, []*codegen.Function) { serialize := []*codegen.Method{ codegen.NewCommentedValueMethod( p.packageName(), p.serializeFnName(), p.StructName(), /*params=*/ nil, []jen.Code{jen.Interface(), jen.Error()}, []jen.Code{ jen.Id("s").Op(":=").Make( jen.Index().Interface(), jen.Lit(0), jen.Len(jen.Id(codegen.This())), ), jen.For( jen.List( jen.Id("_"), jen.Id("iterator"), ).Op(":=").Range().Id(codegen.This()), ).Block( jen.If( jen.List( jen.Id("b"), jen.Err(), ).Op(":=").Id("iterator").Dot(serializeIteratorMethod).Call(), jen.Err().Op("!=").Nil(), ).Block( jen.Return( jen.Id("s"), jen.Err(), ), ).Else().Block( jen.Id("s").Op("=").Append( jen.Id("s"), jen.Id("b"), ), ), ), jen.Return( jen.Id("s"), jen.Nil(), ), }, jen.Commentf("%s converts this into an interface representation suitable for marshalling into a text or binary format.", p.serializeFnName()), ), } deserializeFn := func(variable string) jen.Code { return jen.If( jen.List( jen.Id("p"), jen.Err(), ).Op(":=").Id(p.elementTypeGenerator().deserializeFnName()).Call( jen.Id(variable), ), jen.Err().Op("!=").Nil(), ).Block( jen.Return( jen.Id(codegen.This()), jen.Err(), ), ).Else().If( jen.Id("p").Op("!=").Nil(), ).Block( jen.Id(codegen.This()).Op("=").Append( jen.Id(codegen.This()), jen.Op("*").Id("p"), ), ) } deserialize := []*codegen.Function{ codegen.NewCommentedFunction( p.packageName(), p.deserializeFnName(), []jen.Code{jen.Id("m").Map(jen.String()).Interface()}, []jen.Code{jen.Id(p.StructName()), jen.Error()}, []jen.Code{ jen.Var().Id(codegen.This()).Index().Id(p.iteratorTypeName().CamelName), jen.If( jen.List( jen.Id("i"), jen.Id("ok"), ).Op(":=").Id("m").Index( jen.Lit(p.PropertyName()), ), jen.Id("ok"), ).Block( jen.If( jen.List( jen.Id("list"), jen.Id("ok"), ).Op(":=").Id("i").Assert( jen.Index().Interface(), ), jen.Id("ok"), ).Block( jen.For( jen.List( jen.Id("_"), jen.Id("iterator"), ).Op(":=").Range().Id("list"), ).Block( deserializeFn("iterator"), ), ).Else().Block( deserializeFn("i"), ), ), jen.Return( jen.Id(codegen.This()), jen.Nil(), ), }, jen.Commentf("%s creates a %q property from an interface representation that has been unmarshalled from a text or binary format.", p.deserializeFnName(), p.PropertyName()), ), } return serialize, deserialize }