typespec-rust-emitter 0.8.0 → 0.10.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/.env +1 -0
- package/AGENTS.md +38 -147
- package/CHANGELOG.md +92 -0
- package/README.md +17 -9
- package/dist/src/emitter.d.ts +1 -0
- package/dist/src/emitter.js +49 -43
- 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 +58 -2
- package/dist/test/hello.test.js.map +1 -1
- package/example/output-rust/src/generated/types.rs +26 -27
- package/justfile +3 -1
- package/package.json +1 -1
- package/src/emitter.ts +59 -53
- package/src/index.ts +7 -1
- package/src/lib.tsp +1 -0
- package/test/hello.test.ts +65 -2
package/.env
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
NPM_TOKEN=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
|
package/AGENTS.md
CHANGED
|
@@ -1,167 +1,58 @@
|
|
|
1
|
-
# TypeSpec Rust Emitter
|
|
1
|
+
# TypeSpec Rust Emitter
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
TypeSpec emitter generating idiomatic Rust code (structs, enums, server traits).
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Build, Lint, and Test Commands
|
|
8
|
-
|
|
9
|
-
### Standard Commands
|
|
5
|
+
## Commands
|
|
10
6
|
|
|
11
7
|
```bash
|
|
12
|
-
# Build
|
|
13
|
-
npm run build
|
|
14
|
-
|
|
15
|
-
# Run tests
|
|
16
|
-
npm test # or: just test
|
|
17
|
-
node --test 'dist/test/**/*.test.js'
|
|
18
|
-
|
|
19
|
-
# Run a single test file
|
|
20
|
-
node --test 'dist/test/hello.test.js'
|
|
8
|
+
# Build -> test (lint runs automatically)
|
|
9
|
+
npm run build && npm test
|
|
21
10
|
|
|
22
|
-
# Run
|
|
23
|
-
|
|
11
|
+
# Run tests only
|
|
12
|
+
npm test
|
|
24
13
|
|
|
25
|
-
#
|
|
26
|
-
npm run lint # Check linting
|
|
27
|
-
npm run lint:fix # Auto-fix linting issues
|
|
14
|
+
# Single test: node --test 'dist/test/hello.test.js' --test-name-pattern="emits model"
|
|
28
15
|
|
|
29
|
-
#
|
|
30
|
-
npm run
|
|
31
|
-
npm run format
|
|
16
|
+
# Lint & format (run after every change)
|
|
17
|
+
npm run lint
|
|
18
|
+
npm run format
|
|
32
19
|
|
|
33
20
|
# Compile TypeSpec to Rust
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# Verify generated Rust compiles
|
|
37
|
-
just check-rust # Runs: cd example/output-rust && cargo check
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
### Full Development Cycle
|
|
41
|
-
|
|
42
|
-
After every change, run this sequence:
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
just build && npm test && just compile && just check-rust
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Project Structure
|
|
49
|
-
|
|
50
|
-
| Path | Purpose |
|
|
51
|
-
| ------------------------------------ | -------------------------------------------------------- |
|
|
52
|
-
| `src/emitter.ts` | Main emitter logic (~1500 lines) - TypeSpec→Rust codegen |
|
|
53
|
-
| `src/lib.tsp` | Decorator declarations (`@rustDerive`, `@rustDerives`) |
|
|
54
|
-
| `src/lib.ts` | Decorator implementations |
|
|
55
|
-
| `src/index.ts` | Public API exports |
|
|
56
|
-
| `src/testing/index.ts` | Test utilities |
|
|
57
|
-
| `test/hello.test.ts` | Unit tests using `emit()` helper |
|
|
58
|
-
| `test/test-host.ts` | Test host setup |
|
|
59
|
-
| `example/lib/` | Demo TypeSpec models & operations |
|
|
60
|
-
| `example/output-rust/src/generated/` | Generated Rust output |
|
|
61
|
-
|
|
62
|
-
## Code Style
|
|
63
|
-
|
|
64
|
-
### TypeScript
|
|
65
|
-
|
|
66
|
-
- **Strict mode enabled** - No implicit `any`
|
|
67
|
-
- **No `any` types** - Use `unknown` or proper generics instead
|
|
68
|
-
- **Explicit return types** on public functions
|
|
69
|
-
- **Interfaces over type aliases** for object shapes
|
|
21
|
+
cd example && tsp compile .
|
|
70
22
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
- Named imports from `@typespec/compiler` first, then external packages
|
|
74
|
-
- Group imports: 1) `@typespec/*`, 2) external, 3) internal
|
|
75
|
-
- Use `import type { X }` for type-only imports when appropriate
|
|
76
|
-
|
|
77
|
-
### Formatting (Prettier)
|
|
78
|
-
|
|
79
|
-
Configuration in `prettierrc.yaml`:
|
|
80
|
-
|
|
81
|
-
- `printWidth: 120`
|
|
82
|
-
- `trailingComma: "all"`
|
|
83
|
-
- `arrowParens: always`
|
|
84
|
-
- `endOfLine: lf`
|
|
85
|
-
|
|
86
|
-
Use `npm run format` before committing.
|
|
87
|
-
|
|
88
|
-
### Naming Conventions
|
|
89
|
-
|
|
90
|
-
| Element | Convention | Example |
|
|
91
|
-
| --------------------------- | ---------- | -------------------- |
|
|
92
|
-
| TypeScript functions | camelCase | `$rustDerive` |
|
|
93
|
-
| TypeScript types/interfaces | PascalCase | `RustEmitterOptions` |
|
|
94
|
-
| Rust types | PascalCase | `pub struct User` |
|
|
95
|
-
| Rust functions/methods | snake_case | `get_user_by_id` |
|
|
96
|
-
| Internal symbols | camelCase | `rustDeriveKey` |
|
|
97
|
-
|
|
98
|
-
### Type Mappings (TypeSpec → Rust)
|
|
99
|
-
|
|
100
|
-
| TypeSpec | Rust |
|
|
101
|
-
| ---------------------- | ----------------------- |
|
|
102
|
-
| `string` | `String` |
|
|
103
|
-
| `int32/64` | `i32/i64` |
|
|
104
|
-
| `float32/64` | `f32/f64` |
|
|
105
|
-
| `boolean` | `bool` |
|
|
106
|
-
| `T \| null` | `Option<T>` |
|
|
107
|
-
| `T[]` | `Vec<T>` |
|
|
108
|
-
| `Record<T>` | `HashMap<String, T>` |
|
|
109
|
-
| `@format("uuid")` | `uuid::Uuid` |
|
|
110
|
-
| `@format("date-time")` | `chrono::DateTime<Utc>` |
|
|
111
|
-
|
|
112
|
-
## Error Handling
|
|
113
|
-
|
|
114
|
-
- Use `context.program.reportDiagnostic()` for errors and warnings
|
|
115
|
-
- Diagnostic codes should use kebab-case: `code: "rust-derive-invalid-target"`
|
|
116
|
-
- Always specify `severity: "error"` or `"warning"`
|
|
117
|
-
- Include `target: context.decoratorTarget` for location info
|
|
118
|
-
|
|
119
|
-
## Testing Pattern
|
|
120
|
-
|
|
121
|
-
```typescript
|
|
122
|
-
import { emit } from "./test-host.js";
|
|
123
|
-
|
|
124
|
-
const results = await emit(`model User { name: string; }`);
|
|
125
|
-
const output = results["types.rs"];
|
|
126
|
-
strictEqual(output.includes("pub struct User"), true);
|
|
23
|
+
# Verify Rust compiles
|
|
24
|
+
cd example/output-rust && cargo check
|
|
127
25
|
```
|
|
128
26
|
|
|
129
|
-
##
|
|
130
|
-
|
|
131
|
-
1. Generates `Server<Claims>` trait with async methods per operation
|
|
132
|
-
2. Handler functions: `pub async fn {op}_handler<S, Claims>(...)`
|
|
133
|
-
3. All handlers use `<S, Claims>` generics (even public routes)
|
|
134
|
-
4. Protected routes (`@useAuth`) receive `claims: Claims` parameter
|
|
135
|
-
5. Router splits public/protected routes, merges at end
|
|
136
|
-
6. Public routes include `Claims: Send + Sync + Clone + 'static` bound
|
|
137
|
-
|
|
138
|
-
## Architecture
|
|
27
|
+
## Project Layout
|
|
139
28
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
29
|
+
| Path | Purpose |
|
|
30
|
+
|------|--------|
|
|
31
|
+
| `src/emitter.ts` | Main codegen (~1600 lines) |
|
|
32
|
+
| `src/lib.tsp` | Decorator declarations |
|
|
33
|
+
| `src/index.ts` | Exports |
|
|
34
|
+
| `test/hello.test.ts` | Unit tests |
|
|
35
|
+
| `example/lib/` | Demo TypeSpec specs |
|
|
36
|
+
| `example/output-rust/` | Generated Rust |
|
|
145
37
|
|
|
146
|
-
##
|
|
38
|
+
## Decorators
|
|
147
39
|
|
|
148
|
-
|
|
40
|
+
All decorators go in `src/lib.tsp` (declaration) + `src/emitter.ts` (implementation):
|
|
41
|
+
- `@rustDerive` / `@rustDerives` - derive macros
|
|
42
|
+
- `@rustAttr` / `@rustAttrs` - attribute macros
|
|
43
|
+
- `@rustImpl` - custom impl blocks
|
|
149
44
|
|
|
150
|
-
|
|
151
|
-
- Ignores `dist/**` and `.temp/**`
|
|
152
|
-
- Unused variables: warn with `_` prefix exception
|
|
45
|
+
## Style
|
|
153
46
|
|
|
154
|
-
|
|
47
|
+
- Strict TypeScript: no implicit `any`, explicit return types
|
|
48
|
+
- Prettier: `printWidth: 120`, `trailingComma: "all"` (see `prettierrc.yaml`)
|
|
49
|
+
- Import order: `@typespec/*`, external, internal
|
|
155
50
|
|
|
156
|
-
|
|
157
|
-
- Focus on "why" rather than "what"
|
|
158
|
-
- Keep concise (1-2 sentences)
|
|
159
|
-
- Example: "Fix handler generics for public routes without auth"
|
|
51
|
+
## Release (let user handle)
|
|
160
52
|
|
|
161
|
-
|
|
53
|
+
When releasing a new version:
|
|
54
|
+
1. Bump `version` in `package.json`
|
|
55
|
+
2. Update `CHANGELOG.md` (include issue refs)
|
|
56
|
+
3. Run `just publish` (requires `NPM_TOKEN` in `.env` or env var)
|
|
162
57
|
|
|
163
|
-
|
|
164
|
-
- [ ] Add tests for new features
|
|
165
|
-
- [ ] Run `npm run lint` and `npm run format`
|
|
166
|
-
- [ ] Generated Rust must compile with `cargo check`
|
|
167
|
-
- [ ] No TypeScript errors
|
|
58
|
+
Do not publish yourself—inform user when ready for release.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,98 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.10.0] - 2026-04-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **@rustImpl decorator for custom impl blocks**: New decorator allows adding arbitrary `impl` blocks to models
|
|
13
|
+
- Enables custom implementations like `IntoResponse`, trait implementations, or any custom methods
|
|
14
|
+
- Example: `@rustImpl("impl IntoResponse for ApiError { ... }")`
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Breaking**: Removed automatic `thiserror::Error` derive from `@error` models
|
|
19
|
+
- Users must now add manually via `@rustDerive("thiserror::Error")` and `@rustAttr("error(\"{code}: {message}\")")`
|
|
20
|
+
- Provides full control over error behavior
|
|
21
|
+
- **Breaking**: Removed automatic `IntoResponse` implementation for error models
|
|
22
|
+
- Users can add custom impl via `@rustImpl` decorator
|
|
23
|
+
- Example:
|
|
24
|
+
```typespec
|
|
25
|
+
@rustDerive("thiserror::Error")
|
|
26
|
+
@rustAttr("error(\"{code}: {message}\")")
|
|
27
|
+
@rustImpl("impl IntoResponse for ApiError { fn into_response(self) -> axum::response::Response { ... } }")
|
|
28
|
+
model ApiError { ... }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Example
|
|
32
|
+
|
|
33
|
+
```typespec
|
|
34
|
+
import "typespec-rust-emitter";
|
|
35
|
+
|
|
36
|
+
@rustImpl("impl IntoResponse for NotFoundError {
|
|
37
|
+
fn into_response(self) -> axum::response::Response {
|
|
38
|
+
(StatusCode::NOT_FOUND, Json(self)).into_response()
|
|
39
|
+
}
|
|
40
|
+
}")
|
|
41
|
+
model NotFoundError {
|
|
42
|
+
code: string;
|
|
43
|
+
message: string;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Generates:
|
|
48
|
+
|
|
49
|
+
```rust
|
|
50
|
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
51
|
+
pub struct NotFoundError {
|
|
52
|
+
pub code: String,
|
|
53
|
+
pub message: String,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
impl IntoResponse for NotFoundError {
|
|
57
|
+
fn into_response(self) -> axum::response::Response {
|
|
58
|
+
(StatusCode::NOT_FOUND, Json(self)).into_response()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## [0.9.0] - 2026-03-31
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- **Error `IntoResponse` now uses dynamic status code from `code` field**: Instead of mapping status codes by error type name, errors now use `StatusCode::from_str(&self.code)` to determine the HTTP status code at runtime
|
|
68
|
+
- This allows defining any error type with custom codes without changing the emitter
|
|
69
|
+
- Falls back to `INTERNAL_SERVER_ERROR` if the code is not a valid HTTP status string
|
|
70
|
+
|
|
71
|
+
### Example
|
|
72
|
+
|
|
73
|
+
```rust
|
|
74
|
+
// Error model with any code
|
|
75
|
+
model CustomError {
|
|
76
|
+
code: string; // e.g., "NOT_FOUND", "BAD_REQUEST", "MY_CUSTOM_CODE"
|
|
77
|
+
message: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
impl IntoResponse for CustomError {
|
|
81
|
+
fn into_response(self) -> axum::response::Response {
|
|
82
|
+
(
|
|
83
|
+
StatusCode::from_str(&self.code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
|
|
84
|
+
Json(self),
|
|
85
|
+
)
|
|
86
|
+
.into_response()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Fixed
|
|
92
|
+
|
|
93
|
+
- **Clippy warnings**: Enums and newtype structs with pattern validation now use `#[derive(Default)]` with `#[default]` attribute instead of manual `impl Default`, eliminating `clippy::derivable_impls` warnings
|
|
94
|
+
|
|
95
|
+
### Internal
|
|
96
|
+
|
|
97
|
+
- Added `use std::str::FromStr;` import to generated types.rs
|
|
98
|
+
- Removed unused `getHttpStatusCodeForError()` function
|
|
99
|
+
|
|
8
100
|
## [0.8.0] - 2026-03-30
|
|
9
101
|
|
|
10
102
|
### Added
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ A TypeSpec emitter that generates idiomatic Rust types and structs from TypeSpec
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
14
|
- **Models**: Converts TypeSpec models to Rust structs with serde derive macros
|
|
15
|
-
- **Enums**: Supports both string and integer enums
|
|
15
|
+
- **Enums**: Supports both string and integer enums with `Default` derive
|
|
16
16
|
- **Unions**: Handles nullable types (`T | null` → `Option<T>`) and string literal unions
|
|
17
17
|
- **Scalars**: Maps TypeSpec scalars to Rust equivalents with `@format` support
|
|
18
18
|
- **Inheritance**: Supports model inheritance with `getAllProperties()`
|
|
@@ -78,8 +78,9 @@ enum Status {
|
|
|
78
78
|
Generates:
|
|
79
79
|
|
|
80
80
|
```rust
|
|
81
|
-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
|
81
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
|
|
82
82
|
pub enum Status {
|
|
83
|
+
#[default]
|
|
83
84
|
#[serde(rename = "active")]
|
|
84
85
|
Active,
|
|
85
86
|
#[serde(rename = "inactive")]
|
|
@@ -87,12 +88,6 @@ pub enum Status {
|
|
|
87
88
|
#[serde(rename = "pending")]
|
|
88
89
|
Pending,
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
-
impl Default for Status {
|
|
92
|
-
fn default() -> Self {
|
|
93
|
-
Status::Active
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
91
|
```
|
|
97
92
|
|
|
98
93
|
### Error Model
|
|
@@ -116,8 +111,20 @@ pub struct ApiError {
|
|
|
116
111
|
#[serde(rename = "message")]
|
|
117
112
|
pub message: String,
|
|
118
113
|
}
|
|
114
|
+
|
|
115
|
+
impl IntoResponse for ApiError {
|
|
116
|
+
fn into_response(self) -> axum::response::Response {
|
|
117
|
+
(
|
|
118
|
+
StatusCode::from_str(&self.code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
|
|
119
|
+
Json(self),
|
|
120
|
+
)
|
|
121
|
+
.into_response()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
119
124
|
```
|
|
120
125
|
|
|
126
|
+
Error codes are parsed dynamically, so any valid HTTP status code string (e.g., `"NOT_FOUND"`, `"BAD_REQUEST"`, `"418 I'M_A_TEAPOT"`) will work.
|
|
127
|
+
|
|
121
128
|
### UUID Format
|
|
122
129
|
|
|
123
130
|
```typespec
|
|
@@ -207,9 +214,10 @@ pub struct User {
|
|
|
207
214
|
pub name: String,
|
|
208
215
|
}
|
|
209
216
|
|
|
210
|
-
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, sqlx::Type)]
|
|
217
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize, sqlx::Type)]
|
|
211
218
|
#[sqlx(type_name = "study_status")]
|
|
212
219
|
pub enum StudyStatus {
|
|
220
|
+
#[default]
|
|
213
221
|
#[serde(rename = "Starting")]
|
|
214
222
|
Starting,
|
|
215
223
|
#[serde(rename = "Paused")]
|
package/dist/src/emitter.d.ts
CHANGED
|
@@ -6,4 +6,5 @@ export declare function $rustDerive(context: DecoratorContext, target: Type, der
|
|
|
6
6
|
export declare function $rustDerives(context: DecoratorContext, target: Type, ...derives: string[]): void;
|
|
7
7
|
export declare function $rustAttr(context: DecoratorContext, target: Type, attr: string): void;
|
|
8
8
|
export declare function $rustAttrs(context: DecoratorContext, target: Type, ...attrs: string[]): void;
|
|
9
|
+
export declare function $rustImpl(context: DecoratorContext, target: Type, impl: string): void;
|
|
9
10
|
export declare function $onEmit(context: EmitContext<RustEmitterOptions>, _options?: RustEmitterOptions): Promise<void>;
|
package/dist/src/emitter.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { emitFile, resolvePath, navigateProgram, getDoc, isArrayModelType, isRecordModelType, getFormat, getPattern,
|
|
1
|
+
import { emitFile, resolvePath, navigateProgram, getDoc, isArrayModelType, isRecordModelType, getFormat, getPattern, getNamespaceFullName, getTags, } from "@typespec/compiler";
|
|
2
2
|
const rustDeriveKey = Symbol("rustDerive");
|
|
3
3
|
const rustAttrKey = Symbol("rustAttr");
|
|
4
|
+
const rustImplKey = Symbol("rustImpl");
|
|
4
5
|
export function $rustDerive(context, target, derive) {
|
|
5
6
|
if (target.kind !== "Model" && target.kind !== "Enum") {
|
|
6
7
|
context.program.reportDiagnostic({
|
|
@@ -73,6 +74,22 @@ export function $rustAttrs(context, target, ...attrs) {
|
|
|
73
74
|
$rustAttr(context, target, attr);
|
|
74
75
|
}
|
|
75
76
|
}
|
|
77
|
+
export function $rustImpl(context, target, impl) {
|
|
78
|
+
if (target.kind !== "Model") {
|
|
79
|
+
context.program.reportDiagnostic({
|
|
80
|
+
code: "rust-impl-invalid-target",
|
|
81
|
+
message: `@rustImpl can only be applied to models`,
|
|
82
|
+
severity: "error",
|
|
83
|
+
target: context.decoratorTarget,
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const ns = target.namespace ? getNamespaceFullName(target.namespace) : "";
|
|
88
|
+
if (!ns.startsWith("TypeSpec")) {
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
90
|
+
target[rustImplKey] = { impl: impl };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
76
93
|
function getDecoratorName(decorator) {
|
|
77
94
|
if (!decorator)
|
|
78
95
|
return "";
|
|
@@ -410,15 +427,6 @@ function getHttpStatusCode(statusCode) {
|
|
|
410
427
|
};
|
|
411
428
|
return statusCodes[statusCode] || `StatusCode::from_u16(${statusCode})`;
|
|
412
429
|
}
|
|
413
|
-
function getHttpStatusCodeForError(errorName) {
|
|
414
|
-
if (errorName.endsWith("NotFoundError"))
|
|
415
|
-
return "NOT_FOUND";
|
|
416
|
-
if (errorName.endsWith("ValidationError"))
|
|
417
|
-
return "BAD_REQUEST";
|
|
418
|
-
if (errorName.endsWith("ConflictError"))
|
|
419
|
-
return "CONFLICT";
|
|
420
|
-
return "INTERNAL_SERVER_ERROR";
|
|
421
|
-
}
|
|
422
430
|
function generateServerTrait(program, namespaceGroups, anonymousEnums) {
|
|
423
431
|
const parts = [];
|
|
424
432
|
parts.push(`use super::types::*;
|
|
@@ -880,24 +888,24 @@ function emitStringLiteralUnion(union) {
|
|
|
880
888
|
const parts = [];
|
|
881
889
|
const name = toPascalCase(union.name ?? "Value");
|
|
882
890
|
const variants = Array.from(union.variants.values());
|
|
883
|
-
parts.push(`#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub enum ${name} {`);
|
|
884
|
-
for (
|
|
885
|
-
const literalType =
|
|
891
|
+
parts.push(`#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]\npub enum ${name} {`);
|
|
892
|
+
for (let i = 0; i < variants.length; i++) {
|
|
893
|
+
const literalType = variants[i].type;
|
|
886
894
|
const variantName = toRustVariantName(literalType.value);
|
|
887
895
|
const serdeValue = literalType.value;
|
|
896
|
+
if (i === 0) {
|
|
897
|
+
parts.push(` #[default]`);
|
|
898
|
+
}
|
|
888
899
|
parts.push(` #[serde(rename = "${serdeValue}")]`);
|
|
889
900
|
parts.push(` ${variantName},`);
|
|
890
901
|
}
|
|
891
902
|
parts.push("}");
|
|
892
|
-
const defaultVariant = toRustVariantName(variants[0]?.type ? variants[0].type.value : "");
|
|
893
|
-
parts.push(`\n\nimpl Default for ${name} {\n fn default() -> Self {\n ${name}::${defaultVariant}\n }\n}`);
|
|
894
903
|
return parts.join("\n");
|
|
895
904
|
}
|
|
896
905
|
function emitModel(model, program, anonymousEnums) {
|
|
897
906
|
const parts = [];
|
|
898
907
|
const name = toPascalCase(model.name);
|
|
899
908
|
const allProps = getAllProperties(model, program);
|
|
900
|
-
const isError = isErrorModel(program, model);
|
|
901
909
|
const deriveAttrs = [
|
|
902
910
|
"Debug",
|
|
903
911
|
"Clone",
|
|
@@ -909,13 +917,7 @@ function emitModel(model, program, anonymousEnums) {
|
|
|
909
917
|
if (customDerives) {
|
|
910
918
|
deriveAttrs.push(...customDerives.derives);
|
|
911
919
|
}
|
|
912
|
-
if (isError) {
|
|
913
|
-
deriveAttrs.push("thiserror::Error");
|
|
914
|
-
}
|
|
915
920
|
parts.push(`#[derive(${deriveAttrs.join(", ")})]`);
|
|
916
|
-
if (isError && allProps.size > 0) {
|
|
917
|
-
parts.push('#[error("{code}: {message}")]');
|
|
918
|
-
}
|
|
919
921
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
920
922
|
const customAttrs = model[rustAttrKey];
|
|
921
923
|
if (customAttrs) {
|
|
@@ -950,14 +952,11 @@ ${fields.join("\n")}
|
|
|
950
952
|
parts.push(`pub struct ${name}`);
|
|
951
953
|
parts.push("(());");
|
|
952
954
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
956
|
+
const customImpl = model[rustImplKey];
|
|
957
|
+
if (customImpl) {
|
|
955
958
|
parts.push(`
|
|
956
|
-
|
|
957
|
-
fn into_response(self) -> axum::response::Response {
|
|
958
|
-
(StatusCode::${statusCode}, Json(self)).into_response()
|
|
959
|
-
}
|
|
960
|
-
}`);
|
|
959
|
+
${customImpl.impl}`);
|
|
961
960
|
}
|
|
962
961
|
return parts.join("\n");
|
|
963
962
|
}
|
|
@@ -986,6 +985,7 @@ function emitEnum(enumType) {
|
|
|
986
985
|
"PartialEq",
|
|
987
986
|
"Eq",
|
|
988
987
|
"Hash",
|
|
988
|
+
"Default",
|
|
989
989
|
"serde::Serialize",
|
|
990
990
|
"serde::Deserialize",
|
|
991
991
|
];
|
|
@@ -1009,9 +1009,12 @@ function emitEnum(enumType) {
|
|
|
1009
1009
|
parts.push(...attrLines);
|
|
1010
1010
|
}
|
|
1011
1011
|
parts.push(`pub enum ${name} {`);
|
|
1012
|
-
for (
|
|
1013
|
-
const variantName = toRustVariantName(
|
|
1014
|
-
const serdeValue =
|
|
1012
|
+
for (let i = 0; i < members.length; i++) {
|
|
1013
|
+
const variantName = toRustVariantName(members[i].name);
|
|
1014
|
+
const serdeValue = members[i].value ?? members[i].name;
|
|
1015
|
+
if (i === 0) {
|
|
1016
|
+
parts.push(` #[default]`);
|
|
1017
|
+
}
|
|
1015
1018
|
parts.push(` #[serde(rename = "${serdeValue}")]`);
|
|
1016
1019
|
parts.push(` ${variantName},`);
|
|
1017
1020
|
}
|
|
@@ -1022,15 +1025,16 @@ function emitEnum(enumType) {
|
|
|
1022
1025
|
parts.push(...attrLines);
|
|
1023
1026
|
}
|
|
1024
1027
|
parts.push(`pub enum ${name} {`);
|
|
1025
|
-
for (
|
|
1026
|
-
const variantName = toRustVariantName(
|
|
1027
|
-
const enumValue =
|
|
1028
|
+
for (let i = 0; i < members.length; i++) {
|
|
1029
|
+
const variantName = toRustVariantName(members[i].name);
|
|
1030
|
+
const enumValue = members[i].value ?? 0;
|
|
1031
|
+
if (i === 0) {
|
|
1032
|
+
parts.push(` #[default]`);
|
|
1033
|
+
}
|
|
1028
1034
|
parts.push(` ${variantName} = ${enumValue},`);
|
|
1029
1035
|
}
|
|
1030
1036
|
}
|
|
1031
1037
|
parts.push("}");
|
|
1032
|
-
const defaultVariant = toRustVariantName(members[0]?.name ?? "");
|
|
1033
|
-
parts.push(`\n\nimpl Default for ${name} {\n fn default() -> Self {\n ${name}::${defaultVariant}\n }\n}`);
|
|
1034
1038
|
return parts.join("\n");
|
|
1035
1039
|
}
|
|
1036
1040
|
function emitUnion(union, program) {
|
|
@@ -1065,9 +1069,8 @@ function emitScalar(scalar, program) {
|
|
|
1065
1069
|
if (pattern) {
|
|
1066
1070
|
const rustType = scalarToRust[scalar.name] ?? "String";
|
|
1067
1071
|
impls.push(`\nimpl TryFrom<String> for ${structName} {\n type Error = String;\n\n fn try_from(value: String) -> Result<Self, Self::Error> {\n let re = regex::Regex::new(r"${pattern}").unwrap();\n if re.is_match(&value) { Ok(Self(value)) } else { Err(format!("Invalid value: {}", value)) }\n }\n}`);
|
|
1068
|
-
impls.push(`\nimpl Default for ${structName} {\n fn default() -> Self {\n Self(String::new())\n }\n}`);
|
|
1069
1072
|
return {
|
|
1070
|
-
typeDef: `#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub struct ${structName}(pub ${rustType});`,
|
|
1073
|
+
typeDef: `#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]\npub struct ${structName}(pub ${rustType});`,
|
|
1071
1074
|
impls,
|
|
1072
1075
|
};
|
|
1073
1076
|
}
|
|
@@ -1131,15 +1134,17 @@ export async function $onEmit(context, _options) {
|
|
|
1131
1134
|
}
|
|
1132
1135
|
for (const [enumName, anonEnum] of anonymousEnums) {
|
|
1133
1136
|
const parts = [];
|
|
1134
|
-
parts.push(`#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]\npub enum ${enumName} {`);
|
|
1135
|
-
for (
|
|
1137
|
+
parts.push(`#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]\npub enum ${enumName} {`);
|
|
1138
|
+
for (let i = 0; i < anonEnum.variants.length; i++) {
|
|
1139
|
+
const literal = anonEnum.variants[i];
|
|
1136
1140
|
const variantName = toRustVariantName(literal.value);
|
|
1141
|
+
if (i === 0) {
|
|
1142
|
+
parts.push(` #[default]`);
|
|
1143
|
+
}
|
|
1137
1144
|
parts.push(` #[serde(rename = "${literal.value}")]`);
|
|
1138
1145
|
parts.push(` ${variantName},`);
|
|
1139
1146
|
}
|
|
1140
1147
|
parts.push("}");
|
|
1141
|
-
const defaultVariant = toRustVariantName(anonEnum.variants[0]?.value ?? "");
|
|
1142
|
-
parts.push(`\n\nimpl Default for ${enumName} {\n fn default() -> Self {\n ${enumName}::${defaultVariant}\n }\n}`);
|
|
1143
1148
|
content.push(parts.join("\n"));
|
|
1144
1149
|
content.push("");
|
|
1145
1150
|
}
|
|
@@ -1165,6 +1170,7 @@ export async function $onEmit(context, _options) {
|
|
|
1165
1170
|
}
|
|
1166
1171
|
const namespaceGroups = getAllOperations(context.program);
|
|
1167
1172
|
const outputContent = "#![allow(unused)]\n\n" +
|
|
1173
|
+
"use std::str::FromStr;\n" +
|
|
1168
1174
|
"use axum::http::StatusCode;\n" +
|
|
1169
1175
|
"use axum::response::IntoResponse;\n" +
|
|
1170
1176
|
"use axum::Json;\n\n" +
|