typespec-rust-emitter 0.10.7 → 0.11.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/example/main.tsp CHANGED
@@ -1,6 +1,7 @@
1
1
  import "@typespec/http";
2
2
  import "@typespec/sse";
3
3
  import "@typespec/events";
4
+ import "typespec-rust-emitter";
4
5
 
5
6
  using TypeSpec.Http;
6
7
  using TypeSpec.SSE;
@@ -31,3 +32,40 @@ namespace Pets {
31
32
  @body body: string[];
32
33
  };
33
34
  }
35
+
36
+ model Item {
37
+ name: string;
38
+ value: int32;
39
+ }
40
+
41
+ @route("/items")
42
+ namespace Items {
43
+ @get
44
+ op getItem(@query id: string): {
45
+ @statusCode status: 200;
46
+ @body body: Item;
47
+ };
48
+
49
+ @rustMut
50
+ @post
51
+ op createItem(@body item: Item): {
52
+ @statusCode status: 201;
53
+ @body body: Item;
54
+ };
55
+
56
+ @rustMut
57
+ @put
58
+ op updateItem(@query id: string, @body item: Item): {
59
+ @statusCode status: 200;
60
+ @body body: Item;
61
+ };
62
+ }
63
+
64
+ @route("/consuming")
65
+ namespace Consuming {
66
+ @rustOwn
67
+ @delete
68
+ op consumeAndDelete(@query id: string): {
69
+ @statusCode status: 204;
70
+ };
71
+ }
@@ -16,6 +16,10 @@ pub trait Server: Send + Sync {
16
16
 
17
17
  async fn events_accounts_events(&self, account_id: String) -> Result<EventsAccountsEventsResponse>;
18
18
  async fn pets_list(&self, first_query: String, second_query: String) -> Result<PetsListResponse>;
19
+ async fn items_get_item(&self, id: String) -> Result<ItemsGetItemResponse>;
20
+ async fn items_create_item(&mut self, body: Item) -> Result<ItemsCreateItemResponse>;
21
+ async fn items_update_item(&mut self, id: String, body: Item) -> Result<ItemsUpdateItemResponse>;
22
+ async fn consuming_consume_and_delete(self, id: String) -> Result<ConsumingConsumeAndDeleteResponse>;
19
23
  }
20
24
  #[allow(clippy::type_complexity)]
21
25
  pub enum EventsAccountsEventsResponse {
@@ -45,8 +49,64 @@ impl IntoResponse for PetsListResponse {
45
49
  }
46
50
  }
47
51
 
52
+ #[allow(clippy::type_complexity)]
53
+ pub enum ItemsGetItemResponse {
54
+ Ok(Json<Item>),
55
+ }
56
+
57
+ impl IntoResponse for ItemsGetItemResponse {
58
+ fn into_response(self) -> axum::response::Response {
59
+ match self {
60
+
61
+ ItemsGetItemResponse::Ok(body) => (StatusCode::OK, body).into_response(),
62
+ }
63
+ }
64
+ }
65
+
66
+ #[allow(clippy::type_complexity)]
67
+ pub enum ItemsCreateItemResponse {
68
+ Ok(Json<Item>),
69
+ }
70
+
71
+ impl IntoResponse for ItemsCreateItemResponse {
72
+ fn into_response(self) -> axum::response::Response {
73
+ match self {
74
+
75
+ ItemsCreateItemResponse::Ok(body) => (StatusCode::OK, body).into_response(),
76
+ }
77
+ }
78
+ }
79
+
80
+ #[allow(clippy::type_complexity)]
81
+ pub enum ItemsUpdateItemResponse {
82
+ Ok(Json<Item>),
83
+ }
84
+
85
+ impl IntoResponse for ItemsUpdateItemResponse {
86
+ fn into_response(self) -> axum::response::Response {
87
+ match self {
88
+
89
+ ItemsUpdateItemResponse::Ok(body) => (StatusCode::OK, body).into_response(),
90
+ }
91
+ }
92
+ }
93
+
94
+ #[allow(clippy::type_complexity)]
95
+ pub enum ConsumingConsumeAndDeleteResponse {
96
+ Ok,
97
+ }
98
+
99
+ impl IntoResponse for ConsumingConsumeAndDeleteResponse {
100
+ fn into_response(self) -> axum::response::Response {
101
+ match self {
102
+
103
+ ConsumingConsumeAndDeleteResponse::Ok => StatusCode::OK.into_response(),
104
+ }
105
+ }
106
+ }
107
+
48
108
  use axum::extract::Query;
49
- use axum::routing::{get};
109
+ use axum::routing::{delete, get, post, put};
50
110
  use axum::Router;
51
111
 
52
112
 
@@ -58,6 +118,24 @@ pub struct PetsListQuery {
58
118
  pub second_query: String
59
119
  }
60
120
 
121
+ #[derive(Debug, Clone, serde::Deserialize)]
122
+ pub struct ItemsGetItemQuery {
123
+ #[serde(rename = "id")]
124
+ pub id: String
125
+ }
126
+
127
+ #[derive(Debug, Clone, serde::Deserialize)]
128
+ pub struct ItemsUpdateItemQuery {
129
+ #[serde(rename = "id")]
130
+ pub id: String
131
+ }
132
+
133
+ #[derive(Debug, Clone, serde::Deserialize)]
134
+ pub struct ConsumingConsumeAndDeleteQuery {
135
+ #[serde(rename = "id")]
136
+ pub id: String
137
+ }
138
+
61
139
 
62
140
 
63
141
  pub async fn events_accounts_events_handler<S>(
@@ -65,7 +143,7 @@ pub async fn events_accounts_events_handler<S>(
65
143
  Path(account_id): Path<String>,
66
144
  ) -> impl axum::response::IntoResponse
67
145
  where
68
- S: Server + Clone + Send + Sync + 'static,
146
+ S: Server+ Clone + Send + Sync + 'static,
69
147
  S::Claims: Send + Sync + Clone + 'static,
70
148
  {
71
149
  let result = service.events_accounts_events(account_id).await;
@@ -84,7 +162,7 @@ pub async fn pets_list_handler<S>(
84
162
  Query(params): Query<PetsListQuery>,
85
163
  ) -> impl axum::response::IntoResponse
86
164
  where
87
- S: Server + Clone + Send + Sync + 'static,
165
+ S: Server+ Clone + Send + Sync + 'static,
88
166
  S::Claims: Send + Sync + Clone + 'static,
89
167
  {
90
168
  let result = service.pets_list(params.first_query, params.second_query).await;
@@ -98,6 +176,85 @@ where
98
176
  }
99
177
  }
100
178
 
179
+ pub async fn items_get_item_handler<S>(
180
+ axum::extract::State(service): axum::extract::State<S>,
181
+ Query(params): Query<ItemsGetItemQuery>,
182
+ ) -> impl axum::response::IntoResponse
183
+ where
184
+ S: Server+ Clone + Send + Sync + 'static,
185
+ S::Claims: Send + Sync + Clone + 'static,
186
+ {
187
+ let result = service.items_get_item(params.id).await;
188
+ match result {
189
+ Ok(response) => response.into_response(),
190
+ Err(e) => (
191
+ axum::http::StatusCode::INTERNAL_SERVER_ERROR,
192
+ format!("Internal error: {e}"),
193
+ )
194
+ .into_response(),
195
+ }
196
+ }
197
+
198
+ pub async fn items_create_item_handler<S>(
199
+ axum::extract::State(mut service): axum::extract::State<S>,
200
+ Json(payload): Json<Item>,
201
+ ) -> impl axum::response::IntoResponse
202
+ where
203
+ S: Server+ Clone + Send + Sync + 'static,
204
+ S::Claims: Send + Sync + Clone + 'static,
205
+ {
206
+ let result = service.items_create_item(payload).await;
207
+ match result {
208
+ Ok(response) => response.into_response(),
209
+ Err(e) => (
210
+ axum::http::StatusCode::INTERNAL_SERVER_ERROR,
211
+ format!("Internal error: {e}"),
212
+ )
213
+ .into_response(),
214
+ }
215
+ }
216
+
217
+ pub async fn items_update_item_handler<S>(
218
+ axum::extract::State(mut service): axum::extract::State<S>,
219
+ Query(params): Query<ItemsUpdateItemQuery>,
220
+ Json(payload): Json<Item>,
221
+ ) -> impl axum::response::IntoResponse
222
+ where
223
+ S: Server+ Clone + Send + Sync + 'static,
224
+ S::Claims: Send + Sync + Clone + 'static,
225
+ {
226
+ let result = service.items_update_item(params.id, payload).await;
227
+ match result {
228
+ Ok(response) => response.into_response(),
229
+ Err(e) => (
230
+ axum::http::StatusCode::INTERNAL_SERVER_ERROR,
231
+ format!("Internal error: {e}"),
232
+ )
233
+ .into_response(),
234
+ }
235
+ }
236
+
237
+ // NOTE: consuming_consume_and_delete takes self and cannot be used with the router pattern.
238
+ // It consumes the service, so you need to implement your own handler pattern.
239
+ pub async fn consuming_consume_and_delete_handler<S>(
240
+ axum::extract::State(service): axum::extract::State<S>,
241
+ Query(params): Query<ConsumingConsumeAndDeleteQuery>,
242
+ ) -> impl axum::response::IntoResponse
243
+ where
244
+ S: Server + Send + Sync + 'static,
245
+ S::Claims: Send + Sync + Clone + 'static,
246
+ {
247
+ let result = service.consuming_consume_and_delete(params.id).await;
248
+ match result {
249
+ Ok(response) => response.into_response(),
250
+ Err(e) => (
251
+ axum::http::StatusCode::INTERNAL_SERVER_ERROR,
252
+ format!("Internal error: {e}"),
253
+ )
254
+ .into_response(),
255
+ }
256
+ }
257
+
101
258
  pub fn create_router<S, M>(service: S, middleware: M) -> Router
102
259
  where
103
260
  S: Server + Clone + Send + Sync + 'static,
@@ -108,6 +265,9 @@ where
108
265
  let public = Router::new()
109
266
  .route("/events/{accountId}", get(events_accounts_events_handler::<S>))
110
267
  .route("/pets", get(pets_list_handler::<S>))
268
+ .route("/items", get(items_get_item_handler::<S>))
269
+ .route("/items", post(items_create_item_handler::<S>))
270
+ .route("/items", put(items_update_item_handler::<S>))
111
271
  ;
112
272
  router = router.merge(public);
113
273
  router.with_state(service)
@@ -10,6 +10,12 @@ pub struct MyEventData {
10
10
  pub message: String,
11
11
  }
12
12
 
13
+ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
14
+ pub struct Item {
15
+ pub name: String,
16
+ pub value: i32,
17
+ }
18
+
13
19
  #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
14
20
  #[serde(untagged)]
15
21
  pub enum MyEvent {
@@ -3,14 +3,13 @@ mod generated;
3
3
  use async_trait::async_trait;
4
4
  use axum::response::IntoResponse;
5
5
  use axum::response::sse::{Event, KeepAlive, Sse};
6
- use generated::server::{EventsAccountsEventsResponse, Server};
6
+ use axum::Json;
7
+ use generated::server::{EventsAccountsEventsResponse, ItemsCreateItemResponse, ItemsGetItemResponse, ItemsUpdateItemResponse, ConsumingConsumeAndDeleteResponse, PetsListResponse, Server};
7
8
  use std::convert::Infallible;
8
9
  use std::time::Duration;
9
10
  use tokio::time::interval;
10
11
  use tokio_stream::{StreamExt as _, wrappers::IntervalStream};
11
12
 
12
- use crate::generated::server::PetsListResponse;
13
-
14
13
  #[derive(Clone)]
15
14
  struct AppState;
16
15
 
@@ -24,7 +23,7 @@ impl Server for AppState {
24
23
  ) -> eyre::Result<EventsAccountsEventsResponse> {
25
24
  let stream = IntervalStream::new(interval(Duration::from_millis(100)))
26
25
  .map(|_| Ok::<_, Infallible>(Event::default().data("hi!")))
27
- .take(3); // emit 3 events
26
+ .take(3);
28
27
 
29
28
  let stream = Sse::new(stream).keep_alive(KeepAlive::default());
30
29
  Ok(EventsAccountsEventsResponse::Ok(stream.into_response()))
@@ -35,7 +34,42 @@ impl Server for AppState {
35
34
  _first_query: String,
36
35
  _second_query: String,
37
36
  ) -> eyre::Result<PetsListResponse> {
38
- todo!()
37
+ Ok(PetsListResponse::Ok(Json(vec!["pet1".to_string(), "pet2".to_string()])))
38
+ }
39
+
40
+ async fn items_get_item(&self, _id: String) -> eyre::Result<ItemsGetItemResponse> {
41
+ Ok(ItemsGetItemResponse::Ok(Json(generated::types::Item {
42
+ name: "test".to_string(),
43
+ value: 42,
44
+ })))
45
+ }
46
+
47
+ async fn items_create_item(
48
+ &mut self,
49
+ _body: generated::types::Item,
50
+ ) -> eyre::Result<ItemsCreateItemResponse> {
51
+ Ok(ItemsCreateItemResponse::Ok(Json(generated::types::Item {
52
+ name: "created".to_string(),
53
+ value: 0,
54
+ })))
55
+ }
56
+
57
+ async fn items_update_item(
58
+ &mut self,
59
+ _id: String,
60
+ _body: generated::types::Item,
61
+ ) -> eyre::Result<ItemsUpdateItemResponse> {
62
+ Ok(ItemsUpdateItemResponse::Ok(Json(generated::types::Item {
63
+ name: "updated".to_string(),
64
+ value: 1,
65
+ })))
66
+ }
67
+
68
+ async fn consuming_consume_and_delete(
69
+ self,
70
+ _id: String,
71
+ ) -> eyre::Result<ConsumingConsumeAndDeleteResponse> {
72
+ Ok(ConsumingConsumeAndDeleteResponse::Ok)
39
73
  }
40
74
  }
41
75
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typespec-rust-emitter",
3
- "version": "0.10.7",
3
+ "version": "0.11.0",
4
4
  "description": "TypeSpec emitter that generates idiomatic Rust types and structs",
5
5
  "keywords": [
6
6
  "typespec",
package/src/emitter.ts CHANGED
@@ -39,6 +39,9 @@ interface RustAttrInfo {
39
39
  const rustDeriveKey = Symbol("rustDerive");
40
40
  const rustAttrKey = Symbol("rustAttr");
41
41
  const rustImplKey = Symbol("rustImpl");
42
+ const rustSelfReceiverKey = Symbol("rustSelfReceiver");
43
+
44
+ type SelfReceiver = "&self" | "&mut self" | "self";
42
45
 
43
46
  interface RustImplInfo {
44
47
  impl: string;
@@ -168,6 +171,42 @@ export function $rustImpl(
168
171
  }
169
172
  }
170
173
 
174
+ export function $rustMut(context: DecoratorContext, target: Type) {
175
+ if (target.kind !== "Operation") {
176
+ context.program.reportDiagnostic({
177
+ code: "rust-mut-invalid-target",
178
+ message: `@rustMut can only be applied to operations`,
179
+ severity: "error",
180
+ target: context.decoratorTarget,
181
+ });
182
+ return;
183
+ }
184
+
185
+ const ns = target.namespace ? getNamespaceFullName(target.namespace) : "";
186
+ if (!ns.startsWith("TypeSpec")) {
187
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
188
+ (target as any)[rustSelfReceiverKey] = "&mut self";
189
+ }
190
+ }
191
+
192
+ export function $rustOwn(context: DecoratorContext, target: Type) {
193
+ if (target.kind !== "Operation") {
194
+ context.program.reportDiagnostic({
195
+ code: "rust-own-invalid-target",
196
+ message: `@rustOwn can only be applied to operations`,
197
+ severity: "error",
198
+ target: context.decoratorTarget,
199
+ });
200
+ return;
201
+ }
202
+
203
+ const ns = target.namespace ? getNamespaceFullName(target.namespace) : "";
204
+ if (!ns.startsWith("TypeSpec")) {
205
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
206
+ (target as any)[rustSelfReceiverKey] = "self";
207
+ }
208
+ }
209
+
171
210
  type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
172
211
 
173
212
  interface OperationInfo {
@@ -298,6 +337,14 @@ function hasAuthDecorator(operation: Operation): boolean {
298
337
  return false;
299
338
  }
300
339
 
340
+ function getSelfReceiver(operation: Operation): SelfReceiver {
341
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
342
+ const receiver = (operation as any)[rustSelfReceiverKey] as
343
+ | SelfReceiver
344
+ | undefined;
345
+ return receiver ?? "&self";
346
+ }
347
+
301
348
  function getOperationParameters(
302
349
  program: Program,
303
350
  operation: Operation,
@@ -439,6 +486,7 @@ function getOperationResponses(
439
486
  });
440
487
  return responses;
441
488
  }
489
+ let foundStatusCode = false;
442
490
  for (const [propName, prop] of model.properties) {
443
491
  if (propName === "body") {
444
492
  const { type: rustType } = getRustTypeForProperty(
@@ -451,8 +499,27 @@ function getOperationResponses(
451
499
  bodyType: rustType,
452
500
  bodyDescription: getDoc(program, prop),
453
501
  });
502
+ } else if (propName === "statusCode") {
503
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
504
+ const typeAny = prop.type as any;
505
+ if (typeAny.value !== undefined) {
506
+ const statusCode = typeAny.value as number;
507
+ responses.push({
508
+ statusCode,
509
+ bodyType: undefined,
510
+ bodyDescription: undefined,
511
+ });
512
+ foundStatusCode = true;
513
+ }
454
514
  }
455
515
  }
516
+ if (!foundStatusCode && responses.length === 0) {
517
+ responses.push({
518
+ statusCode: 200,
519
+ bodyType: undefined,
520
+ bodyDescription: undefined,
521
+ });
522
+ }
456
523
  }
457
524
 
458
525
  return responses;
@@ -700,25 +767,26 @@ pub trait Server: Send + Sync {
700
767
  }
701
768
 
702
769
  const paramsStr = paramParts.join(", ");
770
+ const selfReceiver = getSelfReceiver(op);
703
771
 
704
772
  if (isProtected) {
705
773
  if (paramsStr) {
706
774
  parts.push(
707
- ` async fn ${fnName}(&self, claims: Self::Claims, ${paramsStr}) -> Result<${responseName}>;`,
775
+ ` async fn ${fnName}(${selfReceiver}, claims: Self::Claims, ${paramsStr}) -> Result<${responseName}>;`,
708
776
  );
709
777
  } else {
710
778
  parts.push(
711
- ` async fn ${fnName}(&self, claims: Self::Claims) -> Result<${responseName}>;`,
779
+ ` async fn ${fnName}(${selfReceiver}, claims: Self::Claims) -> Result<${responseName}>;`,
712
780
  );
713
781
  }
714
782
  } else {
715
783
  if (paramsStr) {
716
784
  parts.push(
717
- ` async fn ${fnName}(&self, ${paramsStr}) -> Result<${responseName}>;`,
785
+ ` async fn ${fnName}(${selfReceiver}, ${paramsStr}) -> Result<${responseName}>;`,
718
786
  );
719
787
  } else {
720
788
  parts.push(
721
- ` async fn ${fnName}(&self) -> Result<${responseName}>;`,
789
+ ` async fn ${fnName}(${selfReceiver}) -> Result<${responseName}>;`,
722
790
  );
723
791
  }
724
792
  }
@@ -827,6 +895,7 @@ function generateRouter(
827
895
  const handlerFnName = toRustIdent(`${nsName}_${opInfo.name}`);
828
896
  const traitFnName = handlerFnName;
829
897
  const isProtected = hasAuthDecorator(op);
898
+ const selfReceiver = getSelfReceiver(op);
830
899
 
831
900
  const pathParams = opInfo.parameters.filter((p) => p.location === "path");
832
901
  const hasPathParams = pathParams.length > 0;
@@ -843,6 +912,8 @@ function generateRouter(
843
912
  const serverArgs: string[] = [];
844
913
 
845
914
  // State is always first (added in handler template)
915
+ const serviceBinding =
916
+ selfReceiver === "&mut self" ? "mut service" : "service";
846
917
 
847
918
  // Extension (claims) comes after State
848
919
  if (isProtected) {
@@ -901,12 +972,37 @@ function generateRouter(
901
972
  const serverCall = `service.${traitFnName}(${serverArgsStr}).await`;
902
973
 
903
974
  // All handlers use <S> generics, Claims is now an associated type
904
- const handlerCode = `pub async fn ${handlerFnName}_handler<S>(
975
+ // For &mut self, we need Clone because service is extracted multiple times
976
+ // For self, we can't use Clone (would need Arc/Mutex or different pattern)
977
+ const needsClone = selfReceiver !== "self" ? "+ Clone" : "";
978
+ const handlerCode =
979
+ selfReceiver === "self"
980
+ ? `// NOTE: ${handlerFnName} takes self and cannot be used with the router pattern.
981
+ // It consumes the service, so you need to implement your own handler pattern.
982
+ pub async fn ${handlerFnName}_handler<S>(
905
983
  axum::extract::State(service): axum::extract::State<S>,
906
984
  ${extractorLines.join("\n")}
907
985
  ) -> impl axum::response::IntoResponse
908
986
  where
909
- S: Server + Clone + Send + Sync + 'static,
987
+ S: Server + Send + Sync + 'static,
988
+ S::Claims: Send + Sync + Clone + 'static,
989
+ {
990
+ let result = service.${traitFnName}(${serverArgsStr}).await;
991
+ match result {
992
+ Ok(response) => response.into_response(),
993
+ Err(e) => (
994
+ axum::http::StatusCode::INTERNAL_SERVER_ERROR,
995
+ format!("Internal error: {e}"),
996
+ )
997
+ .into_response(),
998
+ }
999
+ }`
1000
+ : `pub async fn ${handlerFnName}_handler<S>(
1001
+ axum::extract::State(${serviceBinding}): axum::extract::State<S>,
1002
+ ${extractorLines.join("\n")}
1003
+ ) -> impl axum::response::IntoResponse
1004
+ where
1005
+ S: Server${needsClone} + Send + Sync + 'static,
910
1006
  S::Claims: Send + Sync + Clone + 'static,
911
1007
  {
912
1008
  let result = ${serverCall};
@@ -922,6 +1018,11 @@ where
922
1018
 
923
1019
  handlers.push(handlerCode);
924
1020
 
1021
+ // Don't add routes for self methods (they consume the service)
1022
+ if (selfReceiver === "self") {
1023
+ continue;
1024
+ }
1025
+
925
1026
  const routePath = `"${opInfo.path}"`;
926
1027
  let routeStmt = "";
927
1028
  if (isProtected) {
package/src/index.ts CHANGED
@@ -5,5 +5,7 @@ export {
5
5
  $rustAttr,
6
6
  $rustAttrs,
7
7
  $rustImpl,
8
+ $rustMut,
9
+ $rustOwn,
8
10
  } from "./emitter.js";
9
11
  export { $lib } from "./lib.js";
package/src/lib.tsp CHANGED
@@ -7,3 +7,5 @@ extern dec rustDerives(target: Model | Enum, ...derives: valueof string[]);
7
7
  extern dec rustAttr(target: Model | Enum | ModelProperty, attr: valueof string);
8
8
  extern dec rustAttrs(target: Model | Enum | ModelProperty, ...attrs: valueof string[]);
9
9
  extern dec rustImpl(target: Model, impl: valueof string);
10
+ extern dec rustMut(target: Operation);
11
+ extern dec rustOwn(target: Operation);
@@ -1,6 +1,6 @@
1
1
  import { strictEqual } from "node:assert";
2
2
  import { describe, it } from "node:test";
3
- import { emit } from "./test-host.js";
3
+ import { emit, emitWithDiagnostics } from "./test-host.js";
4
4
 
5
5
  describe("Rust emitter", () => {
6
6
  it("emits basic model", async () => {
@@ -443,4 +443,122 @@ describe("Rust emitter", () => {
443
443
  true,
444
444
  );
445
445
  });
446
+
447
+ it("uses &self by default in trait methods", async () => {
448
+ const results = await emit(`
449
+ import "@typespec/http";
450
+ import "typespec-rust-emitter";
451
+ using TypeSpec.Http;
452
+
453
+ @route("/test")
454
+ namespace Test {
455
+ @get
456
+ op getItem(): { @statusCode status: 200; @body body: string };
457
+ }
458
+ `);
459
+ const server = results["server.rs"];
460
+ strictEqual(server.includes("async fn test_get_item(&self)"), true);
461
+ });
462
+
463
+ it("uses &mut self with @rustMut decorator", async () => {
464
+ const results = await emit(`
465
+ import "@typespec/http";
466
+ import "typespec-rust-emitter";
467
+ using TypeSpec.Http;
468
+
469
+ @route("/test")
470
+ namespace Test {
471
+ @rustMut
472
+ @post
473
+ op createItem(@body name: string): { @statusCode status: 200; @body body: string };
474
+ }
475
+ `);
476
+ const server = results["server.rs"];
477
+ strictEqual(server.includes("test_create_item(&mut self,"), true);
478
+ });
479
+
480
+ it("uses self with @rustOwn decorator", async () => {
481
+ const results = await emit(`
482
+ import "@typespec/http";
483
+ import "typespec-rust-emitter";
484
+ using TypeSpec.Http;
485
+
486
+ @route("/test")
487
+ namespace Test {
488
+ @rustOwn
489
+ @delete
490
+ op deleteItem(): { @statusCode status: 200; @body body: string };
491
+ }
492
+ `);
493
+ const server = results["server.rs"];
494
+ strictEqual(server.includes("test_delete_item(self)"), true);
495
+ });
496
+
497
+ it("@rustMut works with protected routes", async () => {
498
+ const results = await emit(`
499
+ import "@typespec/http";
500
+ import "typespec-rust-emitter";
501
+ using TypeSpec.Http;
502
+
503
+ @route("/test")
504
+ namespace Test {
505
+ @rustMut
506
+ @post
507
+ op createItem(@body name: string, @header Authorization: string): { @statusCode status: 200; @body body: string };
508
+ }
509
+ `);
510
+ const server = results["server.rs"];
511
+ strictEqual(server.includes("test_create_item(&mut self,"), true);
512
+ });
513
+
514
+ it("@rustOwn works with protected routes", async () => {
515
+ const results = await emit(`
516
+ import "@typespec/http";
517
+ import "typespec-rust-emitter";
518
+ using TypeSpec.Http;
519
+
520
+ @route("/test")
521
+ namespace Test {
522
+ @rustOwn
523
+ @delete
524
+ op deleteItem(@query id: string): { @statusCode status: 200; @body body: string };
525
+ }
526
+ `);
527
+ const server = results["server.rs"];
528
+ strictEqual(server.includes("test_delete_item(self,"), true);
529
+ });
530
+
531
+ it("reports error when @rustMut is applied to non-operation", async () => {
532
+ const [, diagnostics] = await emitWithDiagnostics(`
533
+ import "@typespec/http";
534
+ import "typespec-rust-emitter";
535
+ using TypeSpec.Http;
536
+
537
+ @rustMut
538
+ model Test {
539
+ name: string;
540
+ }
541
+ `);
542
+ const hasError = diagnostics.some(
543
+ (d) => d.code === "decorator-wrong-target",
544
+ );
545
+ strictEqual(hasError, true);
546
+ });
547
+
548
+ it("reports error when @rustOwn is applied to non-operation", async () => {
549
+ const [, diagnostics] = await emitWithDiagnostics(`
550
+ import "@typespec/http";
551
+ import "typespec-rust-emitter";
552
+ using TypeSpec.Http;
553
+
554
+ @rustOwn
555
+ model Test {
556
+ name: string;
557
+ }
558
+ `);
559
+ const hasError = diagnostics.some(
560
+ (d) => d.code === "decorator-wrong-target",
561
+ );
562
+ strictEqual(hasError, true);
563
+ });
446
564
  });