Skip to content

feat: initial implementation - #1

Merged
mickamy merged 19 commits into
mainfrom
feat/initial-implementation
Jul 28, 2026
Merged

feat: initial implementation#1
mickamy merged 19 commits into
mainfrom
feat/initial-implementation

Conversation

@mickamy

@mickamy mickamy commented Jul 28, 2026

Copy link
Copy Markdown
Owner

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.
  • mapgen command (root package main) — works with the Go 1.24+ tool directive, so go get -tool github.com/mickamy/mapgen plus 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 via go/types, a plan resolver built on an op-tree IR, and an emitter that renders gofmt-formatted, hand-written-looking code.

Usage

// lib/converters/converters.go
func init() {
	mapper.Register(ToDate)      // time.Time -> *date.Date
	mapper.Register(UUIDToString)
	mapper.Register(ToTime)      // *date.Date -> time.Time
	mapper.RegisterE(uuid.Parse) // string -> uuid.UUID, can fail
}
//go:generate go tool mapgen -types=model.Employee:*employeev1.Employee -converter-pkg=./lib/converters

See examples/ for a complete runnable module, including the committed generated output.

Design notes

  • Package selectors in -types resolve 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.
  • Strict by default: every destination field must resolve or generation fails with the field's file:line:col and a copy-pasteable mapper.Register(func(S) D { ... }) suggestion. map:"Name" / map:"-" struct tags cover types you own; -ignore covers generated types you cannot tag.
  • Conversions are loss-free only: integer narrowing, sign changes, float precision loss, and numeric <-> string all require an explicit converter.
  • Nil semantics are conservative: nil pointers map to nil pointers (never to a pointer to a zero value), nil slices stay nil, and a nil top-level source yields the zero value.
  • -check verifies generated files are up to date without writing, for CI use on dirty trees.
  • Golden files live inside internal/generator/fixtures as real Go packages, so go 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 tests examples/ (real uuid + genproto roundtrip); the CI examples job then runs git diff --exit-code to 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 -v output. Limitations are documented in the README and rejected with explicit error messages.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: collectImports does not reliably skip files from other packages when neither GOPACKAGE nor GOFILE is available, which can cause selector resolution to become ambiguous or incorporate imports from an unrelated package.

  • Recommendation: Ensure collectImports deterministically filters by a selected package name even when wantPkg starts 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.IsIdent is documented as validating Go identifiers but currently accepts Go keywords (e.g., type, func), allowing invalid -package / -types / -ignore inputs 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/mapper registration + runtime conversion API, with unit tests.
  • Implemented the mapgen CLI 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

Comment on lines +81 to +83
if !isGoFile && wantPkg != "" && file.Name.Name != wantPkg {
continue
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread internal/cli/parse.go Outdated
Comment on lines +136 to +148
// 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 != ""
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e35aaad: replaced the hand-rolled check with token.IsIdentifier, which rejects Go keywords, and added keyword cases to the parse tests.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 44 out of 48 changed files in this pull request and generated no new comments.

Files not reviewed (2)
  • examples/handler/handler_gen.go: Generated file
  • internal/generator/emit.go: Generated file

@mickamy
mickamy merged commit 87750b2 into main Jul 28, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants