Skip to content

Commit

Permalink
Use codegen library Jennifer. Replaces homebrew solution for code gen…
Browse files Browse the repository at this point in the history
…eration. (#14)

* Add codegen library Jennifer and use for enum generation

* Use codegen library Jennifer for packetmap generation

* Add packageAliases collection
Specifies map of packages to their full import paths for ease of setting up aliases within the Jennifer codegen library

* Fix typo Primitize -> Primitive

* Use codegen library Jennifer for struct generation (Part 1)
Implement Jennifer codegen for struct fields (including switch structures)
Does not generate implementations for serialize/deserialize methods

* Ensure empty Packet structs are not on a single line
Maintains compatibility with previous codegen system

* Add eotype codegen package
Representation of different EO types and their string method names.
Helper for writing serialize method implementations

* Fix typo in writer comment

* Encapsulate package alias information
Add 'types' package under 'codegen', for helper functions related to determining types/package imports

* Extract some shared helpers to codegen/types

* Use codegen library Jennifer for struct generation (part 2)
Generate implementations for serialize methods
Major refactor of code structure

* Rename NewSerializationType->NewEoType

* Use codegen library Jennifer for struct generation (part 3)
Generate implementations for deserialize methods
Remove remaining obsolete helpers

* Regenerate protocol code using Jennifer

* Properly apply switch struct qualifier to nested switches
  • Loading branch information
ethanmoffat authored Apr 11, 2024
1 parent 2e63afc commit a695b04
Show file tree
Hide file tree
Showing 20 changed files with 1,071 additions and 818 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/stretchr/testify v1.8.4
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df
golang.org/x/text v0.11.0
github.com/dave/jennifer v1.7.0
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/dave/jennifer v1.7.0 h1:uRbSBH9UTS64yXbh4FrMHfgfY762RD+C7bUPKODpSJE=
github.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
60 changes: 35 additions & 25 deletions internal/codegen/enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package codegen
import (
"fmt"
"path"
"strings"

"github.com/dave/jennifer/jen"
"github.com/ethanmoffat/eolib-go/internal/codegen/types"
"github.com/ethanmoffat/eolib-go/internal/xml"
)

Expand All @@ -16,49 +17,58 @@ func GenerateEnums(outputDir string, enums []xml.ProtocolEnum) error {
return err
}

output := strings.Builder{}
output.WriteString(packageName + "\n\n")
output.WriteString("import \"fmt\"\n\n")
f := jen.NewFile(packageName)

for _, e := range enums {
writeTypeComment(&output, e.Name, e.Comment)
writeTypeCommentJen(f, e.Name, e.Comment)

output.WriteString(fmt.Sprintf("type %s int\n\n", e.Name))
output.WriteString("const (\n")
f.Type().Id(e.Name).Int()

defsList := make([]jen.Code, len(e.Values))
expected := 0
for i, v := range e.Values {
var s *jen.Statement
if i == 0 {
output.WriteString(fmt.Sprintf("\t%s_%s %s = iota", sanitizeTypeName(e.Name), v.Name, e.Name))
s = jen.Id(fmt.Sprintf("%s_%s", types.SanitizeTypeName(e.Name), v.Name)).Qual("", e.Name).Op("=").Iota()

if v.Value > 0 {
output.WriteString(fmt.Sprintf(" + %d", v.Value))
expected = int(v.Value)
s.Op("+").Lit(expected)
}
} else {
output.WriteString(fmt.Sprintf("\t%s_%s", sanitizeTypeName(e.Name), v.Name))
if expected != int(v.Value) {
output.WriteString(fmt.Sprintf(" = %d", v.Value))
s = jen.Id(fmt.Sprintf("%s_%s", types.SanitizeTypeName(e.Name), v.Name))
actual := int(v.Value)
if expected != actual {
s.Op("=").Lit(actual)
}
}

writeInlineComment(&output, v.Comment)

output.WriteString("\n")
writeInlineCommentJen(s, v.Comment)
expected += 1
}

output.WriteString(")\n\n")
defsList[i] = s
}
f.Const().Defs(defsList...)

output.WriteString(fmt.Sprintf("// String converts a %s value into its string representation\n", e.Name))
output.WriteString(fmt.Sprintf("func (e %s) String() (string, error) {\n", e.Name))
output.WriteString("\tswitch e {\n")
for _, v := range e.Values {
output.WriteString(fmt.Sprintf("\tcase %s_%s:\n\t\treturn \"%s\", nil\n", sanitizeTypeName(e.Name), v.Name, v.Name))
caseList := make([]jen.Code, len(e.Values)+1)
for ndx, v := range e.Values {
caseList[ndx] = jen.Case(jen.Id(fmt.Sprintf("%s_%s", types.SanitizeTypeName(e.Name), v.Name))).Block(jen.Return(jen.Lit(v.Name), jen.Nil()))
}
output.WriteString(fmt.Sprintf("\tdefault:\n\t\treturn \"\", fmt.Errorf(\"could not convert value %%d of type %s to string\", e)\n", e.Name))
output.WriteString("\t}\n}\n\n")
caseList[len(e.Values)] = jen.Default().Block().Return(
jen.Lit(""), jen.Qual("fmt", "Errorf").Call(jen.Lit(fmt.Sprintf("could not convert value %%d of type %s to string", e.Name)), jen.Id("e")),
)

f.Commentf("String converts a %s value into its string representation", e.Name)
f.Func().Params(
// enum receiver
jen.Id("e").Id(e.Name),
).Id("String").Params(
// empty parameter list
).Params(jen.String(), jen.Error()).Block(
jen.Switch(jen.Id("e").Block(caseList...)),
)
}

outFileName := path.Join(outputDir, enumFileName)
return writeToFile(outFileName, output.String())
return writeToFileJen(f, outFileName)
}
100 changes: 65 additions & 35 deletions internal/codegen/packet.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,59 @@ package codegen
import (
"fmt"
"path"
"strings"

"github.com/dave/jennifer/jen"
"github.com/ethanmoffat/eolib-go/internal/codegen/types"
"github.com/ethanmoffat/eolib-go/internal/xml"
)

func GeneratePackets(outputDir string, packets []xml.ProtocolPacket, fullSpec xml.Protocol) error {
if len(packets) == 0 {
return nil
}

packageName, err := getPackageName(outputDir)
if err != nil {
return err
}

output := strings.Builder{}
output.WriteString(packageName + "\n\n")
output.WriteString("import (\n\t\"fmt\"\n\t\"reflect\"\n\t\"github.com/ethanmoffat/eolib-go/pkg/eolib/protocol/net\"\n)\n\n")
output.WriteString("var packetMap = map[int]reflect.Type{\n")
f := jen.NewFile(packageName)
types.AddImports(f)

// collect type names to generate packet structs
var typeNames []string
for _, p := range packets {
typeNames = append(typeNames, p.GetTypeName())
f.Var().Id("packetMap").Op("=").Map(jen.Int()).Qual("reflect", "Type").BlockFunc(func(g *jen.Group) {
// Note that this block is using "BlockFunc"
// Official docs advices to use "Values" with "DictFunc". However, default sorting is alphabetical, which
// creates a nasty git diff of the existing generated code
for _, p := range packets {
typeNames = append(typeNames, p.GetTypeName())

output.WriteString(fmt.Sprintf("\tnet.PacketId(net.PacketFamily_%s, net.PacketAction_%s): ", p.Family, p.Action))
output.WriteString(fmt.Sprintf("reflect.TypeOf(%s{}),\n", snakeCaseToCamelCase(p.GetTypeName())))
}
g.Qual(types.PackagePath("net"), "PacketId").Call(
jen.Qual(types.PackagePath("net"), fmt.Sprintf("PacketFamily_%s", p.Family)),
jen.Qual(types.PackagePath("net"), fmt.Sprintf("PacketAction_%s", p.Action)),
).Op(":").Qual("reflect", "TypeOf").Call(
jen.Id(snakeCaseToCamelCase(p.GetTypeName())).Values(),
).Op(",")
}
})

output.WriteString("}\n")
f.Comment("PacketFromId creates a typed packet instance from a [net.PacketFamily] and [net.PacketAction].")
f.Comment("This function calls [PacketFromIntegerId] internally.")

output.WriteString(`
// PacketFromId creates a typed packet instance from a [net.PacketFamily] and [net.PacketAction].
// This function calls [PacketFromIntegerId] internally.
func PacketFromId(family net.PacketFamily, action net.PacketAction) (net.Packet, error) {
return PacketFromIntegerId(net.PacketId(family, action))
}
f.Func().Id("PacketFromId").Params(
jen.Id("family").Qual(types.PackagePath("net"), "PacketFamily"),
jen.Id("action").Qual(types.PackagePath("net"), "PacketAction"),
).Params(
jen.Qual(types.PackagePath("net"), "Packet"),
jen.Error(),
).Block(
jen.Return(jen.Id("PacketFromIntegerId").Call(
jen.Qual(types.PackagePath("net"), "PacketId").Call(jen.Id("family"), jen.Id("action")),
)),
)

// PacketFromIntegerId creates a typed packet instance from a packet's ID. An ID may be converted from a family/action pair via the [net.PacketId] function.
f.Comment(`// PacketFromIntegerId creates a typed packet instance from a packet's ID. An ID may be converted from a family/action pair via the [net.PacketId] function.
// The returned packet implements the [net.Packet] interface. It may be serialized/deserialized without further conversion, or a type assertion may be made to examine the data. The expected type of the assertion is a pointer to a packet structure.
// The following example does both: an incoming CHAIR_REQUEST packet is deserialized from a reader without converting from the interface type, and the data is examined via a type assertion.
//
Expand All @@ -54,25 +72,37 @@ func PacketFromId(family net.PacketFamily, action net.PacketAction) (net.Packet,
// }
// default:
// fmt.Printf("Unknown type: %s\n", reflect.TypeOf(pkt).Elem().Name())
// }
func PacketFromIntegerId(id int) (net.Packet, error) {
packetType, idOk := packetMap[id]
if !idOk {
return nil, fmt.Errorf("could not find packet with id %d", id)
}
// }`)

packetInstance, typeOk := reflect.New(packetType).Interface().(net.Packet)
if !typeOk {
return nil, fmt.Errorf("could not create packet from id %d", id)
}
f.Func().Id("PacketFromIntegerId").Params(
jen.Id("id").Int(), // func declaration: int parameter 'id'
).Params(
jen.Qual(types.PackagePath("net"), "Packet"), // func declaration: return types (net.Packet, error)
jen.Error(),
).Block(
// try to get the packet type out of the map (indexed by the id)
jen.List(jen.Id("packetType"), jen.Id("idOk")).Op(":=").Id("packetMap").Index(jen.Id("id")),
// check that id is ok, return error otherwise
jen.If(jen.Op("!").Id("idOk")).Block(
jen.Return(jen.List(jen.Nil(), jen.Qual("fmt", "Errorf").Call(jen.Lit("could not find packet with id %d"), jen.Id("id")))),
).Line(),
// type assert that creating the packet type results in an interface that satisfies net.Packet
jen.List(jen.Id("packetInstance"), jen.Id("typeOk").Op(":=").Qual("reflect", "New").Call(
jen.Id("packetType"),
).Dot("Interface").Call().Assert(
jen.Qual(types.PackagePath("net"), "Packet"),
)),
// check that type is ok, return error otherwise
jen.If(jen.Op("!").Id("typeOk")).Block(
jen.Return(jen.List(jen.Nil(), jen.Qual("fmt", "Errorf").Call(jen.Lit("could not create packet from id %d"), jen.Id("id")))),
).Line(),
// return packetInstance, nil
jen.Return(jen.Id("packetInstance"), jen.Nil()),
)

return packetInstance, nil
}
`)

if len(packets) > 0 {
const packetMapFileName = "packetmap_generated.go"
writeToFile(path.Join(outputDir, packetMapFileName), output.String())
const packetMapFileName = "packetmap_generated.go"
if err := writeToFileJen(f, path.Join(outputDir, packetMapFileName)); err != nil {
return err
}

const packetFileName = "packets_generated.go"
Expand Down
Loading

0 comments on commit a695b04

Please sign in to comment.