typespec-rust-emitter 0.13.1 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -117,13 +117,13 @@ export function generateRouter(
117
117
  const serverArgsStr = serverArgs.join(", ");
118
118
 
119
119
  if (hasEtagCache(program, op)) {
120
- const cacheKey = `${handlerFnName}:${pathParams.map((p) => `{${p.rustName}}`).join(":")}`;
121
120
  const cachedServerArgs = isProtected
122
121
  ? [serverArgs[0], "&cache", ...serverArgs.slice(1)]
123
122
  : ["&cache", ...serverArgs];
124
123
  const code = generateEtagHandler({
125
124
  handlerFnName,
126
- cacheKey,
125
+ pathParams,
126
+ queryParams,
127
127
  extractorLines,
128
128
  serverArgsStr: cachedServerArgs.join(", "),
129
129
  responseName: `${nsName}${toPascalCase(opInfo.name)}Response`,
@@ -144,7 +144,7 @@ export function generateRouter(
144
144
 
145
145
  const serverCall = `service.${traitFnName}(${serverArgsStr}).await`;
146
146
 
147
- let responseHandling = opInfo.cacheControl
147
+ const responseHandling = opInfo.cacheControl
148
148
  ? `match result {
149
149
  Ok(response) => {
150
150
  let mut res = response.into_response();
@@ -29,7 +29,11 @@ use eyre::Result;
29
29
 
30
30
  `);
31
31
 
32
- if (namespaceGroups.some((g) => g.operations.some((o) => hasEtagCache(program, o)))) {
32
+ if (
33
+ namespaceGroups.some((g) =>
34
+ g.operations.some((o) => hasEtagCache(program, o)),
35
+ )
36
+ ) {
33
37
  parts.push(generateEtagCacheTrait());
34
38
  parts.push("\n");
35
39
  }
@@ -128,7 +128,9 @@ export function emitOperationInfo(
128
128
  const opName = op.name.replace(/[^a-zA-Z0-9_]/g, "_");
129
129
  const etagVal = program.stateMap(etagCacheKey).get(op);
130
130
  const etagKey = typeof etagVal === "string" ? etagVal : undefined;
131
- const cacheControl = program.stateMap(cacheControlKey).get(op) as string | undefined;
131
+ const cacheControl = program.stateMap(cacheControlKey).get(op) as
132
+ | string
133
+ | undefined;
132
134
 
133
135
  return {
134
136
  name: opName,
@@ -144,7 +146,10 @@ export function emitOperationInfo(
144
146
  };
145
147
  }
146
148
 
147
- export function getEtagKey(program: Program, operation: Operation): string | undefined {
149
+ export function getEtagKey(
150
+ program: Program,
151
+ operation: Operation,
152
+ ): string | undefined {
148
153
  const val = program.stateMap(etagCacheKey).get(operation);
149
154
  return typeof val === "string" ? val : undefined;
150
155
  }
@@ -153,6 +158,9 @@ export function hasEtagCache(program: Program, operation: Operation): boolean {
153
158
  return program.stateMap(etagCacheKey).has(operation);
154
159
  }
155
160
 
156
- export function getCacheControl(program: Program, operation: Operation): string | undefined {
161
+ export function getCacheControl(
162
+ program: Program,
163
+ operation: Operation,
164
+ ): string | undefined {
157
165
  return program.stateMap(cacheControlKey).get(operation) as string | undefined;
158
166
  }
@@ -28,17 +28,29 @@ describe("@etagCache decorator", () => {
28
28
  const server = results["server.rs"];
29
29
  strictEqual(server.includes("pub trait EtagCache"), true);
30
30
  strictEqual(
31
- server.includes("fn get(&self, key: &str) -> Option<String>"),
31
+ server.includes("async fn get(&self, key: &str) -> Option<String>"),
32
+ true,
33
+ );
34
+ strictEqual(
35
+ server.includes("async fn set(&self, key: &str, etag: &str)"),
32
36
  true,
33
37
  );
34
- strictEqual(server.includes("fn set(&self, key: &str, etag: &str)"), true);
35
38
  });
36
39
 
37
40
  it("generates cache-aware handler with 304 short-circuit", async () => {
38
41
  const results = await emit(ETAG_SPEC);
39
42
  const server = results["server.rs"];
40
43
  strictEqual(server.includes("axum::http::StatusCode::NOT_MODIFIED"), true);
41
- strictEqual(server.includes("cache.get(cache_key.as_ref())"), true);
44
+ strictEqual(server.includes("cache.get(cache_key.as_ref()).await"), true);
45
+ strictEqual(
46
+ server.includes("stored_etag.parse::<axum_extra::headers::ETag>()"),
47
+ true,
48
+ );
49
+ strictEqual(server.includes("!inm.precondition_passes(&etag)"), true);
50
+ strictEqual(
51
+ server.includes("HeaderValue::from_str(&stored_etag).unwrap()"),
52
+ false,
53
+ );
42
54
  });
43
55
 
44
56
  it("supports custom etagKey and @cacheControl", async () => {
@@ -58,10 +70,117 @@ describe("@etagCache decorator", () => {
58
70
  const results = await emit(spec);
59
71
  const server = results["server.rs"];
60
72
  strictEqual(server.includes('let cache_key = "my-key";'), true);
61
- strictEqual(server.includes('axum::http::header::CACHE_CONTROL'), true);
73
+ strictEqual(server.includes("axum::http::header::CACHE_CONTROL"), true);
62
74
  strictEqual(server.includes('"public"'), true);
63
75
  });
64
76
 
77
+ it("interpolates path parameters in custom etag keys", async () => {
78
+ const spec = `
79
+ import "@typespec/http";
80
+ import "typespec-rust-emitter";
81
+ using TypeSpec.Http;
82
+
83
+ @route("/accounts/{accountId}/images")
84
+ namespace Images {
85
+ @etagCache("img:list:pub:{accountId}")
86
+ @get
87
+ op listPublic(@path accountId: string): { @statusCode statusCode: 200; };
88
+ }
89
+ `;
90
+ const results = await emit(spec);
91
+ const server = results["server.rs"];
92
+ strictEqual(
93
+ server.includes('let cache_key = format!("img:list:pub:{account_id}");'),
94
+ true,
95
+ );
96
+ });
97
+
98
+ it("appends path parameters to static etag keys", async () => {
99
+ const spec = `
100
+ import "@typespec/http";
101
+ import "typespec-rust-emitter";
102
+ using TypeSpec.Http;
103
+
104
+ @route("/accounts/{accountId}/images")
105
+ namespace Images {
106
+ @etagCache("img:list:pub")
107
+ @get
108
+ op listPublic(@path accountId: string): { @statusCode statusCode: 200; };
109
+ }
110
+ `;
111
+ const results = await emit(spec);
112
+ const server = results["server.rs"];
113
+ strictEqual(
114
+ server.includes('let cache_key = format!("img:list:pub:{account_id}");'),
115
+ true,
116
+ );
117
+ });
118
+
119
+ it("does not use mut binding as the call receiver with @rustMut", async () => {
120
+ const spec = `
121
+ import "@typespec/http";
122
+ import "typespec-rust-emitter";
123
+ using TypeSpec.Http;
124
+
125
+ @route("/accounts/{accountId}/groups")
126
+ namespace Groups {
127
+ @etagCache("groups:logs:{accountId}:last:{lastId}:limit:{limit}")
128
+ @rustMut
129
+ @get
130
+ op listLogs(
131
+ @path accountId: string,
132
+ @query lastId: string | null,
133
+ @query limit: int32,
134
+ ): { @statusCode statusCode: 200; };
135
+ }
136
+ `;
137
+ const results = await emit(spec);
138
+ const server = results["server.rs"];
139
+ strictEqual(
140
+ server.includes(
141
+ "axum::extract::State(mut service): axum::extract::State<S>",
142
+ ),
143
+ true,
144
+ );
145
+ strictEqual(server.includes("let result = mut service."), false);
146
+ strictEqual(
147
+ server.includes(
148
+ "let result = service.groups_list_logs(&cache, account_id, params.last_id, params.limit).await;",
149
+ ),
150
+ true,
151
+ );
152
+ });
153
+
154
+ it("interpolates query parameters in custom etag keys", async () => {
155
+ const spec = `
156
+ import "@typespec/http";
157
+ import "typespec-rust-emitter";
158
+ using TypeSpec.Http;
159
+
160
+ @route("/accounts/{accountId}/groups")
161
+ namespace Groups {
162
+ @etagCache("groups:list:{accountId}:tz:{userTimezone}:last:{lastId}:limit:{limit}")
163
+ @get
164
+ op list(
165
+ @path accountId: string,
166
+ @query userTimezone: string,
167
+ @query lastId: string | null,
168
+ @query limit: int32,
169
+ ): { @statusCode statusCode: 200; };
170
+ }
171
+ `;
172
+ const results = await emit(spec);
173
+ const server = results["server.rs"];
174
+ strictEqual(
175
+ server.includes(
176
+ 'let cache_key = format!("groups:list:{account_id}:tz:{}:last:{:?}:limit:{}", params.user_timezone, params.last_id, params.limit);',
177
+ ),
178
+ true,
179
+ );
180
+ strictEqual(server.includes("{userTimezone}"), false);
181
+ strictEqual(server.includes("{lastId}"), false);
182
+ });
183
+
65
184
  it("orders cache after claims when combined with @useAuth", async () => {
66
185
  const spec = `
67
186
  import "@typespec/http";
@@ -12,11 +12,12 @@ use eyre::Result;
12
12
 
13
13
  /// Pluggable ETag cache backend.
14
14
  /// Implement this for Redis, Memcached, in-memory HashMap, or any store.
15
+ #[async_trait]
15
16
  pub trait EtagCache {
16
17
  /// Return the stored ETag string for `key`, or `None` if not cached.
17
- fn get(&self, key: &str) -> Option<String>;
18
+ async fn get(&self, key: &str) -> Option<String>;
18
19
  /// Store `etag` under `key`.
19
- fn set(&self, key: &str, etag: &str);
20
+ async fn set(&self, key: &str, etag: &str);
20
21
  }
21
22
 
22
23
 
@@ -64,24 +65,24 @@ where
64
65
  {
65
66
  // Check If-None-Match against the cache
66
67
  let cache_key = format!("articles_get_article:{id}");
67
- if let (Some(stored_etag), Some(axum_extra::TypedHeader(inm))) = (cache.get(cache_key.as_ref()), if_none_match) {
68
- // If the client's ETag matches, respond 304 immediately
69
- if stored_etag == format!("{:?}", inm).trim_matches('"') {
70
- let mut res = axum::http::StatusCode::NOT_MODIFIED.into_response();
71
-
72
- return res;
73
- }
68
+ if let (Some(stored_etag), Some(axum_extra::TypedHeader(inm))) =
69
+ (cache.get(cache_key.as_ref()).await, if_none_match)
70
+ && let Ok(etag) = stored_etag.parse::<axum_extra::headers::ETag>()
71
+ && !inm.precondition_passes(&etag)
72
+ {
73
+ let mut res = axum::http::StatusCode::NOT_MODIFIED.into_response();
74
+
75
+ return res;
74
76
  }
75
77
  // Forward to business logic
76
78
  let result = service.articles_get_article(&cache, id).await;
77
79
  match result {
78
80
  Ok(response) => {
79
81
  let mut res = response.into_response();
80
- if let Some(stored_etag) = cache.get(cache_key.as_ref()) {
81
- res.headers_mut().insert(
82
- axum::http::header::ETAG,
83
- axum::http::HeaderValue::from_str(&stored_etag).unwrap(),
84
- );
82
+ if let Some(stored_etag) = cache.get(cache_key.as_ref()).await
83
+ && let Ok(etag) = axum::http::HeaderValue::from_str(&stored_etag)
84
+ {
85
+ res.headers_mut().insert(axum::http::header::ETAG, etag);
85
86
  }
86
87
 
87
88
  res