package exp import ( "fmt" "github.com/dave/jennifer/jen" ) // FunctionalPropertyGenerator produces Go code for properties that can have // only one value. The resulting property is a struct type that can have one // value that could be from multiple Kinds of values. If there is only one // allowed Kind, then a smaller API is generated as a special case. type FunctionalPropertyGenerator struct { PropertyGenerator } // isSingleTypeDef determines whether a special-case API can be generated for // one allowed Kind. func (p *FunctionalPropertyGenerator) isSingleTypeDef() bool { return len(p.Kinds) == 1 } // Definition produces the Go Struct code definition, which can generate its Go // implementations. func (p *FunctionalPropertyGenerator) Definition() *Struct { if p.isSingleTypeDef() { return p.singleTypeDef() } else { return p.multiTypeDef() } } // clearNonLanguageMapMembers generates the code required to clear all values, // including unknown values, from this property except for the natural language // map. If this property can handle a natural language map, then it is up to the // calling code to determine whether to set the 'langMapMember' to nil. func (p *FunctionalPropertyGenerator) clearNonLanguageMapMembers() []jen.Code { if p.isSingleTypeDef() { return p.singleTypeClearNonLanguageMapMembers() } else { return p.multiTypeClearNonLanguageMapMembers() } } // singleTypeClearNonLanguageMapMembers generates code to clear all members for // the special case single-Kind property. func (p *FunctionalPropertyGenerator) singleTypeClearNonLanguageMapMembers() []jen.Code { clearCode := []jen.Code{ jen.Id(This()).Dot(unknownMemberName).Op("=").Nil(), } if p.Kinds[0].Nilable { clearCode = append(clearCode, jen.Id(This()).Dot(p.memberName(0)).Op("=").Nil()) } else { clearCode = append(clearCode, jen.Id(This()).Dot(p.hasMemberName(0)).Op("=").False()) } return clearCode } // multiTypeClearNonLanguageMapMembers generates code to clear all members for // a property with multiple Kinds. func (p *FunctionalPropertyGenerator) multiTypeClearNonLanguageMapMembers() []jen.Code { clearLine := make([]jen.Code, len(p.Kinds)+2) // +2 for the unknown, and maybe language map for i, kind := range p.Kinds { if kind.Nilable { clearLine[i] = jen.Id(This()).Dot(p.memberName(i)).Op("=").Nil() } else { clearLine[i] = jen.Id(This()).Dot(p.hasMemberName(i)).Op("=").False() } } clearLine = append(clearLine, jen.Id(This()).Dot(unknownMemberName).Op("=").Nil()) return clearLine } // funcs produces the methods needed for the functional property. func (p *FunctionalPropertyGenerator) funcs() []*Method { kindIndexFns := make([]jen.Code, 0, len(p.Kinds)) for i, _ := range p.Kinds { if p.isSingleTypeDef() { kindIndexFns = append(kindIndexFns, jen.If( jen.Id(This()).Dot(hasMethod).Call(), ).Block( jen.Return(jen.Lit(i)), )) } else { kindIndexFns = append(kindIndexFns, jen.If( jen.Id(This()).Dot(p.isMethodName(i)).Call(), ).Block( jen.Return(jen.Lit(i)), )) } } methods := []*Method{ NewCommentedValueMethod( p.packageName(), kindIndexMethod, p.structName(), /*params=*/ nil, []jen.Code{jen.Int()}, []jen.Code{ join(kindIndexFns), jen.Return(jen.Lit(-1)), }, jen.Commentf("%s computes an arbitrary value for indexing this kind of value.", kindIndexMethod), ), } if p.HasNaturalLanguageMap { // IsLanguageMap Method methods = append(methods, NewCommentedValueMethod( p.packageName(), isLanguageMapMethod, p.structName(), /*params=*/ nil, []jen.Code{jen.Bool()}, []jen.Code{ jen.Return(jen.Id(This()).Dot(langMapMember).Op("!=").Nil()), }, jen.Commentf( "%s determines if this property is represented by a natural language map.", isLanguageMapMethod, ).Line().Commentf("").Line().Commentf( "When true, use %s, %s, and %s methods to access and mutate the natural language map.", hasLanguageMethod, getLanguageMethod, setLanguageMethod, ).Line().Commentf( "The %s method can be used to clear the natural language map.", p.clearMethodName(), ).Line().Commentf("").Line().Commentf( "Note that this method is only used for natural language representations, and does not determine the presence nor absence of other values for this property.", ))) // HasLanguage Method methods = append(methods, NewCommentedValueMethod( p.packageName(), hasLanguageMethod, p.structName(), []jen.Code{jen.Id("bcp47").String()}, []jen.Code{jen.Bool()}, []jen.Code{ jen.If( jen.Id(This()).Dot(langMapMember).Op("==").Nil(), ).Block( jen.Return(jen.False()), ).Else().Block( jen.List( jen.Id("_"), jen.Id("ok"), ).Op(":=").Id(This()).Dot(langMapMember).Index( jen.Id("bcp47"), ), jen.Return(jen.Id("ok")), ), }, jen.Commentf( "%s returns true if the natural language map has an entry for the specified BCP47 language code.", hasLanguageMethod, ), )) // GetLanguage Method methods = append(methods, NewCommentedValueMethod( p.packageName(), getLanguageMethod, p.structName(), []jen.Code{jen.Id("bcp47").String()}, []jen.Code{jen.String()}, []jen.Code{ jen.If( jen.Id(This()).Dot(langMapMember).Op("==").Nil(), ).Block( jen.Return(jen.Lit("")), ).Else().If( jen.List( jen.Id("v"), jen.Id("ok"), ).Op(":=").Id(This()).Dot(langMapMember).Index( jen.Id("bcp47"), ), jen.Id("ok"), ).Block( jen.Return(jen.Id("v")), ).Else().Block( jen.Return(jen.Lit("")), ), }, jen.Commentf( "%s returns the value for the specified BCP47 language code, or an empty string if it is either not a language map or no value is present.", getLanguageMethod, ), )) // SetLanguage Method methods = append(methods, NewCommentedPointerMethod( p.packageName(), setLanguageMethod, p.structName(), []jen.Code{ jen.Id("bcp47"), jen.Id("value").String(), }, /*ret=*/ nil, append(p.clearNonLanguageMapMembers(), []jen.Code{ jen.If( jen.Id(This()).Dot(langMapMember).Op("==").Nil(), ).Block( jen.Id(This()).Dot(langMapMember).Op("=").Make( jen.Map(jen.String()).String(), ), ), jen.Id(This()).Dot(langMapMember).Index( jen.Id("bcp47"), ).Op("=").Id("value"), }..., ), jen.Commentf( "%s sets the value for the specified BCP47 language code.", setLanguageMethod, ), )) } return methods } // serializationFuncs produces the Methods and Functions needed for a // functional property to be serialized and deserialized to and from an // encoding. func (p *FunctionalPropertyGenerator) serializationFuncs() ([]*Method, []*Function) { serializeFns := jen.Empty() for i, kind := range p.Kinds { if i > 0 { serializeFns = serializeFns.Else() } if p.isSingleTypeDef() { serializeFns = serializeFns.If( jen.Id(This()).Dot(hasMethod).Call(), ) } else { serializeFns = serializeFns.If( jen.Id(This()).Dot(p.isMethodName(i)).Call(), ) } serializeFns = serializeFns.Block( jen.Return( jen.Id(kind.SerializeFnName).Call( jen.Id(This()).Dot(p.getFnName(i)).Call(), ), ), ) } serialize := []*Method{ NewCommentedValueMethod( p.packageName(), p.serializeFnName(), p.structName(), /*params=*/ nil, []jen.Code{jen.Interface(), jen.Error()}, []jen.Code{serializeFns, jen.Return( jen.Id(This()).Dot(unknownMemberName), jen.Nil(), )}, jen.Commentf("%s converts this into an interface representation suitable for marshalling into a text or binary format.", p.serializeFnName()), ), } deserializeFns := jen.Empty() for i, kind := range p.Kinds { if i > 0 { deserializeFns = deserializeFns.Else() } values := jen.Dict{ jen.Id(p.memberName(i)): jen.Id("v"), } if !kind.Nilable { values[jen.Id(p.hasMemberName(i))] = jen.True() } deserializeFns = deserializeFns.If( jen.List( jen.Id("v"), jen.Id("handled"), jen.Err(), ).Op(":=").Id(kind.DeserializeFnName).Call( jen.Id("i"), ), jen.Id("handled"), ).Block( jen.Id(This()).Op(":=").Op("&").Id(p.structName()).Values( values, ), jen.Return( jen.Id(This()), jen.Err(), ), ) } var deserialize []*Function if p.asIterator { deserialize = append(deserialize, NewCommentedFunction( p.packageName(), p.deserializeFnName(), []jen.Code{jen.Id("i").Interface()}, []jen.Code{jen.Op("*").Id(p.structName()), jen.Error()}, []jen.Code{ deserializeFns.Add(p.unknownDeserializeCode()), jen.Return( jen.Nil(), jen.Nil(), ), }, jen.Commentf("%s creates an iterator from an element that has been unmarshalled from a text or binary format.", p.deserializeFnName()), )) } else { deserialize = append(deserialize, NewCommentedFunction( p.packageName(), p.deserializeFnName(), []jen.Code{jen.Id("m").Map(jen.String()).Interface()}, []jen.Code{jen.Op("*").Id(p.structName()), jen.Error()}, []jen.Code{ jen.If( jen.List( jen.Id("i"), jen.Id("ok"), ).Op(":=").Id("m").Index( jen.Lit(p.propertyName()), ), jen.Id("ok"), ).Block( deserializeFns.Add(p.unknownDeserializeCode()), ), jen.Return( jen.Nil(), 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 } // singleTypeDef generates a special-case simplified API for a functional // property that can only be a single Kind of value. func (p *FunctionalPropertyGenerator) singleTypeDef() *Struct { var comment jen.Code var kindMembers []jen.Code if p.Kinds[0].Nilable { comment = jen.Commentf("%s is the functional property %q. It is permitted to be a single nilable value type.", p.structName(), p.propertyName()) if p.asIterator { comment = jen.Commentf("%s is an iterator for a property. It is permitted to be a single nilable value type.", p.structName()) } kindMembers = []jen.Code{ jen.Id(p.memberName(0)).Id(p.Kinds[0].ConcreteKind), } } else { comment = jen.Commentf("%s is the functional property %q. It is permitted to be a single default-valued value type.", p.structName(), p.propertyName()) if p.asIterator { comment = jen.Commentf("%s is an iterator for a property. It is permitted to be a single default-valued value type.", p.structName()) } kindMembers = []jen.Code{ jen.Id(p.memberName(0)).Id(p.Kinds[0].ConcreteKind), jen.Id(p.hasMemberName(0)).Bool(), } } kindMembers = append(kindMembers, p.unknownMemberDef()) if p.HasNaturalLanguageMap { kindMembers = append(kindMembers, jen.Id(langMapMember).Map(jen.String()).String()) } methods, funcs := p.serializationFuncs() methods = append(methods, p.singleTypeFuncs()...) methods = append(methods, p.funcs()...) methods = append(methods, p.commonMethods()...) return NewStruct(comment, p.structName(), methods, funcs, kindMembers) } // singleTypeFuncs generates the special-case simplified methods for a // functional property with exactly one Kind of value. func (p *FunctionalPropertyGenerator) singleTypeFuncs() []*Method { var methods []*Method // Has Method hasComment := jen.Commentf("%s returns true if this property is set.", hasMethod) if p.HasNaturalLanguageMap { hasComment = jen.Commentf( "%s returns true if this property is set and is not a natural language map.", hasMethod, ).Line().Commentf("").Line().Commentf( "When true, the %s and %s methods may be used to access and set this property.", getMethod, p.setFnName(0), ).Line().Commentf("").Line().Commentf( "To determine if the property was set as a natural language map, use the %s method instead.", isLanguageMapMethod, ) } if p.Kinds[0].Nilable { methods = append(methods, NewCommentedValueMethod( p.packageName(), hasMethod, p.structName(), /*params=*/ nil, []jen.Code{jen.Bool()}, []jen.Code{jen.Return(jen.Id(This()).Dot(p.memberName(0)).Op("!=").Nil())}, hasComment, )) } else { methods = append(methods, NewCommentedValueMethod( p.packageName(), hasMethod, p.structName(), /*params=*/ nil, []jen.Code{jen.Bool()}, []jen.Code{jen.Return(jen.Id(This()).Dot(p.hasMemberName(0)))}, hasComment, )) } // Get Method getComment := jen.Commentf("%s returns the value of this property. When %s returns false, %s will return any arbitrary value.", getMethod, hasMethod, getMethod) methods = append(methods, NewCommentedValueMethod( p.packageName(), p.getFnName(0), p.structName(), /*params=*/ nil, []jen.Code{jen.Id(p.Kinds[0].ConcreteKind)}, []jen.Code{jen.Return(jen.Id(This()).Dot(p.memberName(0)))}, getComment, )) // Set Method setComment := jen.Commentf("%s sets the value of this property. Calling %s afterwards will return true.", p.setFnName(0), hasMethod) if p.HasNaturalLanguageMap { setComment = jen.Commentf( "%s sets the value of this property and clears the natural language map.", p.setFnName(0), ).Line().Commentf("").Line().Commentf( "Calling %s afterwards will return true. Calling %s afterwards returns false.", hasMethod, isLanguageMapMethod, ) } if p.Kinds[0].Nilable { methods = append(methods, NewCommentedPointerMethod( p.packageName(), p.setFnName(0), p.structName(), []jen.Code{jen.Id("v").Id(p.Kinds[0].ConcreteKind)}, /*ret=*/ nil, []jen.Code{ jen.Id(This()).Dot(p.clearMethodName()).Call(), jen.Id(This()).Dot(p.memberName(0)).Op("=").Id("v"), }, setComment, )) } else { methods = append(methods, NewCommentedPointerMethod( p.packageName(), p.setFnName(0), p.structName(), []jen.Code{jen.Id("v").Id(p.Kinds[0].ConcreteKind)}, /*ret=*/ nil, []jen.Code{ jen.Id(This()).Dot(p.clearMethodName()).Call(), jen.Id(This()).Dot(p.memberName(0)).Op("=").Id("v"), jen.Id(This()).Dot(p.hasMemberName(0)).Op("=").True(), }, setComment, )) } // Clear Method clearComment := jen.Commentf("%s ensures no value of this property is set. Calling %s afterwards will return false.", p.clearMethodName(), hasMethod) clearCode := p.singleTypeClearNonLanguageMapMembers() if p.HasNaturalLanguageMap { clearComment = jen.Commentf( "%s ensures no value and no language map for this property is set.", p.clearMethodName(), ).Line().Commentf("").Line().Commentf( "Calling %s or %s afterwards will return false.", hasMethod, isLanguageMapMethod, ) clearCode = append(clearCode, jen.Id(This()).Dot(langMapMember).Op("=").Nil()) } if p.Kinds[0].Nilable { methods = append(methods, NewCommentedPointerMethod( p.packageName(), p.clearMethodName(), p.structName(), /*params=*/ nil, /*ret=*/ nil, clearCode, clearComment, )) } else { methods = append(methods, NewCommentedPointerMethod( p.packageName(), p.clearMethodName(), p.structName(), /*params=*/ nil, /*ret=*/ nil, clearCode, clearComment, )) } return methods } // multiTypeDef generates an API for a functional property that can be multiple // Kinds of value. func (p *FunctionalPropertyGenerator) multiTypeDef() *Struct { kindMembers := make([]jen.Code, 0, len(p.Kinds)) for i, kind := range p.Kinds { if kind.Nilable { kindMembers = append(kindMembers, jen.Id(p.memberName(i)).Id(p.Kinds[i].ConcreteKind)) } else { kindMembers = append(kindMembers, jen.Id(p.memberName(i)).Id(p.Kinds[i].ConcreteKind)) kindMembers = append(kindMembers, jen.Id(p.hasMemberName(i)).Bool()) } } kindMembers = append(kindMembers, p.unknownMemberDef()) if p.HasNaturalLanguageMap { kindMembers = append(kindMembers, jen.Id(langMapMember).Map(jen.String()).String()) } explanation := jen.Commentf( "At most, one type of value can be present, or none at all. Setting a value will", ).Line().Commentf( "clear the other types of values so that only one of the 'Is' methods will return", ).Line().Commentf( "true.", ).Line().Comment("").Line().Commentf( "It is possible to clear all values, so that this property is empty.", ) comment := jen.Commentf( "%s is the functional property %q. It is permitted to be one of multiple value types.", p.structName(), p.propertyName(), ).Line().Comment("").Line().Add(explanation) if p.asIterator { comment = jen.Commentf( "%s is an iterator for a property. It is permitted to be one of multiple value types.", p.structName(), ).Line().Comment("").Line().Add(explanation) } methods, funcs := p.serializationFuncs() methods = append(methods, p.multiTypeFuncs()...) methods = append(methods, p.funcs()...) methods = append(methods, p.commonMethods()...) return NewStruct(comment, p.structName(), methods, funcs, kindMembers) } // multiTypeFuncs generates the methods for a functional property with more than // one Kind of value. func (p *FunctionalPropertyGenerator) multiTypeFuncs() []*Method { var methods []*Method // HasAny Method hasAnyMethodName := fmt.Sprintf("%sAny", hasMethod) isLine := make([]jen.Code, len(p.Kinds)) for i := range p.Kinds { or := jen.Empty() if i != len(p.Kinds)-1 { or = jen.Op("||") } isLine[i] = jen.Id(This()).Dot(p.isMethodName(i)).Parens(nil).Add(or) } hasAnyComment := jen.Commentf( "%s returns true if any of the different values is set.", hasAnyMethodName, ) if p.HasNaturalLanguageMap { hasAnyComment = jen.Commentf( "%s returns true if any of the values are set, except for the natural language map", hasAnyMethodName, ).Line().Commentf("").Line().Commentf( "When true, the specific has, getter, and setter methods may be used to determine what kind of value there is to access and set this property.", ).Line().Commentf("").Line().Commentf( "To determine if the property was set as a natural language map, use the %s method instead.", isLanguageMapMethod, ) } methods = append(methods, NewCommentedPointerMethod( p.packageName(), hasAnyMethodName, p.structName(), /*params=*/ nil, []jen.Code{jen.Bool()}, []jen.Code{jen.Return(join(isLine))}, hasAnyComment, )) // Clear Method clearComment := jen.Commentf( "%s ensures no value of this property is set. Calling %s or any of the 'Is' methods afterwards will return false.", p.clearMethodName(), hasAnyMethodName, ) clearLine := p.multiTypeClearNonLanguageMapMembers() if p.HasNaturalLanguageMap { clearComment = jen.Commentf( "%s ensures no value and no language map for this property is set.", p.clearMethodName(), ).Line().Commentf("").Line().Commentf( "Calling %s or any of the 'Is' methods afterwards will return false.", hasAnyMethodName, ) clearLine = append(clearLine, jen.Id(This()).Dot(langMapMember).Op("=").Nil()) } methods = append(methods, NewCommentedPointerMethod( p.packageName(), p.clearMethodName(), p.structName(), /*params=*/ nil, /*ret=*/ nil, clearLine, clearComment, )) // Is Method for i, kind := range p.Kinds { isComment := jen.Commentf("%s returns true if this property has a type of value of %q.", p.isMethodName(i), kind.ConcreteKind).Line().Commentf("").Line().Commentf( "When true, use the %s and %s methods to access and set this property.", p.getFnName(i), p.setFnName(i), ) if p.HasNaturalLanguageMap { isComment = isComment.Line().Commentf("").Line().Commentf( "To determine if the property was set as a natural language map, use the %s method instead.", isLanguageMapMethod, ) } if kind.Nilable { methods = append(methods, NewCommentedValueMethod( p.packageName(), p.isMethodName(i), p.structName(), /*params=*/ nil, []jen.Code{jen.Bool()}, []jen.Code{jen.Return(jen.Id(This()).Dot(p.memberName(i)).Op("!=").Nil())}, isComment, )) } else { methods = append(methods, NewCommentedValueMethod( p.packageName(), p.isMethodName(i), p.structName(), /*params=*/ nil, []jen.Code{jen.Bool()}, []jen.Code{jen.Return(jen.Id(This()).Dot(p.hasMemberName(i)))}, isComment, )) } } // Set Method for i, kind := range p.Kinds { setComment := jen.Commentf("%s sets the value of this property. Calling %s afterwards returns true.", p.setFnName(i), p.isMethodName(i)) if p.HasNaturalLanguageMap { setComment = jen.Commentf( "%s sets the value of this property and clears the natural language map.", p.setFnName(i), ).Line().Commentf("").Line().Commentf( "Calling %s afterwards will return true. Calling %s afterwards returns false.", p.isMethodName(i), isLanguageMapMethod, ) } if kind.Nilable { methods = append(methods, NewCommentedPointerMethod( p.packageName(), p.setFnName(i), p.structName(), []jen.Code{jen.Id("v").Id(kind.ConcreteKind)}, /*ret=*/ nil, []jen.Code{ jen.Id(This()).Dot(p.clearMethodName()).Call(), jen.Id(This()).Dot(p.memberName(i)).Op("=").Id("v"), }, setComment, )) } else { methods = append(methods, NewCommentedPointerMethod( p.packageName(), p.setFnName(i), p.structName(), []jen.Code{jen.Id("v").Id(kind.ConcreteKind)}, /*ret=*/ nil, []jen.Code{ jen.Id(This()).Dot(p.clearMethodName()).Call(), jen.Id(This()).Dot(p.memberName(i)).Op("=").Id("v"), jen.Id(This()).Dot(p.hasMemberName(i)).Op("=").True(), }, setComment, )) } } // Get Method for i, kind := range p.Kinds { getComment := jen.Commentf("%s returns the value of this property. When %s returns false, %s will return an arbitrary value.", p.getFnName(i), p.isMethodName(i), p.getFnName(i)) methods = append(methods, NewCommentedValueMethod( p.packageName(), p.getFnName(i), p.structName(), /*params=*/ nil, []jen.Code{jen.Id(kind.ConcreteKind)}, []jen.Code{jen.Return(jen.Id(This()).Dot(p.memberName(i)))}, getComment, )) } return methods } // unknownMemberDef returns the definition of a struct member that handles // a property whose type is unknown. func (p *FunctionalPropertyGenerator) unknownMemberDef() jen.Code { return jen.Id(unknownMemberName).Index().Byte() } // unknownDeserializeCode generates the "else if it's a []byte" code used for // deserializing unknown values. func (p *FunctionalPropertyGenerator) unknownDeserializeCode() jen.Code { return jen.Else().If( jen.List( jen.Id("v"), jen.Id("ok"), ).Op(":=").Id("i").Assert( jen.Index().Byte(), ), jen.Id("ok"), ).Block( jen.Id(This()).Op(":=").Op("&").Id(p.structName()).Values( jen.Dict{ jen.Id(unknownMemberName): jen.Id("v"), }, ), jen.Return( jen.Id(This()), jen.Err(), ), ) }