@workos/oagen 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +12 -0
- package/LICENSE.txt +21 -0
- package/README.md +197 -0
- package/dist/chunk-CIX5NOBF.js +263 -0
- package/dist/chunk-IJ5E5AE7.js +1040 -0
- package/dist/chunk-P5XUDUGY.js +1748 -0
- package/dist/cli/index.js +5603 -0
- package/dist/compat/index.d.ts +195 -0
- package/dist/compat/index.js +4453 -0
- package/dist/index.d.ts +300 -0
- package/dist/index.js +1733 -0
- package/dist/scripts/smoke/baseline.js +1751 -0
- package/dist/scripts/smoke/sdk-test.js +93 -0
- package/dist/smoke.js +1963 -0
- package/dist/types-B5t9O9Wm.d.ts +367 -0
- package/dist/verify/index.d.ts +76 -0
- package/dist/verify/index.js +336 -0
- package/docs/advanced/index.md +11 -0
- package/docs/advanced/oagen-ignore.md +144 -0
- package/docs/agents/architecture.md +53 -0
- package/docs/agents/emitters.md +69 -0
- package/docs/agents/testing.md +60 -0
- package/docs/architecture/dependency-layers.md +75 -0
- package/docs/architecture/emitter-contract.md +160 -0
- package/docs/architecture/extractor-contract.md +227 -0
- package/docs/architecture/ir-types.md +275 -0
- package/docs/architecture/non-additive-changes.md +76 -0
- package/docs/architecture/pipeline.md +110 -0
- package/docs/architecture/workflows.md +215 -0
- package/docs/cli.md +156 -0
- package/docs/contributor/index.md +10 -0
- package/docs/contributor/public-api.md +59 -0
- package/docs/contributor/versioning.md +48 -0
- package/docs/core/index.md +12 -0
- package/docs/core/minimal-emitter.md +134 -0
- package/docs/core/quickstart.md +142 -0
- package/docs/diagrams/generate-sdk-workflow.md +50 -0
- package/docs/index.md +40 -0
- package/examples/reference-emitter/README.md +41 -0
- package/examples/reference-emitter/oagen.config.ts +7 -0
- package/examples/reference-emitter/spec/github-subset.yml +400 -0
- package/examples/reference-emitter/src/emitter.ts +52 -0
- package/examples/reference-emitter/src/generators/client.ts +37 -0
- package/examples/reference-emitter/src/generators/config.ts +19 -0
- package/examples/reference-emitter/src/generators/enums.ts +16 -0
- package/examples/reference-emitter/src/generators/errors.ts +39 -0
- package/examples/reference-emitter/src/generators/models.ts +29 -0
- package/examples/reference-emitter/src/generators/resources.ts +60 -0
- package/examples/reference-emitter/src/generators/tests.ts +7 -0
- package/examples/reference-emitter/src/index.ts +1 -0
- package/examples/reference-emitter/src/naming.ts +21 -0
- package/examples/reference-emitter/src/type-mapper.ts +39 -0
- package/examples/reference-emitter/tsconfig.json +19 -0
- package/package.json +102 -0
- package/skills/generate-emitter/SKILL.md +393 -0
- package/skills/generate-emitter/references/common-pitfalls.md +23 -0
- package/skills/generate-emitter/references/generator-guide.md +201 -0
- package/skills/generate-emitter/references/project-scaffold.md +138 -0
- package/skills/generate-extractor/SKILL.md +200 -0
- package/skills/generate-sdk/SKILL.md +155 -0
- package/skills/generate-smoke-test/SKILL.md +185 -0
- package/skills/integrate/SKILL.md +108 -0
- package/skills/verify-compat/SKILL.md +151 -0
- package/skills/verify-smoke-test/SKILL.md +176 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Dependency Layers
|
|
2
|
+
|
|
3
|
+
## Layer Hierarchy (one-way imports only)
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
Layer 0: ir/types ← Pure type definitions, no imports from other layers
|
|
7
|
+
Layer 1: utils ← Only imports from Layer 0 (naming, tree-sitter helpers)
|
|
8
|
+
Layer 2: parser ← Imports from Layers 0-1
|
|
9
|
+
Layer 3: engine, differ ← Imports from Layers 0-1
|
|
10
|
+
Layer 3: compat ← Imports from Layers 0-1 (extractors, overlay, staleness, differ)
|
|
11
|
+
Layer 4: verify ← Imports from Layers 0-3
|
|
12
|
+
Layer 5: cli ← Imports from Layers 0-4
|
|
13
|
+
|
|
14
|
+
Top-level entrypoints (`src/index.ts`, `src/errors.ts`) are exempt from these directional rules because they exist to re-export public APIs and shared error types.
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Allowed Imports
|
|
18
|
+
|
|
19
|
+
| File in... | Can import from... | Cannot import from... |
|
|
20
|
+
| ------------- | ---------------------------------------------------------------------- | ----------------------------------- |
|
|
21
|
+
| `src/ir/` | (nothing in `src/`) | everything |
|
|
22
|
+
| `src/utils/` | `src/ir/`, `src/errors.ts` | parser, engine, differ, compat... |
|
|
23
|
+
| `src/parser/` | `src/ir/`, `src/utils/`, `src/errors.ts` | engine, differ, compat, verify, cli |
|
|
24
|
+
| `src/engine/` | `src/ir/`, `src/utils/`, `src/errors.ts`, `src/differ/` | parser, verify, cli |
|
|
25
|
+
| `src/differ/` | `src/ir/`, `src/utils/`, `src/errors.ts` | parser, compat, verify, cli |
|
|
26
|
+
| `src/compat/` | `src/ir/`, `src/utils/`, `src/errors.ts`, `src/differ/` | parser, verify, cli |
|
|
27
|
+
| `src/verify/` | `src/ir/`, `src/utils/`, `src/errors.ts`, `src/engine/`, `src/compat/` | parser, cli |
|
|
28
|
+
| `src/cli/` | anything in `src/` | (top level, can import all) |
|
|
29
|
+
|
|
30
|
+
`src/utils/` contains naming utilities (`naming.ts`) and tree-sitter helpers (`tree-sitter.ts`). The `safeParse()` function in `tree-sitter.ts` is used by all tree-sitter-based extractors and the engine merger to work around a 32KB buffer limit in the tree-sitter 0.21.x native binding.
|
|
31
|
+
|
|
32
|
+
`src/compat/` contains the backwards-compatibility system: extractors (per-language API surface extraction), the compat differ, overlay system, and staleness detection. Extractors use tree-sitter grammars for source parsing (via `src/utils/tree-sitter.ts`).
|
|
33
|
+
|
|
34
|
+
## Emitters Are External
|
|
35
|
+
|
|
36
|
+
Emitters live in a separate project (`oagen-emitters`) and import from `@workos/oagen`. They receive IR nodes via their method signatures and never call the parser directly. Each emitter is self-contained with no cross-emitter dependencies.
|
|
37
|
+
|
|
38
|
+
## Violation Examples
|
|
39
|
+
|
|
40
|
+
A violation occurs when a lower-layer module imports from a higher layer:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// BAD: src/parser/schemas.ts importing from engine (Layer 2 → Layer 3)
|
|
44
|
+
import { generate } from "../engine/orchestrator.js";
|
|
45
|
+
|
|
46
|
+
// BAD: src/ir/types.ts importing from utils (Layer 0 → Layer 1)
|
|
47
|
+
import { toSnakeCase } from "../utils/naming.js";
|
|
48
|
+
|
|
49
|
+
// BAD: src/engine/orchestrator.ts importing from cli (Layer 3 → Layer 4)
|
|
50
|
+
import { loadConfig } from "../cli/config-loader.js";
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Remediation:** Move the shared logic to a lower layer, or extract a type-only import.
|
|
54
|
+
|
|
55
|
+
## Cross-Layer Exceptions
|
|
56
|
+
|
|
57
|
+
Two type-only exceptions are currently allowed:
|
|
58
|
+
|
|
59
|
+
- `differ -> engine/types` for shared generation types such as `EmitterContext` and `GeneratedFile`
|
|
60
|
+
- `engine -> compat/types` because overlay-aware generation threads compat types through `EmitterContext` and generation options without depending on compat implementation modules
|
|
61
|
+
|
|
62
|
+
One runtime exception is currently allowed:
|
|
63
|
+
|
|
64
|
+
- `engine -> differ` for incremental generation, which depends on spec diffing and file mapping
|
|
65
|
+
|
|
66
|
+
If you add a new exception, keep it narrow, make it type-only when possible, and document the reason in both code and docs.
|
|
67
|
+
|
|
68
|
+
## Enforcement
|
|
69
|
+
|
|
70
|
+
These layer rules are enforced by `test/architecture/dependency-layers.test.ts`.
|
|
71
|
+
|
|
72
|
+
If you intentionally cross a layer boundary, either:
|
|
73
|
+
|
|
74
|
+
1. refactor the shared code to a lower layer, or
|
|
75
|
+
2. add a narrow documented exception here and update the enforcement test
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Emitter Contract
|
|
2
|
+
|
|
3
|
+
Source: `src/engine/types.ts`
|
|
4
|
+
|
|
5
|
+
If you want the smallest possible implementation first, start with [Minimal Emitter](../core/minimal-emitter.md). This document is the full contract reference.
|
|
6
|
+
|
|
7
|
+
## Interface
|
|
8
|
+
|
|
9
|
+
Every language emitter must implement the `Emitter` interface:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
interface Emitter {
|
|
13
|
+
language: string;
|
|
14
|
+
|
|
15
|
+
generateModels(models: Model[], ctx: EmitterContext): GeneratedFile[];
|
|
16
|
+
generateEnums(enums: Enum[], ctx: EmitterContext): GeneratedFile[];
|
|
17
|
+
generateResources(services: Service[], ctx: EmitterContext): GeneratedFile[];
|
|
18
|
+
generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[];
|
|
19
|
+
generateErrors(ctx: EmitterContext): GeneratedFile[];
|
|
20
|
+
generateConfig(ctx: EmitterContext): GeneratedFile[];
|
|
21
|
+
generateTypeSignatures(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[];
|
|
22
|
+
generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[];
|
|
23
|
+
generateManifest?(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[];
|
|
24
|
+
fileHeader(): string;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## GeneratedFile
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
interface GeneratedFile {
|
|
32
|
+
path: string; // Relative path within output directory
|
|
33
|
+
content: string; // File content (header prepended by orchestrator)
|
|
34
|
+
header?: string; // Optional override of the default file header
|
|
35
|
+
skipIfExists?: boolean; // Don't overwrite if file already exists on disk (defaults to false)
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## EmitterContext
|
|
40
|
+
|
|
41
|
+
Every generator method receives an `EmitterContext`:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
interface EmitterContext {
|
|
45
|
+
namespace: string; // snake_case namespace for file paths
|
|
46
|
+
namespacePascal: string; // PascalCase namespace for code identifiers
|
|
47
|
+
spec: ApiSpec; // The full parsed IR spec
|
|
48
|
+
outputDir?: string; // Output directory path
|
|
49
|
+
apiSurface?: ApiSurface; // Baseline API surface (when --api-surface is provided)
|
|
50
|
+
overlayLookup?: OverlayLookup; // Name preservation overlay (when --api-surface is provided)
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Overlay Integration
|
|
55
|
+
|
|
56
|
+
When a user passes `--api-surface` to `oagen generate` or `oagen diff`, the engine builds an `OverlayLookup` and passes it to emitters via `ctx.overlayLookup`. Emitters should check the overlay before generating default names to preserve backwards compatibility with an existing SDK.
|
|
57
|
+
|
|
58
|
+
**When it's present:** The user has an existing live SDK and wants the generated output to preserve its public API (method names, type names, exports).
|
|
59
|
+
|
|
60
|
+
**How emitters should use it:**
|
|
61
|
+
|
|
62
|
+
1. **Method names:** Before generating a method name, check `methodByOperation` using the HTTP key:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
|
|
66
|
+
const existing = ctx.overlayLookup?.methodByOperation.get(httpKey);
|
|
67
|
+
if (existing) {
|
|
68
|
+
// Use existing.methodName instead of generating a new name
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
2. **Type names:** Check `interfaceByName` and `typeAliasByName` for existing names before applying naming conventions.
|
|
73
|
+
|
|
74
|
+
3. **Barrel exports:** Check `requiredExports` to ensure all expected symbols are re-exported.
|
|
75
|
+
|
|
76
|
+
**`OverlayLookup` fields:**
|
|
77
|
+
|
|
78
|
+
| Field | Type | Purpose |
|
|
79
|
+
| ------------------- | ---------------------------- | ------------------------------------------------ |
|
|
80
|
+
| `methodByOperation` | `Map<string, MethodOverlay>` | HTTP key → existing method info (name, params) |
|
|
81
|
+
| `httpKeyByMethod` | `Map<string, string>` | Reverse map: "Class.method" → HTTP key |
|
|
82
|
+
| `interfaceByName` | `Map<string, string>` | IR interface name → existing interface name |
|
|
83
|
+
| `typeAliasByName` | `Map<string, string>` | IR type alias name → existing type alias name |
|
|
84
|
+
| `requiredExports` | `Map<string, Set<string>>` | Barrel file path → symbols that must be exported |
|
|
85
|
+
|
|
86
|
+
The `httpKeyByMethod` reverse map is only populated when a manifest (`smoke-manifest.json`) is available. Without it, method-level violations cannot be auto-patched in the self-correcting loop. Emitters that support compat verification should implement `generateManifest`.
|
|
87
|
+
|
|
88
|
+
## Rules
|
|
89
|
+
|
|
90
|
+
1. **Pure functions** — Generator methods receive IR nodes and return `GeneratedFile[]`. No I/O, no side effects, no network calls.
|
|
91
|
+
|
|
92
|
+
2. **Empty input = empty output** — If `models` is `[]`, `generateModels` must return `[]` without errors.
|
|
93
|
+
|
|
94
|
+
3. **Namespace threading** — All generated code must use `ctx.namespace` (snake_case) for file paths and `ctx.namespacePascal` for code identifiers.
|
|
95
|
+
|
|
96
|
+
4. **Tests include fixtures** — `generateTests` should internally call fixture generation and return both test files and fixture files in the combined `GeneratedFile[]`.
|
|
97
|
+
|
|
98
|
+
5. **Inapplicable methods return `[]`** — If a language doesn't need type signature files, `generateTypeSignatures` returns `[]`. Optional methods like `generateManifest` can simply be omitted.
|
|
99
|
+
|
|
100
|
+
6. **Composable generators** — Interface methods can compose multiple internal generators. Example: Node's `generateConfig` returns config files plus shared utility files.
|
|
101
|
+
|
|
102
|
+
## New IR Variants for Emitters
|
|
103
|
+
|
|
104
|
+
Emitters should handle these additional variants introduced for open-source readiness:
|
|
105
|
+
|
|
106
|
+
- **Auth:** `'cookie'` in the `apiKey` auth scheme `in` field
|
|
107
|
+
- **Pagination:** `'link-header'` strategy for RFC 5988 Link header pagination; `dataPath` may be `undefined` (meaning the response IS the data)
|
|
108
|
+
- **Encoding:** `'form-urlencoded'` for `application/x-www-form-urlencoded` request bodies
|
|
109
|
+
- **HTTP methods:** `head`, `options`, `trace` — HEAD returns no body, OPTIONS/TRACE are informational
|
|
110
|
+
- **Cookie params:** Optional `cookieParams` array on operations that use cookie parameters
|
|
111
|
+
- **Servers:** Optional `servers: ServerEntry[]` on `ApiSpec` alongside `baseUrl`
|
|
112
|
+
- **Multiple responses:** Optional `successResponses: SuccessResponse[]` on `Operation` when more than one 2xx exists
|
|
113
|
+
- **Idempotency:** `injectIdempotencyKey` is now spec-driven (true only when the spec declares an `Idempotency-Key` header)
|
|
114
|
+
|
|
115
|
+
## OperationPlan
|
|
116
|
+
|
|
117
|
+
Source: `src/engine/operation-plan.ts`
|
|
118
|
+
|
|
119
|
+
Emitters use `planOperation(op)` to compute an `OperationPlan` for each operation, rather than duplicating decision logic inline:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
interface OperationPlan {
|
|
123
|
+
operation: Operation; // back-reference
|
|
124
|
+
isDelete: boolean;
|
|
125
|
+
hasBody: boolean;
|
|
126
|
+
isIdempotentPost: boolean;
|
|
127
|
+
pathParamsInOptions: boolean;
|
|
128
|
+
isPaginated: boolean;
|
|
129
|
+
responseModelName: string | null; // null = void/primitive
|
|
130
|
+
isModelResponse: boolean;
|
|
131
|
+
hasQueryParams: boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function planOperation(op: Operation): OperationPlan;
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- `responseModelName` is `null` for void/primitive responses. Each emitter maps this to its own fallback (`'void'` in Node, `'Object'` in Ruby).
|
|
138
|
+
- `resolveResponseModelName(op)` is also exported for cases that only need the model name.
|
|
139
|
+
|
|
140
|
+
## Existing Emitters
|
|
141
|
+
|
|
142
|
+
Production emitters live in the separate `oagen-emitters` project and import types from `@workos/oagen`.
|
|
143
|
+
|
|
144
|
+
| Language | Location | Notes |
|
|
145
|
+
| ---------------------- | ------------------------------- | ------------------------------------------ |
|
|
146
|
+
| TypeScript (reference) | `examples/reference-emitter/` | Ships with oagen — minimal working example |
|
|
147
|
+
| Ruby | `src/ruby/` (in oagen-emitters) | Production emitter |
|
|
148
|
+
| Node | `src/node/` (in oagen-emitters) | Production emitter |
|
|
149
|
+
|
|
150
|
+
## Adding a New Emitter
|
|
151
|
+
|
|
152
|
+
1. Create design doc at `docs/sdk-architecture/{language}.md` in the emitter project
|
|
153
|
+
2. Scaffold files under `src/{language}/` in the emitter project
|
|
154
|
+
3. Implement all `Emitter` methods
|
|
155
|
+
4. Register in the emitter project's `oagen.config.ts`
|
|
156
|
+
5. Add tests under `test/{language}/` in the emitter project
|
|
157
|
+
|
|
158
|
+
Or use: `/generate-emitter <language>`
|
|
159
|
+
|
|
160
|
+
Emitters are registered via `oagen.config.ts` — no need to modify CLI source. See the Configuration section in the README.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Extractor Contract
|
|
2
|
+
|
|
3
|
+
Source: `src/compat/types.ts`
|
|
4
|
+
|
|
5
|
+
## Interface
|
|
6
|
+
|
|
7
|
+
Every language extractor must implement the `Extractor` interface:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
interface Extractor {
|
|
11
|
+
language: string;
|
|
12
|
+
extract(sdkPath: string): Promise<ApiSurface>;
|
|
13
|
+
hints: LanguageHints;
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- `language` — the language identifier (e.g., `"node"`, `"ruby"`, `"python"`). Must match the emitter's `language` field.
|
|
18
|
+
- `extract` — analyzes a live SDK at `sdkPath` and returns its public API surface. Must be deterministic: same input produces the same output.
|
|
19
|
+
- `hints` — a `LanguageHints` object that tells the differ and overlay how to interpret type strings for this language. Required — every extractor must explicitly declare its conventions.
|
|
20
|
+
|
|
21
|
+
## ApiSurface
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
interface ApiSurface {
|
|
25
|
+
language: string;
|
|
26
|
+
extractedFrom: string;
|
|
27
|
+
extractedAt: string;
|
|
28
|
+
classes: Record<string, ApiClass>;
|
|
29
|
+
interfaces: Record<string, ApiInterface>;
|
|
30
|
+
typeAliases: Record<string, ApiTypeAlias>;
|
|
31
|
+
enums: Record<string, ApiEnum>;
|
|
32
|
+
exports: Record<string, string[]>;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Each sub-type captures a different kind of public symbol:
|
|
37
|
+
|
|
38
|
+
| Type | Fields | What it captures |
|
|
39
|
+
| -------------- | ---------------------------------------------------- | ------------------------------------------------ |
|
|
40
|
+
| `ApiClass` | `name`, `methods`, `properties`, `constructorParams` | Public classes with their methods and properties |
|
|
41
|
+
| `ApiMethod` | `name`, `params`, `returnType`, `async` | Method signatures |
|
|
42
|
+
| `ApiParam` | `name`, `type`, `optional` | Method and constructor parameters |
|
|
43
|
+
| `ApiProperty` | `name`, `type`, `readonly` | Class properties |
|
|
44
|
+
| `ApiInterface` | `name`, `fields`, `extends` | Interface declarations |
|
|
45
|
+
| `ApiField` | `name`, `type`, `optional` | Interface fields |
|
|
46
|
+
| `ApiTypeAlias` | `name`, `value` | Type alias declarations |
|
|
47
|
+
| `ApiEnum` | `name`, `members` | Enum declarations with member values |
|
|
48
|
+
|
|
49
|
+
The `exports` field maps file paths to their exported symbol names, capturing the barrel export structure.
|
|
50
|
+
|
|
51
|
+
## Rules
|
|
52
|
+
|
|
53
|
+
1. **Deterministic output** — Running the extractor twice on the same SDK must produce identical JSON. Sort keys and members consistently.
|
|
54
|
+
|
|
55
|
+
2. **Public surface only** — Extract only public/exported symbols. Internal implementation details, private methods, and unexported types are excluded.
|
|
56
|
+
|
|
57
|
+
3. **Preserve fidelity** — Method signatures should capture parameter names, types, and optionality as they appear in the live SDK, not as the IR would generate them.
|
|
58
|
+
|
|
59
|
+
4. **Handle missing infrastructure gracefully** — If the language's type system files are missing (e.g., no `tsconfig.json`, no `.pyi` stubs), throw a descriptive error rather than returning an empty surface.
|
|
60
|
+
|
|
61
|
+
## Language Hints
|
|
62
|
+
|
|
63
|
+
Every extractor must provide a `hints: LanguageHints` object that tells the differ and overlay how to interpret language-specific type strings. The `LanguageHints` interface includes:
|
|
64
|
+
|
|
65
|
+
| Method/Property | Purpose | Node example | Go example |
|
|
66
|
+
| ----------------------------------- | --------------------------------------------- | ------------------------------------------------ | ------------------------------------ |
|
|
67
|
+
| `stripNullable(type)` | Strip nullable wrapper | `"string \| null"` → `"string"` | `"*Organization"` → `"Organization"` |
|
|
68
|
+
| `isNullableOnlyDifference(a, b)` | True if a and b differ only by nullability | `"string"` vs `"string \| null"` → true | |
|
|
69
|
+
| `isUnionReorder(a, b)` | True if same union members in different order | `"a" \| "b"` vs `"b" \| "a"` → true | Always false (Go has no unions) |
|
|
70
|
+
| `isGenericTypeParam(type)` | True if type is an unresolvable generic param | `"T"`, `"TCustomAttributes"` → true | |
|
|
71
|
+
| `isExtractionArtifact(type)` | True if type is an extraction artifact | `"any"` → true | `"interface{}"` → true |
|
|
72
|
+
| `tolerateCategoryMismatch` | Allow type alias ↔ interface/class mismatch | `true` (TS allows both forms) | `false` |
|
|
73
|
+
| `extractReturnTypeName(returnType)` | Extract innermost type from return type | `"Promise<Organization>"` → `"Organization"` | |
|
|
74
|
+
| `extractParamTypeName(paramType)` | Extract type from param (null for primitives) | `"string"` → null | |
|
|
75
|
+
| `propertyMatchesClass(prop, class)` | True if property maps to class name | camelCase: `"organizations"` → `"Organizations"` | |
|
|
76
|
+
| `derivedModelNames(modelName)` | Additional names a model produces | `["FooResponse", "SerializedFoo"]` | `["FooResponse"]` |
|
|
77
|
+
|
|
78
|
+
Use `resolveHints({...overrides})` to start from Node defaults and override only the methods that differ for your language:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { resolveHints } from "@workos/oagen";
|
|
82
|
+
|
|
83
|
+
const goHints = resolveHints({
|
|
84
|
+
stripNullable: (type) => (type.startsWith("*") ? type.slice(1) : null),
|
|
85
|
+
isExtractionArtifact: (type) => type === "interface{}",
|
|
86
|
+
tolerateCategoryMismatch: false,
|
|
87
|
+
derivedModelNames: (name) => [`${name}Response`],
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Registration
|
|
92
|
+
|
|
93
|
+
Extractors are registered via `oagen.config.ts` in the consumer project. The CLI loads the config at startup and registers all extractors automatically before any command runs.
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// oagen.config.ts
|
|
97
|
+
import { myExtractor } from "./src/compat/extractors/my-language.js";
|
|
98
|
+
import type { OagenConfig } from "@workos/oagen";
|
|
99
|
+
|
|
100
|
+
const config: OagenConfig = {
|
|
101
|
+
extractors: [myExtractor],
|
|
102
|
+
};
|
|
103
|
+
export default config;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Under the hood, the config loader calls `registerExtractor()` for each entry, populating a Map-based registry (`src/compat/extractor-registry.ts`):
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
registerExtractor(extractor: Extractor): void
|
|
110
|
+
getExtractor(language: string): Extractor
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## File Structure: Parser vs Surface
|
|
114
|
+
|
|
115
|
+
Extractors use tree-sitter for source parsing (except Node, which uses the TypeScript compiler API). The internal file layout depends on the complexity of getting from parsed AST nodes to `ApiSurface` types. There are two reasons a language needs a separate `*-surface.ts` file:
|
|
116
|
+
|
|
117
|
+
1. **Structural joining** — the language separates type definitions from method implementations (e.g., Go receiver functions, Rust impl blocks), so parsed symbols must be joined before they can become `ApiClass` etc.
|
|
118
|
+
2. **Classification** — the language co-locates methods inside classes, but deciding _which_ API surface type a class maps to requires non-trivial heuristics (e.g., PHP resource classes → `ApiInterface`, Python Protocols → `ApiClass`).
|
|
119
|
+
|
|
120
|
+
### Parser only
|
|
121
|
+
|
|
122
|
+
When the AST co-locates methods/properties inside their owning declaration **and** the mapping to API surface types is straightforward enough to handle inline.
|
|
123
|
+
|
|
124
|
+
- **Ruby**: `class Foo` contains `def bar` and `attr_accessor :baz` as direct children. The parser has separate functions (`extractClasses`, `extractServiceModules`, `extractEnumModules`) that each return the appropriate `Api*` type directly.
|
|
125
|
+
- **Node**: The TypeScript compiler API resolves classes with their members in one pass.
|
|
126
|
+
|
|
127
|
+
### Parser + Surface (structural joining)
|
|
128
|
+
|
|
129
|
+
When the language separates type definitions from method implementations. The parser extracts raw symbols (structs, functions, consts) independently, then a surface builder joins them by matching receivers to types, consts to type declarations, etc.
|
|
130
|
+
|
|
131
|
+
- **Go**: `type Foo struct` and `func (f *Foo) Bar()` are separate top-level declarations. The parser extracts `GoStruct[]` and `GoFunc[]`; the surface builder matches functions to structs by receiver type to produce `ApiClass`.
|
|
132
|
+
- **Rust**: `pub struct Foo` and `impl Foo { pub fn bar() }` are separate items. The parser extracts `RustStruct[]` and `RustFunc[]`; the surface builder matches impl methods to structs.
|
|
133
|
+
|
|
134
|
+
### Parser + Surface (classification)
|
|
135
|
+
|
|
136
|
+
When the AST co-locates methods inside classes (like Ruby), but the classification logic — deciding whether a parsed class becomes an `ApiClass`, `ApiInterface`, or `ApiEnum` — is complex enough to warrant its own file.
|
|
137
|
+
|
|
138
|
+
- **PHP**: The parser returns `PhpClass[]` with methods, properties, and constants already attached. The surface builder classifies each class: resource classes (extends configured model base class via `hints.modelBaseClasses`) → `ApiInterface`, const-only classes → `ApiEnum`, PHP interface declarations → `ApiInterface`, service/exception classes → `ApiClass`.
|
|
139
|
+
- **Python**: The parser returns `PythonClass[]`, `PythonTypeAlias[]`, and `__all__` exports. The surface builder classifies: `Protocol` subclasses → `ApiClass`, model base class subclasses (configured via `hints.modelBaseClasses`, e.g., `BaseModel` for Pydantic) → `ApiInterface`, `TypedDict` subclasses → `ApiInterface`, `Literal[...]` type aliases → `ApiEnum`, exception subclasses → `ApiClass`. Transitive base class resolution is needed (a class inheriting from another model class is also a model).
|
|
140
|
+
|
|
141
|
+
| Language | Files | Why |
|
|
142
|
+
| -------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
143
|
+
| Node | `node.ts` | TS compiler API resolves everything in one pass |
|
|
144
|
+
| Ruby | `ruby.ts` + `ruby-parser.ts` | AST co-locates methods in classes/modules — classification handled inline by separate extract functions |
|
|
145
|
+
| Go | `go.ts` + `go-parser.ts` + `go-surface.ts` | Structs and methods are separate declarations — surface builder joins by receiver type and qualifies duplicate names across packages |
|
|
146
|
+
| Rust | `rust.ts` + `rust-parser.ts` + `rust-surface.ts` | Structs and impl blocks are separate items — surface builder joins by impl target type |
|
|
147
|
+
| PHP | `php.ts` + `php-parser.ts` + `php-surface.ts` | Methods co-located in classes, but classification (resource vs enum vs service vs interface) requires heuristics |
|
|
148
|
+
| Python | `python.ts` + `python-parser.ts` + `python-surface.ts` | Methods co-located in classes, but classification (Protocol vs BaseModel vs TypedDict vs Literal vs exception) requires heuristics and transitive base resolution |
|
|
149
|
+
|
|
150
|
+
The `*-parser.ts` file handles tree-sitter AST traversal and returns language-specific intermediate types (`GoStruct`, `PhpClass`, `PythonClass`, etc.). The `*-surface.ts` file transforms those into `ApiSurface` types (`ApiClass`, `ApiInterface`, etc.) — either by structural joining or classification. The `*.ts` orchestrator file owns the `Extractor` constant, `LanguageHints`, file walking, and the `extract()` method.
|
|
151
|
+
|
|
152
|
+
All tree-sitter parsers use `safeParse()` from `src/utils/tree-sitter.ts` to handle files larger than 32KB (works around a buffer size limit in tree-sitter 0.21.x's native binding).
|
|
153
|
+
|
|
154
|
+
## Language-Specific Strategies
|
|
155
|
+
|
|
156
|
+
### Node (reference implementation)
|
|
157
|
+
|
|
158
|
+
- Uses the TypeScript compiler API (`typescript` package) to load and analyze the SDK
|
|
159
|
+
- Reads `tsconfig.json` to discover source files
|
|
160
|
+
- Resolves the entry point from `package.json` `main`/`types` fields
|
|
161
|
+
- Extracts classes, interfaces, type aliases, and enums from exported declarations
|
|
162
|
+
- File: `src/compat/extractors/node.ts`
|
|
163
|
+
|
|
164
|
+
### Ruby
|
|
165
|
+
|
|
166
|
+
- Uses tree-sitter-ruby to parse `.rb` files under `lib/`
|
|
167
|
+
- Extracts classes (with `attr_accessor`/`attr_reader` properties and methods), service modules (modules with `class << self`), and enum-like modules (modules with string constants)
|
|
168
|
+
- Respects `private`/`protected`/`public` visibility markers
|
|
169
|
+
- Files: `src/compat/extractors/ruby.ts`, `ruby-parser.ts`
|
|
170
|
+
|
|
171
|
+
### Go
|
|
172
|
+
|
|
173
|
+
- Uses tree-sitter-go to parse `.go` files (excluding `_test.go`, `vendor/`, `internal/`)
|
|
174
|
+
- Extracts structs (with JSON struct tags), functions/methods (matching receivers to structs), const blocks (enum patterns), and type aliases
|
|
175
|
+
- Package-qualifies duplicate names (e.g., `organizations.Client` vs `sso.Client`)
|
|
176
|
+
- Filters `context.Context` params (infrastructure, not API surface)
|
|
177
|
+
- Files: `src/compat/extractors/go.ts`, `go-parser.ts`, `go-surface.ts`
|
|
178
|
+
|
|
179
|
+
### Rust
|
|
180
|
+
|
|
181
|
+
- Uses tree-sitter-rust to parse `.rs` files under `src/` (excluding `target/`)
|
|
182
|
+
- Extracts pub structs (with serde rename attributes), pub enums (with serde variant renames), impl block methods, pub traits, and type aliases
|
|
183
|
+
- Maps `Option<T>` to optional, unwraps `Result<T, E>` return types
|
|
184
|
+
- Matches impl blocks to struct definitions to distinguish `ApiClass` (has methods) from `ApiInterface` (data only)
|
|
185
|
+
- Files: `src/compat/extractors/rust.ts`, `rust-parser.ts`, `rust-surface.ts`
|
|
186
|
+
|
|
187
|
+
### PHP
|
|
188
|
+
|
|
189
|
+
- Uses tree-sitter-php to parse `.php` files under `lib/` (fallback to `src/`), excluding `vendor/`, `tests/`
|
|
190
|
+
- Parser extracts classes and interfaces with methods, properties, constants, and `RESOURCE_ATTRIBUTES` arrays
|
|
191
|
+
- Surface builder classifies: resource classes (extends configured model base class with `RESOURCE_ATTRIBUTES`) → `ApiInterface`, const-only classes → `ApiEnum`, PHP `interface` declarations → `ApiInterface`, service/exception classes → `ApiClass`
|
|
192
|
+
- Reads both native type hints and PHPDoc `@param`/`@return` annotations for type information
|
|
193
|
+
- Files: `src/compat/extractors/php.ts`, `php-parser.ts`, `php-surface.ts`
|
|
194
|
+
|
|
195
|
+
### Python
|
|
196
|
+
|
|
197
|
+
- Uses tree-sitter-python to parse `.py` files, finding the source root via `src/` or top-level package directories (looks for `__init__.py`)
|
|
198
|
+
- Parser extracts classes (with fields, methods, decorators, base classes), module-level type aliases, and `__all__` exports
|
|
199
|
+
- Surface builder classifies: `Protocol` subclasses → `ApiClass` (canonical service), model base class subclasses (configured via `hints.modelBaseClasses`, e.g., `BaseModel` for Pydantic) → `ApiInterface`, `TypedDict` subclasses → `ApiInterface`, `Literal[...]` type aliases → `ApiEnum`, exception subclasses → `ApiClass`. Uses transitive base resolution for model and exception detection.
|
|
200
|
+
- Skips private modules (`_*.py` except `__init__.py`), test files, and `__pycache__`
|
|
201
|
+
- Files: `src/compat/extractors/python.ts`, `python-parser.ts`, `python-surface.ts`
|
|
202
|
+
|
|
203
|
+
## Building a New Extractor
|
|
204
|
+
|
|
205
|
+
1. Create the extractor in `src/compat/extractors/{language}.ts` in oagen core
|
|
206
|
+
2. Implement the `Extractor` interface — export a named constant (e.g., `export const rubyExtractor: Extractor = { ... }`)
|
|
207
|
+
3. Provide `hints` as a `LanguageHints` object. Every extractor must have hints — the differ and overlay delegate all language-specific logic through them.
|
|
208
|
+
4. Split into `{language}-parser.ts` and `{language}-surface.ts` if either: (a) the language separates type definitions from method implementations (structural joining needed), or (b) the classification logic for mapping parsed types to `ApiClass`/`ApiInterface`/`ApiEnum` is non-trivial. If the AST co-locates symbols and classification is straightforward, a single `{language}-parser.ts` returning `ApiClass[]` directly is sufficient.
|
|
209
|
+
5. Use `safeParse()` from `src/utils/tree-sitter.ts` instead of calling `parser.parse()` directly.
|
|
210
|
+
6. Export from `src/index.ts`
|
|
211
|
+
7. Register in the consumer project's `oagen.config.ts`
|
|
212
|
+
8. Add tests in `test/compat/extractors/{language}.test.ts`
|
|
213
|
+
9. Create a fixture SDK in `test/fixtures/sample-sdk-{language}/` with known classes, methods, and exports
|
|
214
|
+
10. Verify deterministic output: extracting twice produces identical JSON
|
|
215
|
+
|
|
216
|
+
Use `/generate-extractor <language>` for a guided workflow.
|
|
217
|
+
|
|
218
|
+
## Existing Extractors
|
|
219
|
+
|
|
220
|
+
| Language | Files |
|
|
221
|
+
| -------- | ---------------------------------------------------- |
|
|
222
|
+
| Node | `node.ts` |
|
|
223
|
+
| Ruby | `ruby.ts`, `ruby-parser.ts` |
|
|
224
|
+
| Go | `go.ts`, `go-parser.ts`, `go-surface.ts` |
|
|
225
|
+
| Rust | `rust.ts`, `rust-parser.ts`, `rust-surface.ts` |
|
|
226
|
+
| PHP | `php.ts`, `php-parser.ts`, `php-surface.ts` |
|
|
227
|
+
| Python | `python.ts`, `python-parser.ts`, `python-surface.ts` |
|