diff --git a/.golangci.yml b/.golangci.yml index fafdbb0..b95c11b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,14 +19,19 @@ linters: - ireturn # ireturn is disabled, since it's not needed. exclusions: + generated: lax rules: - path: cmd/generator/ linters: - - forbidigo # fmt functions are not forbidden here - - gochecknoglobals # global variables are not forbidden here + - forbidigo # fmt functions are not forbidden here. + - gochecknoglobals # global variables are not forbidden here. + - path: cmd/gentypes/ + linters: + - forbidigo # fmt functions are not forbidden here. + - gochecknoglobals # global variables are not forbidden here. - path: _test.go linters: - - wrapcheck + - wrapcheck settings: godot: @@ -48,8 +53,10 @@ linters: - "!$test" allow: - $gostd - - "github.com/vmihailenco/msgpack/v5" - "golang.org/x/text" + - "golang.org/x/tools" + - "github.com/vmihailenco/msgpack/v5" + - "github.com/tarantool/go-option" test: files: - "$test" diff --git a/README.md b/README.md index 21bc5e0..2009091 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,91 @@ [![Telegram EN][telegram-badge]][telegram-en-url] [![Telegram RU][telegram-badge]][telegram-ru-url] +# go-option: library to work with optional types + +## Pre-generated basic optional types + +## Gentype Utility + +A Go code generator for creating optional types with MessagePack +serialization support. + +### Overview + +Gentype generates wrapper types for various Go primitives and +custom types that implement optional (some/none) semantics with +full MessagePack serialization capabilities. These generated types +are useful for representing values that may or may not be present, +while ensuring proper encoding and decoding when using MessagePack. + +### Features + +- Generates optional types for built-in types (bool, int, float, string, etc.) +- Supports custom types with MessagePack extension serialization +- Provides common optional type operations: + - `SomeXxx(value)` - Create an optional with a value + - `NoneXxx()` - Create an empty optional + - `Unwrap()`, `UnwrapOr()`, `UnwrapOrElse()` - Value extraction + - `IsSome()`, `IsNone()` - Presence checking +- Full MessagePack `CustomEncoder` and `CustomDecoder` implementation +- Type-safe operations + +### Installation + +```bash +go install github.com/tarantool/go-option/cmd/gentypes@latest +# OR (for go version 1.24+) +go get -tool github.com/tarantool/go-option/cmd/gentypes@latest +``` + +### Usage + +#### Generating Optional Types + +To generate optional types for existing types in a package: + +```bash +gentypes -package ./path/to/package -ext-code 123 +# OR (for go version 1.24+) +go tool gentypes -package ./path/to/package -ext-code 123 +``` + +Or you can use it to generate file from go: +```go +//go:generate go run github.com/tarantool/go-option/cmd/gentypes@latest -ext-code 123 +// OR (for go version 1.24+) +//go:generate go tool gentypes -ext-code 123 +``` + +Flags: + + • `-package`: Path to the Go package containing types to wrap (default: `"."`) + • `-ext-code`: MessagePack extension code to use for custom types (must be between + -128 and 127, no default value) + • `-verbose`: Enable verbose output (default: `false`) + +#### Using Generated Types + +Generated types follow the pattern Optional and provide methods for working +with optional values: + +```go +// Create an optional with a value. +opt := SomeOptionalString("hello") + +// Check if a value is present. +if opt.IsSome() { + value := opt.Unwrap() + fmt.Println(value) +} + +// Use a default value if none. +value := opt.UnwrapOr("default") + +// Encode to MessagePack. +err := opt.EncodeMsgpack(encoder) +``` + [godoc-badge]: https://pkg.go.dev/badge/github.com/tarantool/go-option.svg [godoc-url]: https://pkg.go.dev/github.com/tarantool/go-option [actions-badge]: https://github.com/tarantool/go-option/actions/workflows/testing.yaml/badge.svg diff --git a/cmd/gentypes/extractor/analyzer.go b/cmd/gentypes/extractor/analyzer.go new file mode 100644 index 0000000..f3dbbbe --- /dev/null +++ b/cmd/gentypes/extractor/analyzer.go @@ -0,0 +1,90 @@ +// Package extractor is a package, that extracts type specs and methods from given ast tree. +package extractor + +import ( + "go/ast" +) + +// TypeSpecEntry is an entry, that defines ast's TypeSpec and contains type name and methods. +type TypeSpecEntry struct { + Name string + Methods []string + + methodMap map[string]struct{} + + rawType *ast.TypeSpec + rawMethods []*ast.FuncDecl +} + +// HasMethod returns true if type spec has method with given name. +func (e TypeSpecEntry) HasMethod(name string) bool { + _, ok := e.methodMap[name] + return ok +} + +// Analyzer is an analyzer, that extracts type specs and methods from package and groups +// them for quick access. +type Analyzer struct { + pkgPath string + pkgName string + entries map[string]*TypeSpecEntry +} + +// NewAnalyzerFromPackage parses ast tree for TypeSpecs and associated methods. +func NewAnalyzerFromPackage(pkg Package) (*Analyzer, error) { + typeSpecs := ExtractTypeSpecsFromPackage(pkg) + methodsDefs := ExtractMethodsFromPackage(pkg) + + analyzer := &Analyzer{ + entries: make(map[string]*TypeSpecEntry, len(typeSpecs)), + pkgPath: pkg.PkgPath(), + pkgName: pkg.Name(), + } + + for _, typeSpec := range typeSpecs { + tsName := typeSpec.Name.String() + if _, ok := analyzer.entries[tsName]; ok { + // Duplicate type spec, skipping. + continue + } + + entry := &TypeSpecEntry{ + Name: tsName, + Methods: nil, + methodMap: make(map[string]struct{}), + rawType: typeSpec, + rawMethods: nil, + } + + for _, methodDef := range methodsDefs { + typeName := ExtractRecvTypeName(methodDef) + if typeName != tsName { + continue + } + + entry.Methods = append(entry.Methods, methodDef.Name.String()) + entry.rawMethods = append(entry.rawMethods, methodDef) + entry.methodMap[methodDef.Name.String()] = struct{}{} + } + + analyzer.entries[tsName] = entry + } + + return analyzer, nil +} + +// PackagePath returns package path of analyzed package. +func (a Analyzer) PackagePath() string { + return a.pkgPath +} + +// PackageName returns package name of analyzed package. +func (a Analyzer) PackageName() string { + return a.pkgName +} + +// TypeSpecEntryByName returns TypeSpecEntry entry by name. +func (a Analyzer) TypeSpecEntryByName(name string) (*TypeSpecEntry, bool) { + structEntry, ok := a.entries[name] + return structEntry, ok +} diff --git a/cmd/gentypes/extractor/analyzer_test.go b/cmd/gentypes/extractor/analyzer_test.go new file mode 100644 index 0000000..20478a1 --- /dev/null +++ b/cmd/gentypes/extractor/analyzer_test.go @@ -0,0 +1,96 @@ +package extractor_test + +import ( + "go/ast" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/tarantool/go-option/cmd/gentypes/extractor" +) + +type MockPackage struct { + NameValue string + PkgPathValue string + SyntaxValue []*ast.File +} + +func (p *MockPackage) Name() string { + return p.NameValue +} + +func (p *MockPackage) PkgPath() string { + return p.PkgPathValue +} + +func (p *MockPackage) Syntax() []*ast.File { + return p.SyntaxValue +} + +func TestNewAnalyzerFromPackage_Success(t *testing.T) { + t.Parallel() + + pkg := &MockPackage{ + SyntaxValue: []*ast.File{ + astFromString(t, s("package pkg", "type T struct{}", "func (t *T) Method() {}")), + }, + NameValue: "pkg", + PkgPathValue: "some-pkg-path", + } + + analyzer, err := extractor.NewAnalyzerFromPackage(pkg) + require.NoError(t, err) + require.NotNil(t, analyzer) +} + +func TestNewAnalyzerFromPackage_PkgInfo(t *testing.T) { + t.Parallel() + + pkg := &MockPackage{ + SyntaxValue: []*ast.File{ + astFromString(t, s("package pkg", "type T struct{}", "func (t *T) Method() {}")), + }, + NameValue: "pkg", + PkgPathValue: "some-pkg-path", + } + + analyzer, err := extractor.NewAnalyzerFromPackage(pkg) + require.NoError(t, err) + + assert.Equal(t, pkg.Name(), analyzer.PackageName()) + assert.Equal(t, pkg.PkgPath(), analyzer.PackagePath()) +} + +func TestNewAnalyzerFromPackage_TypeInfo(t *testing.T) { + t.Parallel() + + pkg := &MockPackage{ + SyntaxValue: []*ast.File{ + astFromString(t, s("package pkg", "type T struct{}", "func (t *T) Method() {}")), + }, + NameValue: "pkg", + PkgPathValue: "some-pkg-path", + } + + analyzer, err := extractor.NewAnalyzerFromPackage(pkg) + require.NoError(t, err) + + entry, found := analyzer.TypeSpecEntryByName("T") + assert.True(t, found) + + assert.Equal(t, "T", entry.Name) + assert.Equal(t, []string{"Method"}, entry.Methods) + assert.True(t, entry.HasMethod("Method")) + + _, found = analyzer.TypeSpecEntryByName("U") + assert.False(t, found) +} + +func TestNewAnalyzerFromPackage_NilPackage(t *testing.T) { + t.Parallel() + + assert.Panics(t, func() { + _, _ = extractor.NewAnalyzerFromPackage(nil) + }) +} diff --git a/cmd/gentypes/extractor/methods.go b/cmd/gentypes/extractor/methods.go new file mode 100644 index 0000000..9be0179 --- /dev/null +++ b/cmd/gentypes/extractor/methods.go @@ -0,0 +1,61 @@ +package extractor + +import ( + "go/ast" +) + +type methodVisitor struct { + Methods []*ast.FuncDecl +} + +func (t *methodVisitor) Visit(node ast.Node) ast.Visitor { + funcDecl, ok := node.(*ast.FuncDecl) + if !ok || funcDecl.Recv == nil { + return t + } + + t.Methods = append(t.Methods, funcDecl) + + return t +} + +// ExtractMethodsFromPackage is a function to extract methods from package. +func ExtractMethodsFromPackage(pkg Package) []*ast.FuncDecl { + visitor := &methodVisitor{ + Methods: nil, + } + for _, file := range pkg.Syntax() { + ast.Walk(visitor, file) + } + + return visitor.Methods +} + +// ExtractRecvTypeName is a helper function to extract receiver type name (string) from method. +func ExtractRecvTypeName(method *ast.FuncDecl) string { + if method.Recv == nil { + return "" + } + + name := method.Recv.List[0] + tpExpr := name.Type + + // This is used to remove pointer from type. + if star, ok := tpExpr.(*ast.StarExpr); ok { + tpExpr = star.X + } + + switch convertedExpr := tpExpr.(type) { + case *ast.IndexExpr: // This is used for generic structs or typedefs. + tpExpr = convertedExpr.X + case *ast.IndexListExpr: // This is used for multi-type generic structs or typedefs. + tpExpr = convertedExpr.X + } + + switch rawTp := tpExpr.(type) { + case *ast.Ident: // This is used for usual structs or typedefs. + return rawTp.Name + default: + panic("unexpected type") + } +} diff --git a/cmd/gentypes/extractor/methods_test.go b/cmd/gentypes/extractor/methods_test.go new file mode 100644 index 0000000..ec963cc --- /dev/null +++ b/cmd/gentypes/extractor/methods_test.go @@ -0,0 +1,198 @@ +package extractor_test + +import ( + "go/ast" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/tarantool/go-option/cmd/gentypes/extractor" +) + +func TestExtractMethodsFromPackageSimple(t *testing.T) { + t.Parallel() + + t.Run("empty", func(t *testing.T) { + t.Parallel() + + pkg := &MockPackage{NameValue: "", PkgPathValue: "", SyntaxValue: nil} + funcDecls := extractor.ExtractMethodsFromPackage(pkg) + require.Empty(t, funcDecls) + }) + + t.Run("single file, zero methods", func(t *testing.T) { + t.Parallel() + + pkg := &MockPackage{ + NameValue: "", + PkgPathValue: "", + SyntaxValue: []*ast.File{ + astFromString(t, s("package pkg", "type T struct{}")), + }, + } + + funcDecls := extractor.ExtractMethodsFromPackage(pkg) + require.Empty(t, funcDecls) + }) + + t.Run("single file, single method", func(t *testing.T) { + t.Parallel() + + pkg := &MockPackage{ + NameValue: "", + PkgPathValue: "", + SyntaxValue: []*ast.File{ + astFromString(t, s("package pkg", "type T struct{}", "func (t *T) Method() {}")), + }, + } + + funcDecls := extractor.ExtractMethodsFromPackage(pkg) + require.Len(t, funcDecls, 1) + assert.Equal(t, "Method", funcDecls[0].Name.Name) + }) +} + +func TestExtractMethodsFromPackageMultiple(t *testing.T) { + t.Parallel() + + t.Run("multiple files, couple of methods", func(t *testing.T) { + t.Parallel() + + pkg := &MockPackage{ + NameValue: "", + PkgPathValue: "", + SyntaxValue: []*ast.File{ + astFromString(t, s("package pkg", "type T struct{}", "func (t *T) Method1() {}")), + astFromString(t, s("package pkg", "func (t *T) Method2() {}")), + }, + } + + funcDecls := extractor.ExtractMethodsFromPackage(pkg) + require.Len(t, funcDecls, 2) + assert.Equal(t, "Method1", funcDecls[0].Name.Name) + assert.Equal(t, "Method2", funcDecls[1].Name.Name) + }) + + t.Run("function is ignored", func(t *testing.T) { + t.Parallel() + + pkg := &MockPackage{ + NameValue: "", + PkgPathValue: "", + SyntaxValue: []*ast.File{ + astFromString(t, s("package pkg", "func Method() {}")), + }, + } + + funcDecls := extractor.ExtractMethodsFromPackage(pkg) + require.Empty(t, funcDecls) + }) +} + +func TestExtractRecvTypeNameSimple(t *testing.T) { + t.Parallel() + + t.Run("method", func(t *testing.T) { + t.Parallel() + + pkg := &MockPackage{ + NameValue: "", + PkgPathValue: "", + SyntaxValue: []*ast.File{astFromString(t, + s("package pkg", "type T struct{}", "func (t T) Method() {}"), + )}, + } + + funcDecls := extractor.ExtractMethodsFromPackage(pkg) + require.Len(t, funcDecls, 1) + assert.Equal(t, "T", extractor.ExtractRecvTypeName(funcDecls[0])) + }) + + t.Run("ptr method", func(t *testing.T) { + t.Parallel() + + pkg := &MockPackage{ + NameValue: "", + PkgPathValue: "", + SyntaxValue: []*ast.File{astFromString(t, + s("package pkg", "type T struct{}", "func (t *T) Method() {}"), + )}, + } + + funcDecls := extractor.ExtractMethodsFromPackage(pkg) + require.Len(t, funcDecls, 1) + assert.Equal(t, "T", extractor.ExtractRecvTypeName(funcDecls[0])) + }) +} + +func TestExtractRecvTypeNameGenericSingle(t *testing.T) { + t.Parallel() + + t.Run("single-type generic method", func(t *testing.T) { + t.Parallel() + + pkg := &MockPackage{ + NameValue: "", + PkgPathValue: "", + SyntaxValue: []*ast.File{astFromString(t, + s("package pkg", "type T[K any] struct{}", "func (t T[K]) Method() {}"), + )}, + } + + funcDecls := extractor.ExtractMethodsFromPackage(pkg) + require.Len(t, funcDecls, 1) + assert.Equal(t, "T", extractor.ExtractRecvTypeName(funcDecls[0])) + }) + + t.Run("single-type generic method with ptr receiver", func(t *testing.T) { + t.Parallel() + + pkg := &MockPackage{ + NameValue: "", + PkgPathValue: "", + SyntaxValue: []*ast.File{astFromString(t, + s("package pkg", "type T[K any] struct{}", "func (t *T[K]) Method() {}"), + )}, + } + + funcDecls := extractor.ExtractMethodsFromPackage(pkg) + require.Len(t, funcDecls, 1) + assert.Equal(t, "T", extractor.ExtractRecvTypeName(funcDecls[0])) + }) +} + +func TestExtractRecvTypeNameGenericMulti(t *testing.T) { + t.Parallel() + + t.Run("multi-type generic method", func(t *testing.T) { + t.Parallel() + + pkg := &MockPackage{ + NameValue: "", + PkgPathValue: "", + SyntaxValue: []*ast.File{astFromString(t, + s("package pkg", "type T[K any, V any] struct{}", "func (t T[K, V]) Method() {}"), + )}, + } + + funcDecls := extractor.ExtractMethodsFromPackage(pkg) + require.Len(t, funcDecls, 1) + assert.Equal(t, "T", extractor.ExtractRecvTypeName(funcDecls[0])) + }) + + t.Run("multi-type generic method with ptr receiver", func(t *testing.T) { + t.Parallel() + + pkg := &MockPackage{ + NameValue: "", + PkgPathValue: "", + SyntaxValue: []*ast.File{astFromString(t, + s("package pkg", "type T[K any, V any] struct{}", "func (t *T[K, V]) Method() {}"), + )}, + } + funcDecls := extractor.ExtractMethodsFromPackage(pkg) + require.Len(t, funcDecls, 1) + assert.Equal(t, "T", extractor.ExtractRecvTypeName(funcDecls[0])) + }) +} diff --git a/cmd/gentypes/extractor/package.go b/cmd/gentypes/extractor/package.go new file mode 100644 index 0000000..43ba509 --- /dev/null +++ b/cmd/gentypes/extractor/package.go @@ -0,0 +1,11 @@ +package extractor + +import "go/ast" + +// Package is an interface that provides access to package data. +// It's used to abstract away the `packages.Package` type. +type Package interface { + Name() string + PkgPath() string + Syntax() []*ast.File +} diff --git a/cmd/gentypes/extractor/package_impl.go b/cmd/gentypes/extractor/package_impl.go new file mode 100644 index 0000000..f487874 --- /dev/null +++ b/cmd/gentypes/extractor/package_impl.go @@ -0,0 +1,28 @@ +package extractor + +import ( + "go/ast" + + "golang.org/x/tools/go/packages" +) + +type packageImpl struct { + pkg *packages.Package +} + +// NewPackage creates a new Package from a packages.Package. +func NewPackage(pkg *packages.Package) Package { + return &packageImpl{pkg: pkg} +} + +func (p *packageImpl) Name() string { + return p.pkg.Name +} + +func (p *packageImpl) PkgPath() string { + return p.pkg.PkgPath +} + +func (p *packageImpl) Syntax() []*ast.File { + return p.pkg.Syntax +} diff --git a/cmd/gentypes/extractor/test_helper_test.go b/cmd/gentypes/extractor/test_helper_test.go new file mode 100644 index 0000000..553cdf3 --- /dev/null +++ b/cmd/gentypes/extractor/test_helper_test.go @@ -0,0 +1,26 @@ +package extractor_test + +import ( + "go/ast" + "go/parser" + "go/token" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func s(lines ...string) string { + return strings.Join(lines, "\n") +} + +func astFromString(t *testing.T, s string) *ast.File { + t.Helper() + + fset := token.NewFileSet() + + f, err := parser.ParseFile(fset, "test.go", s, parser.AllErrors) + require.NoError(t, err) + + return f +} diff --git a/cmd/gentypes/extractor/types.go b/cmd/gentypes/extractor/types.go new file mode 100644 index 0000000..e05f5db --- /dev/null +++ b/cmd/gentypes/extractor/types.go @@ -0,0 +1,40 @@ +package extractor + +import ( + "go/ast" + "go/token" +) + +type typeSpecVisitor struct { + Types []*ast.TypeSpec +} + +func (t *typeSpecVisitor) Visit(node ast.Node) ast.Visitor { + genDecl, ok := node.(*ast.GenDecl) + if !ok || genDecl.Tok != token.TYPE { + return t + } + + for _, spec := range genDecl.Specs { + ts, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + + t.Types = append(t.Types, ts) + } + + return nil +} + +// ExtractTypeSpecsFromPackage extracts type specs from a ast tree. +func ExtractTypeSpecsFromPackage(pkg Package) []*ast.TypeSpec { + visitor := &typeSpecVisitor{ + Types: nil, + } + for _, file := range pkg.Syntax() { + ast.Walk(visitor, file) + } + + return visitor.Types +} diff --git a/cmd/gentypes/extractor/types_test.go b/cmd/gentypes/extractor/types_test.go new file mode 100644 index 0000000..4e032ba --- /dev/null +++ b/cmd/gentypes/extractor/types_test.go @@ -0,0 +1,55 @@ +package extractor_test + +import ( + "go/ast" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/tarantool/go-option/cmd/gentypes/extractor" +) + +func TestExtractTypeSpecsFromPackage(t *testing.T) { + t.Parallel() + t.Run("empty", func(t *testing.T) { + t.Parallel() + + pkg := &MockPackage{NameValue: "", PkgPathValue: "", SyntaxValue: nil} + typeSpecs := extractor.ExtractTypeSpecsFromPackage(pkg) + require.Empty(t, typeSpecs) + }) + + t.Run("single file, single type", func(t *testing.T) { + t.Parallel() + + pkg := &MockPackage{ + NameValue: "", + PkgPathValue: "", + SyntaxValue: []*ast.File{ + astFromString(t, s("package pkg", "type T struct{}")), + }, + } + + typeSpecs := extractor.ExtractTypeSpecsFromPackage(pkg) + require.Len(t, typeSpecs, 1) + require.Equal(t, "T", typeSpecs[0].Name.String()) + }) + + t.Run("multiple files, multiple types", func(t *testing.T) { + t.Parallel() + + pkg := &MockPackage{ + NameValue: "", + PkgPathValue: "", + SyntaxValue: []*ast.File{ + astFromString(t, s("package pkg", "type T struct{}")), + astFromString(t, s("package pkg", "type U[K any, V any] struct{}")), + }, + } + + typeSpecs := extractor.ExtractTypeSpecsFromPackage(pkg) + require.Len(t, typeSpecs, 2) + require.Equal(t, "T", typeSpecs[0].Name.String()) + require.Equal(t, "U", typeSpecs[1].Name.String()) + }) +} diff --git a/cmd/gentypes/generate.go b/cmd/gentypes/generate.go new file mode 100644 index 0000000..64dcbb1 --- /dev/null +++ b/cmd/gentypes/generate.go @@ -0,0 +1,2 @@ +//go:generate go run github.com/tarantool/go-option/cmd/gentypes -ext-code 1 -package test FullMsgpackExtType +package main diff --git a/cmd/gentypes/generator/extension.go b/cmd/gentypes/generator/extension.go new file mode 100644 index 0000000..7e3f7fa --- /dev/null +++ b/cmd/gentypes/generator/extension.go @@ -0,0 +1,51 @@ +// Package generator is a package that defines how code should be generated. +package generator + +import ( + "bytes" + _ "embed" + "fmt" + "strconv" + "text/template" +) + +//go:embed type_gen.go.tpl +var typeGenTemplate string + +//go:embed type_gen_test.go.tpl +var typeGenTestTemplate string + +var ( + cTypeGenTemplate *template.Template + cTypeGenTestTemplate *template.Template //nolint:unused +) + +// InitializeTemplates initializes the templates, should be called at the start of the main program loop. +func InitializeTemplates() { + cTypeGenTemplate = template.Must(template.New("type_gen.go.tpl").Parse(typeGenTemplate)) + cTypeGenTestTemplate = template.Must(template.New("type_gen_test.go.tpl").Parse(typeGenTestTemplate)) +} + +// GenerateByType generates the code for the optional type. +func GenerateByType(typeName string, code int, packageName string) ([]byte, error) { + var buf bytes.Buffer + + err := cTypeGenTemplate.Execute(&buf, struct { + Name string + Type string + ExtCode string + PackageName string + Imports []string + }{ + Name: "Optional" + typeName, + Type: typeName, + ExtCode: strconv.Itoa(code), + PackageName: packageName, + Imports: nil, + }) + if err != nil { + return nil, fmt.Errorf("failed to generateByType: %w", err) + } + + return buf.Bytes(), nil +} diff --git a/cmd/gentypes/generator/type_gen.go.tpl b/cmd/gentypes/generator/type_gen.go.tpl new file mode 100644 index 0000000..6111ff7 --- /dev/null +++ b/cmd/gentypes/generator/type_gen.go.tpl @@ -0,0 +1,246 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package {{ .PackageName }} + +import ( + {{ range $i, $import := .Imports }} + "{{ $import }}" + {{ end }} + + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// {{.Name}} represents an optional value of type {{.Type}}. +// It can either hold a valid {{.Type}} (IsSome == true) or be empty (IsZero == true). +type {{.Name}} struct { + value {{.Type}} + exists bool +} + +// Some{{.Name}} creates an optional {{.Name}} with the given {{.Type}} value. +// The returned {{.Name}} will have IsSome() == true and IsZero() == false. +func Some{{.Name}}(value {{.Type}}) {{.Name}} { + return {{.Name}}{ + value: value, + exists: true, + } +} + +// None{{.Name}} creates an empty optional {{.Name}} value. +// The returned {{.Name}} will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := None{{.Name}}() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func None{{.Name}}() {{.Name}} { + return {{.Name}}{} +} + +func (o {{.Name}}) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "{{.Name}}", + Parent: err, + } +} + +func (o {{.Name}}) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "{{.Name}}", + Parent: err, + } +} + +// IsSome returns true if the {{.Name}} contains a value. +// This indicates the value is explicitly set (not None). +func (o {{.Name}}) IsSome() bool { + return o.exists +} + +// IsZero returns true if the {{.Name}} does not contain a value. +// Equivalent to !IsSome(). Useful for consistency with types where +// zero value (e.g. 0, false, zero struct) is valid and needs to be distinguished. +func (o {{.Name}}) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o {{.Name}}) IsNil() bool { + return o.IsZero() +} + +// Get returns the stored value and a boolean flag indicating its presence. +// If the value is present, returns (value, true). +// If the value is absent, returns (zero value of {{.Type}}, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o {{.Name}}) Get() ({{.Type}}, bool) { + return o.value, o.exists +} + +// MustGet returns the stored value if it is present. +// Panics if the value is absent (i.e., IsZero() == true). +// +// Use with caution — only when you are certain the value exists. +// +// Panics with: "optional value is not set" if no value is set. +func (o {{.Name}}) MustGet() {{.Type}} { + if !o.exists { + panic("optional value is not set") + } + + return o.value +} + +// Unwrap returns the stored value regardless of presence. +// If no value is set, returns the zero value for {{.Type}}. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o {{.Name}}) Unwrap() {{.Type}} { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := None{{.Name}}() +// v := o.UnwrapOr(someDefault{{.Name}}) +func (o {{.Name}}) UnwrapOr(defaultValue {{.Type}}) {{.Type}} { + if o.exists { + return o.value + } + + return defaultValue +} + +// UnwrapOrElse returns the stored value if present. +// Otherwise, calls the provided function and returns its result. +// Useful when the default value requires computation or side effects. +// +// Example: +// +// o := None{{.Name}}() +// v := o.UnwrapOrElse(func() {{.Type}} { return computeDefault() }) +func (o {{.Name}}) UnwrapOrElse(defaultValue func() {{.Type}}) {{.Type}} { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o {{.Name}}) encodeValue(encoder *msgpack.Encoder) error { + value, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = encoder.EncodeExtHeader({{ .ExtCode }}, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + + +// EncodeMsgpack encodes the {{.Name}} value using MessagePack format. +// - If the value is present, it is encoded as {{.Type}}. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o {{.Name}}) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *{{.Name}}) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != {{ .ExtCode }}: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := o.value.UnmarshalMsgpack(a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *{{.Name}}) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a {{.Name}} value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (None{{.Name}}) +// - {{.Type}}: interpreted as a present value (Some{{.Name}}) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on {{.Type}}: exists = true, value = decoded value +func (o *{{.Name}}) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/cmd/gentypes/generator/type_gen_test.go.tpl b/cmd/gentypes/generator/type_gen_test.go.tpl new file mode 100644 index 0000000..aa29457 --- /dev/null +++ b/cmd/gentypes/generator/type_gen_test.go.tpl @@ -0,0 +1,215 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package {{ .packageName }}_test + +import ( + {{ range $i, $import := .imports }} + "{{ $import }}" + {{ end }} + + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vmihailenco/msgpack/v5" + + "github.com/tarantool/go-option" +) + +func Test{{.Name}}_IsSome(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + some{{.Name}} := option.Some{{.Name}}({{.TestingValue}}) + assert.True(t, some{{.Name}}.IsSome()) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + empty{{.Name}} := option.None{{.Name}}() + assert.False(t, empty{{.Name}}.IsSome()) + }) +} + +func Test{{.Name}}_IsZero(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + some{{.Name}} := option.Some{{.Name}}({{.TestingValue}}) + assert.False(t, some{{.Name}}.IsZero()) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + empty{{.Name}} := option.None{{.Name}}() + assert.True(t, empty{{.Name}}.IsZero()) + }) +} + +func Test{{.Name}}_IsNil(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + some{{.Name}} := option.Some{{.Name}}({{.TestingValue}}) + assert.False(t, some{{.Name}}.IsNil()) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + empty{{.Name}} := option.None{{.Name}}() + assert.True(t, empty{{.Name}}.IsNil()) + }) +} + +func Test{{.Name}}_Get(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + some{{.Name}} := option.Some{{.Name}}({{.TestingValue}}) + val, ok := some{{.Name}}.Get() + require.True(t, ok) + assert.EqualValues(t, {{.TestingValue}}, val) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + empty{{.Name}} := option.None{{.Name}}() + _, ok := empty{{.Name}}.Get() + require.False(t, ok) + }) +} + +func Test{{.Name}}_MustGet(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + some{{.Name}} := option.Some{{.Name}}({{.TestingValue}}) + assert.EqualValues(t, {{.TestingValue}}, some{{.Name}}.MustGet()) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + empty{{.Name}} := option.None{{.Name}}() + assert.Panics(t, func() { + empty{{.Name}}.MustGet() + }) + }) +} + +func Test{{.Name}}_Unwrap(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + some{{.Name}} := option.Some{{.Name}}({{.TestingValue}}) + assert.EqualValues(t, {{.TestingValue}}, some{{.Name}}.Unwrap()) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + empty{{.Name}} := option.None{{.Name}}() + assert.NotPanics(t, func() { + empty{{.Name}}.Unwrap() + }) + }) +} + +func Test{{.Name}}_UnwrapOr(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + some{{.Name}} := option.Some{{.Name}}({{.TestingValue}}) + assert.EqualValues(t, {{.TestingValue}}, some{{.Name}}.UnwrapOr({{.UnexpectedTestingValue}})) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + empty{{.Name}} := option.None{{.Name}}() + assert.EqualValues(t, {{.UnexpectedTestingValue}}, empty{{.Name}}.UnwrapOr({{.UnexpectedTestingValue}})) + }) +} + +func Test{{.Name}}_UnwrapOrElse(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + some{{.Name}} := option.Some{{.Name}}({{.TestingValue}}) + assert.EqualValues(t, {{.TestingValue}}, some{{.Name}}.UnwrapOrElse(func() {{.Type}} { + return {{.UnexpectedTestingValue}} + })) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + empty{{.Name}} := option.None{{.Name}}() + assert.EqualValues(t, {{.UnexpectedTestingValue}}, empty{{.Name}}.UnwrapOrElse(func() {{.Type}} { + return {{.UnexpectedTestingValue}} + })) + }) +} + +func Test{{.Name}}_EncodeDecodeMsgpack(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + some{{.Name}} := option.Some{{.Name}}({{.TestingValue}}) + err := some{{.Name}}.EncodeMsgpack(enc) + require.NoError(t, err) + + var unmarshaled option.{{.Name}} + err = unmarshaled.DecodeMsgpack(dec) + require.NoError(t, err) + assert.True(t, unmarshaled.IsSome()) + assert.EqualValues(t, {{.TestingValue}}, unmarshaled.Unwrap()) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + + enc := msgpack.NewEncoder(&buf) + dec := msgpack.NewDecoder(&buf) + + empty{{.Name}} := option.None{{.Name}}() + err := empty{{.Name}}.EncodeMsgpack(enc) + require.NoError(t, err) + + var unmarshaled option.{{.Name}} + err = unmarshaled.DecodeMsgpack(dec) + + require.NoError(t, err) + assert.False(t, unmarshaled.IsSome()) + }) +} \ No newline at end of file diff --git a/cmd/gentypes/main.go b/cmd/gentypes/main.go new file mode 100644 index 0000000..67e5a4c --- /dev/null +++ b/cmd/gentypes/main.go @@ -0,0 +1,203 @@ +// Package main is a binary, that generates optional types for types with support for MessagePack Extensions +// fast encoding/decoding. +package main + +import ( + "bytes" + "context" + "flag" + "fmt" + "go/format" + "math" + "os" + "path/filepath" + "strings" + + "golang.org/x/tools/go/packages" + + "github.com/tarantool/go-option/cmd/gentypes/extractor" + "github.com/tarantool/go-option/cmd/gentypes/generator" +) + +const ( + defaultGoPermissions = 0644 +) + +var ( + packagePath string + extCode int + verbose bool +) + +func logfuncf(format string, args ...interface{}) { + if verbose { + fmt.Printf("> "+format+"\n", args...) + } +} + +func readGoFiles(ctx context.Context, folder string) ([]*packages.Package, error) { + return packages.Load(&packages.Config{ //nolint:wrapcheck + Mode: packages.LoadAllSyntax, + Context: ctx, + Logf: logfuncf, + Dir: folder, + + Env: nil, + BuildFlags: nil, + Fset: nil, + ParseFile: nil, + Tests: false, + Overlay: nil, + }) +} + +func extractFirstPackageFromList(packageList []*packages.Package, name string) *packages.Package { + if len(packageList) == 0 { + panic("no packages found") + } + + if name == "" { + for _, pkg := range packageList { + if !strings.HasSuffix(pkg.Name, "_test") { + return pkg + } + } + + return packageList[0] // If no non-test packages found, return the first one. + } + + for _, pkg := range packageList { + if pkg.Name == name { + return pkg + } + } + + fmt.Println("failed to find package with name:", name) + fmt.Println("available packages:") + + for _, pkg := range packageList { + fmt.Println(" ", pkg.Name) + } + + os.Exit(1) + + return nil // Unreachable. +} + +const ( + undefinedExtCode = math.MinInt8 - 1 +) + +func checkMsgpackExtCode(code int) bool { + return code >= math.MinInt8 && code <= math.MaxInt8 +} + +func printFile(prefix string, data []byte) { + for lineNo, line := range bytes.Split(data, []byte("\n")) { + fmt.Printf("%03d%s%s\n", lineNo, prefix, string(line)) + } +} + +func main() { //nolint:funlen + generator.InitializeTemplates() + + ctx := context.Background() + + flag.StringVar(&packagePath, "package", "./", "input and output path") + flag.IntVar(&extCode, "ext-code", undefinedExtCode, "extension code") + flag.BoolVar(&verbose, "verbose", false, "print verbose output") + + flag.Parse() + + switch { + case extCode == undefinedExtCode: + fmt.Println("extension code is not set") + + flag.PrintDefaults() + os.Exit(1) + case !checkMsgpackExtCode(extCode): + fmt.Println("invalid extension code:", extCode) + fmt.Println("extension code must be in range [-128, 127]") + + flag.PrintDefaults() + os.Exit(1) + } + + packageList, err := readGoFiles(ctx, packagePath) + switch { + case err != nil: + fmt.Println("failed to parse packages:") + fmt.Println(" ", err) + os.Exit(1) + case packages.PrintErrors(packageList) > 0: + os.Exit(1) + case len(packageList) == 0: + fmt.Println("no packages found") + os.Exit(1) + } + + pkg := extractFirstPackageFromList(packageList, "") + + analyzer, err := extractor.NewAnalyzerFromPackage(extractor.NewPackage(pkg)) + if err != nil { + fmt.Println("failed to extract types and methods:") + fmt.Println(" ", err) + + os.Exit(1) + } + + args := flag.Args() // Args contains names of struct to generate optional types. + switch { + case len(args) == 0: + fmt.Println("no struct name provided") + + flag.PrintDefaults() + os.Exit(1) + case len(args) > 1: + fmt.Println("too many arguments") + + flag.PrintDefaults() + os.Exit(1) + } + + typeName := args[0] + + // Check for existence of all types that we want to generate. + typeSpecDef, ok := analyzer.TypeSpecEntryByName(typeName) + if !ok { + fmt.Println("failed to find struct:", typeName) + os.Exit(1) + } + + fmt.Println("generating optional for:", typeName) + + if !typeSpecDef.HasMethod("MarshalMsgpack") || !typeSpecDef.HasMethod("UnmarshalMsgpack") { + fmt.Println("failed to find MarshalMsgpack or UnmarshalMsgpack method for struct:", typeName) + os.Exit(1) + } + + generatedGoSources, err := generator.GenerateByType(typeName, extCode, analyzer.PackageName()) + if err != nil { + fmt.Println("failed to generate optional types:") + fmt.Println(" ", err) + os.Exit(1) + } + + formattedGoSource, err := format.Source(generatedGoSources) + if err != nil { + fmt.Println("failed to format generated code: ", err) + printFile("> ", generatedGoSources) + os.Exit(1) + } + + err = os.WriteFile( + filepath.Join(packagePath, strings.ToLower(typeName)+"_gen.go"), + formattedGoSource, + defaultGoPermissions, + ) + if err != nil { + fmt.Println("failed to write generated code:") + fmt.Println(" ", err) + os.Exit(1) + } +} diff --git a/cmd/gentypes/test/fullmsgpackexttype.go b/cmd/gentypes/test/fullmsgpackexttype.go new file mode 100644 index 0000000..1c96b63 --- /dev/null +++ b/cmd/gentypes/test/fullmsgpackexttype.go @@ -0,0 +1,59 @@ +// Package test contains testing types and scenarios. +package test + +import ( + "bytes" + + "github.com/vmihailenco/msgpack/v5" +) + +// FullMsgpackExtType is a test type with both MarshalMsgpack and UnmarshalMsgpack methods. +type FullMsgpackExtType struct { + A int + B string +} + +// NewEmptyFullMsgpackExtType is an empty constructor for FullMsgpackExtType. +func NewEmptyFullMsgpackExtType() (out FullMsgpackExtType) { //nolint:nonamedreturns + return +} + +// MarshalMsgpack . +func (t *FullMsgpackExtType) MarshalMsgpack() ([]byte, error) { + var buf bytes.Buffer + + enc := msgpack.NewEncoder(&buf) + + err := enc.EncodeInt(int64(t.A)) + if err != nil { + return nil, err //nolint:wrapcheck + } + + err = enc.EncodeString(t.B) + if err != nil { + return nil, err //nolint:wrapcheck + } + + return buf.Bytes(), nil +} + +// UnmarshalMsgpack . +func (t *FullMsgpackExtType) UnmarshalMsgpack(in []byte) error { + dec := msgpack.NewDecoder(bytes.NewReader(in)) + + a, err := dec.DecodeInt() + if err != nil { + return err //nolint:wrapcheck + } + + t.A = a + + b, err := dec.DecodeString() + if err != nil { + return err //nolint:wrapcheck + } + + t.B = b + + return nil +} diff --git a/cmd/gentypes/test/fullmsgpackexttype_gen.go b/cmd/gentypes/test/fullmsgpackexttype_gen.go new file mode 100644 index 0000000..a65c9a2 --- /dev/null +++ b/cmd/gentypes/test/fullmsgpackexttype_gen.go @@ -0,0 +1,241 @@ +// Code generated by github.com/tarantool/go-option; DO NOT EDIT. + +package test + +import ( + "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" + + "github.com/tarantool/go-option" +) + +// OptionalFullMsgpackExtType represents an optional value of type FullMsgpackExtType. +// It can either hold a valid FullMsgpackExtType (IsSome == true) or be empty (IsZero == true). +type OptionalFullMsgpackExtType struct { + value FullMsgpackExtType + exists bool +} + +// SomeOptionalFullMsgpackExtType creates an optional OptionalFullMsgpackExtType with the given FullMsgpackExtType value. +// The returned OptionalFullMsgpackExtType will have IsSome() == true and IsZero() == false. +func SomeOptionalFullMsgpackExtType(value FullMsgpackExtType) OptionalFullMsgpackExtType { + return OptionalFullMsgpackExtType{ + value: value, + exists: true, + } +} + +// NoneOptionalFullMsgpackExtType creates an empty optional OptionalFullMsgpackExtType value. +// The returned OptionalFullMsgpackExtType will have IsSome() == false and IsZero() == true. +// +// Example: +// +// o := NoneOptionalFullMsgpackExtType() +// if o.IsZero() { +// fmt.Println("value is absent") +// } +func NoneOptionalFullMsgpackExtType() OptionalFullMsgpackExtType { + return OptionalFullMsgpackExtType{} +} + +func (o OptionalFullMsgpackExtType) newEncodeError(err error) error { + if err == nil { + return nil + } + return &option.EncodeError{ + Type: "OptionalFullMsgpackExtType", + Parent: err, + } +} + +func (o OptionalFullMsgpackExtType) newDecodeError(err error) error { + if err == nil { + return nil + } + + return &option.DecodeError{ + Type: "OptionalFullMsgpackExtType", + Parent: err, + } +} + +// IsSome returns true if the OptionalFullMsgpackExtType contains a value. +// This indicates the value is explicitly set (not None). +func (o OptionalFullMsgpackExtType) IsSome() bool { + return o.exists +} + +// IsZero returns true if the OptionalFullMsgpackExtType does not contain a value. +// Equivalent to !IsSome(). Useful for consistency with types where +// zero value (e.g. 0, false, zero struct) is valid and needs to be distinguished. +func (o OptionalFullMsgpackExtType) IsZero() bool { + return !o.exists +} + +// IsNil is an alias for IsZero. +// +// This method is provided for compatibility with the msgpack Encoder interface. +func (o OptionalFullMsgpackExtType) IsNil() bool { + return o.IsZero() +} + +// Get returns the stored value and a boolean flag indicating its presence. +// If the value is present, returns (value, true). +// If the value is absent, returns (zero value of FullMsgpackExtType, false). +// +// Recommended usage: +// +// if value, ok := o.Get(); ok { +// // use value +// } +func (o OptionalFullMsgpackExtType) Get() (FullMsgpackExtType, bool) { + return o.value, o.exists +} + +// MustGet returns the stored value if it is present. +// Panics if the value is absent (i.e., IsZero() == true). +// +// Use with caution — only when you are certain the value exists. +// +// Panics with: "optional value is not set" if no value is set. +func (o OptionalFullMsgpackExtType) MustGet() FullMsgpackExtType { + if !o.exists { + panic("optional value is not set") + } + + return o.value +} + +// Unwrap returns the stored value regardless of presence. +// If no value is set, returns the zero value for FullMsgpackExtType. +// +// Warning: Does not check presence. Use IsSome() before calling if you need +// to distinguish between absent value and explicit zero value. +func (o OptionalFullMsgpackExtType) Unwrap() FullMsgpackExtType { + return o.value +} + +// UnwrapOr returns the stored value if present. +// Otherwise, returns the provided default value. +// +// Example: +// +// o := NoneOptionalFullMsgpackExtType() +// v := o.UnwrapOr(someDefaultOptionalFullMsgpackExtType) +func (o OptionalFullMsgpackExtType) UnwrapOr(defaultValue FullMsgpackExtType) FullMsgpackExtType { + if o.exists { + return o.value + } + + return defaultValue +} + +// UnwrapOrElse returns the stored value if present. +// Otherwise, calls the provided function and returns its result. +// Useful when the default value requires computation or side effects. +// +// Example: +// +// o := NoneOptionalFullMsgpackExtType() +// v := o.UnwrapOrElse(func() FullMsgpackExtType { return computeDefault() }) +func (o OptionalFullMsgpackExtType) UnwrapOrElse(defaultValue func() FullMsgpackExtType) FullMsgpackExtType { + if o.exists { + return o.value + } + + return defaultValue() +} + +func (o OptionalFullMsgpackExtType) encodeValue(encoder *msgpack.Encoder) error { + value, err := o.value.MarshalMsgpack() + if err != nil { + return err + } + + err = encoder.EncodeExtHeader(1, len(value)) + if err != nil { + return err + } + + _, err = encoder.Writer().Write(value) + if err != nil { + return err + } + + return nil +} + +// EncodeMsgpack encodes the OptionalFullMsgpackExtType value using MessagePack format. +// - If the value is present, it is encoded as FullMsgpackExtType. +// - If the value is absent (None), it is encoded as nil. +// +// Returns an error if encoding fails. +func (o OptionalFullMsgpackExtType) EncodeMsgpack(encoder *msgpack.Encoder) error { + if o.exists { + return o.newEncodeError(o.encodeValue(encoder)) + } + + return o.newEncodeError(encoder.EncodeNil()) +} + +func (o *OptionalFullMsgpackExtType) decodeValue(decoder *msgpack.Decoder) error { + tp, length, err := decoder.DecodeExtHeader() + switch { + case err != nil: + return o.newDecodeError(err) + case tp != 1: + return o.newDecodeError(fmt.Errorf("invalid extension code: %d", tp)) + } + + a := make([]byte, length) + if err := decoder.ReadFull(a); err != nil { + return o.newDecodeError(err) + } + + if err := o.value.UnmarshalMsgpack(a); err != nil { + return o.newDecodeError(err) + } + + o.exists = true + return nil +} + +func (o *OptionalFullMsgpackExtType) checkCode(code byte) bool { + return msgpcode.IsExt(code) +} + +// DecodeMsgpack decodes a OptionalFullMsgpackExtType value from MessagePack format. +// Supports two input types: +// - nil: interpreted as no value (NoneOptionalFullMsgpackExtType) +// - FullMsgpackExtType: interpreted as a present value (SomeOptionalFullMsgpackExtType) +// +// Returns an error if the input type is unsupported or decoding fails. +// +// After successful decoding: +// - on nil: exists = false, value = default zero value +// - on FullMsgpackExtType: exists = true, value = decoded value +func (o *OptionalFullMsgpackExtType) DecodeMsgpack(decoder *msgpack.Decoder) error { + code, err := decoder.PeekCode() + if err != nil { + return o.newDecodeError(err) + } + + switch { + case code == msgpcode.Nil: + o.exists = false + + return o.newDecodeError(decoder.Skip()) + case o.checkCode(code): + err := o.decodeValue(decoder) + if err != nil { + return o.newDecodeError(err) + } + o.exists = true + + return err + default: + return o.newDecodeError(fmt.Errorf("unexpected code: %d", code)) + } +} diff --git a/cmd/gentypes/test/fullmsgpackexttype_test.go b/cmd/gentypes/test/fullmsgpackexttype_test.go new file mode 100644 index 0000000..b3a57d3 --- /dev/null +++ b/cmd/gentypes/test/fullmsgpackexttype_test.go @@ -0,0 +1,251 @@ +package test_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vmihailenco/msgpack/v5" + + td "github.com/tarantool/go-option/cmd/gentypes/test" +) + +func TestOptionalMsgpackExtType_RoundtripLL(t *testing.T) { + t.Parallel() + + input := td.FullMsgpackExtType{ + A: 412, + B: "bababa", + } + + opt := td.SomeOptionalFullMsgpackExtType(input) + + b := bytes.Buffer{} + enc := msgpack.NewEncoder(&b) + dec := msgpack.NewDecoder(&b) + + require.NoError(t, opt.EncodeMsgpack(enc)) + + opt2 := td.NoneOptionalFullMsgpackExtType() + require.NoError(t, opt2.DecodeMsgpack(dec)) + + assert.Equal(t, opt, opt2) + assert.Equal(t, input, opt2.Unwrap()) +} + +func TestOptionalMsgpackExtType_RoundtripHL(t *testing.T) { + t.Parallel() + + input := td.FullMsgpackExtType{ + A: 412, + B: "bababa", + } + + opt := td.SomeOptionalFullMsgpackExtType(input) + + b := bytes.Buffer{} + enc := msgpack.NewEncoder(&b) + dec := msgpack.NewDecoder(&b) + + require.NoError(t, enc.Encode(opt)) + + opt2 := td.NoneOptionalFullMsgpackExtType() + require.NoError(t, dec.Decode(&opt2)) + + assert.Equal(t, opt, opt2) + assert.Equal(t, input, opt2.Unwrap()) +} + +func TestOptionalFullMsgpackExtType_IsSome(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + input := td.FullMsgpackExtType{ + A: 412, + B: "bababa", + } + + opt := td.SomeOptionalFullMsgpackExtType(input) + + assert.True(t, opt.IsSome()) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + opt := td.NoneOptionalFullMsgpackExtType() + + assert.False(t, opt.IsSome()) + }) +} + +func TestOptionalFullMsgpackExtType_IsZero(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + input := td.FullMsgpackExtType{ + A: 412, + B: "bababa", + } + + opt := td.SomeOptionalFullMsgpackExtType(input) + + assert.False(t, opt.IsZero()) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + opt := td.NoneOptionalFullMsgpackExtType() + + assert.True(t, opt.IsZero()) + }) +} + +func TestOptionalFullMsgpackExtType_Get(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + input := td.FullMsgpackExtType{ + A: 412, + B: "bababa", + } + + opt := td.SomeOptionalFullMsgpackExtType(input) + + val, ok := opt.Get() + require.True(t, ok) + assert.Equal(t, input, val) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + opt := td.NoneOptionalFullMsgpackExtType() + val, ok := opt.Get() + require.False(t, ok) + assert.Equal(t, td.NewEmptyFullMsgpackExtType(), val) + }) +} + +func TestOptionalFullMsgpackExtType_MustGet(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + input := td.FullMsgpackExtType{ + A: 412, + B: "bababa", + } + + opt := td.SomeOptionalFullMsgpackExtType(input) + + var val td.FullMsgpackExtType + + require.NotPanics(t, func() { + val = opt.MustGet() + }) + assert.Equal(t, input, val) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + opt := td.NoneOptionalFullMsgpackExtType() + + require.Panics(t, func() { opt.MustGet() }) + }) +} + +func TestOptionalFullMsgpackExtType_Unwrap(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + input := td.FullMsgpackExtType{ + A: 412, + B: "bababa", + } + + opt := td.SomeOptionalFullMsgpackExtType(input) + + assert.Equal(t, input, opt.Unwrap()) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + opt := td.NoneOptionalFullMsgpackExtType() + assert.Equal(t, td.NewEmptyFullMsgpackExtType(), opt.Unwrap()) + }) +} + +func TestOptionalFullMsgpackExtType_UnwrapOr(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + input := td.FullMsgpackExtType{ + A: 412, + B: "bababa", + } + + opt := td.SomeOptionalFullMsgpackExtType(input) + + assert.Equal(t, input, opt.UnwrapOr(td.NewEmptyFullMsgpackExtType())) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + alt := td.FullMsgpackExtType{ + A: 1, + B: "b", + } + + opt := td.NoneOptionalFullMsgpackExtType() + assert.Equal(t, alt, opt.UnwrapOr(alt)) + }) +} + +func TestOptionalFullMsgpackExtType_UnwrapOrElse(t *testing.T) { + t.Parallel() + + t.Run("some", func(t *testing.T) { + t.Parallel() + + input := td.FullMsgpackExtType{ + A: 412, + B: "bababa", + } + + opt := td.SomeOptionalFullMsgpackExtType(input) + + assert.Equal(t, input, opt.UnwrapOrElse(td.NewEmptyFullMsgpackExtType)) + }) + + t.Run("none", func(t *testing.T) { + t.Parallel() + + alt := td.FullMsgpackExtType{ + A: 1, + B: "b", + } + + opt := td.NoneOptionalFullMsgpackExtType() + + assert.Equal(t, alt, opt.UnwrapOrElse(func() td.FullMsgpackExtType { + return alt + })) + }) +} diff --git a/go.mod b/go.mod index a343002..3dcb760 100644 --- a/go.mod +++ b/go.mod @@ -2,19 +2,21 @@ module github.com/tarantool/go-option go 1.23.0 -toolchain go1.24.4 - require ( - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.1 github.com/vmihailenco/msgpack/v5 v5.4.1 golang.org/x/text v0.28.0 + golang.org/x/tools v0.36.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/sync v0.16.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ca5934d..4c300ea 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -11,16 +13,23 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=