# gnr8

**One tool for the whole loop between your API code and OpenAPI + client SDKs, end to end, as a single native binary.**

`gnr8` reads your service code, builds a language- and router-agnostic model of its API, and generates
an **OpenAPI 3.1** document and a client **SDK** from it. The generation lifecycle is **configured in
code**, so you, or an AI agent, can adapt exactly how it parses and generates for your project.

> Status: **early release candidate.** Supported frontends are **Go + Gin**, **Python FastAPI**,
> **Python Flask typed-envelope**, and **TypeScript NestJS class DTOs**, with OpenAPI 3.1 plus
> Go/Python/TypeScript SDK generation.

> **Agents using gnr8 in an application repo:** start with
> [`docs/AGENT-USAGE.md`](https://github.com/emilwareus/gnr8/blob/main/docs/AGENT-USAGE.md)
> or run `gnr8 guide`. For concrete scenarios, run `gnr8 guide <topic>`.

> **Full reference (CLI, config, type mapping, recipes):** [`docs/USAGE.md`](docs/USAGE.md).

---

## Why I built it

- **One tool, both directions, end to end.** Generate OpenAPI from your code, and generate code (client
  SDKs) from that, in one place, one pass, instead of stitching a chain of tools together.
- **Fast.** A native binary with no warm-up.
- **Incremental.** It regenerates only what actually changed; built for a save-and-see loop, not full
  rebuilds.
- **No runtime.** A single binary. No Docker, no JVM, nothing to stand up.
- **Configured by code, not YAML, and built for AI agents.** The customization surface is *code*, not a
  config file with a handful of human-friendly flags. The point isn't minimal-config "ease of use" for
  humans; it's that an AI agent can write code to adapt the entire parse-and-generate lifecycle to your
  project by extending the framework directly.
- **Modern SDKs, easy to shape.** Generate SDKs that follow modern idioms for each target, and change
  *how* they're generated through that same configuration-as-code model.

---

## How it works

```
your API code
   │  read the routes, handlers, and types directly; your code is the source of truth
   ▼
internal API model   ── language- & router-agnostic: methods, paths, params, schemas, provenance
   │
   ├─▶ OpenAPI 3.1 document
   └─▶ client SDK   (typed client + models + errors; it compiles)
```

The first supported frontend reads **Go + Gin** via `go/types`; Python and TypeScript frontends use
static sidecars for FastAPI, Flask, and NestJS. A native engine builds a deterministic
model (identical input → byte-identical output) and generates the OpenAPI document and SDKs. The
generation lifecycle is **a Rust crate at `.gnr8/`** — `gnr8 init` scaffolds it, and `gnr8 generate`
compiles and runs it, so adapting how your code is parsed and generated is just editing code. That same
`.gnr8/` workspace tracks what it generated (so it never overwrites your edits), regenerates only what
changed, can watch for source edits, and a `doctor` command reports anything it couldn't represent.

---

## Quick look

A small Gin service (the first supported frontend): [`examples/bookstore/`](examples/bookstore/):

```go
func registerRoutes(r *gin.Engine) {
    books := r.Group("/books")
    books.POST("",     createBook)
    books.GET("",      listBooks)   // ?genre= filter
    books.GET("/:id",  getBook)
    books.PUT("/:id",  updateBook)
    books.DELETE("/:id", deleteBook)
}

type Book struct {
    ID          string    `json:"id"`
    Title       string    `json:"title"`
    Genre       Genre     `json:"genre"`              // a code-defined enum → OpenAPI enum
    Price       float64   `json:"price"`
    PublishedAt time.Time `json:"publishedAt"`
    Subtitle    *string   `json:"subtitle,omitempty"` // optional
    Publisher   Publisher `json:"publisher"`          // nested → $ref
    Tags        []string  `json:"tags"`
}
```

Run it:

```bash
gnr8 init        # scaffold the .gnr8/ Rust crate (this is your config)
```

`gnr8 init` writes `.gnr8/src/main.rs` — the lifecycle, in code. Edit the `Pipeline` to say how your
API is parsed and generated (no TOML, no flags file):

```rust
use gnr8::sdk::prelude::*;

fn main() -> std::process::ExitCode {
    gnr8::runner::run(
        Pipeline::new()
            .source(GoGin::new().inputs(["."]))
            .transform(SetBasePath::new("/books"))
            .transform(SetTitle::new("Bookstore API"))
            .transform(ApplySecurity::api_key("ApiKeyAuth", "X-API-Key"))
            .target(OpenApi31::new().to("generated/openapi.yaml"))
            .target(GoSdk::new().module("example.com/bookstore/sdk").to("generated/sdk"))
            .post(Header::generated()),
    )
}
```

```bash
gnr8 generate    # compile + run .gnr8/, write openapi.yaml + the client SDK
```

Out comes OpenAPI 3.1:

```yaml
paths:
  '/books/':
    get:
      operationId: listBooks
      parameters:
        - { name: genre, in: query, required: false, schema: { type: string } }
      responses:
        '200': { content: { application/json: { schema: { $ref: '#/components/schemas/BookList' } } } }
```

…and an SDK you can call:

```go
func (c *Client) CreateBook(ctx context.Context, in CreateBookRequest) (Book, error)
func (c *Client) GetBook(ctx context.Context, id string) (Book, error)
```

The full input **and** the real generated output are committed in
[`examples/bookstore/`](examples/bookstore/) - see [its README](examples/bookstore/README.md) for the
side-by-side.

---

## Try the example

Install the CLI from the GitHub release archive:

```bash
curl -fsSL https://raw.githubusercontent.com/emilwareus/gnr8/main/scripts/install.sh | bash
```

The crates.io package named `gnr8` is the Rust API that `.gnr8/Cargo.toml` depends on; it is not the
primary CLI install path yet. The release archive includes the `gnr8` binary plus the extractor
resources needed by `gnr8 init`/`gnr8 generate`.

For local development from this checkout:

```bash
cargo build --release -p gnr8-cli
cd examples/bookstore
../../target/release/gnr8 generate
# see generated/openapi.yaml and generated/sdk/
```

## GitHub Action

Use the official action to fail CI when generated OpenAPI, SDKs, CLIs, or other `.gnr8` artifacts are
stale:

```yaml
name: gnr8

on: [push, pull_request]

jobs:
  check-generated:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: emilwareus/gnr8@v0
        with:
          setup-go: "true" # for Go/Gin projects; use setup-python/setup-node for other source stacks
```

For multiple `.gnr8` projects in one repo:

```yaml
- uses: emilwareus/gnr8@v0
  with:
    working-directories: |
      services/api
      services/admin-api
```

The action restores and saves `.gnr8/cache` plus `.gnr8/target`, so repeated `gnr8 check` runs can use
gnr8's verified no-op path. `gnr8 check` exits non-zero if a fresh generation would write different
artifacts.

---

## Status

**Today:** Go + Gin, FastAPI, Flask typed-envelope, and NestJS class-DTO sources work end to end →
OpenAPI 3.1 plus compiling/typechecked Go, Python, and TypeScript client SDKs; the `.gnr8/` lifecycle
(`init` / `generate` / `check` / `watch` / `doctor`), incremental regeneration, and edit-protection.

**Configuration is code.** `gnr8 init` scaffolds a small Rust crate at `.gnr8/` that drives the
lifecycle — there is **no TOML/YAML config file**. You (or an agent) edit `.gnr8/src/main.rs`: it builds
a `Pipeline` of a source, transforms, targets, and post-processors, and `gnr8 generate` compiles and
runs it. Built-ins cover the common cases (`GoGin`, `SetBasePath`/`SetTitle`/`ApplySecurity`,
`OpenApi31`, `GoSdk`); anything else is ordinary Rust — implement a trait and add it to the pipeline.

**By design, additive:** the internal model is language- and router-agnostic, so additional source
frameworks and SDK targets extend it rather than reshape it — a new source or target is a Rust type that
implements one trait and composes into the same pipeline.

---

## Principles (see [`CLAUDE.md`](CLAUDE.md))

1. Everything is derived from **your code and your config**: one deterministic source per fact.
2. **Self-contained:** a single native binary, no runtime to install.
3. **Deterministic:** identical input → byte-identical output.
4. **Extensible in code:** adapt the lifecycle by extending the framework, not by toggling flags.

---

## Repo layout

| Path | What |
|------|------|
| `crates/gnr8-core/` | the engine: model, OpenAPI lowering, SDK generation, lifecycle, diagnostics |
| `crates/gnr8/` | the `gnr8` CLI (`init`, `guide`, `generate`, `check`, `inspect`, `watch`, `doctor`) |
| `goextract/` | the Go frontend that reads Gin source via `go/types` |
| `examples/bookstore/` | a runnable example + its real generated output (the basic `.gnr8/` lifecycle) |
| `examples/taskflow/` | a richer example: a custom `Transform` + a custom `Target` (writes `API.md`) in `.gnr8/` |
| `fixtures/goalservice/` | the test fixture (a realistic Gin service) driving the contract tests |
| `docs/AGENT-USAGE.md` | agent-facing init, generation, SDK usage, scenario, and recovery guide |
| `docs/USAGE.md` | full reference: CLI, config, patterns, type mapping, recipes |
| `llms.txt` / `llms-full.txt` | agent-readable docs index and compact full-context export |
| `docs/` | `demo.md` (walkthrough), `evidence.md` (what's verified) |

Build & verify: `make check` (format, lint, tests) · `make gates` (the full contract suite).
