README
¶
Lexicodegen
Generate Swift models and XRPC client surface from AT Protocol lexicons with a single Go CLI.
Implementation notes:
- lexicon parsing and validation use Indigo
- config, source loading, IR building, and Swift emission are repo-owned Go code
- the generated output target is Swift
Current Output
- Generated Swift files are written to
output/swiftby default. - Files are namespace-grouped, for example:
AppBskyActor.generated.swiftAppBskyFeed.generated.swift
- Shared helper files are emitted as:
Models.swiftEndpoints.swift
output.swiftFilePrefixcan prepend every generated Swift filename, for exampleGenerated_Models.swift.
Project Layout
cmd/lexicodegen: CLI entrypointinternal/config: CLI and config-file parsing, defaults, normalization, validationinternal/source: local, HTTP, and git-archive lexicon loadinginternal/schema: raw normalized schema model plus Indigo-backed validation adapterinternal/ir: generator IR for Swift emissioninternal/swiftgen: Swift-specific code generationlexicons/: checked-in lexicon JSON inputtestdata/golden/minimal-endpoints/: pinned parity snapshot for focused generated Swift outputscripts/get-lexicons.sh: refreshes lexicon snapshots from upstreamscripts/check-swift-compile.sh: typechecks generated Swift withswiftcdocs/: focused documentation for CLI usage and TOML configuration
Installation
Requirements:
- Go 1.26+
- Swift 6.2+ if you want to run generated-code compilation checks
Install with Homebrew after the tap repository exists and a tagged release has published the formula:
brew install charliewilco/lexicodegen/lexicodegen
The Homebrew shorthand above expects a charliewilco/homebrew-lexicodegen tap repository. If you are working from an unreleased commit, use go install or build from source instead.
Install with Go:
go install github.com/charliewilco/lexicodegen/cmd/lexicodegen@v0.1.0
Build the CLI from source:
go build -o ./lexicodegen ./cmd/lexicodegen
Or run it directly from a checkout:
go run ./cmd/lexicodegen ./lexicons --output ./output/swift
Git Hooks
Lefthook is configured as a project-local Go tool and tracked in go.mod. The repo config lives in lefthook.toml.
Install the hooks with:
go tool github.com/evilmartians/lefthook/v2 install
Or use the repo helper:
just hooks-install
Configured hooks:
pre-commit: rungofmt -won staged Go files and restage fixespre-push: rungo test ./...
Testing
The test suite is organized around generator regression safety:
- unit tests cover config, source loading, schema parsing, IR construction, and small utility behavior
- emitter behavior tests assert focused Swift snippets for generator contracts that should be easy to diagnose
- acceptance tests under
testdata/acceptancerun small lexicon suites through the CLI path, verify stable generated files and API snippets, and typecheck tiny Swift usage fixtures whenswiftcis available - the pinned golden snapshot under
testdata/golden/minimal-endpointscatches generated-output drift against a stable fixture - the generated Swift compile check verifies the full generated surface with
swiftc
Run the default suite with:
go test ./...
Inspect statement coverage without enforcing a threshold:
just coverage
Usage
Basic usage:
go run ./cmd/lexicodegen <source1> [source2 ...] [--source <source>] [--output <swift-output-dir>]
Examples:
go run ./cmd/lexicodegen ./lexicons ./frontpage-lexicons --output ./output/swift
go run ./cmd/lexicodegen ./lexicons https://example.com/namespace.json --output ./output/swift
go run ./cmd/lexicodegen --source local:./leaflet --source https://example.com/namespace.json --output ./output/swift
Supported source kinds:
- local directory or file
- HTTP JSON document or JSON array of documents
git-archive:tarball URL with optionalstripPathin config
Flags
Supported CLI flags:
--config <path>--source <path-or-url>(repeatable)--allow-prefix <prefix>(repeatable or comma-separated)--deny-prefix <prefix>(repeatable or comma-separated)--deny-unspecced--deny-deprecated--output <dir>--swift-output-dir <dir>--target <target>/--targets <target>--help/-h--version
Notes:
swiftis the only supported targetbothis accepted as a compatibility alias forswift- unknown flags are rejected
- unknown targets are rejected
output.swiftFilePrefixis currently config-only
To inspect the installed command without generating files:
lexicodegen --help
lexicodegen --version
Config Files
JSON and TOML config files are supported:
go run ./cmd/lexicodegen --config ./lexicodegen.toml
go run ./cmd/lexicodegen --config ./lexicodegen.json
Config Shape
| Key | Type | Default | Notes |
|---|---|---|---|
sources |
LexiconSource[] |
[{ kind: "local", path: "./lexicons", recursive: true }] |
Replaced entirely if any CLI source is provided |
filters.allowPrefixes |
string[] |
[] |
Keep only matching lexicon IDs |
filters.denyPrefixes |
string[] |
[] |
Exclude matching lexicon IDs |
filters.denyUnspecced |
boolean |
false |
Skip unspecced defs |
filters.denyDeprecated |
boolean |
false |
Skip deprecated defs |
targets |
string[] |
["swift"] |
Swift is the only supported target |
output.swiftOutDir |
string |
"./output/swift" |
Resolved relative to the current working directory |
output.swiftFilePrefix |
string |
"" |
Prepends every generated Swift filename; path separators are rejected |
This repo already supports filter prefixes in config via filters.allowPrefixes and filters.denyPrefixes. output.swiftFilePrefix is separate: it changes generated filenames, not lexicon filtering.
sources Schema
Each source is one of:
- Local source:
{
"kind": "local",
"path": "./lexicons",
"recursive": true
}
- HTTP source:
{
"kind": "http",
"url": "https://example.com/lexicons.json"
}
- Git archive source:
{
"kind": "git-archive",
"url": "https://github.com/org/repo/archive/refs/heads/main.tar.gz",
"stripPath": "some/subfolder"
}
stripPath is used during archive extraction. If you need a specific branch or tag, point url at the archive for that ref directly.
JSON Example
{
"sources": [
{
"kind": "local",
"path": "./lexicons",
"recursive": true
}
],
"filters": {
"allowPrefixes": ["app.bsky", "frontpage"],
"denyPrefixes": ["com.atproto.lexicon.resolveLexicon"],
"denyUnspecced": false,
"denyDeprecated": false
},
"targets": ["swift"],
"output": {
"swiftOutDir": "./output/swift",
"swiftFilePrefix": "Generated_"
}
}
TOML Example
[[sources]]
kind = "local"
path = "./lexicons"
recursive = true
[filters]
allowPrefixes = ["app.bsky", "frontpage"]
denyPrefixes = ["com.atproto.lexicon.resolveLexicon"]
denyUnspecced = false
denyDeprecated = false
targets = ["swift"]
[output]
swiftOutDir = "./output/swift"
swiftFilePrefix = "Generated_"
output.swiftFilePrefix is applied verbatim. If you want a separator, include it yourself, for example Generated_ or ATProto.
Precedence
Merge behavior is intentionally simple:
- CLI sources replace config-file sources when any positional source or
--sourceis provided - CLI filter flags override config filters individually
--outputand--swift-output-diroverrideoutput.swiftOutDiroutput.swiftFilePrefixcomes from config only- resolved local paths are normalized relative to the current working directory
Development
Common commands:
just buildjust testjust lintjust generatejust verify-swiftjust all
Primary verification flow:
go test ./...go build -o ./lexicodegen ./cmd/lexicodegengo run ./cmd/lexicodegen ./lexicons --output ./output/swiftbash ./scripts/check-swift-compile.sh ./output/swift
Releases
Tagged releases are published with GoReleaser via .github/workflows/release.yml.
Release tags are the installable version contract for downstream automation such as GitHub Actions. Prefer pinned SemVer tags such as v0.1.0 over latest when another repository needs reproducible generation.
Release flow:
- create and push a semver tag such as
v0.1.0 - GitHub Actions runs GoReleaser
- GoReleaser uploads release archives to GitHub Releases
- GoReleaser updates
Formula/lexicodegen.rbin thecharliewilco/homebrew-lexicodegentap repository whenHOMEBREW_TAP_GITHUB_TOKENis configured
First release:
git tag -a v0.1.0 -m "Release v0.1.0"
git push origin v0.1.0
After the release workflow completes, verify the published binary:
lexicodegen --version
One-time setup:
- create the
charliewilco/homebrew-lexicodegentap repository with amainbranch - add a
HOMEBREW_TAP_GITHUB_TOKENrepository secret that can write to the tap repository if Homebrew publishing should happen during release - the release workflow uses the default
GITHUB_TOKENto publish this repository's GitHub Release - GoReleaser uses
HOMEBREW_TAP_GITHUB_TOKENto publish the Homebrew formula to the tap repository; without that secret, the formula is generated locally indist/and the tap upload is skipped
Local release validation:
goreleaser check
goreleaser release --snapshot --clean
If you later move the tap to a different repository, update the brews[0].repository values in .goreleaser.yaml and provide a token that can write to that repo.