Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ OpenAPI tooling for the ArchAstro platform. Two tools live here:

| Package | What it does | Install / run |
| --- | --- | --- |
| [`@archastro/sdk-generator`](./packages/sdk-generator) | Reads an OpenAPI spec and emits typed TypeScript / Python / Swift / Go SDKs plus cross-language contract tests. | `npx @archastro/sdk-generator` / `sdk-generator` |
| [`@archastro/sdk-generator`](./packages/sdk-generator) | Reads an OpenAPI spec and emits typed TypeScript / Python / Swift / Go / Elixir / Rust SDKs plus cross-language contract tests. | `npx @archastro/sdk-generator` / `sdk-generator` |
| [`@archastro/channel-harness`](./packages/channel-harness) | Runtime contract-testing harness for Phoenix `x-channels` declared in the spec. Exposes a WebSocket + HTTP control API so TS, Python, (or any other) test suites can drive the same server. | `npx @archastro/channel-harness` / `channel-harness` |

---
Expand Down Expand Up @@ -33,11 +33,13 @@ Supported `--lang` values:
- `go` — emit a typed Go SDK (structs with JSON tags, context-taking resource
methods, channel helpers)
- `elixir` — emit a typed Elixir SDK under `lib/archastro/generated`
- `rust` — emit typed Rust models, async/blocking resources, SSE, auth, and channel facades
- `contract-tests-ts` — emit TS contract tests that drive the channel harness
- `contract-tests-py` — emit Python contract tests (pytest + prism mock server)
- `contract-tests-swift` — emit swift-testing contract tests
- `contract-tests-go` — emit Go contract tests (`go test`, prism + harness)
- `contract-tests-elixir` — emit Elixir contract tests (ExUnit + prism + harness)
- `contract-tests-rust` — emit Rust integration contracts (cargo test + Prism + harness)

Other flags:

Expand Down
6 changes: 4 additions & 2 deletions packages/sdk-generator/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @archastro/sdk-generator

Generate typed TypeScript, Python, Swift, Go, and Elixir SDKs — plus cross-language
Generate typed TypeScript, Python, Swift, Go, Elixir, and Rust SDKs — plus cross-language
contract tests — from an OpenAPI spec produced by the ArchAstro API DSL.

## Install
Expand All @@ -18,7 +18,7 @@ sdk-generator --spec ./openapi.json --lang python --out ./sdk

```
sdk-generator --spec <openapi.json> \
[--lang typescript|python|swift|go|elixir|contract-tests-ts|contract-tests-py|contract-tests-swift|contract-tests-go|contract-tests-elixir] \
[--lang typescript|python|swift|go|elixir|rust|contract-tests-ts|contract-tests-py|contract-tests-swift|contract-tests-go|contract-tests-elixir|contract-tests-rust] \
[--out <dir>] \
[--config <config.json>] \
[--ast-only]
Expand All @@ -33,11 +33,13 @@ Targets:
| `swift` | Swift SDK: Codable models, resources, channels, async client |
| `go` | Go SDK: JSON-tagged structs, context-taking resources, channels, client |
| `elixir` | Elixir SDK: structs, resource modules, auth, and Phoenix Channel facades |
| `rust` | Rust SDK: serde models, async/blocking resources, typed SSE, auth, and channel facades |
| `contract-tests-ts` | TS contract tests that drive `@archastro/channel-harness` |
| `contract-tests-py` | Python contract tests (pytest + Prism mock server) |
| `contract-tests-swift` | Swift contract tests (swift-testing + Prism + harness) |
| `contract-tests-go` | Go contract tests (`go test` + Prism + harness) |
| `contract-tests-elixir` | Elixir contract tests (ExUnit + Prism + harness) |
| `contract-tests-rust` | Rust integration contracts (`cargo test` + Prism + harness) |

### Go target configuration

Expand Down
112 changes: 112 additions & 0 deletions packages/sdk-generator/__tests__/backends/rust.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";

import { emitRustContractTests } from "../../src/backends/contract-tests/rust-emitter.js";
import { rustIdent, rustTypeName, uniqueRustNames } from "../../src/backends/rust/identifiers.js";
import { generateRust, rustType } from "../../src/backends/rust/index.js";
import { parseOpenApiSpec } from "../../src/frontend/index.js";

const here = dirname(fileURLToPath(import.meta.url));
const fixture = JSON.parse(readFileSync(resolve(here, "../fixtures/sample-spec.json"), "utf8"));

function ast() { return parseOpenApiSpec(fixture, { apiBase: "/api", defaultVersion: "v1" }); }

describe("rust identifiers", () => {
it("uses idiomatic snake and Pascal case while escaping keywords", () => {
expect(rustIdent("createdAt")).toBe("created_at");
expect(rustIdent("type")).toBe("type_");
expect(rustTypeName("api_chat_channel")).toBe("ApiChatChannel");
expect(uniqueRustNames(["type", "type", "type_2"])).toEqual(["type_", "type__2", "type_2"]);
});
});

describe("rust type mapping", () => {
it("maps primitives, collections, nullability, and schema refs", () => {
expect(rustType({ kind: "primitive", type: "datetime" })).toBe("chrono::DateTime<chrono::Utc>");
expect(rustType({ kind: "array", items: { kind: "ref", schema: "Team" } })).toBe("Vec<Team>");
expect(rustType({ kind: "nullable", inner: { kind: "primitive", type: "string" } })).toBe("Option<String>");
expect(rustType({ kind: "unknown" })).toBe("Value");
});
});

describe("rust backend", () => {
it("generates models, auth, channels, version resources, and module wiring", () => {
const files = generateRust(ast(), { outDir: "sdk" });
expect(Object.keys(files)).toEqual(expect.arrayContaining([
"sdk/src/generated/types.rs",
"sdk/src/generated/auth.rs",
"sdk/src/generated/channels.rs",
"sdk/src/generated/v1.rs",
"sdk/src/generated/mod.rs",
]));
const output = Object.values(files).join("\n");
expect(output).toContain("pub struct V1");
expect(output).toContain("pub async fn");
expect(output).toContain("_blocking(");
expect(output).toContain("SseDecode, SseStream");
expect(output).toContain("ChannelEventStream<");
});

it("lifts synthetic api/version wrappers out of the public namespace", () => {
const files = generateRust(ast(), { outDir: "sdk" });
const version = files["sdk/src/generated/v1.rs"]!;
expect(version).not.toContain("pub fn api(&self)");
expect(version).not.toContain("pub fn v1(&self)");
});

it("emits typed enums and unions while boxing only inline recursive references", () => {
const spec = ast();
spec.schemas.push(
{
name: "Leaf",
fields: [{ name: "value", type: { kind: "primitive", type: "string" }, required: true }],
},
{
name: "RecursiveNode",
fields: [
{ name: "leaf", type: { kind: "ref", schema: "Leaf" }, required: true },
{ name: "next", type: { kind: "optional", inner: { kind: "ref", schema: "RecursiveNode" } }, required: false },
{ name: "children", type: { kind: "array", items: { kind: "ref", schema: "RecursiveNode" } }, required: true },
{ name: "status", type: { kind: "enum", values: ["in-progress", "in_progress"] }, required: true },
{
name: "choice",
type: { kind: "union", variants: [
{ kind: "primitive", type: "string" },
{ kind: "primitive", type: "integer" },
] },
required: true,
},
],
},
);

const types = generateRust(spec, { outDir: "sdk" })["sdk/src/generated/types.rs"]!;
expect(types).toContain("pub leaf: Leaf,");
expect(types).toContain("pub next: Option<Box<RecursiveNode>>,");
expect(types).toContain("pub children: Vec<RecursiveNode>,");
expect(types).toContain("pub enum RecursiveNodeStatus");
expect(types).toContain("#[serde(rename = \"in-progress\")]");
expect(types).toContain("InProgress2,");
expect(types).toContain("pub enum RecursiveNodeChoice");
expect(types).toContain("Variant1(String)");
expect(types).toContain("Variant2(i64)");
});
});

describe("rust contract emitter", () => {
it("emits REST happy/error cases and SSE harness cases", () => {
const spec = ast();
const operation = spec.versions[0]!.resources[0]!.children[0]!.operations[0]!;
operation.streaming = { style: "sse", events: [{ event: "updated", dataType: { kind: "unknown" } }] };
const files = emitRustContractTests(spec, "sdk");
const rest = files["sdk/tests/generated_rest_contract.rs"]!;
const streams = files["sdk/tests/generated_stream_contract.rs"]!;
expect(rest).toContain("#[tokio::test]");
expect(rest).toContain("support::assert_api_error");
expect(rest).toContain("serde_json::from_str");
expect(streams).toContain("register_stream");
expect(streams).toContain("StreamExt");
});
});
3 changes: 2 additions & 1 deletion packages/sdk-generator/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@archastro/sdk-generator",
"version": "0.11.0",
"description": "Generate typed TypeScript, Python, Swift, Go, and Elixir SDKs (plus contract tests) from an OpenAPI spec.",
"description": "Generate typed TypeScript, Python, Swift, Go, Elixir, and Rust SDKs (plus contract tests) from an OpenAPI spec.",
"keywords": [
"openapi",
"sdk",
Expand All @@ -11,6 +11,7 @@
"swift",
"golang",
"elixir",
"rust",
"archastro"
],
"license": "MIT",
Expand Down
5 changes: 4 additions & 1 deletion packages/sdk-generator/src/backends/contract-tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { emitPythonContractTests } from "./python-emitter.js";
import { emitSwiftContractTests } from "./swift-emitter.js";
import { emitGoContractTests } from "./go-emitter.js";
import { emitElixirContractTests } from "./elixir-emitter.js";
import { emitRustContractTests } from "./rust-emitter.js";

export type GeneratedFiles = Record<string, string>;

export interface ContractTestOptions {
outDir: string;
lang: "typescript" | "python" | "swift" | "go" | "elixir";
lang: "typescript" | "python" | "swift" | "go" | "elixir" | "rust";
/** Go only: import path of the generated SDK package. */
goImportPath?: string;
/** Go only: package alias the generated tests reference the SDK through. */
Expand Down Expand Up @@ -39,6 +40,8 @@ export function generateContractTests(
});
} else if (options.lang === "elixir") {
return emitElixirContractTests(spec, options.outDir);
} else if (options.lang === "rust") {
return emitRustContractTests(spec, options.outDir);
} else {
return emitPythonContractTests(spec, options);
}
Expand Down
Loading
Loading