typespec-rust-emitter 0.13.0 → 0.14.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/.prettierignore +2 -0
- package/CHANGELOG.md +15 -0
- package/dist/src/generator/etag_router.d.ts +6 -7
- package/dist/src/generator/etag_router.js +59 -70
- package/dist/src/generator/etag_router.js.map +1 -1
- package/dist/src/generator/router.js +7 -4
- 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 +65 -4
- package/dist/test/etag_cache.test.js.map +1 -1
- package/example/main.tsp +28 -1
- package/example/output-rust/src/generated/server.rs +186 -29
- package/example/output-rust/src/main.rs +54 -6
- package/package.json +1 -1
- package/scripts/update-golden.js +12 -12
- package/src/generator/etag_router.ts +78 -78
- package/src/generator/router.ts +7 -4
- package/src/generator/server_trait.ts +5 -1
- package/src/parser/operations.ts +11 -3
- package/test/etag_cache.test.ts +93 -4
- package/test/golden/etag_cache/server.rs +16 -14
package/src/generator/router.ts
CHANGED
|
@@ -117,15 +117,18 @@ export function generateRouter(
|
|
|
117
117
|
const serverArgsStr = serverArgs.join(", ");
|
|
118
118
|
|
|
119
119
|
if (hasEtagCache(program, op)) {
|
|
120
|
-
const
|
|
120
|
+
const cachedServerArgs = isProtected
|
|
121
|
+
? [serverArgs[0], "&cache", ...serverArgs.slice(1)]
|
|
122
|
+
: ["&cache", ...serverArgs];
|
|
121
123
|
const code = generateEtagHandler({
|
|
122
124
|
handlerFnName,
|
|
123
|
-
cacheKey,
|
|
124
125
|
pathParams,
|
|
125
|
-
|
|
126
|
+
extractorLines,
|
|
127
|
+
serverArgsStr: cachedServerArgs.join(", "),
|
|
126
128
|
responseName: `${nsName}${toPascalCase(opInfo.name)}Response`,
|
|
127
129
|
etagKey: opInfo.etagKey,
|
|
128
130
|
cacheControl: opInfo.cacheControl,
|
|
131
|
+
serviceBinding,
|
|
129
132
|
});
|
|
130
133
|
handlers.push(code);
|
|
131
134
|
const routePath = `"${opInfo.path}"`;
|
|
@@ -140,7 +143,7 @@ export function generateRouter(
|
|
|
140
143
|
|
|
141
144
|
const serverCall = `service.${traitFnName}(${serverArgsStr}).await`;
|
|
142
145
|
|
|
143
|
-
|
|
146
|
+
const responseHandling = opInfo.cacheControl
|
|
144
147
|
? `match result {
|
|
145
148
|
Ok(response) => {
|
|
146
149
|
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,87 @@ 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("orders cache after claims when combined with @useAuth", async () => {
|
|
120
|
+
const spec = `
|
|
121
|
+
import "@typespec/http";
|
|
122
|
+
import "typespec-rust-emitter";
|
|
123
|
+
using TypeSpec.Http;
|
|
124
|
+
|
|
125
|
+
model Article { id: string; title: string; }
|
|
126
|
+
|
|
127
|
+
@route("/articles")
|
|
128
|
+
namespace Articles {
|
|
129
|
+
@etagCache("article-list")
|
|
130
|
+
@useAuth(BearerAuth)
|
|
131
|
+
@get
|
|
132
|
+
op getArticle(@path id: string): {
|
|
133
|
+
@statusCode statusCode: 200;
|
|
134
|
+
@body body: Article;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
`;
|
|
138
|
+
const results = await emit(spec);
|
|
139
|
+
const server = results["server.rs"];
|
|
140
|
+
strictEqual(
|
|
141
|
+
server.includes(
|
|
142
|
+
"async fn articles_get_article<C: EtagCache + Send + Sync>(\n &self, claims: Self::Claims, cache: &C, id: String",
|
|
143
|
+
),
|
|
144
|
+
true,
|
|
145
|
+
);
|
|
146
|
+
strictEqual(
|
|
147
|
+
server.includes(
|
|
148
|
+
"let result = service.articles_get_article(claims, &cache, id).await;",
|
|
149
|
+
),
|
|
150
|
+
true,
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
65
154
|
it("matches golden file", async () => {
|
|
66
155
|
const results = await emit(ETAG_SPEC);
|
|
67
156
|
compareWithGolden(results, "etag_cache", "server.rs");
|
|
@@ -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
|
|
|
@@ -59,28 +60,29 @@ pub async fn articles_get_article_handler<S, C>(
|
|
|
59
60
|
) -> impl axum::response::IntoResponse
|
|
60
61
|
where
|
|
61
62
|
S: Server + Send + Sync + 'static,
|
|
63
|
+
S::Claims: Send + Sync + Clone + 'static,
|
|
62
64
|
C: EtagCache + Clone + Send + Sync + 'static,
|
|
63
65
|
{
|
|
64
66
|
// Check If-None-Match against the cache
|
|
65
67
|
let cache_key = format!("articles_get_article:{id}");
|
|
66
|
-
if let (Some(stored_etag), Some(axum_extra::TypedHeader(inm))) =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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;
|
|
73
76
|
}
|
|
74
77
|
// Forward to business logic
|
|
75
78
|
let result = service.articles_get_article(&cache, id).await;
|
|
76
79
|
match result {
|
|
77
80
|
Ok(response) => {
|
|
78
81
|
let mut res = response.into_response();
|
|
79
|
-
if let Some(stored_etag) = cache.get(cache_key.as_ref())
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
);
|
|
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);
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
res
|