Skip to content

Commit 8483a46

Browse files
authored
Add BSON, SQL, and YAML support with conditional generation (#16)
* feat(generator): add BSON, SQL, and YAML support with conditional generation Implement conditional code generation for database and serialization support: - Add -sql flag for database/sql driver.Valuer and sql.Scanner interfaces - Add -bson flag for MongoDB BSON marshaling/unmarshaling support - Add -yaml flag for gopkg.in/yaml.v3 support - All features are opt-in via flags to avoid forcing dependencies Key improvements: - BSON values stored as strings in MongoDB (fixes empty document issue) - Smart SQL NULL handling using zero values when available - Comprehensive integration tests with real MongoDB and SQLite - Runtime integration test that builds binary and verifies generated code - Test coverage for all marshal/unmarshal operations The generator now supports multiple database and serialization formats while maintaining zero runtime dependencies when features aren't used. * fix: update example tests for Values variable and add SQL flag - Fix StatusValues and JobStatusValues references (now variables, not functions) - Add -sql flag to go:generate directives since SQL is no longer default - Update expected output in example tests to match actual enum order - These tests were broken since the August 11 major refactor that changed Values from function to variable * test: skip runtime integration test in CI to avoid timeout The test downloads dependencies and runs go mod tidy which times out in GitHub Actions at 60 seconds. This test is more suitable for local development where network access is faster. * fix: add support for negative enum values Copilot review identified that negative values like -1 were being incorrectly parsed as 0. Added UnaryExpr handling in processExplicitValue to properly support negative number literals. Also fixed test expectation for SQL NULL handling with Priority enum where PriorityLow (0) is the zero value, not PriorityNone (-1). * Add comment explaining error fallthrough in negative number handling - Added explanatory comment for error handling pattern in UnaryExpr case - Added comprehensive TestNegativeEnumValues with subtests - Improved test coverage to 99.6% - Addresses Copilot review feedback about duplicated error-ignoring pattern
1 parent 1834756 commit 8483a46

22 files changed

Lines changed: 2077 additions & 112 deletions

CLAUDE.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
`enum` is a Go code generator that creates type-safe, marshalable enum implementations from simple type definitions. It generates idiomatic Go code with zero runtime dependencies and supports JSON, SQL, MongoDB BSON, and YAML marshaling through optional flags.
8+
9+
## Commands
10+
11+
### Build and Test
12+
```bash
13+
# Run all tests (excludes examples)
14+
go test ./...
15+
16+
# Run tests with race detection and coverage
17+
go test -race -cover ./...
18+
19+
# Run a specific test
20+
go test -run TestRuntimeIntegration ./internal/generator
21+
22+
# Run integration tests (includes MongoDB container tests)
23+
go test ./internal/generator -v -run TestRuntimeIntegration
24+
25+
# Build the enum generator
26+
go build
27+
28+
# Install globally
29+
go install github.com/go-pkgz/enum@latest
30+
```
31+
32+
### Linting and Formatting
33+
```bash
34+
# Run linter (golangci-lint v2.0.2)
35+
golangci-lint run
36+
37+
# Format code
38+
gofmt -s -w .
39+
goimports -w .
40+
```
41+
42+
### Generate Enums
43+
```bash
44+
# Generate using go:generate directives
45+
go generate ./...
46+
47+
# Direct invocation examples
48+
go run github.com/go-pkgz/enum@latest -type status -lower
49+
go run github.com/go-pkgz/enum@latest -type status -lower -sql -bson -yaml
50+
```
51+
52+
## Architecture
53+
54+
### Core Components
55+
56+
1. **main.go** - CLI entry point that parses flags and invokes the generator
57+
2. **internal/generator/generator.go** - Core generator logic:
58+
- Parses Go AST to find enum constants
59+
- Evaluates constant values including iota and binary expressions
60+
- Generates code from template with conditional blocks for features
61+
3. **internal/generator/enum.go.tmpl** - Go template for generated code with conditional sections for SQL/BSON/YAML
62+
63+
### Key Design Decisions
64+
65+
1. **Type name must be lowercase (private)** - Enforced to prevent confusion with public types
66+
2. **Constants must be prefixed with type name** - Ensures clear namespacing (e.g., `statusActive` for type `status`)
67+
3. **Generated public types are capitalized** - Follows Go conventions (private `status` → public `Status`)
68+
4. **Zero runtime dependencies** - Generated code uses only stdlib unless optional features are enabled
69+
5. **Conditional feature generation** - SQL/BSON/YAML code only generated when flags are set to avoid forcing dependencies
70+
71+
### Integration Support
72+
73+
- **JSON**: Via `encoding.TextMarshaler`/`TextUnmarshaler` (always generated)
74+
- **SQL** (`-sql` flag): Implements `database/sql/driver.Valuer` and `sql.Scanner`
75+
- Smart NULL handling: uses zero value if available, errors otherwise
76+
- **BSON** (`-bson` flag): Implements `MarshalBSONValue`/`UnmarshalBSONValue` for MongoDB
77+
- Stores as string values, not documents
78+
- **YAML** (`-yaml` flag): Implements `yaml.Marshaler`/`yaml.Unmarshaler` for gopkg.in/yaml.v3
79+
80+
### Testing Strategy
81+
82+
1. **Unit tests** (`generator_test.go`): Test parsing, generation, edge cases
83+
2. **Integration tests** (`integration_test.go`):
84+
- `TestRuntimeIntegration`: Full pipeline test that builds binary, generates code, runs tests with real databases
85+
- Uses testcontainers for MongoDB integration testing
86+
- SQLite for SQL testing (in-memory)
87+
3. **Test data** in `testdata/integration/`:
88+
- `enum_test.go`: Real database tests run by runtime integration
89+
- `status.go`, `priority.go`: Sample enums for testing
90+
91+
### Parsing and Generation Flow
92+
93+
1. Parse Go source files to find type definition
94+
2. Extract constants prefixed with type name
95+
3. Evaluate constant values:
96+
- Handle iota increments
97+
- Evaluate binary expressions (e.g., `iota + 1`, `1 << iota`)
98+
- Support explicit values
99+
4. Generate code with:
100+
- String() method
101+
- Parse/Must functions
102+
- Marshal/Unmarshal methods based on flags
103+
- Iterator for Go 1.23+ range-over-func
104+
- Optional GetByID function (requires unique values)
105+
106+
## Important Constraints
107+
108+
1. **Enum type must be lowercase** - Generator validates and rejects uppercase type names
109+
2. **Constants must start with type name** - e.g., for type `status`, constants must be `statusXxx`
110+
3. **Unique IDs required for -getter flag** - Generator fails if duplicate values exist when getter is requested
111+
4. **Declaration order preserved** - Enums maintain source order, not alphabetical
112+
5. **Type fidelity** - Generated code preserves underlying type (uint8, int32, etc.)
113+
114+
## CI/CD
115+
116+
GitHub Actions workflow (`.github/workflows/ci.yml`):
117+
- Runs on all pushes and PRs
118+
- Tests with Go 1.24
119+
- Runs tests with race detection and coverage
120+
- Runs golangci-lint
121+
- Submits coverage to Coveralls
122+
- Tests examples separately
123+
124+
## Integration Testing Architecture
125+
126+
### Test Structure
127+
The integration tests use a unique two-stage approach:
128+
129+
1. **`TestRuntimeIntegration`** in `integration_test.go`:
130+
- Builds the enum binary from source
131+
- Creates a temporary package with enum definitions
132+
- Runs the built binary to generate enum code
133+
- Creates a temporary `go.mod` with test dependencies
134+
- Copies test file from `testdata/integration/enum_test.go`
135+
- Runs the generated tests in isolation
136+
137+
2. **Actual database tests** in `testdata/integration/enum_test.go`:
138+
- `TestGeneratedEnumWithMongoDB`: Uses `github.com/go-pkgz/testutils` to spin up MongoDB 7 container
139+
- `TestGeneratedEnumWithSQL`: Uses in-memory SQLite for SQL testing
140+
- Tests real marshal/unmarshal operations, not just code generation
141+
142+
### Key Integration Test Patterns
143+
144+
1. **Test files in testdata are NOT compiled by Go** - They're copied and run in temp directory
145+
2. **MongoDB container via testutils**:
146+
```go
147+
mongoContainer := containers.NewMongoTestContainer(ctx, t, 7)
148+
defer mongoContainer.Close(ctx)
149+
coll := mongoContainer.Collection("test_db")
150+
```
151+
3. **Verifies storage format** - Confirms enums stored as strings in MongoDB, not empty documents
152+
4. **Full round-trip testing** - Writes enum to database, reads back, verifies correct unmarshaling
153+
154+
### Running Integration Tests
155+
```bash
156+
# Run full integration test (builds binary, generates code, tests with real databases)
157+
go test ./internal/generator -v -run TestRuntimeIntegration
158+
159+
# Skip integration tests in short mode
160+
go test -short ./...
161+
162+
# Clean test cache before running to ensure fresh MongoDB container
163+
go clean -testcache && go test ./internal/generator -v -run TestRuntimeIntegration
164+
```
165+
166+
### Test Dependencies
167+
Integration tests require:
168+
- Docker for MongoDB containers (via testcontainers)
169+
- Network access for go mod tidy in temp package
170+
- Write access to temp directory for generated code
171+
172+
### Important Testing Details
173+
- `TestMongoDBIntegration` in main `integration_test.go` only verifies code generation, not actual MongoDB
174+
- Real MongoDB testing happens via `TestRuntimeIntegration``TestGeneratedEnumWithMongoDB`
175+
- Tests verify both success and error paths (NULL handling, invalid values)
176+
- Uses `require` for critical assertions, `assert` for non-critical ones

README.md

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# enum [![Build Status](https://github.com/go-pkgz/enum/workflows/build/badge.svg)](https://github.com/go-pkgz/enum/actions) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/enum/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/enum?branch=master) [![godoc](https://godoc.org/github.com/go-pkgz/enum?status.svg)](https://godoc.org/github.com/go-pkgz/enum)
22

33

4-
`enum` is a Go package that provides a code generator for type-safe, json/bson/text-marshalable enumerations. It creates idiomatic Go code from simple type definitions, supporting both case-sensitive and case-insensitive string representations.
4+
`enum` is a Go package that provides a code generator for type-safe, json/text-marshalable enumerations. Optional flags add SQL, BSON (MongoDB), and YAML support. It creates idiomatic Go code from simple type definitions, supporting both case-sensitive and case-insensitive string representations.
55

66
## Features
77

88
- Type-safe enum implementations
9-
- JSON, BSON, SQL and text marshaling/unmarshaling support
9+
- Text marshaling/unmarshaling (JSON works via TextMarshaler)
10+
- Optional SQL, BSON (MongoDB), and YAML support via flags
1011
- Case-sensitive or case-insensitive string representations
1112
- Panic-free parsing with error handling
1213
- Must-style parsing variants for convenience
@@ -97,12 +98,17 @@ const (
9798
go generate ./...
9899
```
99100

101+
By default the generated type supports `encoding.TextMarshaler`/`Unmarshaler` (used by `encoding/json`). To include other integrations, enable flags as needed (see below).
102+
100103
### Generator Options
101104

102105
- `-type` (required): the name of the type to generate enum for (must be lowercase/private)
103106
- `-path`: output directory path (default: same as source)
104-
- `-lower`: use lowercase for string representations when marshaling/unmarshaling (affects only the output strings, not the naming pattern)
107+
- `-lower`: use lowercase for string representations when marshaling/unmarshaling
105108
- `-getter`: enables the generation of an additional function, `Get{{Type}}ByID`, which attempts to find the corresponding enum element by its underlying integer ID. The `-getter` flag requires enum elements to have unique IDs to prevent undefined behavior.
109+
- `-sql` (default: off): add SQL support via `database/sql/driver.Valuer` and `sql.Scanner`
110+
- `-bson` (default: off): add MongoDB BSON support via `MarshalBSONValue`/`UnmarshalBSONValue`
111+
- `-yaml` (default: off): add YAML support via `gopkg.in/yaml.v3` `Marshaler`/`Unmarshaler`
106112
- `-version`: print version information
107113
- `-help`: show usage information
108114

@@ -112,7 +118,7 @@ The generator creates a new type with the following features:
112118

113119
- String representation (implements `fmt.Stringer`)
114120
- Text marshaling (implements `encoding.TextMarshaler` and `encoding.TextUnmarshaler`)
115-
- SQL support (implements `database/sql/driver.Valuer` and `sql.Scanner`)
121+
- SQL support when `-sql` is set (implements `database/sql/driver.Valuer` and `sql.Scanner`)
116122
- Parse function with error handling (`ParseStatus`) - uses efficient O(1) map lookup
117123
- Must-style parse function that panics on error (`MustStatus`)
118124
- All possible values as package variable (`StatusValues`) - preserves declaration order
@@ -123,6 +129,38 @@ The generator creates a new type with the following features:
123129

124130
Additionally, if the `-getter` flag is set, a getter function (`GetStatusByID`) will be generated. This function allows retrieving an enum element using its raw integer ID.
125131

132+
### JSON, BSON, YAML
133+
134+
- JSON: works out of the box through `encoding.TextMarshaler`/`Unmarshaler`.
135+
- BSON (MongoDB): enable `-bson` to generate `MarshalBSONValue`/`UnmarshalBSONValue`; values are stored as strings.
136+
- YAML: enable `-yaml` to generate `MarshalYAML`/`UnmarshalYAML`; values are encoded as strings.
137+
138+
Example (MongoDB using `-bson`):
139+
140+
```go
141+
//go:generate go run github.com/go-pkgz/enum@latest -type status -bson -lower
142+
143+
type status uint8
144+
const (
145+
statusUnknown status = iota
146+
statusActive
147+
statusInactive
148+
)
149+
150+
// Using mongo-go-driver
151+
type User struct {
152+
ID primitive.ObjectID `bson:"_id,omitempty"`
153+
Status Status `bson:"status"`
154+
}
155+
156+
// insert and read
157+
u := User{Status: StatusActive}
158+
_, _ = coll.InsertOne(ctx, u) // stores { status: "active" }
159+
160+
var out User
161+
_ = coll.FindOne(ctx, bson.M{"status": "active"}).Decode(&out) // decodes via UnmarshalBSONValue
162+
```
163+
126164
### Case Sensitivity
127165

128166
By default, the generator creates case-sensitive string representations. Use `-lower` flag for lowercase output:
@@ -156,7 +194,7 @@ if err != nil {
156194
status := MustStatus("active") // panics if invalid
157195
```
158196

159-
### SQL Database Support
197+
### SQL Database Support (with `-sql`)
160198

161199
The generated enums implement `database/sql/driver.Valuer` and `sql.Scanner` interfaces for seamless database integration:
162200

@@ -186,4 +224,4 @@ Contributions are welcome! Please feel free to submit a Pull Request.
186224

187225
## License
188226

189-
MIT License
227+
MIT License

_examples/status/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Example: status enum
2+
3+
This example shows how to generate enums with optional integrations.
4+
5+
Current files were generated previously; to regenerate with the new flags, run from this folder:
6+
7+
```
8+
# JSON only (default TextMarshaler/Unmarshaler, no extra deps)
9+
go run ../../main.go -type status -lower
10+
11+
# Add SQL support
12+
go run ../../main.go -type status -lower -sql
13+
14+
# Add MongoDB BSON support
15+
go run ../../main.go -type status -lower -bson
16+
17+
# Add YAML support
18+
go run ../../main.go -type status -lower -yaml
19+
20+
# Combine as needed, e.g. BSON + SQL
21+
go run ../../main.go -type status -lower -bson -sql
22+
```
23+
24+
Notes
25+
- `-bson` uses mongo-go-driver BSON interfaces; values are stored as strings.
26+
- `-sql` implements `driver.Valuer` and `sql.Scanner`.
27+
- `-yaml` implements `yaml.Marshaler`/`yaml.Unmarshaler` (gopkg.in/yaml.v3).
28+

0 commit comments

Comments
 (0)