Repository: hamba/avro
Pull request #405 (2024-05-19 21:52:17)

Output doc comments for package, structs, and fields.
Support for generating doc comments from avro "doc" attributes in the schema.

Also added an optional command-line argument to supply a package doc comment.


Patch:
diff --git a/cmd/avrogen/main.go b/cmd/avrogen/main.go
index e2a70d5..a2adfb4 100644
--- a/cmd/avrogen/main.go
+++ b/cmd/avrogen/main.go
@@ -19,6 +19,7 @@ type config struct {
 	TemplateFileName string
 
 	Pkg         string
+	PkgDoc      string
 	Out         string
 	Tags        string
 	FullName    bool
@@ -36,6 +37,7 @@ func realMain(args []string, stdout, stderr io.Writer) int {
 	flgs := flag.NewFlagSet("avrogen", flag.ExitOnError)
 	flgs.SetOutput(stderr)
 	flgs.StringVar(&cfg.Pkg, "pkg", "", "The package name of the output file.")
+	flgs.StringVar(&cfg.PkgDoc, "pkgdoc", "", "The package doc comment to output.")
 	flgs.StringVar(&cfg.Out, "o", "", "The output file path to write to instead of stdout.")
 	flgs.StringVar(&cfg.Tags, "tags", "", "The additional field tags <tag-name>:{snake|camel|upper-camel|kebab}>[,...]")
 	flgs.BoolVar(&cfg.FullName, "fullname", false, "Use the full name of the Record schema to create the struct name.")
@@ -77,6 +79,7 @@ func realMain(args []string, stdout, stderr io.Writer) int {
 
 	opts := []gen.OptsFunc{
 		gen.WithFullName(cfg.FullName),
+		gen.WithPackageDoc(cfg.PkgDoc),
 		gen.WithEncoders(cfg.Encoders),
 		gen.WithInitialisms(initialisms),
 		gen.WithTemplate(string(template)),
diff --git a/gen/gen.go b/gen/gen.go
index 720d620..250bd7a 100644
--- a/gen/gen.go
+++ b/gen/gen.go
@@ -10,6 +10,7 @@ import (
 	"maps"
 	"strings"
 	"text/template"
+	"unicode/utf8"
 
 	"github.com/ettle/strcase"
 	"github.com/hamba/avro/v2"
@@ -147,10 +148,28 @@ func WithStrictTypes(b bool) OptsFunc {
 	}
 }
 
+// WithPackageDoc configures the generator to output the given text as a package doc comment.
+func WithPackageDoc(text string) OptsFunc {
+	return func(g *Generator) {
+		g.pkgdoc = ensureTrailingPeriod(text)
+	}
+}
+
+func ensureTrailingPeriod(text string) string {
+	if text == "" {
+		return text
+	}
+	if last, _ := utf8.DecodeLastRuneInString(text); last == '.' {
+		return text
+	}
+	return text + "."
+}
+
 // Generator generates Go structs from schemas.
 type Generator struct {
 	template    string
 	pkg         string
+	pkgdoc      string
 	tags        map[string]TagStyle
 	fullName    bool
 	encoders    bool
@@ -257,7 +276,7 @@ func (g *Generator) resolveRecordSchema(schema *avro.RecordSchema) string {
 
 	typeName := g.resolveTypeName(schema)
 	if !g.hasTypeDef(typeName) {
-		g.typedefs = append(g.typedefs, newType(typeName, fields, schema.String()))
+		g.typedefs = append(g.typedefs, newType(typeName, schema.Doc(), fields, schema.String()))
 	}
 	return typeName
 }
@@ -319,12 +338,12 @@ func (g *Generator) resolveLogicalSchema(logicalType avro.LogicalType) string {
 	return typ
 }
 
-func (g *Generator) newField(name, typ, avroFieldDoc, avroFieldName string) field {
+func (g *Generator) newField(name, typ, doc, avroFieldName string) field {
 	return field{
 		Name:          name,
 		Type:          typ,
 		AvroFieldName: avroFieldName,
-		AvroFieldDoc:  avroFieldDoc,
+		Doc:           ensureTrailingPeriod(doc),
 		Tags:          g.tags,
 	}
 }
@@ -364,12 +383,14 @@ func (g *Generator) Write(w io.Writer) error {
 	data := struct {
 		WithEncoders      bool
 		PackageName       string
+		PackageDoc        string
 		Imports           []string
 		ThirdPartyImports []string
 		Typedefs          []typedef
 	}{
 		WithEncoders: g.encoders,
 		PackageName:  g.pkg,
+		PackageDoc:   g.pkgdoc,
 		Imports:      append(g.imports, g.thirdPartyImports...),
 		Typedefs:     g.typedefs,
 	}
@@ -378,13 +399,15 @@ func (g *Generator) Write(w io.Writer) error {
 
 type typedef struct {
 	Name   string
+	Doc    string
 	Fields []field
 	Schema string
 }
 
-func newType(name string, fields []field, schema string) typedef {
+func newType(name, doc string, fields []field, schema string) typedef {
 	return typedef{
 		Name:   name,
+		Doc:    ensureTrailingPeriod(doc),
 		Fields: fields,
 		Schema: schema,
 	}
@@ -393,7 +416,7 @@ func newType(name string, fields []field, schema string) typedef {
 type field struct {
 	Name          string
 	Type          string
+	Doc           string
 	AvroFieldName string
-	AvroFieldDoc  string
 	Tags          map[string]TagStyle
 }
diff --git a/gen/output_template.tmpl b/gen/output_template.tmpl
index 94cbcf6..1143fe5 100644
--- a/gen/output_template.tmpl
+++ b/gen/output_template.tmpl
@@ -1,3 +1,6 @@
+{{- if len .PackageDoc }}
+// {{ .PackageDoc }}
+{{- end }}
 package {{ .PackageName }}
 
 // Code generated by avro/gen. DO NOT EDIT.
@@ -12,10 +15,17 @@ package {{ .PackageName }}
 {{ end }}
 
 {{- range .Typedefs }}
+	{{- if len .Doc }}
+	// {{ .Doc }}
+	{{- else }}
 	// {{ .Name }} is a generated struct.
+	{{- end }}	
 	type {{ .Name }} struct {
 	{{- range .Fields }}
 		{{- $f := . }}
+		{{- if len $f.Doc }}
+		// {{ $f.Doc }}
+		{{- end }}
 		{{ .Name }} {{ .Type }} `avro:"{{ $f.AvroFieldName }}"
 		{{- range $tag, $style := .Tags }}
 			{{- " "}}{{ $tag }}:"


Test patch:
diff --git a/cmd/avrogen/main_test.go b/cmd/avrogen/main_test.go
index 31f089f..4788be0 100644
--- a/cmd/avrogen/main_test.go
+++ b/cmd/avrogen/main_test.go
@@ -65,7 +65,7 @@ func TestAvroGen_RequiredFlags(t *testing.T) {
 func TestAvroGen_GeneratesSchemaStdout(t *testing.T) {
 	var buf bytes.Buffer
 
-	args := []string{"avrogen", "-pkg", "testpkg", "testdata/schema.avsc"}
+	args := []string{"avrogen", "-pkg", "testpkg", "-pkgdoc", "package testpkg is generated from schema.avsc", "testdata/schema.avsc"}
 	gotCode := realMain(args, &buf, io.Discard)
 	require.Equal(t, 0, gotCode)
 
@@ -80,7 +80,7 @@ func TestAvroGen_GeneratesSchema(t *testing.T) {
 	t.Cleanup(func() { _ = os.RemoveAll(path) })
 
 	file := filepath.Join(path, "test.go")
-	args := []string{"avrogen", "-pkg", "testpkg", "-o", file, "testdata/schema.avsc"}
+	args := []string{"avrogen", "-o", file, "-pkg", "testpkg", "-pkgdoc", "package testpkg is generated from schema.avsc", "testdata/schema.avsc"}
 	gotCode := realMain(args, io.Discard, io.Discard)
 	require.Equal(t, 0, gotCode)
 
diff --git a/cmd/avrogen/testdata/golden.go b/cmd/avrogen/testdata/golden.go
index cf6eda1..d6f5ca8 100644
--- a/cmd/avrogen/testdata/golden.go
+++ b/cmd/avrogen/testdata/golden.go
@@ -1,9 +1,11 @@
+// package testpkg is generated from schema.avsc.
 package testpkg
 
 // Code generated by avro/gen. DO NOT EDIT.
 
-// Test is a generated struct.
+// Test is a test struct.
 type Test struct {
+	// SomeString is a string.
 	SomeString string `avro:"someString"`
 	SomeInt    int    `avro:"someInt"`
 }
diff --git a/cmd/avrogen/testdata/golden_encoders.go b/cmd/avrogen/testdata/golden_encoders.go
index 2cd1308..5f45e05 100644
--- a/cmd/avrogen/testdata/golden_encoders.go
+++ b/cmd/avrogen/testdata/golden_encoders.go
@@ -6,8 +6,9 @@ import (
 	"github.com/hamba/avro/v2"
 )
 
-// Test is a generated struct.
+// Test is a test struct.
 type Test struct {
+	// SomeString is a string.
 	SomeString string `avro:"someString"`
 	SomeInt    int    `avro:"someInt"`
 }
diff --git a/cmd/avrogen/testdata/golden_fullname.go b/cmd/avrogen/testdata/golden_fullname.go
index 43b51d3..fff8517 100644
--- a/cmd/avrogen/testdata/golden_fullname.go
+++ b/cmd/avrogen/testdata/golden_fullname.go
@@ -2,8 +2,9 @@ package testpkg
 
 // Code generated by avro/gen. DO NOT EDIT.
 
-// ABTest is a generated struct.
+// Test is a test struct.
 type ABTest struct {
+	// SomeString is a string.
 	SomeString string `avro:"someString"`
 	SomeInt    int    `avro:"someInt"`
 }
diff --git a/cmd/avrogen/testdata/golden_stricttypes.go b/cmd/avrogen/testdata/golden_stricttypes.go
index 49f3dcb..9b2a0ae 100644
--- a/cmd/avrogen/testdata/golden_stricttypes.go
+++ b/cmd/avrogen/testdata/golden_stricttypes.go
@@ -2,8 +2,9 @@ package testpkg
 
 // Code generated by avro/gen. DO NOT EDIT.
 
-// Test is a generated struct.
+// Test is a test struct.
 type Test struct {
+	// SomeString is a string.
 	SomeString string `avro:"someString"`
 	SomeInt    int32  `avro:"someInt"`
 }
diff --git a/cmd/avrogen/testdata/schema.avsc b/cmd/avrogen/testdata/schema.avsc
index 8196629..8754f39 100644
--- a/cmd/avrogen/testdata/schema.avsc
+++ b/cmd/avrogen/testdata/schema.avsc
@@ -2,8 +2,9 @@
   "type": "record",
   "name": "test",
   "namespace": "a.b",
+  "doc": "Test is a test struct",
   "fields": [
-    { "name": "someString", "type": "string" },
+    { "name": "someString", "type": "string", "doc": "SomeString is a string" },
     { "name": "someInt", "type": "int" }
   ]
 }
\ No newline at end of file
diff --git a/gen/testdata/golden.avsc b/gen/testdata/golden.avsc
index 5c2300e..438d842 100644
--- a/gen/testdata/golden.avsc
+++ b/gen/testdata/golden.avsc
@@ -1,14 +1,17 @@
 {
   "type": "record",
   "name": "test",
+  "doc": "Test represents a golden record",
   "namespace": "a.b",
   "fields": [
     {
       "name": "aString",
+      "doc": "aString is just a string",
       "type": "string"
     },
     {
       "name": "aBoolean",
+      "doc": "aBoolean is just a boolean.",
       "type": "boolean"
     },
     {
diff --git a/gen/testdata/golden.go b/gen/testdata/golden.go
index 6629773..f0cbfbd 100644
--- a/gen/testdata/golden.go
+++ b/gen/testdata/golden.go
@@ -50,9 +50,11 @@ type Record2InNullableUnion struct {
 	AString string `avro:"aString"`
 }
 
-// Test is a generated struct.
+// Test represents a golden record.
 type Test struct {
-	AString                         string                 `avro:"aString"`
+	// 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"`
diff --git a/gen/testdata/golden_encoders.go b/gen/testdata/golden_encoders.go
index d3c0fbb..73d3518 100644
--- a/gen/testdata/golden_encoders.go
+++ b/gen/testdata/golden_encoders.go
@@ -186,9 +186,11 @@ func (o *Record2InNullableUnion) Marshal() ([]byte, error) {
 	return avro.Marshal(o.Schema(), o)
 }
 
-// Test is a generated struct.
+// Test represents a golden record.
 type Test struct {
-	AString                         string                 `avro:"aString"`
+	// 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"`
diff --git a/gen/testdata/golden_fullname.go b/gen/testdata/golden_fullname.go
index cf81a16..4f39813 100644
--- a/gen/testdata/golden_fullname.go
+++ b/gen/testdata/golden_fullname.go
@@ -50,9 +50,11 @@ type ABRecord2InNullableUnion struct {
 	AString string `avro:"aString"`
 }
 
-// ABTest is a generated struct.
+// Test represents a golden record.
 type ABTest struct {
-	AString                         string                   `avro:"aString"`
+	// 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"`