typespec-rust-emitter 0.1.0 → 0.3.0
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/.qwen/settings.json +11 -0
- package/CHANGELOG.md +86 -0
- package/DEV.md +81 -0
- package/QWEN.md +238 -0
- package/README.md +194 -4
- package/dist/src/emitter.d.ts +2 -0
- package/dist/src/emitter.js +662 -16
- package/dist/src/emitter.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/test/hello.test.js +78 -0
- package/dist/test/hello.test.js.map +1 -1
- package/example/lib/learning/models.tsp +2 -0
- package/example/output-rust/Cargo.lock +639 -1
- package/example/output-rust/Cargo.toml +9 -1
- package/example/output-rust/src/generated/mod.rs +2 -1
- package/example/output-rust/src/generated/server.rs +712 -0
- package/example/output-rust/src/generated/types.rs +20 -42
- package/example/package-lock.json +1 -2
- package/justfile +4 -1
- package/package.json +8 -3
- package/src/emitter.ts +868 -15
- package/src/index.ts +1 -1
- package/src/lib.tsp +4 -2
- package/test/hello.test.ts +100 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.3.0] - 2026-03-27
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `@rustAttr` and `@rustAttrs` decorators for adding custom Rust attributes to models and enums
|
|
13
|
+
- Example: `@rustAttr("sqlx(type_name = \"user\")")` generates `#[sqlx(type_name = "user")]`
|
|
14
|
+
- Support for `@rustDerive` and `@rustDerives` on enums (previously only models)
|
|
15
|
+
- Enables sqlx and other derive macros on enum types
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Custom attributes are now rendered after `#[derive(...)]` for proper Rust syntax
|
|
20
|
+
- Updated example to demonstrate `@rustDerive` + `@rustAttr` combination on enums
|
|
21
|
+
|
|
22
|
+
### Example
|
|
23
|
+
|
|
24
|
+
```typespec
|
|
25
|
+
@rustDerive("sqlx::Type")
|
|
26
|
+
@rustAttr("sqlx(type_name = \"study_status\")")
|
|
27
|
+
enum StudyStatus {
|
|
28
|
+
Starting,
|
|
29
|
+
Paused,
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Generates:
|
|
34
|
+
|
|
35
|
+
```rust
|
|
36
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, sqlx::Type)]
|
|
37
|
+
#[sqlx(type_name = "study_status")]
|
|
38
|
+
pub enum StudyStatus {
|
|
39
|
+
#[serde(rename = "Starting")]
|
|
40
|
+
Starting,
|
|
41
|
+
#[serde(rename = "Paused")]
|
|
42
|
+
Paused,
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## [0.2.0] - 2026-03-27
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- **Breaking**: Server trait now uses associated type instead of generic parameter
|
|
51
|
+
- `pub trait Server<Claims>` → `pub trait Server` with `type Claims: Send + Sync + 'static`
|
|
52
|
+
- Users must now define their own `Claims` type instead of using the generated one
|
|
53
|
+
- **Breaking**: `create_router` signature changed
|
|
54
|
+
- Now accepts middleware function: `create_router<S, M>(service: S, middleware: M)`
|
|
55
|
+
- Middleware type: `FnOnce(Router<S>) -> Router<S>`
|
|
56
|
+
- Protected routes wrapped with: `middleware(protected)`
|
|
57
|
+
- Router now applies `.with_state(service)` once at the end instead of per-route
|
|
58
|
+
- Removed generated `Claims` struct from `types.rs`
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- Demo server implementation in `example/output-rust/src/main.rs`
|
|
63
|
+
- Full server trait documentation in README.md
|
|
64
|
+
|
|
65
|
+
### Benefits
|
|
66
|
+
|
|
67
|
+
- Cleaner API - Claims type defined by user, not generated
|
|
68
|
+
- Flexible middleware - any function matching the trait bound
|
|
69
|
+
- No duplicate `.with_state(service.clone())` calls
|
|
70
|
+
- Better separation of concerns - auth logic owned by user
|
|
71
|
+
|
|
72
|
+
## [0.1.0] - 2026-03-27
|
|
73
|
+
|
|
74
|
+
### Added
|
|
75
|
+
|
|
76
|
+
- Initial release
|
|
77
|
+
- TypeSpec to Rust code generation for:
|
|
78
|
+
- Models → `pub struct` with serde derives
|
|
79
|
+
- Enums → `pub enum` with serde rename
|
|
80
|
+
- Unions → `Option<T>` for nullable types
|
|
81
|
+
- Scalars → `pub type` or `pub struct` with pattern validation
|
|
82
|
+
- Error models → `thiserror::Error` derive
|
|
83
|
+
- Custom derives via `@rustDerive` and `@rustDerives` decorators
|
|
84
|
+
- Server trait generation for axum HTTP operations
|
|
85
|
+
- Support for `@useAuth` decorator to mark protected routes
|
|
86
|
+
- Type mappings for common TypeSpec types and formats
|
package/DEV.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# TypeSpec Rust Emitter - LLM Context
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
TypeSpec emitter that generates idiomatic Rust code (structs, enums, server traits) from TypeSpec specifications.
|
|
6
|
+
|
|
7
|
+
## Key Paths
|
|
8
|
+
|
|
9
|
+
| Path | Purpose |
|
|
10
|
+
| ------------------------------------ | ------------------------------------------------------- |
|
|
11
|
+
| `src/emitter.ts` | Main emitter logic (1464 lines) - TypeSpec→Rust codegen |
|
|
12
|
+
| `src/lib.tsp` | Decorator declarations (`@rustDerive`, `@rustDerives`) |
|
|
13
|
+
| `test/hello.test.ts` | Unit tests using `emit()` helper |
|
|
14
|
+
| `example/lib/learning/` | Demo TypeSpec models & operations |
|
|
15
|
+
| `example/output-rust/src/generated/` | Generated Rust output |
|
|
16
|
+
| `oas3-gen/crates/oas3-gen/` | Reference template for codegen patterns |
|
|
17
|
+
|
|
18
|
+
## Commands (Run After Every Change)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
just build # npm run build - Compile TypeScript
|
|
22
|
+
just test # npm test - Run unit tests
|
|
23
|
+
just compile # tsp compile example → generates Rust code
|
|
24
|
+
just check-rust # cargo check - Verify Rust compiles
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Type Mappings
|
|
28
|
+
|
|
29
|
+
| TypeSpec | Rust |
|
|
30
|
+
| ---------------------- | ----------------------- |
|
|
31
|
+
| `string` | `String` |
|
|
32
|
+
| `int32/64` | `i32/i64` |
|
|
33
|
+
| `T \| null` | `Option<T>` |
|
|
34
|
+
| `T[]` | `Vec<T>` |
|
|
35
|
+
| `Record<T>` | `HashMap<String, T>` |
|
|
36
|
+
| `@format("uuid")` | `uuid::Uuid` |
|
|
37
|
+
| `@format("date-time")` | `chrono::DateTime<Utc>` |
|
|
38
|
+
|
|
39
|
+
## Architecture
|
|
40
|
+
|
|
41
|
+
1. `navigateProgram()` walks TypeSpec AST
|
|
42
|
+
2. Collects models, enums, operations
|
|
43
|
+
3. Processes decorators
|
|
44
|
+
4. Generates Rust code via string templates
|
|
45
|
+
5. Emits to `example/output-rust/src/generated/`
|
|
46
|
+
|
|
47
|
+
## Server Trait Generation
|
|
48
|
+
|
|
49
|
+
- Generates `Server<Claims>` trait with async methods per operation
|
|
50
|
+
- Handler functions: `pub async fn {op}_handler<S, Claims>(...)`
|
|
51
|
+
- All handlers use `<S, Claims>` generics (even public routes)
|
|
52
|
+
- Protected routes (`@useAuth`) receive `claims: Claims` parameter
|
|
53
|
+
- Router splits public/protected routes, merges at end
|
|
54
|
+
|
|
55
|
+
## Test Pattern
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { emit } from "./test-host.js";
|
|
59
|
+
const results = await emit(`model User { name: string; }`);
|
|
60
|
+
const output = results["types.rs"];
|
|
61
|
+
strictEqual(output.includes("pub struct User"), true);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Recent Fix (2026-03-27)
|
|
65
|
+
|
|
66
|
+
All handler functions now use `<S, Claims>` generics uniformly. Public routes include `Claims: Send + Sync + Clone + 'static` bound even without using Claims, because Server trait requires it.
|
|
67
|
+
|
|
68
|
+
## Rules
|
|
69
|
+
|
|
70
|
+
1. Always run full cycle: `build → test → compile → check-rust`
|
|
71
|
+
2. TypeScript strict mode enabled - no implicit any
|
|
72
|
+
3. Match existing code patterns in `src/emitter.ts`
|
|
73
|
+
4. Add tests for new features
|
|
74
|
+
5. Generated Rust must compile
|
|
75
|
+
|
|
76
|
+
## Todo
|
|
77
|
+
|
|
78
|
+
- [ ] More server trait test coverage
|
|
79
|
+
- [ ] Integration tests with Rust compilation
|
|
80
|
+
- [ ] Expand scalar format mappings
|
|
81
|
+
- [ ] Full working Rust server example
|
package/QWEN.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# QWEN.md - Project Context
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
**TypeSpec Rust Emitter** - A TypeScript-based code generator that converts TypeSpec specifications into idiomatic Rust code.
|
|
6
|
+
|
|
7
|
+
- **Type**: TypeScript/Node.js project (TypeSpec emitter)
|
|
8
|
+
- **Output**: Generates Rust structs, enums, server traits with serde, thiserror, axum support
|
|
9
|
+
- **Main Entry**: `src/emitter.ts` - core codegen logic
|
|
10
|
+
- **Package**: `typespec-rust-emitter` v0.3.0
|
|
11
|
+
|
|
12
|
+
## Directory Structure
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
typespec-emitter/
|
|
16
|
+
├── src/
|
|
17
|
+
│ ├── emitter.ts # MAIN: TypeSpec→Rust code generation
|
|
18
|
+
│ ├── index.ts # Exports ($onEmit, $rustDerive, $rustDerives, $rustAttr, $rustAttrs)
|
|
19
|
+
│ ├── lib.ts # TypeSpec library definition
|
|
20
|
+
│ ├── lib.tsp # Decorator declarations
|
|
21
|
+
│ └── testing/ # Test utilities
|
|
22
|
+
├── test/
|
|
23
|
+
│ ├── hello.test.ts # Unit tests
|
|
24
|
+
│ └── test-host.ts # Test infrastructure (emit() helper)
|
|
25
|
+
├── example/
|
|
26
|
+
│ ├── main.tsp # Demo TypeSpec entry
|
|
27
|
+
│ ├── lib/learning/ # Demo models & operations
|
|
28
|
+
│ ├── tspconfig.yaml # Emitter configuration
|
|
29
|
+
│ └── output-rust/ # Generated Rust crate
|
|
30
|
+
├── oas3-gen/ # Reference template repo
|
|
31
|
+
├── dist/ # Compiled TypeScript output
|
|
32
|
+
├── package.json # Dependencies & scripts
|
|
33
|
+
├── tsconfig.json # TypeScript config (strict mode)
|
|
34
|
+
└── justfile # Build automation
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Build & Run Commands
|
|
38
|
+
|
|
39
|
+
### Primary Workflow (Run After Every Change)
|
|
40
|
+
```bash
|
|
41
|
+
just build # Compile TypeScript (npm run build)
|
|
42
|
+
just test # Run unit tests (npm test)
|
|
43
|
+
just compile # Generate Rust code from example
|
|
44
|
+
just check-rust # Verify Rust compiles (cargo check)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Individual Commands
|
|
48
|
+
```bash
|
|
49
|
+
# Development
|
|
50
|
+
npm run build # tsc compile
|
|
51
|
+
npm run watch # Watch mode
|
|
52
|
+
npm run lint # ESLint check
|
|
53
|
+
npm run lint:fix # Auto-fix lint
|
|
54
|
+
npm run format # Prettier format
|
|
55
|
+
npm run format:check
|
|
56
|
+
|
|
57
|
+
# Full pipeline
|
|
58
|
+
just install # npm install (root + example)
|
|
59
|
+
just publish # build + npm publish
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Type Mappings (TypeSpec → Rust)
|
|
63
|
+
|
|
64
|
+
| TypeSpec | Rust |
|
|
65
|
+
|----------|------|
|
|
66
|
+
| `string` | `String` |
|
|
67
|
+
| `int8/16/32/64` | `i8/i16/i32/i64` |
|
|
68
|
+
| `uint8/16/32/64` | `u8/u16/u32/u64` |
|
|
69
|
+
| `float32/64` | `f32/f64` |
|
|
70
|
+
| `boolean` | `bool` |
|
|
71
|
+
| `bytes` | `Vec<u8>` |
|
|
72
|
+
| `T[]` | `Vec<T>` |
|
|
73
|
+
| `Record<T>` | `HashMap<String, T>` |
|
|
74
|
+
| `T \| null` | `Option<T>` |
|
|
75
|
+
|
|
76
|
+
### Format Mappings
|
|
77
|
+
| `@format()` | Rust Type |
|
|
78
|
+
|-------------|-----------|
|
|
79
|
+
| `"uuid"` | `uuid::Uuid` |
|
|
80
|
+
| `"date"` | `chrono::NaiveDate` |
|
|
81
|
+
| `"time"` | `chrono::NaiveTime` |
|
|
82
|
+
| `"date-time"` | `chrono::DateTime<Utc>` |
|
|
83
|
+
|
|
84
|
+
## Decorators
|
|
85
|
+
|
|
86
|
+
| Decorator | Effect |
|
|
87
|
+
|-----------|--------|
|
|
88
|
+
| `@error` | Adds `thiserror::Error` derive + `#[error("{code}: {message}")]` |
|
|
89
|
+
| `@pattern("regex")` | Generates `TryFrom<String>` with regex validation |
|
|
90
|
+
| `@rustDerive("...")` | Adds custom derive (models & enums, e.g., `sqlx::FromRow`, `sqlx::Type`) |
|
|
91
|
+
| `@rustDerives("...", "...")` | Adds multiple custom derives (models & enums) |
|
|
92
|
+
| `@rustAttr("...")` | Adds custom Rust attribute (models & enums, e.g., `sqlx(type_name = "...")`) |
|
|
93
|
+
| `@rustAttrs("...", "...")` | Adds multiple custom attributes (models & enums) |
|
|
94
|
+
| `@doc("...")` | Generates `///` doc comments |
|
|
95
|
+
| `@useAuth(BearerAuth)` | Marks operation as protected (adds `Claims` param) |
|
|
96
|
+
| `@maxLength(n)` | Documentation only |
|
|
97
|
+
|
|
98
|
+
## Output Files
|
|
99
|
+
|
|
100
|
+
### types.rs
|
|
101
|
+
- All models → `pub struct` with serde derives
|
|
102
|
+
- Enums → `pub enum` with serde rename
|
|
103
|
+
- Unions → `Option<T>` for nullable, or sum types
|
|
104
|
+
- Scalars → `pub type` or `pub struct` (if pattern validation)
|
|
105
|
+
|
|
106
|
+
### server.rs
|
|
107
|
+
- `Server` trait with `type Claims` associated type
|
|
108
|
+
- Request structs per operation
|
|
109
|
+
- Response enums with status code variants
|
|
110
|
+
- `create_router<S, M>(service: S, middleware: M)` function with axum routes
|
|
111
|
+
|
|
112
|
+
## Key Implementation Details
|
|
113
|
+
|
|
114
|
+
### emitter.ts Architecture
|
|
115
|
+
1. `navigateProgram()` - Walks TypeSpec AST
|
|
116
|
+
2. Collects: models, enums, unions, scalars, operations
|
|
117
|
+
3. Processes decorators (`@error`, `@pattern`, `@rustDerive`)
|
|
118
|
+
4. Generates Rust via string templates
|
|
119
|
+
5. Emits files to configured output directory
|
|
120
|
+
|
|
121
|
+
### Server Trait Generation
|
|
122
|
+
- Generates `Server` trait with `type Claims: Send + Sync + 'static` associated type
|
|
123
|
+
- Handler functions: `pub async fn {op}_handler<S>(...)` with `<S>` generics only
|
|
124
|
+
- Protected routes (`@useAuth`) receive `claims: Self::Claims` via `Extension<Self::Claims>`
|
|
125
|
+
- Public routes don't require claims
|
|
126
|
+
- `create_router<S, M>(service: S, middleware: M)` accepts middleware function
|
|
127
|
+
- Middleware wraps protected routes: `middleware(protected)`
|
|
128
|
+
- Router merges public/protected, applies `.with_state(service)` once at end
|
|
129
|
+
|
|
130
|
+
### Test Pattern
|
|
131
|
+
```typescript
|
|
132
|
+
import { emit } from "./test-host.js";
|
|
133
|
+
const results = await emit(`model User { name: string; }`);
|
|
134
|
+
const output = results["types.rs"];
|
|
135
|
+
strictEqual(output.includes("pub struct User"), true);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Dependencies
|
|
139
|
+
|
|
140
|
+
### Peer (Required)
|
|
141
|
+
- `@typespec/compiler` >=1.0.0
|
|
142
|
+
- `@typespec/emitter-framework` ^0.17.0
|
|
143
|
+
|
|
144
|
+
### Dev
|
|
145
|
+
- `typescript` ^5.3.3
|
|
146
|
+
- `eslint` ^9.15.0
|
|
147
|
+
- `prettier` ^3.3.3
|
|
148
|
+
|
|
149
|
+
### Generated Rust (example/output-rust/Cargo.toml)
|
|
150
|
+
```toml
|
|
151
|
+
serde = { version = "1.0", features = ["derive"] }
|
|
152
|
+
thiserror = "2.0"
|
|
153
|
+
uuid = { version = "1.23", features = ["serde"] }
|
|
154
|
+
chrono = { version = "0.4", features = ["serde"] }
|
|
155
|
+
regex = "1.12"
|
|
156
|
+
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "derive"] }
|
|
157
|
+
axum = "0.8"
|
|
158
|
+
eyre = "0.6"
|
|
159
|
+
async-trait = "0.1"
|
|
160
|
+
tokio = { version = "1.50", features = ["full"] }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Configuration Files
|
|
164
|
+
|
|
165
|
+
### tspconfig.yaml
|
|
166
|
+
```yaml
|
|
167
|
+
emit:
|
|
168
|
+
- "@typespec/openapi3"
|
|
169
|
+
- "typespec-rust-emitter"
|
|
170
|
+
options:
|
|
171
|
+
"typespec-rust-emitter":
|
|
172
|
+
emitter-output-dir: "{output-dir}/../output-rust/src/generated"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### tsconfig.json
|
|
176
|
+
- Target: ES2022
|
|
177
|
+
- Module: NodeNext
|
|
178
|
+
- Strict: true
|
|
179
|
+
- Root/Out: `.` → `dist`
|
|
180
|
+
|
|
181
|
+
## Development Conventions
|
|
182
|
+
|
|
183
|
+
1. **TypeScript**: Strict mode, ES2022, NodeNext modules
|
|
184
|
+
2. **Testing**: Node.js built-in test runner (`node:test`)
|
|
185
|
+
3. **Formatting**: Prettier (yaml config)
|
|
186
|
+
4. **Linting**: ESLint + typescript-eslint
|
|
187
|
+
5. **Code Style**: Match existing patterns in `src/emitter.ts`
|
|
188
|
+
|
|
189
|
+
## Rules
|
|
190
|
+
|
|
191
|
+
1. Always run full cycle after changes: `build → test → compile → check-rust`
|
|
192
|
+
2. Add tests for new features in `test/hello.test.ts`
|
|
193
|
+
3. Generated Rust must compile (verify with `just check-rust`)
|
|
194
|
+
4. Reference `oas3-gen/` for advanced codegen patterns
|
|
195
|
+
5. No implicit any (strict TypeScript)
|
|
196
|
+
|
|
197
|
+
## Recent Changes
|
|
198
|
+
|
|
199
|
+
### 2026-03-27: v0.3.0 - Custom Attributes & Enum Derives
|
|
200
|
+
|
|
201
|
+
**New Features**:
|
|
202
|
+
- `@rustAttr` / `@rustAttrs` decorators for custom Rust attributes (models & enums)
|
|
203
|
+
- `@rustDerive` / `@rustDerives` now work on enums (previously models only)
|
|
204
|
+
- Attributes render after `#[derive(...)]` for proper Rust syntax
|
|
205
|
+
|
|
206
|
+
**Example**:
|
|
207
|
+
```typespec
|
|
208
|
+
@rustDerive("sqlx::Type")
|
|
209
|
+
@rustAttr("sqlx(type_name = \"study_status\")")
|
|
210
|
+
enum StudyStatus { Starting, Paused }
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
```rust
|
|
214
|
+
#[derive(..., sqlx::Type)]
|
|
215
|
+
#[sqlx(type_name = "study_status")]
|
|
216
|
+
pub enum StudyStatus { ... }
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### 2026-03-27: v0.2.0 - Server Trait Refactoring
|
|
220
|
+
|
|
221
|
+
**Changes**:
|
|
222
|
+
- `pub trait Server<Claims>` → `pub trait Server` with `type Claims` associated type
|
|
223
|
+
- Removed generated `Claims` struct from `types.rs` (users define their own)
|
|
224
|
+
- `create_router<S, M>(service: S, middleware: M)` now accepts middleware function
|
|
225
|
+
- Middleware wraps protected routes: `middleware(protected)`
|
|
226
|
+
- Single `.with_state(service)` call at end of router creation
|
|
227
|
+
|
|
228
|
+
**Benefits**:
|
|
229
|
+
- Cleaner API - Claims type defined by user, not generated
|
|
230
|
+
- Flexible middleware - any function `FnOnce(Router<S>) -> Router<S>`
|
|
231
|
+
- No duplicate `.with_state(service.clone())` calls
|
|
232
|
+
|
|
233
|
+
## TODO
|
|
234
|
+
|
|
235
|
+
- [ ] More server trait test coverage
|
|
236
|
+
- [ ] Integration tests with Rust compilation
|
|
237
|
+
- [ ] Expand scalar format mappings (more chrono types)
|
|
238
|
+
- [ ] Support additional HTTP decorators
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Authored by [opencode](https://opencode.ai)
|
|
1
|
+
Authored by [opencode](https://opencode.ai) & Qwen
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
@@ -18,7 +18,9 @@ A TypeSpec emitter that generates idiomatic Rust types and structs from TypeSpec
|
|
|
18
18
|
- **Inheritance**: Supports model inheritance with `getAllProperties()`
|
|
19
19
|
- **Error Models**: Generates `thiserror::Error` derive with `#[error(...)]` attributes
|
|
20
20
|
- **Pattern Validation**: Supports `@pattern` decorators with `TryFrom<String>` validation
|
|
21
|
-
- **Custom Derives**: Add arbitrary Rust derive macros via `@rustDerive` decorator
|
|
21
|
+
- **Custom Derives**: Add arbitrary Rust derive macros via `@rustDerive` decorator (models & enums)
|
|
22
|
+
- **Custom Attributes**: Add arbitrary Rust attributes via `@rustAttr` decorator (models & enums)
|
|
23
|
+
- **Server Trait**: Generates axum server trait and router from HTTP operations
|
|
22
24
|
|
|
23
25
|
## Installation
|
|
24
26
|
|
|
@@ -36,8 +38,6 @@ npm install @typespec/compiler @typespec/emitter-framework
|
|
|
36
38
|
|
|
37
39
|
## Usage
|
|
38
40
|
|
|
39
|
-
## Usage
|
|
40
|
-
|
|
41
41
|
### Basic Model
|
|
42
42
|
|
|
43
43
|
```typespec
|
|
@@ -179,6 +179,174 @@ pub struct GroupStatistics {
|
|
|
179
179
|
}
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
+
### Custom Attributes
|
|
183
|
+
|
|
184
|
+
```typespec
|
|
185
|
+
import "typespec-emitter";
|
|
186
|
+
|
|
187
|
+
@rustAttr("sqlx(type_name = \"user\")")
|
|
188
|
+
model User {
|
|
189
|
+
name: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@rustDerive("sqlx::Type")
|
|
193
|
+
@rustAttr("sqlx(type_name = \"study_status\")")
|
|
194
|
+
enum StudyStatus {
|
|
195
|
+
Starting,
|
|
196
|
+
Paused,
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Generates:
|
|
201
|
+
|
|
202
|
+
```rust
|
|
203
|
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
204
|
+
#[sqlx(type_name = "user")]
|
|
205
|
+
pub struct User {
|
|
206
|
+
#[serde(rename = "name")]
|
|
207
|
+
pub name: String,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, sqlx::Type)]
|
|
211
|
+
#[sqlx(type_name = "study_status")]
|
|
212
|
+
pub enum StudyStatus {
|
|
213
|
+
#[serde(rename = "Starting")]
|
|
214
|
+
Starting,
|
|
215
|
+
#[serde(rename = "Paused")]
|
|
216
|
+
Paused,
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Server Trait Generation
|
|
221
|
+
|
|
222
|
+
The emitter can generate a complete axum server trait and router from TypeSpec HTTP operations.
|
|
223
|
+
|
|
224
|
+
### Example TypeSpec
|
|
225
|
+
|
|
226
|
+
```typespec
|
|
227
|
+
@route("/groups")
|
|
228
|
+
namespace Groups {
|
|
229
|
+
@get
|
|
230
|
+
@route("")
|
|
231
|
+
op list(): Group[];
|
|
232
|
+
|
|
233
|
+
@post
|
|
234
|
+
@route("")
|
|
235
|
+
@useAuth(BearerAuth)
|
|
236
|
+
op create(@body body: CreateGroupBody): Group;
|
|
237
|
+
|
|
238
|
+
@get
|
|
239
|
+
@route("/{id}")
|
|
240
|
+
op getById(@path id: int64): Group;
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Generated Server Trait
|
|
245
|
+
|
|
246
|
+
```rust
|
|
247
|
+
#[async_trait]
|
|
248
|
+
pub trait Server: Send + Sync {
|
|
249
|
+
type Claims: Send + Sync + 'static;
|
|
250
|
+
|
|
251
|
+
async fn groups_list(&self, request: GroupsListRequest) -> Result<GroupsListResponse>;
|
|
252
|
+
async fn groups_create(&self, claims: Self::Claims, request: GroupsCreateRequest) -> Result<GroupsCreateResponse>;
|
|
253
|
+
async fn groups_get_by_id(&self, request: GroupsGetByIdRequest) -> Result<GroupsGetByIdResponse>;
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Implementing the Server
|
|
258
|
+
|
|
259
|
+
```rust
|
|
260
|
+
#[derive(Clone)]
|
|
261
|
+
pub struct AppState {
|
|
262
|
+
pub db: Arc<SqlitePool>,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
pub struct MyServer {
|
|
266
|
+
state: AppState,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
#[async_trait::async_trait]
|
|
270
|
+
impl Server for MyServer {
|
|
271
|
+
type Claims = Claims; // Your custom claims type
|
|
272
|
+
|
|
273
|
+
async fn groups_list(
|
|
274
|
+
&self,
|
|
275
|
+
_request: GroupsListRequest,
|
|
276
|
+
) -> eyre::Result<GroupsListResponse> {
|
|
277
|
+
let groups = sqlx::query_as::<_, Group>("SELECT * FROM groups")
|
|
278
|
+
.fetch_all(&self.state.db)
|
|
279
|
+
.await?;
|
|
280
|
+
Ok(GroupsListResponse::Ok(Json(groups)))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async fn groups_create(
|
|
284
|
+
&self,
|
|
285
|
+
claims: Self::Claims,
|
|
286
|
+
request: GroupsCreateRequest,
|
|
287
|
+
) -> eyre::Result<GroupsCreateResponse> {
|
|
288
|
+
// claims.sub contains the authenticated user ID
|
|
289
|
+
let group = sqlx::query_as::<_, Group>(
|
|
290
|
+
"INSERT INTO groups (name, owner_id) VALUES ($1, $2) RETURNING *"
|
|
291
|
+
)
|
|
292
|
+
.bind(&request.body.name)
|
|
293
|
+
.bind(&claims.sub)
|
|
294
|
+
.fetch_one(&self.state.db)
|
|
295
|
+
.await?;
|
|
296
|
+
Ok(GroupsCreateResponse::Created(Json(group)))
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ... implement other methods
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Creating the Router
|
|
304
|
+
|
|
305
|
+
```rust
|
|
306
|
+
// Define your claims type
|
|
307
|
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
308
|
+
pub struct Claims {
|
|
309
|
+
pub sub: String,
|
|
310
|
+
pub exp: usize,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Create auth middleware
|
|
314
|
+
async fn auth_middleware(
|
|
315
|
+
Extension(claims): Extension<Claims>,
|
|
316
|
+
request: Request<axum::body::Body>,
|
|
317
|
+
next: Next,
|
|
318
|
+
) -> Result<Response, StatusCode> {
|
|
319
|
+
// Validate claims (e.g., check expiration)
|
|
320
|
+
if claims.exp < (chrono::Utc::now().timestamp() as usize) {
|
|
321
|
+
return Err(StatusCode::UNAUTHORIZED);
|
|
322
|
+
}
|
|
323
|
+
Ok(next.run(request).await)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Apply middleware to protected routes
|
|
327
|
+
fn with_auth<S>(router: Router<S>) -> Router<S>
|
|
328
|
+
where
|
|
329
|
+
S: Server + Clone + Send + Sync + 'static,
|
|
330
|
+
S::Claims: Send + Sync + Clone + 'static,
|
|
331
|
+
{
|
|
332
|
+
router.layer(axum::middleware::from_fn(auth_middleware))
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Create the router
|
|
336
|
+
let server = MyServer::new(state);
|
|
337
|
+
let router = create_router(server, with_auth);
|
|
338
|
+
axum::serve(listener, router).await?;
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Protected Routes
|
|
342
|
+
|
|
343
|
+
Operations decorated with `@useAuth` will:
|
|
344
|
+
|
|
345
|
+
- Receive `claims: Self::Claims` as the first parameter
|
|
346
|
+
- Be wrapped with the middleware passed to `create_router`
|
|
347
|
+
|
|
348
|
+
Public routes do not receive claims and are not wrapped with auth middleware.
|
|
349
|
+
|
|
182
350
|
## Type Mappings
|
|
183
351
|
|
|
184
352
|
| TypeSpec Type | Rust Type |
|
|
@@ -193,6 +361,28 @@ pub struct GroupStatistics {
|
|
|
193
361
|
| `Record<string>` | `std::collections::HashMap<String, String>` |
|
|
194
362
|
| `T \| null` | `Option<T>` |
|
|
195
363
|
|
|
364
|
+
### Format Mappings
|
|
365
|
+
|
|
366
|
+
| `@format()` | Rust Type |
|
|
367
|
+
| ------------- | ----------------------- |
|
|
368
|
+
| `"uuid"` | `uuid::Uuid` |
|
|
369
|
+
| `"date"` | `chrono::NaiveDate` |
|
|
370
|
+
| `"time"` | `chrono::NaiveTime` |
|
|
371
|
+
| `"date-time"` | `chrono::DateTime<Utc>` |
|
|
372
|
+
|
|
373
|
+
## Decorators
|
|
374
|
+
|
|
375
|
+
| Decorator | Description |
|
|
376
|
+
| ---------------------------- | ---------------------------------------------------------------- |
|
|
377
|
+
| `@error` | Adds `thiserror::Error` derive with error message |
|
|
378
|
+
| `@pattern("regex")` | Generates `TryFrom<String>` with regex validation |
|
|
379
|
+
| `@rustDerive("...")` | Adds a custom derive macro (models & enums) |
|
|
380
|
+
| `@rustDerives("...", "...")` | Adds multiple custom derive macros (models & enums) |
|
|
381
|
+
| `@rustAttr("...")` | Adds a custom Rust attribute (models & enums) |
|
|
382
|
+
| `@rustAttrs("...", "...")` | Adds multiple custom Rust attributes (models & enums) |
|
|
383
|
+
| `@doc("...")` | Generates `///` doc comments |
|
|
384
|
+
| `@useAuth(AuthType)` | Marks operation as protected, adds `Claims` parameter |
|
|
385
|
+
|
|
196
386
|
## Building
|
|
197
387
|
|
|
198
388
|
```bash
|
package/dist/src/emitter.d.ts
CHANGED
|
@@ -4,4 +4,6 @@ export interface RustEmitterOptions {
|
|
|
4
4
|
}
|
|
5
5
|
export declare function $rustDerive(context: DecoratorContext, target: Type, derive: string): void;
|
|
6
6
|
export declare function $rustDerives(context: DecoratorContext, target: Type, ...derives: string[]): void;
|
|
7
|
+
export declare function $rustAttr(context: DecoratorContext, target: Type, attr: string): void;
|
|
8
|
+
export declare function $rustAttrs(context: DecoratorContext, target: Type, ...attrs: string[]): void;
|
|
7
9
|
export declare function $onEmit(context: EmitContext<RustEmitterOptions>, _options?: RustEmitterOptions): Promise<void>;
|