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, 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}