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.
- package/.prettierignore +2 -0
- package/CHANGELOG.md +16 -0
- package/dist/src/generator/etag_router.d.ts +4 -2
- package/dist/src/generator/etag_router.js +71 -18
- package/dist/src/generator/etag_router.js.map +1 -1
- package/dist/src/generator/router.js +3 -3
- package/dist/src/generator/router.js.map +1 -1
- package/dist/src/generator/server_trait.js.map +1 -1
- package/dist/src/parser/operations.js.map +1 -1
- package/dist/test/etag_cache.test.js +89 -4
- package/dist/test/etag_cache.test.js.map +1 -1
- package/example/main.tsp +41 -0
- package/example/output-rust/src/generated/server.rs +228 -27
- package/example/output-rust/src/main.rs +74 -6
- package/package.json +1 -1
- package/scripts/update-golden.js +12 -12
- package/src/generator/etag_router.ts +89 -19
- package/src/generator/router.ts +3 -3
- package/src/generator/server_trait.ts +5 -1
- package/src/parser/operations.ts +11 -3
- package/test/etag_cache.test.ts +123 -4
- package/test/golden/etag_cache/server.rs +15 -14
package/src/generator/router.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
}
|
package/src/parser/operations.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
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
|
}
|
package/test/etag_cache.test.ts
CHANGED
|
@@ -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(
|
|
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))) =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|