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/CHANGELOG.md +40 -0
- package/dist/src/emitter.d.ts +2 -0
- package/dist/src/emitter.js +96 -6
- package/dist/src/emitter.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/test/hello.test.js +108 -1
- package/dist/test/hello.test.js.map +1 -1
- package/example/main.tsp +38 -0
- package/example/output-rust/src/generated/server.rs +163 -3
- package/example/output-rust/src/generated/types.rs +6 -0
- package/example/output-rust/src/main.rs +39 -5
- package/package.json +1 -1
- package/src/emitter.ts +107 -6
- package/src/index.ts +2 -0
- package/src/lib.tsp +2 -0
- package/test/hello.test.ts +119 -1
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
|
|
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
|
|
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
|
|
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);
|
|
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
|
-
|
|
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
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}(
|
|
775
|
+
` async fn ${fnName}(${selfReceiver}, claims: Self::Claims, ${paramsStr}) -> Result<${responseName}>;`,
|
|
708
776
|
);
|
|
709
777
|
} else {
|
|
710
778
|
parts.push(
|
|
711
|
-
` async fn ${fnName}(
|
|
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}(
|
|
785
|
+
` async fn ${fnName}(${selfReceiver}, ${paramsStr}) -> Result<${responseName}>;`,
|
|
718
786
|
);
|
|
719
787
|
} else {
|
|
720
788
|
parts.push(
|
|
721
|
-
` async fn ${fnName}(
|
|
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
|
-
|
|
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 +
|
|
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
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);
|
package/test/hello.test.ts
CHANGED
|
@@ -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
|
});
|