rocal_core/
lib.rs

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, quer: &str) -> 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            _ => panic!("Method should be get or post"),
85        };
86        let path = route.get_path().clone().expect("Path should be here");
87        let action = route.get_action().clone().expect("Action shuold be here");
88
89        let ctrl = Ident::new(
90            &format!("{}_{}", "ctrl_", controller.to_string()),
91            Span::call_site(),
92        );
93
94        quote! {
95            let #ctrl = std::rc::Rc::new(crate::controllers::#controller_mod_name::#controller::new(
96                router.clone(),
97                crate::views::#view_mod_name::#view::new(router.clone()),
98            ));
99
100            router
101                .clone()
102                .borrow_mut()
103                .register(#method, #path, {
104                    let #ctrl = std::rc::Rc::clone(&#ctrl);
105                    Box::new(move |args| {
106                        Box::pin({
107                            let #ctrl = std::rc::Rc::clone(&#ctrl);
108                            async move { #ctrl.#action(args).await }
109                        })
110                    })
111                });
112        }
113    });
114
115    quote! {
116        let router = std::rc::Rc::new(std::cell::RefCell::new(rocal::rocal_core::router::Router::new()));
117
118        #(#routes)*
119
120        let route_handler = rocal::rocal_core::route_handler::RouteHandler::new(router, None);
121        let route_handler = std::rc::Rc::new(route_handler);
122
123        route_handler.handle_route().await;
124
125        let handle_route_closure = {
126            let route_handler = std::rc::Rc::clone(&route_handler);
127            Closure::wrap(Box::new(move || {
128                let route_handler = std::rc::Rc::clone(&route_handler);
129                wasm_bindgen_futures::spawn_local(async move {
130                    route_handler.handle_route().await;
131                });
132            }) as Box<dyn Fn()>)
133        };
134
135        web_sys::window()
136            .unwrap()
137            .add_event_listener_with_callback("popstate", handle_route_closure.as_ref().unchecked_ref())
138            .unwrap();
139        handle_route_closure.forget();
140    }
141}
142
143pub fn build_config(item: TokenStream) -> TokenStream {
144    let config_struct = build_config_struct();
145    let config = match parse_config(item) {
146        Ok(config) => config,
147        Err(err) => return err.to_compile_error().into(),
148    };
149
150    let app_id = config.get_app_id().clone().unwrap_or(String::new());
151    let sync_server_endpoint = config
152        .get_sync_server_endpoint()
153        .clone()
154        .unwrap_or(String::new());
155    let database_directory_name = config
156        .get_database_directory_name()
157        .clone()
158        .unwrap_or(String::new());
159    let database_file_name = config
160        .get_database_file_name()
161        .clone()
162        .unwrap_or(String::new());
163
164    quote! {
165        #config_struct
166
167        static CONFIG: std::sync::LazyLock<crate::Configuration> = std::sync::LazyLock::new(|| {
168            crate::Configuration::new(
169                #app_id.to_string(),
170                #sync_server_endpoint.to_string(),
171                std::sync::Arc::new(Database::new(
172                    #database_directory_name.to_string(),
173                    #database_file_name.to_string(),
174                )),
175            )
176        });
177    }
178}
179
180pub fn build_action(item: TokenStream) -> TokenStream {
181    let ast: ItemFn = syn::parse2(item).unwrap();
182
183    let parsed_action = match parse_action(&ast) {
184        Ok(action) => action,
185        Err(err) => return err.to_compile_error().into(),
186    };
187
188    let fn_name = parsed_action.get_name();
189
190    let build_args = parsed_action.get_args().iter().map(|arg| {
191        let name = arg.get_name();
192        let name_str = name.to_string();
193
194        let is_optional = arg.get_is_optional();
195
196        let mut result = if *is_optional {
197            quote! {
198                let #name = if let Some(#name) = args.get(#name_str) {
199                    Some(#name.clone())
200                } else {
201                    None
202                };
203            }
204        } else {
205            quote! {
206                let #name = args.get(#name_str).expect(&format!("{} is required", #name_str));
207            }
208        };
209
210        let ty = arg.get_ty();
211        let ty_str = ty.to_string();
212
213        result = if ty == "String" || ty == "str" {
214            quote!(#result)
215        } else {
216            if *is_optional {
217                quote! {
218                    #result
219
220                    let #name = if let Some(#name) = #name {
221                        if let Ok(#name) = #name.parse::<#ty>() {
222                            Some(#name)
223                        } else {
224                            None
225                        }
226                    } else {
227                        None
228                    }
229                }
230            } else {
231                quote! {
232                    #result
233
234                    let #name = #name.parse::<#ty>().expect(&format!("{} cannot be parsed as {}", #name_str, #ty_str));
235                }
236            }
237        };
238
239        result
240    });
241
242    let stmts = &ast.block.stmts;
243
244    quote! {
245        pub async fn #fn_name(&self, args: std::collections::HashMap<String, String>) {
246            #(#build_args)*
247
248            #(#stmts)*
249        }
250    }
251}
252
253pub fn run_migration(item: TokenStream) -> TokenStream {
254    let query = match get_migrations(&item) {
255        Ok(query) => query,
256        Err(err) => return err.to_compile_error().into(),
257    };
258
259    quote! {
260        match CONFIG.get_database().exec(#query).await {
261            Ok(_) => (),
262            Err(err) => web_sys::console::error_1(&err),
263        }
264    }
265}