Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"
Expand Down
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TypeName> 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
Expand Down
90 changes: 90 additions & 0 deletions cmd/gentypes/extractor/analyzer.go
Original file line number Diff line number Diff line change
@@ -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
}
96 changes: 96 additions & 0 deletions cmd/gentypes/extractor/analyzer_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
61 changes: 61 additions & 0 deletions cmd/gentypes/extractor/methods.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
Loading