Skip to content

Commit 93a7028

Browse files
committed
generator: add generator for user-defined types
Add infrastructure to generate optional wrapper types for custom types that implement MessagePack extension methods (MarshalMsgpack/UnmarshalMsgpack). Changes: * New CLI tool (cmd/gentypes) to generate code for specific types. * Support for MessagePack extension format with configurable extension codes. * Compatibility with existing option package patterns. * Updated linter exclusions for generator code paths. * Added import allow lists for new dependencies. Example usage: go run cmd/gentypes -ext-code 1 -package ./path/to/package TypeName Closes #TNTP-3734.
1 parent 7a7e26c commit 93a7028

21 files changed

+1986
-10
lines changed

.golangci.yml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,19 @@ linters:
1919
- ireturn # ireturn is disabled, since it's not needed.
2020

2121
exclusions:
22+
generated: lax
2223
rules:
2324
- path: cmd/generator/
2425
linters:
25-
- forbidigo # fmt functions are not forbidden here
26-
- gochecknoglobals # global variables are not forbidden here
26+
- forbidigo # fmt functions are not forbidden here.
27+
- gochecknoglobals # global variables are not forbidden here.
28+
- path: cmd/gentypes/
29+
linters:
30+
- forbidigo # fmt functions are not forbidden here.
31+
- gochecknoglobals # global variables are not forbidden here.
2732
- path: _test.go
2833
linters:
29-
- wrapcheck
34+
- wrapcheck
3035

3136
settings:
3237
godot:
@@ -48,8 +53,10 @@ linters:
4853
- "!$test"
4954
allow:
5055
- $gostd
51-
- "github.com/vmihailenco/msgpack/v5"
5256
- "golang.org/x/text"
57+
- "golang.org/x/tools"
58+
- "github.com/vmihailenco/msgpack/v5"
59+
- "github.com/tarantool/go-option"
5360
test:
5461
files:
5562
- "$test"

README.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,91 @@
88
[![Telegram EN][telegram-badge]][telegram-en-url]
99
[![Telegram RU][telegram-badge]][telegram-ru-url]
1010

11+
# go-option: library to work with optional types
12+
13+
## Pre-generated basic optional types
14+
15+
## Gentype Utility
16+
17+
A Go code generator for creating optional types with MessagePack
18+
serialization support.
19+
20+
### Overview
21+
22+
Gentype generates wrapper types for various Go primitives and
23+
custom types that implement optional (some/none) semantics with
24+
full MessagePack serialization capabilities. These generated types
25+
are useful for representing values that may or may not be present,
26+
while ensuring proper encoding and decoding when using MessagePack.
27+
28+
### Features
29+
30+
- Generates optional types for built-in types (bool, int, float, string, etc.)
31+
- Supports custom types with MessagePack extension serialization
32+
- Provides common optional type operations:
33+
- `SomeXxx(value)` - Create an optional with a value
34+
- `NoneXxx()` - Create an empty optional
35+
- `Unwrap()`, `UnwrapOr()`, `UnwrapOrElse()` - Value extraction
36+
- `IsSome()`, `IsNone()` - Presence checking
37+
- Full MessagePack `CustomEncoder` and `CustomDecoder` implementation
38+
- Type-safe operations
39+
40+
### Installation
41+
42+
```bash
43+
go install github.com/tarantool/go-option/cmd/gentypes@latest
44+
# OR (for go version 1.24+)
45+
go get -tool github.com/tarantool/go-option/cmd/gentypes@latest
46+
```
47+
48+
### Usage
49+
50+
#### Generating Optional Types
51+
52+
To generate optional types for existing types in a package:
53+
54+
```bash
55+
gentypes -package ./path/to/package -ext-code 123
56+
# OR (for go version 1.24+)
57+
go tool gentypes -package ./path/to/package -ext-code 123
58+
```
59+
60+
Or you can use it to generate file from go:
61+
```go
62+
//go:generate go run github.com/tarantool/go-option/cmd/gentypes@latest -ext-code 123
63+
// OR (for go version 1.24+)
64+
//go:generate go tool gentypes -ext-code 123
65+
```
66+
67+
Flags:
68+
69+
`-package`: Path to the Go package containing types to wrap (default: `"."`)
70+
`-ext-code`: MessagePack extension code to use for custom types (must be between
71+
-128 and 127, no default value)
72+
`-verbose`: Enable verbose output (default: `false`)
73+
74+
#### Using Generated Types
75+
76+
Generated types follow the pattern Optional<TypeName> and provide methods for working
77+
with optional values:
78+
79+
```go
80+
// Create an optional with a value.
81+
opt := SomeOptionalString("hello")
82+
83+
// Check if a value is present.
84+
if opt.IsSome() {
85+
value := opt.Unwrap()
86+
fmt.Println(value)
87+
}
88+
89+
// Use a default value if none.
90+
value := opt.UnwrapOr("default")
91+
92+
// Encode to MessagePack.
93+
err := opt.EncodeMsgpack(encoder)
94+
```
95+
1196
[godoc-badge]: https://pkg.go.dev/badge/github.com/tarantool/go-option.svg
1297
[godoc-url]: https://pkg.go.dev/github.com/tarantool/go-option
1398
[actions-badge]: https://github.com/tarantool/go-option/actions/workflows/testing.yaml/badge.svg

cmd/gentypes/extractor/analyzer.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Package extractor is a package, that extracts type specs and methods from given ast tree.
2+
package extractor
3+
4+
import (
5+
"go/ast"
6+
)
7+
8+
// TypeSpecEntry is an entry, that defines ast's TypeSpec and contains type name and methods.
9+
type TypeSpecEntry struct {
10+
Name string
11+
Methods []string
12+
13+
methodMap map[string]struct{}
14+
15+
rawType *ast.TypeSpec
16+
rawMethods []*ast.FuncDecl
17+
}
18+
19+
// HasMethod returns true if type spec has method with given name.
20+
func (e TypeSpecEntry) HasMethod(name string) bool {
21+
_, ok := e.methodMap[name]
22+
return ok
23+
}
24+
25+
// Analyzer is an analyzer, that extracts type specs and methods from package and groups
26+
// them for quick access.
27+
type Analyzer struct {
28+
pkgPath string
29+
pkgName string
30+
entries map[string]*TypeSpecEntry
31+
}
32+
33+
// NewAnalyzerFromPackage parses ast tree for TypeSpecs and associated methods.
34+
func NewAnalyzerFromPackage(pkg Package) (*Analyzer, error) {
35+
typeSpecs := ExtractTypeSpecsFromPackage(pkg)
36+
methodsDefs := ExtractMethodsFromPackage(pkg)
37+
38+
analyzer := &Analyzer{
39+
entries: make(map[string]*TypeSpecEntry, len(typeSpecs)),
40+
pkgPath: pkg.PkgPath(),
41+
pkgName: pkg.Name(),
42+
}
43+
44+
for _, typeSpec := range typeSpecs {
45+
tsName := typeSpec.Name.String()
46+
if _, ok := analyzer.entries[tsName]; ok {
47+
// Duplicate type spec, skipping.
48+
continue
49+
}
50+
51+
entry := &TypeSpecEntry{
52+
Name: tsName,
53+
Methods: nil,
54+
methodMap: make(map[string]struct{}),
55+
rawType: typeSpec,
56+
rawMethods: nil,
57+
}
58+
59+
for _, methodDef := range methodsDefs {
60+
typeName := ExtractRecvTypeName(methodDef)
61+
if typeName != tsName {
62+
continue
63+
}
64+
65+
entry.Methods = append(entry.Methods, methodDef.Name.String())
66+
entry.rawMethods = append(entry.rawMethods, methodDef)
67+
entry.methodMap[methodDef.Name.String()] = struct{}{}
68+
}
69+
70+
analyzer.entries[tsName] = entry
71+
}
72+
73+
return analyzer, nil
74+
}
75+
76+
// PackagePath returns package path of analyzed package.
77+
func (a Analyzer) PackagePath() string {
78+
return a.pkgPath
79+
}
80+
81+
// PackageName returns package name of analyzed package.
82+
func (a Analyzer) PackageName() string {
83+
return a.pkgName
84+
}
85+
86+
// TypeSpecEntryByName returns TypeSpecEntry entry by name.
87+
func (a Analyzer) TypeSpecEntryByName(name string) (*TypeSpecEntry, bool) {
88+
structEntry, ok := a.entries[name]
89+
return structEntry, ok
90+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package extractor_test
2+
3+
import (
4+
"go/ast"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/tarantool/go-option/cmd/gentypes/extractor"
11+
)
12+
13+
type MockPackage struct {
14+
NameValue string
15+
PkgPathValue string
16+
SyntaxValue []*ast.File
17+
}
18+
19+
func (p *MockPackage) Name() string {
20+
return p.NameValue
21+
}
22+
23+
func (p *MockPackage) PkgPath() string {
24+
return p.PkgPathValue
25+
}
26+
27+
func (p *MockPackage) Syntax() []*ast.File {
28+
return p.SyntaxValue
29+
}
30+
31+
func TestNewAnalyzerFromPackage_Success(t *testing.T) {
32+
t.Parallel()
33+
34+
pkg := &MockPackage{
35+
SyntaxValue: []*ast.File{
36+
astFromString(t, s("package pkg", "type T struct{}", "func (t *T) Method() {}")),
37+
},
38+
NameValue: "pkg",
39+
PkgPathValue: "some-pkg-path",
40+
}
41+
42+
analyzer, err := extractor.NewAnalyzerFromPackage(pkg)
43+
require.NoError(t, err)
44+
require.NotNil(t, analyzer)
45+
}
46+
47+
func TestNewAnalyzerFromPackage_PkgInfo(t *testing.T) {
48+
t.Parallel()
49+
50+
pkg := &MockPackage{
51+
SyntaxValue: []*ast.File{
52+
astFromString(t, s("package pkg", "type T struct{}", "func (t *T) Method() {}")),
53+
},
54+
NameValue: "pkg",
55+
PkgPathValue: "some-pkg-path",
56+
}
57+
58+
analyzer, err := extractor.NewAnalyzerFromPackage(pkg)
59+
require.NoError(t, err)
60+
61+
assert.Equal(t, pkg.Name(), analyzer.PackageName())
62+
assert.Equal(t, pkg.PkgPath(), analyzer.PackagePath())
63+
}
64+
65+
func TestNewAnalyzerFromPackage_TypeInfo(t *testing.T) {
66+
t.Parallel()
67+
68+
pkg := &MockPackage{
69+
SyntaxValue: []*ast.File{
70+
astFromString(t, s("package pkg", "type T struct{}", "func (t *T) Method() {}")),
71+
},
72+
NameValue: "pkg",
73+
PkgPathValue: "some-pkg-path",
74+
}
75+
76+
analyzer, err := extractor.NewAnalyzerFromPackage(pkg)
77+
require.NoError(t, err)
78+
79+
entry, found := analyzer.TypeSpecEntryByName("T")
80+
assert.True(t, found)
81+
82+
assert.Equal(t, "T", entry.Name)
83+
assert.Equal(t, []string{"Method"}, entry.Methods)
84+
assert.True(t, entry.HasMethod("Method"))
85+
86+
_, found = analyzer.TypeSpecEntryByName("U")
87+
assert.False(t, found)
88+
}
89+
90+
func TestNewAnalyzerFromPackage_NilPackage(t *testing.T) {
91+
t.Parallel()
92+
93+
assert.Panics(t, func() {
94+
_, _ = extractor.NewAnalyzerFromPackage(nil)
95+
})
96+
}

cmd/gentypes/extractor/methods.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package extractor
2+
3+
import (
4+
"go/ast"
5+
)
6+
7+
type methodVisitor struct {
8+
Methods []*ast.FuncDecl
9+
}
10+
11+
func (t *methodVisitor) Visit(node ast.Node) ast.Visitor {
12+
funcDecl, ok := node.(*ast.FuncDecl)
13+
if !ok || funcDecl.Recv == nil {
14+
return t
15+
}
16+
17+
t.Methods = append(t.Methods, funcDecl)
18+
19+
return t
20+
}
21+
22+
// ExtractMethodsFromPackage is a function to extract methods from package.
23+
func ExtractMethodsFromPackage(pkg Package) []*ast.FuncDecl {
24+
visitor := &methodVisitor{
25+
Methods: nil,
26+
}
27+
for _, file := range pkg.Syntax() {
28+
ast.Walk(visitor, file)
29+
}
30+
31+
return visitor.Methods
32+
}
33+
34+
// ExtractRecvTypeName is a helper function to extract receiver type name (string) from method.
35+
func ExtractRecvTypeName(method *ast.FuncDecl) string {
36+
if method.Recv == nil {
37+
return ""
38+
}
39+
40+
name := method.Recv.List[0]
41+
tpExpr := name.Type
42+
43+
// This is used to remove pointer from type.
44+
if star, ok := tpExpr.(*ast.StarExpr); ok {
45+
tpExpr = star.X
46+
}
47+
48+
switch convertedExpr := tpExpr.(type) {
49+
case *ast.IndexExpr: // This is used for generic structs or typedefs.
50+
tpExpr = convertedExpr.X
51+
case *ast.IndexListExpr: // This is used for multi-type generic structs or typedefs.
52+
tpExpr = convertedExpr.X
53+
}
54+
55+
switch rawTp := tpExpr.(type) {
56+
case *ast.Ident: // This is used for usual structs or typedefs.
57+
return rawTp.Name
58+
default:
59+
panic("unexpected type")
60+
}
61+
}

0 commit comments

Comments
 (0)