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 ADDED
@@ -0,0 +1 @@
1
+ NPM_TOKEN=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
package/AGENTS.md CHANGED
@@ -1,167 +1,58 @@
1
- # TypeSpec Rust Emitter - Agent Guidelines
1
+ # TypeSpec Rust Emitter
2
2
 
3
- ## Overview
3
+ TypeSpec emitter generating idiomatic Rust code (structs, enums, server traits).
4
4
 
5
- TypeSpec emitter that generates idiomatic Rust code (structs, enums, server traits) from TypeSpec specifications.
6
-
7
- ## Build, Lint, and Test Commands
8
-
9
- ### Standard Commands
5
+ ## Commands
10
6
 
11
7
  ```bash
12
- # Build TypeScript
13
- npm run build # or: just 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 a specific test
23
- node --test 'dist/test/hello.test.js' --test-name-pattern="emits model"
11
+ # Run tests only
12
+ npm test
24
13
 
25
- # Lint
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
- # Format code
30
- npm run format # Format all files
31
- npm run format:check # Check formatting without changes
16
+ # Lint & format (run after every change)
17
+ npm run lint
18
+ npm run format
32
19
 
33
20
  # Compile TypeSpec to Rust
34
- just compile # Runs: cd example && tsp compile .
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
- ### Imports
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
- ## Server Trait Generation Rules
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
- 1. `navigateProgram()` walks TypeSpec AST
141
- 2. Collects models, enums, operations
142
- 3. Processes decorators
143
- 4. Generates Rust code via string templates
144
- 5. Emits to `example/output-rust/src/generated/`
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
- ## Linting
38
+ ## Decorators
147
39
 
148
- ESLint config (`eslint.config.js`):
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
- - Uses `typescript-eslint` recommended rules
151
- - Ignores `dist/**` and `.temp/**`
152
- - Unused variables: warn with `_` prefix exception
45
+ ## Style
153
46
 
154
- ## Commit Message Style
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
- - Use clear, descriptive titles
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
- ## Checklist Before PR
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
- - [ ] Run full cycle: `just build && npm test && just compile && just check-rust`
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")]
@@ -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>;
@@ -1,6 +1,7 @@
1
- import { emitFile, resolvePath, navigateProgram, getDoc, isArrayModelType, isRecordModelType, getFormat, getPattern, isErrorModel, getNamespaceFullName, getTags, } from "@typespec/compiler";
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 (const variant of variants) {
885
- const literalType = variant.type;
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
- if (isError && allProps.size > 0) {
954
- const statusCode = getHttpStatusCodeForError(name);
955
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
956
+ const customImpl = model[rustImplKey];
957
+ if (customImpl) {
955
958
  parts.push(`
956
- impl IntoResponse for ${name} {
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 (const member of members) {
1013
- const variantName = toRustVariantName(member.name);
1014
- const serdeValue = member.value ?? member.name;
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 (const member of members) {
1026
- const variantName = toRustVariantName(member.name);
1027
- const enumValue = member.value ?? 0;
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 (const literal of anonEnum.variants) {
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" +