Repository: hamba/avro

Issue:
Feature request: generate enums
Hi,

I've noticed that the enums are not being generated in the schema. Is it possible to enable the generation of enums?

Thank you

Hints from issue comments:
@nrwiersma: Can you give an example of what you would expect it to generate?@EduardoLaranjo: hi @nrwiersma thanks for prompt reply 

So with gogen-avro any schema with an enum is gonna be generated like so: 

```
{
    "name": "status",
    "type": {
        "name": "status",
        "type": "enum",
        "symbols": [
        "undefined",
        "pending",
        "confirmed",
        "failed"
        ]
    }
}
```
```
type Status int32

const (
	StatusUndefined Status = 0
	StatusPending   Status = 1
	StatusConfirmed Status = 2
	StatusFailed    Status = 3
)

func (e Status) String() string {
	switch e {
	case StatusUndefined:
		return "undefined"
	case StatusPending:
		return "pending"
	case StatusConfirmed:
		return "confirmed"
	case StatusFailed:
		return "failed"
	}
	return "unknown"
}
```

The approach taken to generate the enum is not important to me. It could be like the example above or a group of string constants. The important thing is to have access to the values in the generated code without requiring me to search through the Avro schema.

Thanks Eduardo
@EduardoLaranjo: hi @nrwiersma 

I'm happy to-do this work if it's ok 

I just need some guidelines for what do we want
@nrwiersma: Hey. You are welcome to take a crack at it. Your proposed typed enum is good with me, but you need to consider that using the name directly is likely to cause conflicts, so making it unique is going to be the challenge.

Solution patch:
diff --git a/cmd/avrogen/main.go b/cmd/avrogen/main.go
index 39651a2..c48b7b2 100644
--- a/cmd/avrogen/main.go
+++ b/cmd/avrogen/main.go
@@ -32,6 +32,7 @@ type config struct {
 	Initialisms    string
 	SchemaRegistry string
 	LogicalTypes   logicalTypes
+	EnumsGen       bool
 }
 
 type logicalTypes []string
@@ -64,6 +65,7 @@ func realMain(args []string, stdout, stderr io.Writer) int {
 	flgs.StringVar(&cfg.Initialisms, "initialisms", "", "Custom initialisms <VAL>[,...] for struct and field names.")
 	flgs.StringVar(&cfg.TemplateFileName, "template-filename", "", "Override output template with one loaded from file.")
 	flgs.StringVar(&cfg.SchemaRegistry, "schemaregistry", "", "The URL to schema registry, e.g.: http://localhost:8081.")
+	flgs.BoolVar(&cfg.EnumsGen, "enums", false, "Generate Go enums for Avro enums.")
 	var lt logicalTypes
 	flgs.Var(&lt, "logicaltype",
 		"A logical type mapping of the form logicalType,goType[,import]. Can be specified multiple times.")
@@ -108,7 +110,9 @@ func realMain(args []string, stdout, stderr io.Writer) int {
 		gen.WithTemplate(string(template)),
 		gen.WithStrictTypes(cfg.StrictTypes),
 		gen.WithFullSchema(cfg.FullSchema),
+		gen.WithEnums(cfg.EnumsGen),
 	}
+
 	for _, entry := range lt {
 		logicalType, err := parseLogicalType(entry)
 		if err != nil {
diff --git a/gen/gen.go b/gen/gen.go
index 8ac94b8..79e0a33 100644
--- a/gen/gen.go
+++ b/gen/gen.go
@@ -177,6 +177,13 @@ func WithMetadata(m any) OptsFunc {
 	}
 }
 
+// WithEnums configures the generator to output the enum symbols.
+func WithEnums(b bool) OptsFunc {
+	return func(g *Generator) {
+		g.genEnums = b
+	}
+}
+
 // LogicalType used when the name of the "LogicalType" field in the Avro schema matches the Name attribute.
 type LogicalType struct {
 	// Name of the LogicalType
@@ -220,6 +227,7 @@ type Generator struct {
 	encoders     bool
 	fullSchema   bool
 	strictTypes  bool
+	genEnums     bool
 	initialisms  []string
 	logicalTypes map[avro.LogicalType]LogicalType
 	metadata     any
@@ -227,8 +235,8 @@ type Generator struct {
 	imports           []string
 	thirdPartyImports []string
 	typedefs          []typedef
-
-	nameCaser *strcase.Caser
+	typeenums         []typeenum
+	nameCaser         *strcase.Caser
 }
 
 // NewGenerator returns a generator.
@@ -298,6 +306,9 @@ func (g *Generator) generate(schema avro.Schema, metadata any) string {
 	case *avro.ArraySchema:
 		return "[]" + g.generate(s.Items(), metadata)
 	case *avro.EnumSchema:
+		if g.genEnums {
+			return g.resolveEnum(s)
+		}
 		return "string"
 	case *avro.FixedSchema:
 		typ := fmt.Sprintf("[%d]byte", s.Size())
@@ -314,6 +325,11 @@ func (g *Generator) generate(schema avro.Schema, metadata any) string {
 	}
 }
 
+func (g *Generator) resolveEnum(s *avro.EnumSchema) string {
+	g.typeenums = append(g.typeenums, newTypeEnum(s.Name(), s.Symbols()))
+	return s.Name()
+}
+
 func (g *Generator) resolveTypeName(s avro.NamedSchema) string {
 	if g.fullName {
 		return g.nameCaser.ToPascal(s.FullName())
@@ -471,6 +487,7 @@ func (g *Generator) Write(w io.Writer) error {
 		ThirdPartyImports []string
 		Typedefs          []typedef
 		Metadata          any
+		Typeenums         []typeenum
 	}{
 		WithEncoders: g.encoders,
 		PackageName:  g.pkg,
@@ -478,6 +495,7 @@ func (g *Generator) Write(w io.Writer) error {
 		Imports:      append(g.imports, g.thirdPartyImports...),
 		Typedefs:     g.typedefs,
 		Metadata:     g.metadata,
+		Typeenums:    g.typeenums,
 	}
 	return parsed.Execute(w, data)
 }
@@ -510,3 +528,15 @@ type field struct {
 	Tags          map[string]TagStyle
 	Props         map[string]any
 }
+
+type typeenum struct {
+	Name    string
+	Symbols []string
+}
+
+func newTypeEnum(name string, symbols []string) typeenum {
+	return typeenum{
+		Name:    name,
+		Symbols: symbols,
+	}
+}
diff --git a/gen/output_template.tmpl b/gen/output_template.tmpl
index 54311f2..ec41b09 100644
--- a/gen/output_template.tmpl
+++ b/gen/output_template.tmpl
@@ -13,6 +13,16 @@ package {{ .PackageName }}
 	)
 {{ end }}
 
+{{- range .Typeenums }}
+{{- $t := . }}
+type {{ $t.Name }} string
+const (
+	{{- range .Symbols }}
+	{{ printf "%s%s" $t.Name . | upperCamel }} {{ $t.Name }} = "{{ . }}"
+	{{- end }} 
+)
+{{- end }} 
+
 {{- range .Typedefs }}
 	{{- if len .Doc }}
 	// {{ replace .Doc "\n" "\n// " -1 }}



Test patch:
diff --git a/gen/gen_test.go b/gen/gen_test.go
index 2ee43df..11f73f2 100644
--- a/gen/gen_test.go
+++ b/gen/gen_test.go
@@ -327,6 +327,30 @@ func TestStruct_GenFromRecordSchemaWithFullSchema(t *testing.T) {
 	assert.Equal(t, string(want), string(file))
 }
 
+func TestGenerator_GenEnum(t *testing.T) {
+	goldenSchema, err := avro.ParseFiles("testdata/golden.avsc")
+	require.NoError(t, err)
+
+	g := gen.NewGenerator("something", map[string]gen.TagStyle{}, gen.WithEnums(true))
+	g.Parse(goldenSchema)
+
+	var buf bytes.Buffer
+	err = g.Write(&buf)
+	require.NoError(t, err)
+
+	formatted, err := format.Source(buf.Bytes())
+	require.NoError(t, err)
+
+	if *update {
+		err = os.WriteFile("testdata/golden_enum.go", formatted, 0600)
+		require.NoError(t, err)
+	}
+
+	want, err := os.ReadFile("testdata/golden_enum.go")
+	require.NoError(t, err)
+	assert.Equal(t, string(want), string(formatted))
+}
+
 func TestGenerator(t *testing.T) {
 	unionSchema, err := avro.ParseFiles("testdata/uniontype.avsc")
 	require.NoError(t, err)
diff --git a/gen/testdata/golden_enum.go b/gen/testdata/golden_enum.go
new file mode 100644
index 0000000..6d62cdc
--- /dev/null
+++ b/gen/testdata/golden_enum.go
@@ -0,0 +1,91 @@
+// Code generated by avro/gen. DO NOT EDIT.
+package something
+
+import (
+	"github.com/hamba/avro/v2"
+	"math/big"
+	"time"
+)
+
+type Cards string
+
+const (
+	CardsSpades   Cards = "SPADES"
+	CardsHearts   Cards = "HEARTS"
+	CardsDiamonds Cards = "DIAMONDS"
+	CardsClubs    Cards = "CLUBS"
+)
+
+// InnerRecord is a generated struct.
+type InnerRecord struct {
+	InnerJustBytes                   []byte    `avro:"innerJustBytes"`
+	InnerPrimitiveNullableArrayUnion *[]string `avro:"innerPrimitiveNullableArrayUnion"`
+}
+
+// RecordInMap is a generated struct.
+type RecordInMap struct {
+	Name string `avro:"name"`
+}
+
+// RecordInArray is a generated struct.
+type RecordInArray struct {
+	AString string `avro:"aString"`
+}
+
+// RecordInNullableUnion is a generated struct.
+type RecordInNullableUnion struct {
+	AString string `avro:"aString"`
+}
+
+// Record1InNonNullableUnion is a generated struct.
+type Record1InNonNullableUnion struct {
+	AString string `avro:"aString"`
+}
+
+// Record2InNonNullableUnion is a generated struct.
+type Record2InNonNullableUnion struct {
+	AString string `avro:"aString"`
+}
+
+// Record1InNullableUnion is a generated struct.
+type Record1InNullableUnion struct {
+	AString string `avro:"aString"`
+}
+
+// Record2InNullableUnion is a generated struct.
+type Record2InNullableUnion struct {
+	AString string `avro:"aString"`
+}
+
+// Test represents a golden record.
+type Test struct {
+	// aString is just a string.
+	AString string `avro:"aString"`
+	// aBoolean is just a boolean.
+	ABoolean                        bool                   `avro:"aBoolean"`
+	AnInt                           int                    `avro:"anInt"`
+	AFloat                          float32                `avro:"aFloat"`
+	ADouble                         float64                `avro:"aDouble"`
+	ALong                           int64                  `avro:"aLong"`
+	JustBytes                       []byte                 `avro:"justBytes"`
+	PrimitiveNullableArrayUnion     *[]string              `avro:"primitiveNullableArrayUnion"`
+	InnerRecord                     InnerRecord            `avro:"innerRecord"`
+	AnEnum                          Cards                  `avro:"anEnum"`
+	AFixed                          [7]byte                `avro:"aFixed"`
+	ALogicalFixed                   avro.LogicalDuration   `avro:"aLogicalFixed"`
+	AnotherLogicalFixed             avro.LogicalDuration   `avro:"anotherLogicalFixed"`
+	MapOfStrings                    map[string]string      `avro:"mapOfStrings"`
+	MapOfRecords                    map[string]RecordInMap `avro:"mapOfRecords"`
+	ADate                           time.Time              `avro:"aDate"`
+	ADuration                       time.Duration          `avro:"aDuration"`
+	ALongTimeMicros                 time.Duration          `avro:"aLongTimeMicros"`
+	ALongTimestampMillis            time.Time              `avro:"aLongTimestampMillis"`
+	ALongTimestampMicro             time.Time              `avro:"aLongTimestampMicro"`
+	ABytesDecimal                   *big.Rat               `avro:"aBytesDecimal"`
+	ARecordArray                    []RecordInArray        `avro:"aRecordArray"`
+	NullableRecordUnion             *RecordInNullableUnion `avro:"nullableRecordUnion"`
+	NonNullableRecordUnion          any                    `avro:"nonNullableRecordUnion"`
+	NullableRecordUnionWith3Options any                    `avro:"nullableRecordUnionWith3Options"`
+	Ref                             Record2InNullableUnion `avro:"ref"`
+	UUID                            string                 `avro:"uuid"`
+}