~repos /tide-jsx

#rust#proc-macro#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


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
- app.at("/").get(index);
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 crate::view;
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
- --> $DIR/unexpected-attribute.rs:9:22
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: available fields are: `title`
7
+ = note: all struct fields are already assigned