1#![doc = include_str!("../README.md")]
2
3use configuration::{build_config_struct, parse_config};
4use database::build_database_struct;
5use enums::request_method::RequestMethod;
6use migrator::get_migrations;
7use parsed_action::parse_action;
8use parsed_route::parse_routes;
9use proc_macro2::{Span, TokenStream};
10use quote::quote;
11use syn::{parse_str, Ident, ItemFn, Path};
12use utils::to_snake_case;
13use workers::db_sync_worker::build_db_sync_worker_struct;
14
15mod configuration;
16mod database;
17pub mod enums;
18mod migrator;
19mod parsed_action;
20mod parsed_route;
21pub mod route_handler;
22pub mod router;
23pub mod traits;
24mod utils;
25pub mod workers;
26
27pub fn start_app(item: TokenStream) -> TokenStream {
28 let ast: ItemFn = syn::parse(item.into()).unwrap();
29 let stmts = &ast.block.stmts;
30
31 let database_struct = build_database_struct();
32 let db_sync_worker_struct = build_db_sync_worker_struct();
33
34 quote! {
35 use wasm_bindgen::prelude::*;
36 use rocal::rocal_core::traits::{Controller, View};
37
38 #[wasm_bindgen]
39 extern "C" {
40 #[wasm_bindgen(js_name = execSQL)]
41 fn exec_sql(db: &str, query: &str, bindings: Box<[JsValue]>) -> JsValue;
42 }
43
44 #[wasm_bindgen(start)]
45 pub async fn run() {
46 #(#stmts)*
47
48 let db_sync_worker = crate::DbSyncWorker::new(
49 "./js/db_sync_worker.js",
50 crate::ForceType::None
51 );
52 db_sync_worker.run();
53 }
54
55 #database_struct
56 #db_sync_worker_struct
57 }
58}
59
60pub fn build_route(item: TokenStream) -> TokenStream {
61 let routes = match parse_routes(item) {
62 Ok(routes) => routes,
63 Err(err) => return err.to_compile_error().into(),
64 };
65
66 let routes = routes.into_iter().map(|route| {
67 let controller = route
68 .get_controller()
69 .clone()
70 .expect("Controller should be here");
71 let controller_mod_name =
72 Ident::new(&to_snake_case(&controller.to_string()), Span::call_site());
73 let view = route.get_view().clone().expect("View should be here");
74 let view_mod_name = Ident::new(&to_snake_case(&view.to_string()), Span::call_site());
75 let method: Path = match route.get_method() {
76 Some(RequestMethod::Get) => {
77 parse_str("rocal::rocal_core::enums::request_method::RequestMethod::Get")
78 .expect("Failed to parse the enum")
79 }
80 Some(RequestMethod::Post) => {
81 parse_str("rocal::rocal_core::enums::request_method::RequestMethod::Post")
82 .expect("Failed to parse the enum")
83 }
84 Some(RequestMethod::Put) => {
85 parse_str("rocal::rocal_core::enums::request_method::RequestMethod::Put")
86 .expect("Failed to parse the enum")
87 }
88 Some(RequestMethod::Patch) => {
89 parse_str("rocal::rocal_core::enums::request_method::RequestMethod::Patch")
90 .expect("Failed to parse the enum")
91 }
92 Some(RequestMethod::Delete) => {
93 parse_str("rocal::rocal_core::enums::request_method::RequestMethod::Delete")
94 .expect("Failed to parse the enum")
95 }
96 _ => panic!("Method should be get or post"),
97 };
98 let path = route.get_path().clone().expect("Path should be here");
99 let action = route.get_action().clone().expect("Action shuold be here");
100
101 let ctrl = Ident::new(
102 &format!("{}_{}", "ctrl_", controller.to_string()),
103 Span::call_site(),
104 );
105
106 quote! {
107 let #ctrl = std::rc::Rc::new(crate::controllers::#controller_mod_name::#controller::new(
108 router.clone(),
109 crate::views::#view_mod_name::#view::new(router.clone()),
110 ));
111
112 router
113 .clone()
114 .borrow_mut()
115 .register(#method, #path, {
116 let #ctrl = std::rc::Rc::clone(&#ctrl);
117 Box::new(move |args| {
118 Box::pin({
119 let #ctrl = std::rc::Rc::clone(&#ctrl);
120 async move { #ctrl.#action(args).await }
121 })
122 })
123 });
124 }
125 });
126
127 quote! {
128 let router = std::rc::Rc::new(std::cell::RefCell::new(rocal::rocal_core::router::Router::new()));
129
130 #(#routes)*
131
132 let route_handler = rocal::rocal_core::route_handler::RouteHandler::new(router, None);
133 let route_handler = std::rc::Rc::new(route_handler);
134
135 route_handler.handle_route().await;
136
137 let handle_route_closure = {
138 let route_handler = std::rc::Rc::clone(&route_handler);
139 Closure::wrap(Box::new(move || {
140 let route_handler = std::rc::Rc::clone(&route_handler);
141 wasm_bindgen_futures::spawn_local(async move {
142 route_handler.handle_route().await;
143 });
144 }) as Box<dyn Fn()>)
145 };
146
147 web_sys::window()
148 .unwrap()
149 .add_event_listener_with_callback("popstate", handle_route_closure.as_ref().unchecked_ref())
150 .unwrap();
151 handle_route_closure.forget();
152 }
153}
154
155pub fn build_config(item: TokenStream) -> TokenStream {
156 let config_struct = build_config_struct();
157 let config = match parse_config(item) {
158 Ok(config) => config,
159 Err(err) => return err.to_compile_error().into(),
160 };
161
162 let app_id = config.get_app_id().clone().unwrap_or(String::new());
163 let sync_server_endpoint = config
164 .get_sync_server_endpoint()
165 .clone()
166 .unwrap_or(String::new());
167 let database_directory_name = config
168 .get_database_directory_name()
169 .clone()
170 .unwrap_or(String::new());
171 let database_file_name = config
172 .get_database_file_name()
173 .clone()
174 .unwrap_or(String::new());
175
176 quote! {
177 #config_struct
178
179 static CONFIG: std::sync::LazyLock<crate::Configuration> = std::sync::LazyLock::new(|| {
180 crate::Configuration::new(
181 #app_id.to_string(),
182 #sync_server_endpoint.to_string(),
183 std::sync::Arc::new(Database::new(
184 #database_directory_name.to_string(),
185 #database_file_name.to_string(),
186 )),
187 )
188 });
189 }
190}
191
192pub fn build_action(item: TokenStream) -> TokenStream {
193 let ast: ItemFn = syn::parse2(item).unwrap();
194
195 let parsed_action = match parse_action(&ast) {
196 Ok(action) => action,
197 Err(err) => return err.to_compile_error().into(),
198 };
199
200 let fn_name = parsed_action.get_name();
201
202 let build_args = parsed_action.get_args().iter().map(|arg| {
203 let name = arg.get_name();
204 let name_str = name.to_string();
205
206 let is_optional = arg.get_is_optional();
207
208 let mut result = if *is_optional {
209 quote! {
210 let #name = if let Some(#name) = args.get(#name_str) {
211 Some(#name.clone())
212 } else {
213 None
214 };
215 }
216 } else {
217 quote! {
218 let #name = args.get(#name_str).expect(&format!("{} is required", #name_str));
219 }
220 };
221
222 let ty = arg.get_ty();
223 let ty_str = ty.to_string();
224
225 result = if ty == "String" || ty == "str" {
226 quote!(#result)
227 } else {
228 if *is_optional {
229 quote! {
230 #result
231
232 let #name = if let Some(#name) = #name {
233 if let Ok(#name) = #name.parse::<#ty>() {
234 Some(#name)
235 } else {
236 None
237 }
238 } else {
239 None
240 };
241 }
242 } else {
243 quote! {
244 #result
245
246 let #name = #name.parse::<#ty>().expect(&format!("{} cannot be parsed as {}", #name_str, #ty_str));
247 }
248 }
249 };
250
251 result
252 });
253
254 let stmts = &ast.block.stmts;
255
256 quote! {
257 pub async fn #fn_name(&self, args: std::collections::HashMap<String, String>) {
258 #(#build_args)*
259
260 #(#stmts)*
261 }
262 }
263}
264
265pub fn run_migration(item: TokenStream) -> TokenStream {
266 let query = match get_migrations(&item) {
267 Ok(query) => query,
268 Err(err) => return err.to_compile_error().into(),
269 };
270
271 if !query.is_empty() {
272 quote! {
273 match CONFIG.get_database().query(#query).execute().await {
274 Ok(_) => (),
275 Err(err) => web_sys::console::error_1(&err),
276 }
277 }
278 } else {
279 quote!()
280 }
281}