feat: initial implementation - #1
Conversation
There was a problem hiding this comment.
Pull request overview
Initial implementation of mapgen, a Go 1.25+ type-safe struct mapping code generator driven by statically discovered mapper.Register / RegisterE converter registrations and invoked via go generate.
Key Issues
-
Severity: Medium
-
Issue:
collectImportsdoes not reliably skip files from other packages when neitherGOPACKAGEnorGOFILEis available, which can cause selector resolution to become ambiguous or incorporate imports from an unrelated package. -
Recommendation: Ensure
collectImportsdeterministically filters by a selected package name even whenwantPkgstarts empty (e.g., set it from the first parsed file, then skip mismatches), or explicitly error if the directory contains multiple packages and the expected one cannot be determined. (type "custom") -
Severity: Medium
-
Issue:
cli.IsIdentis documented as validating Go identifiers but currently accepts Go keywords (e.g.,type,func), allowing invalid-package/-types/-ignoreinputs to slip through and fail later with less actionable errors. -
Recommendation: Reject keywords as part of identifier validation (for example via
go/token.Lookup(s) == token.IDENT). (type "custom")
Changes:
- Added
runtime/mapperregistration + runtime conversion API, with unit tests. - Implemented the
mapgenCLI tool and core generator pipeline (import resolution, converter extraction, plan resolution, code emission) with fixtures and tests. - Added runnable
examples/module (including committed generated output) and CI targets to keep it up to date.
Reviewed changes
Copilot reviewed 44 out of 48 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| runtime/mapper/mapper.go | Runtime converter registry API (Register, RegisterE, Convert). |
| runtime/mapper/mapper_test.go | Unit tests for runtime registry behavior and error cases. |
| README.md | Project documentation: usage, flags, rules, limitations, installation. |
| Makefile | Adds test-examples target; adjusts lint guard output. |
| main.go | mapgen command entrypoint wiring CLI parsing to generator execution. |
| LICENSE | Adds MIT license text. |
| internal/generator/selectors.go | Collects imports and resolves package selectors for -types. |
| internal/generator/selectors_test.go | Tests for import collection + selector resolution behavior. |
| internal/generator/resolve.go | Core mapping-plan resolver (field matching + conversion rules). |
| internal/generator/resolve_test.go | Resolver tests covering conversions, errors, and edge cases. |
| internal/generator/plan.go | IR types for mapping plans and ops + debug description. |
| internal/generator/imports.go | Deterministic import naming/tracking for emitted code. |
| internal/generator/generator.go | High-level orchestration: load packages, resolve, emit, write/check. |
| internal/generator/generator_test.go | E2E-style tests generating into a temp module and reloading types. |
| internal/generator/fixtures/protolike/protolike.go | Protobuf-like fixture types with getters for generator tests. |
| internal/generator/fixtures/output/output_gen.go | Golden generated output fixture for emitter tests. |
| internal/generator/fixtures/model/model.go | Domain-model fixture types (with map tags) for tests. |
| internal/generator/fixtures/model/model_gen.go | Golden generated output fixture for self-import behavior. |
| internal/generator/fixtures/errcases/errcases.go | Negative fixture pairs to exercise resolver error reporting. |
| internal/generator/fixtures/converters/unexported/unexported.go | Fixture: unexported converter registration. |
| internal/generator/fixtures/converters/ok/ok.go | Fixture: supported converter registration patterns. |
| internal/generator/fixtures/converters/ok/helper.go | Fixture: registration via aliased import outside init. |
| internal/generator/fixtures/converters/method/method.go | Fixture: rejected method-expression registration. |
| internal/generator/fixtures/converters/genericfn/genericfn.go | Fixture: rejected generic converter registration. |
| internal/generator/fixtures/converters/funcvar/funcvar.go | Fixture: rejected function-variable registration. |
| internal/generator/fixtures/converters/dup/dup.go | Fixture: duplicate converter registration for same pair. |
| internal/generator/fixtures/converters/closure/closure.go | Fixture: rejected function-literal registration. |
| internal/generator/fixtures/conv/conv.go | Fixture: converters between model and protolike types. |
| internal/generator/export_test.go | Test-only exports of internal generator types/helpers. |
| internal/generator/emit.go | Code emitter producing gofmt-formatted mapping functions. |
| internal/generator/emit_test.go | Emitter tests validating generated output against goldens. |
| internal/generator/converters.go | Static analysis of converter registrations using go/types. |
| internal/generator/converters_test.go | Converter extraction tests + error-case fixtures. |
| internal/cli/parse.go | Flag parsing and type/field reference parsing/validation. |
| internal/cli/parse_test.go | CLI parsing tests for valid configs and error cases. |
| internal/cli/config.go | CLI config model types (TypeRef, TypePair, etc.). |
| go.sum | Adds checksums for module dependencies. |
| go.mod | Declares Go 1.25.0 and generator dependencies (x/tools, etc.). |
| examples/model/model.go | Example domain types for end-to-end usage. |
| examples/lib/converters/converters.go | Example converter registrations used by generated code. |
| examples/handler/handler.go | Example //go:generate directive + blank imports for selectors. |
| examples/handler/handler_test.go | Example roundtrip + nil + invalid-input tests. |
| examples/handler/handler_gen.go | Committed generated output for the example module. |
| examples/go.sum | Dependency checksums for the example module. |
| examples/go.mod | Example module using the Go tool directive + replace to repo root. |
| examples/gen/employeev1/employee.go | Protobuf-like example wire types with getters. |
| .golangci.yaml | Excludes generator fixture packages from golangci-lint. |
| .github/workflows/ci.yml | Adds CI job to regenerate + test examples and verify clean diff. |
Files not reviewed (2)
- examples/handler/handler_gen.go: Generated file
- internal/generator/emit.go: Generated file
| if !isGoFile && wantPkg != "" && file.Name.Name != wantPkg { | ||
| continue | ||
| } |
There was a problem hiding this comment.
Fixed in 43f4a9a: when neither GOPACKAGE nor GOFILE is available, collectImports now determines the package from the package clauses of the directory and errors out when the directory contains multiple packages.
| // IsIdent reports whether s is a valid Go identifier. | ||
| func IsIdent(s string) bool { | ||
| for i, r := range s { | ||
| if unicode.IsLetter(r) || r == '_' { | ||
| continue | ||
| } | ||
| if i > 0 && unicode.IsDigit(r) { | ||
| continue | ||
| } | ||
| return false | ||
| } | ||
| return s != "" | ||
| } |
There was a problem hiding this comment.
Fixed in e35aaad: replaced the hand-rolled check with token.IsIdentifier, which rejects Go keywords, and added keyword cases to the parse tests.
Summary
Initial implementation of mapgen: a type-safe struct mapping code generator for Go, driven by registered converters and
go generate.runtime/mapper— the registration API (Register/RegisterE/Convert) users import; zero third-party dependencies. Registrations double as a declaration DSL: the generator discovers them by static analysis, and generated code calls the converter functions directly with no runtime lookup.mapgencommand (rootpackage main) — works with the Go 1.24+tooldirective, sogo get -tool github.com/mickamy/mapgenplus a//go:generate go tool mapgen ...line is all a consumer needs.internal/cli— flag and type-spec parsing (-types,-converter-pkg,-output,-direction,-ignore,-package,-check).internal/generator— selector resolution from the directive package's imports, converter extraction viago/types, a plan resolver built on an op-tree IR, and an emitter that renders gofmt-formatted, hand-written-looking code.Usage
//go:generate go tool mapgen -types=model.Employee:*employeev1.Employee -converter-pkg=./lib/convertersSee
examples/for a complete runnable module, including the committed generated output.Design notes
-typesresolve from the imports of the package containing the directive (directive file's imports take priority), so an existing file needs only the one-line directive; a dedicated directive file needs only blank imports.file:line:coland a copy-pasteablemapper.Register(func(S) D { ... })suggestion.map:"Name"/map:"-"struct tags cover types you own;-ignorecovers generated types you cannot tag.-checkverifies generated files are up to date without writing, for CI use on dirty trees.internal/generator/fixturesas real Go packages, sogo build ./...continuously proves that emitted code compiles.Testing
make test— unit tests for every stage; the E2E test generates into a synthesized temp module and re-loads it to assert zero type errors.make test-examples— regenerates and testsexamples/(realuuid+genprotoroundtrip); the CIexamplesjob then runsgit diff --exit-codeto keep the committed generated code current.make lint— golangci-lint clean.Not in scope (v2 candidates)
map-typed fields, protobuf
oneof, the protobuf opaque API, generic types, bulk pairing (-all-types=model:pb), and a debug-voutput. Limitations are documented in the README and rejected with explicit error messages.