~repos /tide-jsx
GIT_CONFIG_PARAMETERS="'http.version=HTTP/1.1'" git clone
https://git.pyrossh.dev/tide-jsx/.git
tide-jsx
Discussions:
https://groups.google.com/g/rust-embed-devs
Tide + JSX
509577f5
—
Peter John 1 month ago
new stuff
- examples/basic.rs +19 -2
- impl/src/lib.rs +43 -0
- impl/src/route.rs +38 -0
- impl/src/tags.rs +0 -2
- src/lib.rs +34 -1
- tests/lib.rs +163 -1
- tests/ui/fail/unexpected-attribute.stderr +2 -2
examples/basic.rs
CHANGED
|
@@ -2,13 +2,14 @@ use tide::http::mime;
|
|
|
2
2
|
use tide::utils::After;
|
|
3
3
|
use tide::{log, Request, Response};
|
|
4
4
|
use tide_jsx::html::HTML5Doctype;
|
|
5
|
-
use tide_jsx::{component, rsx, view};
|
|
5
|
+
use tide_jsx::{component, get, post, routes, rsx, view};
|
|
6
6
|
|
|
7
7
|
#[component]
|
|
8
8
|
fn Heading<'title>(title: &'title str) {
|
|
9
9
|
rsx! { <h1 class={"title"}>{title}</h1> }
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
#[get("/")]
|
|
12
13
|
async fn index(_req: Request<()>) -> tide::Result {
|
|
13
14
|
view! {
|
|
14
15
|
<>
|
|
@@ -25,6 +26,22 @@ async fn index(_req: Request<()>) -> tide::Result {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
#[post("/submit")]
|
|
30
|
+
async fn submit(mut req: Request<()>) -> tide::Result {
|
|
31
|
+
let body = req.body_string().await?;
|
|
32
|
+
view! {
|
|
33
|
+
<>
|
|
34
|
+
<HTML5Doctype />
|
|
35
|
+
<html>
|
|
36
|
+
<body>
|
|
37
|
+
<h1>{"Submitted"}</h1>
|
|
38
|
+
<p>{format!("Received: {}", body)}</p>
|
|
39
|
+
</body>
|
|
40
|
+
</html>
|
|
41
|
+
</>
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
28
45
|
#[async_std::main]
|
|
29
46
|
async fn main() -> tide::Result<()> {
|
|
30
47
|
log::start();
|
|
@@ -39,7 +56,7 @@ async fn main() -> tide::Result<()> {
|
|
|
39
56
|
}
|
|
40
57
|
Ok(res)
|
|
41
58
|
}));
|
|
42
|
-
|
|
59
|
+
routes!(app, [index, submit]);
|
|
43
60
|
app.listen("127.0.0.1:5000").await?;
|
|
44
61
|
Ok(())
|
|
45
62
|
}
|
impl/src/lib.rs
CHANGED
|
@@ -6,6 +6,7 @@ mod element;
|
|
|
6
6
|
mod element_attribute;
|
|
7
7
|
mod element_attributes;
|
|
8
8
|
mod function_component;
|
|
9
|
+
mod route;
|
|
9
10
|
mod tags;
|
|
10
11
|
|
|
11
12
|
use element::Element;
|
|
@@ -50,3 +51,45 @@ pub fn component(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
|
50
51
|
let f = parse_macro_input!(item as syn::ItemFn);
|
|
51
52
|
function_component::create_function_component(f)
|
|
52
53
|
}
|
|
54
|
+
|
|
55
|
+
#[proc_macro_attribute]
|
|
56
|
+
#[proc_macro_error]
|
|
57
|
+
pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
58
|
+
route::create_route("get", attr, item)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#[proc_macro_attribute]
|
|
62
|
+
#[proc_macro_error]
|
|
63
|
+
pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
64
|
+
route::create_route("post", attr, item)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[proc_macro_attribute]
|
|
68
|
+
#[proc_macro_error]
|
|
69
|
+
pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
70
|
+
route::create_route("put", attr, item)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#[proc_macro_attribute]
|
|
74
|
+
#[proc_macro_error]
|
|
75
|
+
pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
76
|
+
route::create_route("delete", attr, item)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#[proc_macro_attribute]
|
|
80
|
+
#[proc_macro_error]
|
|
81
|
+
pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
82
|
+
route::create_route("patch", attr, item)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[proc_macro_attribute]
|
|
86
|
+
#[proc_macro_error]
|
|
87
|
+
pub fn head(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
88
|
+
route::create_route("head", attr, item)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[proc_macro_attribute]
|
|
92
|
+
#[proc_macro_error]
|
|
93
|
+
pub fn options(attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
94
|
+
route::create_route("options", attr, item)
|
|
95
|
+
}
|
impl/src/route.rs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
use proc_macro::TokenStream;
|
|
2
|
+
use quote::quote;
|
|
3
|
+
use syn::LitStr;
|
|
4
|
+
|
|
5
|
+
pub fn create_route(method: &str, attr: TokenStream, item: TokenStream) -> TokenStream {
|
|
6
|
+
let path = match syn::parse::<LitStr>(attr) {
|
|
7
|
+
Ok(lit) => lit,
|
|
8
|
+
Err(err) => return TokenStream::from(err.to_compile_error()),
|
|
9
|
+
};
|
|
10
|
+
let f = match syn::parse::<syn::ItemFn>(item) {
|
|
11
|
+
Ok(f) => f,
|
|
12
|
+
Err(err) => return TokenStream::from(err.to_compile_error()),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let fn_name = &f.sig.ident;
|
|
16
|
+
let vis = &f.vis;
|
|
17
|
+
let block = &f.block;
|
|
18
|
+
let inputs = &f.sig.inputs;
|
|
19
|
+
let output = &f.sig.output;
|
|
20
|
+
let attrs = &f.attrs;
|
|
21
|
+
let asyncness = &f.sig.asyncness;
|
|
22
|
+
|
|
23
|
+
let method_str = method;
|
|
24
|
+
|
|
25
|
+
TokenStream::from(quote! {
|
|
26
|
+
#[allow(non_camel_case_types)]
|
|
27
|
+
#vis struct #fn_name;
|
|
28
|
+
|
|
29
|
+
impl #fn_name {
|
|
30
|
+
pub const PATH: &'static str = #path;
|
|
31
|
+
pub const METHOD: &'static str = #method_str;
|
|
32
|
+
|
|
33
|
+
#(#attrs)*
|
|
34
|
+
#vis #asyncness fn handler(#inputs) #output
|
|
35
|
+
#block
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
}
|
impl/src/tags.rs
CHANGED
|
@@ -8,7 +8,6 @@ pub struct OpenTag {
|
|
|
8
8
|
pub name: syn::Path,
|
|
9
9
|
pub attributes: ElementAttributes,
|
|
10
10
|
pub self_closing: bool,
|
|
11
|
-
pub is_custom_element: bool,
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
fn name_or_fragment(maybe_name: Result<syn::Path>) -> syn::Path {
|
|
@@ -40,7 +39,6 @@ impl Parse for OpenTag {
|
|
|
40
39
|
name,
|
|
41
40
|
attributes,
|
|
42
41
|
self_closing,
|
|
43
|
-
is_custom_element,
|
|
44
42
|
})
|
|
45
43
|
}
|
|
46
44
|
}
|
src/lib.rs
CHANGED
|
@@ -10,4 +10,37 @@ pub use self::render::Render;
|
|
|
10
10
|
pub use fragment::Fragment;
|
|
11
11
|
pub use simple_element::SimpleElement;
|
|
12
12
|
pub use text_element::Raw;
|
|
13
|
-
pub use tide_jsx_impl::{component, html, rsx, view};
|
|
13
|
+
pub use tide_jsx_impl::{component, delete, get, head, html, options, patch, post, put, rsx, view};
|
|
14
|
+
|
|
15
|
+
#[macro_export]
|
|
16
|
+
macro_rules! routes {
|
|
17
|
+
($app:expr, [$($route:ident),* $(,)?]) => {{
|
|
18
|
+
$(
|
|
19
|
+
$crate::__register_route!($app, $route);
|
|
20
|
+
)*
|
|
21
|
+
}};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#[macro_export]
|
|
25
|
+
#[doc(hidden)]
|
|
26
|
+
macro_rules! __register_route {
|
|
27
|
+
($app:expr, $route:ident) => {
|
|
28
|
+
if $route::METHOD == "get" {
|
|
29
|
+
$app.at($route::PATH).get($route::handler);
|
|
30
|
+
} else if $route::METHOD == "post" {
|
|
31
|
+
$app.at($route::PATH).post($route::handler);
|
|
32
|
+
} else if $route::METHOD == "put" {
|
|
33
|
+
$app.at($route::PATH).put($route::handler);
|
|
34
|
+
} else if $route::METHOD == "delete" {
|
|
35
|
+
$app.at($route::PATH).delete($route::handler);
|
|
36
|
+
} else if $route::METHOD == "patch" {
|
|
37
|
+
$app.at($route::PATH).patch($route::handler);
|
|
38
|
+
} else if $route::METHOD == "head" {
|
|
39
|
+
$app.at($route::PATH).head($route::handler);
|
|
40
|
+
} else if $route::METHOD == "options" {
|
|
41
|
+
$app.at($route::PATH).options($route::handler);
|
|
42
|
+
} else {
|
|
43
|
+
panic!("Unsupported HTTP method: {}", $route::METHOD);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
tests/lib.rs
CHANGED
|
@@ -253,7 +253,9 @@ mod other {
|
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
mod integration {
|
|
256
|
+
use crate::{component, rsx, view};
|
|
256
|
-
use
|
|
257
|
+
use tide::Request;
|
|
258
|
+
use tide_jsx::{delete, get, patch, post, put, routes};
|
|
257
259
|
use tide_testing::TideTestingExt;
|
|
258
260
|
|
|
259
261
|
#[async_std::test]
|
|
@@ -277,4 +279,164 @@ mod integration {
|
|
|
277
279
|
);
|
|
278
280
|
Ok(())
|
|
279
281
|
}
|
|
282
|
+
|
|
283
|
+
#[get("/")]
|
|
284
|
+
async fn index(_req: Request<()>) -> tide::Result {
|
|
285
|
+
view! { <h1>{"Home"}</h1> }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#[get("/about")]
|
|
289
|
+
async fn about(_req: Request<()>) -> tide::Result {
|
|
290
|
+
view! { <h1>{"About"}</h1> }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
#[post("/submit")]
|
|
294
|
+
async fn submit(mut req: Request<()>) -> tide::Result {
|
|
295
|
+
let body = req.body_string().await?;
|
|
296
|
+
view! { <p>{format!("Received: {}", body)}</p> }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
#[put("/update")]
|
|
300
|
+
async fn update(_req: Request<()>) -> tide::Result {
|
|
301
|
+
view! { <p>{"Updated"}</p> }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#[patch("/patch")]
|
|
305
|
+
async fn patch_it(_req: Request<()>) -> tide::Result {
|
|
306
|
+
view! { <p>{"Patched"}</p> }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#[delete("/remove")]
|
|
310
|
+
async fn remove(_req: Request<()>) -> tide::Result {
|
|
311
|
+
view! { <p>{"Deleted"}</p> }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#[async_std::test]
|
|
315
|
+
async fn test_get_route_macro() -> tide::Result<()> {
|
|
316
|
+
let mut app = tide::new();
|
|
317
|
+
routes!(app, [index]);
|
|
318
|
+
|
|
319
|
+
let res = app.get("/").recv_string().await?;
|
|
320
|
+
assert_eq!(res, "<h1>Home</h1>");
|
|
321
|
+
Ok(())
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#[async_std::test]
|
|
325
|
+
async fn test_multiple_get_routes() -> tide::Result<()> {
|
|
326
|
+
let mut app = tide::new();
|
|
327
|
+
routes!(app, [index, about]);
|
|
328
|
+
|
|
329
|
+
assert_eq!(app.get("/").recv_string().await?, "<h1>Home</h1>");
|
|
330
|
+
assert_eq!(app.get("/about").recv_string().await?, "<h1>About</h1>");
|
|
331
|
+
Ok(())
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
#[async_std::test]
|
|
335
|
+
async fn test_post_route_macro() -> tide::Result<()> {
|
|
336
|
+
let mut app = tide::new();
|
|
337
|
+
routes!(app, [submit]);
|
|
338
|
+
|
|
339
|
+
let res = app.post("/submit").body_string("hello".into()).recv_string().await?;
|
|
340
|
+
assert_eq!(res, "<p>Received: hello</p>");
|
|
341
|
+
Ok(())
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
#[async_std::test]
|
|
345
|
+
async fn test_put_route_macro() -> tide::Result<()> {
|
|
346
|
+
let mut app = tide::new();
|
|
347
|
+
routes!(app, [update]);
|
|
348
|
+
|
|
349
|
+
let mut res = app.put("/update").await?;
|
|
350
|
+
assert_eq!(res.body_string().await?, "<p>Updated</p>");
|
|
351
|
+
Ok(())
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
#[async_std::test]
|
|
355
|
+
async fn test_patch_route_macro() -> tide::Result<()> {
|
|
356
|
+
let mut app = tide::new();
|
|
357
|
+
routes!(app, [patch_it]);
|
|
358
|
+
|
|
359
|
+
let mut res = app.patch("/patch").await?;
|
|
360
|
+
assert_eq!(res.body_string().await?, "<p>Patched</p>");
|
|
361
|
+
Ok(())
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
#[async_std::test]
|
|
365
|
+
async fn test_delete_route_macro() -> tide::Result<()> {
|
|
366
|
+
let mut app = tide::new();
|
|
367
|
+
routes!(app, [remove]);
|
|
368
|
+
|
|
369
|
+
let mut res = app.delete("/remove").await?;
|
|
370
|
+
assert_eq!(res.body_string().await?, "<p>Deleted</p>");
|
|
371
|
+
Ok(())
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#[async_std::test]
|
|
375
|
+
async fn test_all_methods_together() -> tide::Result<()> {
|
|
376
|
+
let mut app = tide::new();
|
|
377
|
+
routes!(app, [index, about, submit, update, patch_it, remove]);
|
|
378
|
+
|
|
379
|
+
assert_eq!(app.get("/").recv_string().await?, "<h1>Home</h1>");
|
|
380
|
+
assert_eq!(app.get("/about").recv_string().await?, "<h1>About</h1>");
|
|
381
|
+
assert_eq!(
|
|
382
|
+
app.post("/submit").body_string("data".into()).recv_string().await?,
|
|
383
|
+
"<p>Received: data</p>"
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
let mut put_res = app.put("/update").await?;
|
|
387
|
+
assert_eq!(put_res.body_string().await?, "<p>Updated</p>");
|
|
388
|
+
|
|
389
|
+
let mut patch_res = app.patch("/patch").await?;
|
|
390
|
+
assert_eq!(patch_res.body_string().await?, "<p>Patched</p>");
|
|
391
|
+
|
|
392
|
+
let mut del_res = app.delete("/remove").await?;
|
|
393
|
+
assert_eq!(del_res.body_string().await?, "<p>Deleted</p>");
|
|
394
|
+
|
|
395
|
+
Ok(())
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
#[component]
|
|
399
|
+
fn Greeting<'name>(name: &'name str) {
|
|
400
|
+
rsx! { <h1>{format!("Hello, {}!", name)}</h1> }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
#[get("/greet")]
|
|
404
|
+
async fn greet(_req: Request<()>) -> tide::Result {
|
|
405
|
+
view! { <Greeting name={"World"} /> }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
#[async_std::test]
|
|
409
|
+
async fn test_route_with_component() -> tide::Result<()> {
|
|
410
|
+
let mut app = tide::new();
|
|
411
|
+
routes!(app, [greet]);
|
|
412
|
+
|
|
413
|
+
assert_eq!(
|
|
414
|
+
app.get("/greet").recv_string().await?,
|
|
415
|
+
"<h1>Hello, World!</h1>"
|
|
416
|
+
);
|
|
417
|
+
Ok(())
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
#[async_std::test]
|
|
421
|
+
async fn test_route_returns_html_content_type() -> tide::Result<()> {
|
|
422
|
+
let mut app = tide::new();
|
|
423
|
+
routes!(app, [index]);
|
|
424
|
+
|
|
425
|
+
let res = app.get("/").await?;
|
|
426
|
+
assert_eq!(
|
|
427
|
+
res.header("content-type").unwrap().as_str(),
|
|
428
|
+
tide::http::mime::HTML.to_string()
|
|
429
|
+
);
|
|
430
|
+
Ok(())
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
#[async_std::test]
|
|
434
|
+
async fn test_wrong_method_returns_not_found() -> tide::Result<()> {
|
|
435
|
+
let mut app = tide::new();
|
|
436
|
+
routes!(app, [index]);
|
|
437
|
+
|
|
438
|
+
let res = app.post("/").await?;
|
|
439
|
+
assert_eq!(res.status(), tide::http::StatusCode::MethodNotAllowed);
|
|
440
|
+
Ok(())
|
|
441
|
+
}
|
|
280
442
|
}
|
tests/ui/fail/unexpected-attribute.stderr
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
error[E0560]: struct `Heading<'_>` has no field named `t`
|
|
2
|
-
-->
|
|
2
|
+
--> tests/ui/fail/unexpected-attribute.rs:9:22
|
|
3
3
|
|
|
|
4
4
|
9 | html! { <Heading t={"Hello world!"} /> };
|
|
5
5
|
| ^ `Heading<'_>` does not have this field
|
|
6
6
|
|
|
|
7
|
-
= note:
|
|
7
|
+
= note: all struct fields are already assigned
|