├── .gitattributes ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── MIT-LICENSE ├── README.md ├── examples ├── hello_world │ ├── Cargo.toml │ ├── db │ │ └── migrations │ │ │ ├── .keep │ │ │ └── 202402090340_create_sync_connection_table.sql │ ├── index.html │ ├── js │ │ ├── db_query_worker.js │ │ ├── db_sync_worker.js │ │ ├── global.js │ │ ├── sqlite3-opfs-async-proxy.js │ │ ├── sqlite3.mjs │ │ └── sqlite3.wasm │ ├── src │ │ ├── controllers.rs │ │ ├── controllers │ │ │ ├── root_controller.rs │ │ │ └── sync_connections_controller.rs │ │ ├── lib.rs │ │ ├── models.rs │ │ ├── models │ │ │ ├── .keep │ │ │ └── sync_connection.rs │ │ ├── repositories.rs │ │ ├── repositories │ │ │ └── sync_connection_repository.rs │ │ ├── templates.rs │ │ ├── templates │ │ │ ├── root_template.rs │ │ │ └── sync_connection_edit_template.rs │ │ ├── views.rs │ │ └── views │ │ │ ├── root_view.rs │ │ │ └── sync_connection_view.rs │ └── sw.js ├── self_checkout │ ├── Cargo.lock │ ├── Cargo.toml │ ├── db │ │ └── migrations │ │ │ ├── .keep │ │ │ ├── 202504120810_create_product_table.sql │ │ │ ├── 202504121026_create_cart_table.sql │ │ │ └── 202504130331_create_sales_table.sql │ ├── index.html │ ├── js │ │ ├── db_query_worker.js │ │ ├── db_sync_worker.js │ │ ├── global.js │ │ ├── sqlite3-opfs-async-proxy.js │ │ ├── sqlite3.mjs │ │ └── sqlite3.wasm │ ├── public │ │ ├── favicon.ico │ │ ├── images │ │ │ └── loading.gif │ │ └── twind.js │ ├── src │ │ ├── controllers.rs │ │ ├── controllers │ │ │ ├── carts_controller.rs │ │ │ ├── root_controller.rs │ │ │ └── sales_controller.rs │ │ ├── lib.rs │ │ ├── models.rs │ │ ├── models │ │ │ ├── .keep │ │ │ ├── cart_item.rs │ │ │ ├── flash_memory.rs │ │ │ ├── product.rs │ │ │ ├── sales.rs │ │ │ ├── sales_item.rs │ │ │ └── sales_log.rs │ │ ├── repositories.rs │ │ ├── repositories │ │ │ ├── cart_repository.rs │ │ │ ├── product_repository.rs │ │ │ └── sales_repository.rs │ │ ├── templates.rs │ │ ├── templates │ │ │ ├── root_template.rs │ │ │ ├── sales_item_template.rs │ │ │ └── sales_log_template.rs │ │ ├── view_models.rs │ │ ├── view_models │ │ │ ├── root_view_model.rs │ │ │ ├── sales_item_view_model.rs │ │ │ └── sales_log_view_model.rs │ │ ├── views.rs │ │ └── views │ │ │ ├── empty_view.rs │ │ │ ├── root_view.rs │ │ │ └── sales_view.rs │ └── sw.js └── simple_note │ ├── .gitignore │ ├── Cargo.toml │ ├── db │ └── migrations │ │ ├── .keep │ │ └── 20250506055848-create-notes-table.sql │ ├── index.html │ ├── js │ ├── db_query_worker.js │ ├── db_sync_worker.js │ ├── global.js │ ├── sqlite3-opfs-async-proxy.js │ ├── sqlite3.mjs │ └── sqlite3.wasm │ ├── public │ ├── .keep │ ├── favicon.ico │ └── twind.js │ ├── src │ ├── controllers.rs │ ├── controllers │ │ ├── notes_controller.rs │ │ └── root_controller.rs │ ├── lib.rs │ ├── models.rs │ ├── models │ │ ├── .keep │ │ ├── note.rs │ │ └── note_id.rs │ ├── templates.rs │ ├── templates │ │ └── root_template.rs │ ├── view_models.rs │ ├── view_models │ │ └── root_view_model.rs │ ├── views.rs │ └── views │ │ ├── notes_view.rs │ │ └── root_view.rs │ └── sw.js ├── rocal ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ └── main.rs ├── rocal_cli ├── Cargo.toml ├── README.md ├── build.rs ├── js │ ├── db_query_worker.js │ ├── db_sync_worker.js │ ├── global.js │ ├── sqlite3-opfs-async-proxy.js │ ├── sqlite3.mjs │ ├── sqlite3.wasm │ └── sw.js ├── seeds │ └── root_template.rs └── src │ ├── commands.rs │ ├── commands │ ├── build.rs │ ├── init.rs │ ├── login.rs │ ├── migrate.rs │ ├── password.rs │ ├── publish.rs │ ├── register.rs │ ├── subscribe.rs │ ├── sync_servers.rs │ ├── unsubscribe.rs │ ├── utils.rs │ └── utils │ │ ├── color.rs │ │ ├── indicator.rs │ │ ├── list.rs │ │ ├── open_link.rs │ │ ├── project.rs │ │ └── refresh_user_token.rs │ ├── generators.rs │ ├── generators │ ├── cargo_file_generator.rs │ ├── controller_generator.rs │ ├── entrypoint_generator.rs │ ├── gitignore_generator.rs │ ├── js_generator.rs │ ├── lib_generator.rs │ ├── migration_generator.rs │ ├── model_generator.rs │ ├── public_generator.rs │ ├── template_generator.rs │ └── view_generator.rs │ ├── lib.rs │ ├── response.rs │ ├── rocal_api_client.rs │ ├── rocal_api_client │ ├── cancel_subscription.rs │ ├── create_app.rs │ ├── create_payment_link.rs │ ├── create_user.rs │ ├── login_user.rs │ ├── oob_code_response.rs │ ├── payment_link.rs │ ├── registered_sync_server.rs │ ├── send_email_verification.rs │ ├── send_password_reset_email.rs │ ├── subdomain.rs │ ├── subscription_status.rs │ ├── user_login_token.rs │ └── user_refresh_token.rs │ ├── runner.rs │ └── token_manager.rs ├── rocal_core ├── Cargo.toml ├── README.md └── src │ ├── configuration.rs │ ├── database.rs │ ├── enums.rs │ ├── enums │ └── request_method.rs │ ├── lib.rs │ ├── migrator.rs │ ├── parsed_action.rs │ ├── parsed_route.rs │ ├── route_handler.rs │ ├── router.rs │ ├── traits.rs │ ├── utils.rs │ ├── workers.rs │ └── workers │ └── db_sync_worker.rs ├── rocal_dev_server ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ ├── models.rs │ ├── models │ └── content_type.rs │ ├── utils.rs │ └── utils │ └── color.rs ├── rocal_macro ├── Cargo.toml ├── README.md └── src │ └── lib.rs └── rocal_ui ├── Cargo.toml ├── README.md ├── src ├── data_types.rs ├── data_types │ ├── queue.rs │ └── stack.rs ├── enums.rs ├── enums │ └── html_element.rs ├── html.rs ├── html │ └── to_tokens.rs └── lib.rs └── tests ├── test_html_parse.rs ├── test_html_to_tokens.rs ├── test_queue.rs └── test_stack.rs /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-detectable=false 2 | *.mjs linguist-detectable=false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /rocal_macro_usage 3 | 4 | /rocal_core/Cargo.lock 5 | /rocal_macro/Cargo.lock 6 | 7 | /rocal_core/target 8 | /rocal_macro/target 9 | /examples/hello_world/target 10 | /examples/self_checkout/target 11 | 12 | /rocal/target 13 | 14 | /rocal/.git 15 | /rocal_core/.git 16 | /rocal_macro/.git 17 | /rocal_ui/.git 18 | /rocal_dev_server/.git 19 | /examples/hello_world/.git 20 | /examples/self_checkout/.git 21 | 22 | /rocal/.gitignore 23 | /rocal_core/.gitignore 24 | /rocal_macro/.gitignore 25 | /rocal_ui/.gitignore 26 | /rocal_dev_server/.gitignore 27 | /examples/hello_world/.gitignore 28 | /examples/self_checkout/.gitignore 29 | 30 | .env 31 | .env.debug -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "rocal", 5 | "rocal_macro", 6 | "rocal_core", 7 | "rocal_cli", 8 | "rocal_ui", 9 | "rocal_dev_server", 10 | "examples/hello_world", 11 | "examples/self_checkout", 12 | "examples/simple_note" 13 | ] 14 | 15 | [patch.crates-io] 16 | rocal = { path = "rocal" } 17 | rocal-cli = { path = "rocal_cli", optional = true } 18 | rocal-core = { path = "rocal_core" } 19 | rocal-macro = { path = "rocal_macro" } 20 | rocal-ui = { path = "rocal_ui" } 21 | rocal-dev-server = { path = "rocal_dev_server" } 22 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Yoshiki Sashiyama 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Rocal 2 | 3 | ## What's Rocal? 4 | 5 | Rocal is Full-Stack WASM framework that can be used to build fast and robust web apps thanks to high performance of WebAssembly and Rust's typing system and smart memory management. 6 | 7 | Rocal adopts MVC(Model-View-Controller) architecture, so if you are not familiarized with the architecture, we highly recommend learning the architecture first before using Rocal. That's the essential part to make your application with Rocal effectively. 8 | 9 | ## Getting Started 10 | 11 | ```rust 12 | fn run() { 13 | migrate!("db/migrations"); 14 | 15 | route! { 16 | get "/hello-world" => { controller: HelloWorldController, action: index, view: HelloWorldView } 17 | } 18 | } 19 | 20 | // ... in HelloWorldController 21 | impl Controller for HelloWorldController { 22 | type View = UserView; 23 | } 24 | 25 | #[rocal::action] 26 | pub fn index(&self) { 27 | self.view.index("Hello, World!"); 28 | } 29 | 30 | // ... in HelloWorldView 31 | pub fn index(&self, message: &str) { 32 | let template = HelloWorldTemplate::new(self.router.clone()); 33 | template.render(message); 34 | } 35 | 36 | // ... in HelloWorldTemplate 37 | fn body(&self, data: Self::Data) -> String { 38 | view! { 39 |

{"Welcome to Rocal World!"}

40 | 41 | if data.is_empty() { 42 |

{"There is no message."}

43 | } else { 44 |

{{ data }}

45 | } 46 | 47 |
48 | 49 | {{ &button("submit", "btn btn-primary", "Submit") }} 50 |
51 | } 52 | } 53 | 54 | fn button(ty: &str, class: &str, label: &str) -> String { 55 | view! { 56 | 59 | } 60 | } 61 | ``` 62 | As you can see the quick example, to render HTML with MVC architecture, in this case, the router and each controller, view, and template can be written like that. 63 | 64 | ### Requirements 65 | 1. Install Rocal by the command below if you haven't yet: 66 | 67 | On MacOS or Linux 68 | 69 | ```bash 70 | $ curl -fsSL https://www.rocal.dev/install.sh | sh 71 | ``` 72 | 73 | On Windows 74 | - [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) which is used to build an Rocal application 75 | - [brotli](https://github.com/google/brotli) to be used compressing releasing files to publish. 76 | 77 | ```bash 78 | $ cargo install rocal --features="cli" 79 | ``` 80 | 81 | 2. Create a new Rocal application: 82 | 83 | ```bash 84 | $ rocal new -n myapp 85 | ``` 86 | 87 | where `myapp` is the application name 88 | 89 | 3. Run to access the application: 90 | 91 | ```bash 92 | $ cd myapp 93 | $ rocal run # you can change a port where the app runs with `-p `. An app runs on 3000 by default 94 | ``` 95 | 96 | Go to `http://127.0.0.1:3000` and you'll see the welcome message! 97 | 98 | 4. Build the application without running it: 99 | 100 | ```bash 101 | $ rocal build 102 | ``` 103 | 104 | 5. See the generated directories and files: 105 | 106 | Probably, you could find some directories and files in the application directory after executing the leading commands. 107 | 108 | Especially, if you want to learn how the application works, you should take a look at lib.rs, controllers, views, and models. 109 | 110 | Some Rocal macros are used to build the application such as `config!` and `#[rocal::main]` which are in `src/lib.rs` and required to run. On top of that, you could see `route!` macro that provides you with an easy way to set up application routing. 111 | 112 | Other than the macros, there is an essential struct to communicate with an embedded database which is now we utilize [SQLite WASM](https://sqlite.org/wasm/doc/trunk/index.md). 113 | 114 | You could write like below to execute queries to the database. 115 | 116 | ```rust 117 | use serde::Deserialize; 118 | 119 | #[derive(Deserialize)] 120 | struct User { 121 | id: u32, 122 | first_name: String, 123 | last_name: String, 124 | } 125 | 126 | let database = crate::CONFIG.get_database().clone(); 127 | 128 | let result: Result, JsValue> = database.query("select id, first_name, last_name from users;").fetch().await; 129 | 130 | let first_name = "John"; 131 | let last_name = "Smith"; 132 | 133 | database 134 | .query("insert users (first_name, last_name) into ($1, $2);") 135 | .bind(first_name) 136 | .bind(last_name) 137 | .execute() 138 | .await; 139 | ``` 140 | 141 | And, to create tables, you are able to put SQL files in `db/migrations` directory. 142 | 143 | e.g. db/migrations/202502090330_create_user_table.sql 144 | 145 | ```sql 146 | create table if not exists users ( 147 | id integer primary key, 148 | first_name text not null, 149 | last_name text not null, 150 | created_at datetime default current_timestamp 151 | ); 152 | ``` 153 | 154 | 6. (Optional) Publish a Rocal application: 155 | ```bash 156 | $ cd myapp 157 | $ rocal publish 158 | ``` 159 | 160 | where `myapp` is the application name 161 | 162 | Then you can find `release/` and `release.tar.gz` to publish to your hosting server. 163 | 164 | 165 | ## License 166 | 167 | Rocal is released under the [MIT License](https://opensource.org/licenses/MIT). 168 | -------------------------------------------------------------------------------- /examples/hello_world/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "hello_world" 4 | version = "0.1.0" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | rocal = { path = "../../rocal" } 12 | wasm-bindgen = "0.2" 13 | wasm-bindgen-futures = "0.4" 14 | web-sys = { version = "0.3", features = [ 15 | "Window", 16 | "History", 17 | "console", 18 | "Location", 19 | "Document", 20 | "DocumentFragment", 21 | "Element", 22 | "HtmlElement", 23 | "Node", 24 | "NodeList", 25 | "Event", 26 | "FormData", 27 | "HtmlFormElement", 28 | "Worker", 29 | "WorkerOptions", 30 | "WorkerType" 31 | ]} 32 | js-sys = "0.3" 33 | serde = { version = "1.0", features = ["derive"] } 34 | serde-wasm-bindgen = "0.6" 35 | -------------------------------------------------------------------------------- /examples/hello_world/db/migrations/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocal-dev/rocal/ac1ad999f29e80a2cd7e4be135f14c7e2433063b/examples/hello_world/db/migrations/.keep -------------------------------------------------------------------------------- /examples/hello_world/db/migrations/202402090340_create_sync_connection_table.sql: -------------------------------------------------------------------------------- 1 | create table if not exists sync_connections ( 2 | id text primary key, 3 | password text not null, 4 | created_at datetime default current_timestamp 5 | ); 6 | -------------------------------------------------------------------------------- /examples/hello_world/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | hello_world 7 | 8 | 9 | 16 | 17 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/hello_world/js/db_query_worker.js: -------------------------------------------------------------------------------- 1 | import sqlite3InitModule from './sqlite3.mjs'; 2 | 3 | self.onmessage = function (message) { 4 | const db_name = message.data.db; 5 | 6 | self.sqlite3InitModule().then((sqlite3) => { 7 | if (sqlite3.capi.sqlite3_vfs_find("opfs")) { 8 | const db = new sqlite3.oo1.OpfsDb(db_name, "ct"); 9 | if (!!message.data.query) { 10 | self.postMessage(db.exec(message.data.query, { rowMode: 'object' })); 11 | } 12 | } else { 13 | console.error("OPFS not available because of your browser capability."); 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /examples/hello_world/js/db_sync_worker.js: -------------------------------------------------------------------------------- 1 | import sqlite3InitModule from './sqlite3.mjs'; 2 | 3 | self.onmessage = async function (message) { 4 | const { app_id, directory_name, file_name, endpoint, force } = message.data; 5 | 6 | self.sqlite3InitModule().then((sqlite3) => { 7 | if (sqlite3.capi.sqlite3_vfs_find("opfs")) { 8 | const db = new sqlite3.oo1.OpfsDb(`${directory_name}/${file_name}`, "ct"); 9 | const query = "select id, password from sync_connections order by created_at asc limit 1;"; 10 | const result = db.exec(query, { rowMode: 'array' }); 11 | 12 | if (0 < result.length && 1 < result[0].length) { 13 | const user_id = result[0][0]; 14 | const password = result[0][1]; 15 | 16 | if (force !== "none") { 17 | sync(app_id, user_id, password, directory_name, file_name, endpoint, force); 18 | setInterval(sync, 30000, app_id, user_id, password, directory_name, file_name, endpoint, null); 19 | } else { 20 | setInterval(sync, 30000, app_id, user_id, password, directory_name, file_name, endpoint, force); 21 | } 22 | } 23 | } else { 24 | console.error("OPFS not available because of your browser capability."); 25 | } 26 | }); 27 | }; 28 | 29 | async function sync(app_id, user_id, password, directory_name, file_name, endpoint, force) { 30 | console.log('Syncing..'); 31 | 32 | try { 33 | const file = await getFile(directory_name, file_name); 34 | 35 | const last_modified = file === null || force === "remote" ? 0 : Math.floor(file.lastModified / 1000); 36 | 37 | const response = await fetch(endpoint, { 38 | method: "POST", 39 | headers: { "Content-Type": "application/json" }, 40 | body: JSON.stringify({ app_id, user_id, password, file_name, unix_timestamp: last_modified }) 41 | }); 42 | 43 | if (!response.ok) { 44 | console.error("Sync API is not working now"); 45 | return; 46 | } 47 | 48 | const json = await response.json(); 49 | 50 | const obj = JSON.parse(json); 51 | 52 | if (obj.presigned_url === null || obj.last_modified_url === null || obj.action === null) { 53 | console.log("No need to sync your database"); 54 | return; 55 | } 56 | 57 | if (obj.action === "get_object") { 58 | const res = await fetch(obj.presigned_url, { method: "GET" }); 59 | 60 | const fileHandler = await getFileHandler(directory_name, file_name, file === null); 61 | 62 | if (fileHandler === null) { 63 | return; 64 | } 65 | 66 | const fileAccessHandler = await fileHandler.createSyncAccessHandle(); 67 | 68 | const arrayBuffer = await res.arrayBuffer(); 69 | const uint8Array = new Uint8Array(arrayBuffer); 70 | 71 | fileAccessHandler.write(uint8Array, { at: 0 }); 72 | fileAccessHandler.flush(); 73 | 74 | fileAccessHandler.close(); 75 | } else if (obj.action === "put_object") { 76 | const arrayBuffer = await file.arrayBuffer(); 77 | await Promise.all([ 78 | fetch(obj.presigned_url, { method: "PUT", headers: { "Content-Type": "application/vnd.sqlite3" }, body: arrayBuffer }), 79 | fetch(obj.last_modified_url, { method: "PUT", headers: { "Content-Type": "text/plain" }, body: new File([last_modified], "LASTMODIFIED", { type: "text/plain" }) }) 80 | ]); 81 | } 82 | 83 | console.log('Synced'); 84 | } catch (err) { 85 | console.error(err.message); 86 | } 87 | } 88 | 89 | async function getFile(directory_name, file_name) { 90 | try { 91 | const fileHandler = await getFileHandler(directory_name, file_name); 92 | return await fileHandler.getFile(); 93 | } catch (err) { 94 | console.error(err.message, ": Cannot find the file"); 95 | return null; 96 | } 97 | } 98 | 99 | async function getFileHandler(directory_name, file_name, create = false) { 100 | try { 101 | const root = await navigator.storage.getDirectory(); 102 | const dirHandler = await root.getDirectoryHandle(directory_name, { create: create }); 103 | return await dirHandler.getFileHandle(file_name, { create: create }); 104 | } catch (err) { 105 | console.error(err.message, ": Cannot get file handler"); 106 | return null; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /examples/hello_world/js/global.js: -------------------------------------------------------------------------------- 1 | function execSQL(db, query) { 2 | return new Promise((resolve, reject) => { 3 | const worker = new Worker("./js/db_query_worker.js", { type: 'module' }); 4 | worker.postMessage({ db: db, query: query }); 5 | 6 | worker.onmessage = function (message) { 7 | resolve(message.data); 8 | worker.terminate(); 9 | }; 10 | 11 | worker.onerror = function (err) { 12 | reject(err); 13 | worker.terminate(); 14 | }; 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /examples/hello_world/js/sqlite3.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocal-dev/rocal/ac1ad999f29e80a2cd7e4be135f14c7e2433063b/examples/hello_world/js/sqlite3.wasm -------------------------------------------------------------------------------- /examples/hello_world/src/controllers.rs: -------------------------------------------------------------------------------- 1 | pub mod root_controller; 2 | pub mod sync_connections_controller; 3 | -------------------------------------------------------------------------------- /examples/hello_world/src/controllers/root_controller.rs: -------------------------------------------------------------------------------- 1 | use crate::views::root_view::RootView; 2 | use rocal::rocal_core::traits::{Controller, SharedRouter}; 3 | pub struct RootController { 4 | router: SharedRouter, 5 | view: RootView, 6 | } 7 | impl Controller for RootController { 8 | type View = RootView; 9 | fn new(router: SharedRouter, view: Self::View) -> Self { 10 | RootController { router, view } 11 | } 12 | } 13 | impl RootController { 14 | #[rocal::action] 15 | pub fn index(&self) { 16 | self.view.index(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/hello_world/src/controllers/sync_connections_controller.rs: -------------------------------------------------------------------------------- 1 | use rocal::rocal_core::{ 2 | enums::request_method::RequestMethod, 3 | traits::{Controller, SharedRouter}, 4 | }; 5 | 6 | use crate::{ 7 | repositories::sync_connection_repository::SyncConnectionRepository, 8 | views::sync_connection_view::SyncConnectionView, DbSyncWorker, ForceType, CONFIG, 9 | }; 10 | 11 | pub struct SyncConnectionsController { 12 | router: SharedRouter, 13 | view: SyncConnectionView, 14 | } 15 | 16 | impl Controller for SyncConnectionsController { 17 | type View = SyncConnectionView; 18 | 19 | fn new(router: SharedRouter, view: Self::View) -> Self { 20 | Self { router, view } 21 | } 22 | } 23 | 24 | impl SyncConnectionsController { 25 | #[rocal::action] 26 | pub async fn edit(&self) { 27 | let repo = SyncConnectionRepository::new(CONFIG.get_database()); 28 | 29 | if let Ok(connection) = repo.get().await { 30 | self.view.edit(connection); 31 | } else { 32 | self.view.edit(None); 33 | } 34 | } 35 | 36 | #[rocal::action] 37 | pub async fn connect(&self, id: String, password: String) { 38 | let repo = SyncConnectionRepository::new(CONFIG.get_database()); 39 | 40 | match repo.create(&id, &password).await { 41 | Ok(_) => { 42 | self.router 43 | .borrow() 44 | .resolve(RequestMethod::Get, "/", None) 45 | .await; 46 | 47 | let db_sync_worker = DbSyncWorker::new("./js/db_sync_worker.js", ForceType::Remote); 48 | db_sync_worker.run(); 49 | } 50 | Err(err) => web_sys::console::error_1(&err), 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/hello_world/src/lib.rs: -------------------------------------------------------------------------------- 1 | use rocal::{config, migrate, route}; 2 | mod controllers; 3 | mod models; 4 | mod repositories; 5 | mod templates; 6 | mod views; 7 | 8 | config! { 9 | app_id: "a917e367-3484-424d-9302-f09bdaf647ae" , 10 | sync_server_endpoint: "http://127.0.0.1:3000/presigned-url" , 11 | database_directory_name: "local" , 12 | database_file_name: "local.sqlite3" 13 | } 14 | 15 | #[rocal::main] 16 | fn app() { 17 | route! { 18 | get "/" => { controller: RootController , action: index , view: RootView }, 19 | get "/sync-connections" => { controller: SyncConnectionsController, action: edit, view: SyncConnectionView }, 20 | post "/sync-connections" => { controller: SyncConnectionsController, action: connect, view: SyncConnectionView } 21 | } 22 | migrate!("db/migrations"); 23 | } 24 | -------------------------------------------------------------------------------- /examples/hello_world/src/models.rs: -------------------------------------------------------------------------------- 1 | pub mod sync_connection; 2 | -------------------------------------------------------------------------------- /examples/hello_world/src/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocal-dev/rocal/ac1ad999f29e80a2cd7e4be135f14c7e2433063b/examples/hello_world/src/models/.keep -------------------------------------------------------------------------------- /examples/hello_world/src/models/sync_connection.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize)] 4 | pub struct SyncConnection { 5 | id: String, 6 | } 7 | 8 | impl SyncConnection { 9 | pub fn new(id: String) -> Self { 10 | SyncConnection { id } 11 | } 12 | 13 | pub fn get_id(&self) -> &str { 14 | &self.id 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/hello_world/src/repositories.rs: -------------------------------------------------------------------------------- 1 | pub mod sync_connection_repository; 2 | -------------------------------------------------------------------------------- /examples/hello_world/src/repositories/sync_connection_repository.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use wasm_bindgen::JsValue; 4 | 5 | use crate::{models::sync_connection::SyncConnection, Database}; 6 | 7 | pub struct SyncConnectionRepository { 8 | database: Arc, 9 | } 10 | 11 | impl SyncConnectionRepository { 12 | pub fn new(database: Arc) -> Self { 13 | SyncConnectionRepository { database } 14 | } 15 | 16 | pub async fn get(&self) -> Result, JsValue> { 17 | let mut result: Vec = self 18 | .database 19 | .query("select id from sync_connections limit 1;") 20 | .fetch() 21 | .await?; 22 | 23 | match result.pop() { 24 | Some(conn) => Ok(Some(SyncConnection::new(conn.get_id().to_string()))), 25 | None => Ok(None), 26 | } 27 | } 28 | 29 | pub async fn create(&self, id: &str, password: &str) -> Result<(), JsValue> { 30 | match self 31 | .database 32 | .query(&format!( 33 | "insert into sync_connections (id, password) values ('{}', '{}')", 34 | id, password 35 | )) 36 | .execute() 37 | .await 38 | { 39 | Ok(_) => Ok(()), 40 | Err(err) => Err(err), 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/hello_world/src/templates.rs: -------------------------------------------------------------------------------- 1 | pub mod root_template; 2 | pub mod sync_connection_edit_template; 3 | -------------------------------------------------------------------------------- /examples/hello_world/src/templates/root_template.rs: -------------------------------------------------------------------------------- 1 | use rocal::{ 2 | rocal_core::traits::{SharedRouter, Template}, 3 | view, 4 | }; 5 | pub struct RootTemplate { 6 | router: SharedRouter, 7 | } 8 | impl Template for RootTemplate { 9 | type Data = String; 10 | 11 | fn new(router: SharedRouter) -> Self { 12 | RootTemplate { router } 13 | } 14 | 15 | fn body(&self, data: Self::Data) -> String { 16 | view! { 17 |

{"Welcome to rocal world!"}

18 |

{{ &data }}

19 |

{"Sync settings"}

20 | } 21 | } 22 | 23 | fn router(&self) -> SharedRouter { 24 | self.router.clone() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/hello_world/src/templates/sync_connection_edit_template.rs: -------------------------------------------------------------------------------- 1 | use rocal::{ 2 | rocal_core::traits::{SharedRouter, Template}, 3 | view, 4 | }; 5 | 6 | use crate::models::sync_connection::SyncConnection; 7 | 8 | pub struct SyncConnectionEditTemplate { 9 | router: SharedRouter, 10 | } 11 | 12 | impl Template for SyncConnectionEditTemplate { 13 | type Data = Option; 14 | 15 | fn new(router: SharedRouter) -> Self { 16 | Self { router } 17 | } 18 | 19 | fn body(&self, data: Self::Data) -> String { 20 | view! { 21 |

{"DB sync connection"}

22 | if let Some(connection) = data { 23 |

{{ connection.get_id() }} {" has been already connected."}

24 | } else { 25 |
26 |

27 |

28 |

29 |
30 | } 31 | 32 | } 33 | } 34 | 35 | fn router(&self) -> SharedRouter { 36 | self.router.clone() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/hello_world/src/views.rs: -------------------------------------------------------------------------------- 1 | pub mod root_view; 2 | pub mod sync_connection_view; 3 | -------------------------------------------------------------------------------- /examples/hello_world/src/views/root_view.rs: -------------------------------------------------------------------------------- 1 | use crate::templates::root_template::RootTemplate; 2 | use rocal::rocal_core::traits::{SharedRouter, Template, View}; 3 | pub struct RootView { 4 | router: SharedRouter, 5 | } 6 | impl View for RootView { 7 | fn new(router: SharedRouter) -> Self { 8 | RootView { router } 9 | } 10 | } 11 | impl RootView { 12 | pub fn index(&self) { 13 | let template = RootTemplate::new(self.router.clone()); 14 | template.render(String::new()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/hello_world/src/views/sync_connection_view.rs: -------------------------------------------------------------------------------- 1 | use rocal::rocal_core::traits::{SharedRouter, Template, View}; 2 | 3 | use crate::{ 4 | models::sync_connection::SyncConnection, 5 | templates::sync_connection_edit_template::SyncConnectionEditTemplate, 6 | }; 7 | 8 | pub struct SyncConnectionView { 9 | router: SharedRouter, 10 | } 11 | 12 | impl View for SyncConnectionView { 13 | fn new(router: SharedRouter) -> Self { 14 | Self { router } 15 | } 16 | } 17 | 18 | impl SyncConnectionView { 19 | pub fn edit(&self, connection: Option) { 20 | let template = SyncConnectionEditTemplate::new(self.router.clone()); 21 | template.render(connection); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/hello_world/sw.js: -------------------------------------------------------------------------------- 1 | const version = "v1"; 2 | const assets = [ 3 | "./", 4 | "./index.html", 5 | "./js/db_query_worker.js", 6 | "./js/db_sync_worker.js", 7 | "./js/global.js", 8 | "./js/sqlite3-opfs-async-proxy.js", 9 | "./js/sqlite3.mjs", 10 | "./js/sqlite3.wasm", 11 | ]; 12 | 13 | self.addEventListener('install', (e) => { 14 | // Do precache assets 15 | e.waitUntil( 16 | caches 17 | .open(version) 18 | .then((cache) => { 19 | cache.addAll(assets); 20 | }) 21 | .then(() => self.skipWaiting()) 22 | ); 23 | }); 24 | 25 | self.addEventListener('activate', (e) => { 26 | // Delete old versions of the cache 27 | e.waitUntil( 28 | caches.keys().then((keys) => { 29 | return Promise.all( 30 | keys.filter((key) => key != version).map((name) => caches.delete(name)) 31 | ); 32 | }) 33 | ); 34 | }); 35 | 36 | self.addEventListener('fetch', (e) => { 37 | if (e.request.method !== "GET") { 38 | return; 39 | } 40 | 41 | const isOnline = self.navigator.onLine; 42 | 43 | const url = new URL(e.request.url); 44 | 45 | if (isOnline) { 46 | e.respondWith(staleWhileRevalidate(e)); 47 | } else { 48 | e.respondWith(cacheOnly(e)); 49 | } 50 | }); 51 | 52 | function cacheOnly(e) { 53 | return caches.match(e.request); 54 | } 55 | 56 | function staleWhileRevalidate(ev) { 57 | return caches.match(ev.request).then((cacheResponse) => { 58 | let fetchResponse = fetch(ev.request).then((response) => { 59 | return caches.open(version).then((cache) => { 60 | cache.put(ev.request, response.clone()); 61 | return response; 62 | }); 63 | }); 64 | return cacheResponse || fetchResponse; 65 | }); 66 | } 67 | 68 | function networkRevalidateAndCache(ev) { 69 | return fetch(ev.request, { mode: 'cors', credentials: 'omit' }).then( 70 | (fetchResponse) => { 71 | if (fetchResponse.ok) { 72 | return caches.open(version).then((cache) => { 73 | cache.put(ev.request, fetchResponse.clone()); 74 | return fetchResponse; 75 | }); 76 | } else { 77 | return caches.match(ev.request); 78 | } 79 | } 80 | ); 81 | } 82 | 83 | 84 | -------------------------------------------------------------------------------- /examples/self_checkout/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "self-checkout" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | rocal = { path = "../../rocal" } 11 | wasm-bindgen = "0.2" 12 | wasm-bindgen-futures = "0.4" 13 | web-sys = { version = "0.3", features = [ 14 | "Window", 15 | "History", 16 | "console", 17 | "Location", 18 | "Document", 19 | "DocumentFragment", 20 | "Element", 21 | "HtmlElement", 22 | "Node", 23 | "NodeList", 24 | "Event", 25 | "FormData", 26 | "HtmlFormElement", 27 | "Worker", 28 | "WorkerOptions", 29 | "WorkerType" 30 | ]} 31 | js-sys = "0.3" 32 | serde = { version = "1.0", features = ["derive"] } 33 | serde-wasm-bindgen = "0.6" 34 | -------------------------------------------------------------------------------- /examples/self_checkout/db/migrations/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocal-dev/rocal/ac1ad999f29e80a2cd7e4be135f14c7e2433063b/examples/self_checkout/db/migrations/.keep -------------------------------------------------------------------------------- /examples/self_checkout/db/migrations/202504120810_create_product_table.sql: -------------------------------------------------------------------------------- 1 | create table if not exists products ( 2 | id integer primary key, 3 | name text not null, 4 | price real not null, 5 | created_at datetime default current_timestamp 6 | ); 7 | 8 | insert or ignore into products (id, name, price) 9 | values 10 | (1, 'Apple', 2.99), 11 | (2, 'Orange', 1.99), 12 | (3, 'Banana', 0.99), 13 | (4, 'Grapes', 3.49), 14 | (5, 'Strawberry', 4.99), 15 | (6, 'Watermelon', 5.99), 16 | (7, 'Blueberry', 6.49), 17 | (8, 'Pineapple', 3.99), 18 | (9, 'Mango', 2.49), 19 | (10, 'Kiwi', 1.49), 20 | (11, 'Potato Chips', 2.79), 21 | (12, 'Chocolate Bar', 1.49), 22 | (13, 'Milk (1L)', 1.99), 23 | (14, 'Bread Loaf', 2.49), 24 | (15, 'Eggs (12-pack)', 3.59), 25 | (16, 'Butter (250g)', 3.19), 26 | (17, 'Cheddar Cheese', 4.29), 27 | (18, 'Orange Juice (1L)', 3.89), 28 | (19, 'Yogurt (500g)', 2.29), 29 | (20, 'Bottled Water (500ml)', 0.99); 30 | -------------------------------------------------------------------------------- /examples/self_checkout/db/migrations/202504121026_create_cart_table.sql: -------------------------------------------------------------------------------- 1 | create table if not exists cart_items ( 2 | id integer primary key, 3 | product_id integer not null unique, 4 | number_of_items integer not null default 1, 5 | foreign key(product_id) references products(id) 6 | ); 7 | -------------------------------------------------------------------------------- /examples/self_checkout/db/migrations/202504130331_create_sales_table.sql: -------------------------------------------------------------------------------- 1 | create table if not exists sales ( 2 | id integer primary key, 3 | created_at datetime default current_timestamp 4 | ); 5 | 6 | create table if not exists sales_items ( 7 | id integer primary key, 8 | sales_id integer not null, 9 | product_id integer not null, 10 | product_name text not null, 11 | product_price real not null, 12 | number_of_items integer not null, 13 | foreign key(sales_id) references sales(id), 14 | foreign key(product_id) references products(id) 15 | ); 16 | -------------------------------------------------------------------------------- /examples/self_checkout/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | demo 6 | 7 | 8 | 9 | 10 |
11 |
12 | 13 |

Loading...

14 |
15 | 16 |
17 | 18 | 25 | 26 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/self_checkout/js/db_query_worker.js: -------------------------------------------------------------------------------- 1 | import sqlite3InitModule from './sqlite3.mjs'; 2 | 3 | self.onmessage = function (message) { 4 | const db_name = message.data.db; 5 | 6 | self.sqlite3InitModule().then((sqlite3) => { 7 | if (sqlite3.capi.sqlite3_vfs_find("opfs")) { 8 | const db = new sqlite3.oo1.OpfsDb(db_name, "ct"); 9 | if (!!message.data.query) { 10 | self.postMessage(db.exec(message.data.query, { rowMode: 'object' })); 11 | } 12 | } else { 13 | console.error("OPFS not available because of your browser capability."); 14 | } 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /examples/self_checkout/js/db_sync_worker.js: -------------------------------------------------------------------------------- 1 | import sqlite3InitModule from './sqlite3.mjs'; 2 | 3 | self.onmessage = async function (message) { 4 | const { app_id, directory_name, file_name, endpoint, force } = message.data; 5 | 6 | self.sqlite3InitModule().then((sqlite3) => { 7 | if (sqlite3.capi.sqlite3_vfs_find("opfs")) { 8 | const db = new sqlite3.oo1.OpfsDb(`${directory_name}/${file_name}`, "ct"); 9 | const query = "select id, password from sync_connections order by created_at asc limit 1;"; 10 | 11 | try { 12 | const result = db.exec(query, { rowMode: 'array' }); 13 | 14 | if (0 < result.length && 1 < result[0].length) { 15 | const user_id = result[0][0]; 16 | const password = result[0][1]; 17 | 18 | if (force !== "none") { 19 | sync(app_id, user_id, password, directory_name, file_name, endpoint, force); 20 | setInterval(sync, 30000, app_id, user_id, password, directory_name, file_name, endpoint, null); 21 | } else { 22 | setInterval(sync, 30000, app_id, user_id, password, directory_name, file_name, endpoint, force); 23 | } 24 | } 25 | } catch { 26 | } 27 | } else { 28 | console.error("OPFS not available because of your browser capability."); 29 | } 30 | }); 31 | }; 32 | 33 | async function sync(app_id, user_id, password, directory_name, file_name, endpoint, force) { 34 | console.log('Syncing..'); 35 | 36 | try { 37 | const file = await getFile(directory_name, file_name); 38 | 39 | const last_modified = file === null || force === "remote" ? 0 : Math.floor(file.lastModified / 1000); 40 | 41 | const response = await fetch(endpoint, { 42 | method: "POST", 43 | headers: { "Content-Type": "application/json" }, 44 | body: JSON.stringify({ app_id, user_id, password, file_name, unix_timestamp: last_modified }), 45 | credentials: "include" 46 | }); 47 | 48 | if (!response.ok) { 49 | console.error("Sync API is not working now"); 50 | return; 51 | } 52 | 53 | const json = await response.json(); 54 | 55 | const obj = JSON.parse(json); 56 | 57 | if (obj.presigned_url === null || obj.last_modified_url === null || obj.action === null) { 58 | console.log("No need to sync your database"); 59 | return; 60 | } 61 | 62 | if (obj.action === "get_object") { 63 | const res = await fetch(obj.presigned_url, { method: "GET" }); 64 | 65 | const fileHandler = await getFileHandler(directory_name, file_name, file === null); 66 | 67 | if (fileHandler === null) { 68 | return; 69 | } 70 | 71 | const fileAccessHandler = await fileHandler.createSyncAccessHandle(); 72 | 73 | const arrayBuffer = await res.arrayBuffer(); 74 | const uint8Array = new Uint8Array(arrayBuffer); 75 | 76 | fileAccessHandler.write(uint8Array, { at: 0 }); 77 | fileAccessHandler.flush(); 78 | 79 | fileAccessHandler.close(); 80 | } else if (obj.action === "put_object") { 81 | const arrayBuffer = await file.arrayBuffer(); 82 | await Promise.all([ 83 | fetch(obj.presigned_url, { method: "PUT", headers: { "Content-Type": "application/vnd.sqlite3" }, body: arrayBuffer }), 84 | fetch(obj.last_modified_url, { method: "PUT", headers: { "Content-Type": "text/plain" }, body: new File([last_modified], "LASTMODIFIED", { type: "text/plain" }) }) 85 | ]); 86 | } 87 | 88 | console.log('Synced'); 89 | } catch (err) { 90 | console.error(err.message); 91 | } 92 | } 93 | 94 | async function getFile(directory_name, file_name) { 95 | try { 96 | const fileHandler = await getFileHandler(directory_name, file_name); 97 | return await fileHandler.getFile(); 98 | } catch (err) { 99 | console.error(err.message, ": Cannot find the file"); 100 | return null; 101 | } 102 | } 103 | 104 | async function getFileHandler(directory_name, file_name, create = false) { 105 | try { 106 | const root = await navigator.storage.getDirectory(); 107 | const dirHandler = await root.getDirectoryHandle(directory_name, { create: create }); 108 | return await dirHandler.getFileHandle(file_name, { create: create }); 109 | } catch (err) { 110 | console.error(err.message, ": Cannot get file handler"); 111 | return null; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /examples/self_checkout/js/global.js: -------------------------------------------------------------------------------- 1 | function execSQL(db, query) { 2 | return new Promise((resolve, reject) => { 3 | const worker = new Worker("./js/db_query_worker.js", { type: 'module' }); 4 | worker.postMessage({ db: db, query: query }); 5 | 6 | worker.onmessage = function (message) { 7 | resolve(message.data); 8 | worker.terminate(); 9 | }; 10 | 11 | worker.onerror = function (err) { 12 | reject(err); 13 | worker.terminate(); 14 | }; 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /examples/self_checkout/js/sqlite3.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocal-dev/rocal/ac1ad999f29e80a2cd7e4be135f14c7e2433063b/examples/self_checkout/js/sqlite3.wasm -------------------------------------------------------------------------------- /examples/self_checkout/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocal-dev/rocal/ac1ad999f29e80a2cd7e4be135f14c7e2433063b/examples/self_checkout/public/favicon.ico -------------------------------------------------------------------------------- /examples/self_checkout/public/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocal-dev/rocal/ac1ad999f29e80a2cd7e4be135f14c7e2433063b/examples/self_checkout/public/images/loading.gif -------------------------------------------------------------------------------- /examples/self_checkout/src/controllers.rs: -------------------------------------------------------------------------------- 1 | pub mod carts_controller; 2 | pub mod root_controller; 3 | pub mod sales_controller; 4 | -------------------------------------------------------------------------------- /examples/self_checkout/src/controllers/carts_controller.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repositories::cart_repository::CartRepository, views::empty_view::EmptyView, CONFIG, 3 | FLASH_MEMORY, 4 | }; 5 | use rocal::rocal_core::{ 6 | enums::request_method::RequestMethod, 7 | traits::{Controller, SharedRouter}, 8 | }; 9 | 10 | pub struct CartsController { 11 | router: SharedRouter, 12 | view: EmptyView, 13 | } 14 | 15 | impl Controller for CartsController { 16 | type View = EmptyView; 17 | 18 | fn new(router: SharedRouter, view: Self::View) -> Self { 19 | Self { router, view } 20 | } 21 | } 22 | 23 | impl CartsController { 24 | #[rocal::action] 25 | pub async fn add(&self, product_id: u32) { 26 | let cart_repo = CartRepository::new(CONFIG.database.clone()); 27 | 28 | if let Err(Some(err)) = cart_repo.add_item(product_id).await { 29 | if let Ok(mut flash) = FLASH_MEMORY.lock() { 30 | let _ = flash.set("add_item_to_cart_error", &err); 31 | } 32 | return; 33 | } 34 | 35 | self.router 36 | .borrow() 37 | .resolve(RequestMethod::Get, "/", None) 38 | .await; 39 | } 40 | 41 | #[rocal::action] 42 | pub async fn delete(&self, product_id: u32) { 43 | let cart_repo = CartRepository::new(CONFIG.database.clone()); 44 | 45 | if let Err(Some(err)) = cart_repo.remove_item(product_id).await { 46 | if let Ok(mut flash) = FLASH_MEMORY.lock() { 47 | let _ = flash.set("delete_item_from_cart_error", &err); 48 | } 49 | return; 50 | } 51 | 52 | self.router 53 | .borrow() 54 | .resolve(RequestMethod::Get, "/", None) 55 | .await; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/self_checkout/src/controllers/root_controller.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | repositories::{cart_repository::CartRepository, product_repository::ProductRepository}, 3 | view_models::root_view_model::RootViewModel, 4 | views::root_view::RootView, 5 | CONFIG, 6 | }; 7 | use rocal::rocal_core::traits::{Controller, SharedRouter}; 8 | 9 | pub struct RootController { 10 | router: SharedRouter, 11 | view: RootView, 12 | } 13 | 14 | impl Controller for RootController { 15 | type View = RootView; 16 | fn new(router: SharedRouter, view: Self::View) -> Self { 17 | RootController { router, view } 18 | } 19 | } 20 | 21 | impl RootController { 22 | #[rocal::action] 23 | pub async fn index(&self) { 24 | let product_repo = ProductRepository::new(CONFIG.database.clone()); 25 | let cart_repo = CartRepository::new(CONFIG.database.clone()); 26 | 27 | let products = if let Ok(products) = product_repo.get_all().await { 28 | products 29 | } else { 30 | vec![] 31 | }; 32 | 33 | let cart_items = if let Ok(cart_items) = cart_repo.get_all_items().await { 34 | cart_items 35 | } else { 36 | vec![] 37 | }; 38 | 39 | let vm = RootViewModel::new(products, cart_items); 40 | 41 | self.view.index(vm); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/self_checkout/src/controllers/sales_controller.rs: -------------------------------------------------------------------------------- 1 | use rocal::rocal_core::{ 2 | enums::request_method::RequestMethod, 3 | traits::{Controller, SharedRouter}, 4 | }; 5 | 6 | use crate::{ 7 | models::sales::Sales, 8 | repositories::{cart_repository::CartRepository, sales_repository::SalesRepository}, 9 | view_models::{ 10 | sales_item_view_model::SalesItemViewModel, sales_log_view_model::SalesLogViewModel, 11 | }, 12 | views::sales_view::SalesView, 13 | CONFIG, FLASH_MEMORY, 14 | }; 15 | 16 | pub struct SalesController { 17 | router: SharedRouter, 18 | view: SalesView, 19 | } 20 | 21 | impl Controller for SalesController { 22 | type View = SalesView; 23 | 24 | fn new(router: SharedRouter, view: Self::View) -> Self { 25 | Self { router, view } 26 | } 27 | } 28 | 29 | impl SalesController { 30 | #[rocal::action] 31 | pub async fn index(&self) { 32 | let sales_repo = SalesRepository::new(CONFIG.database.clone()); 33 | 34 | let sales_logs = if let Ok(sales_logs) = sales_repo.get_all().await { 35 | sales_logs 36 | } else { 37 | vec![] 38 | }; 39 | 40 | let vm = SalesLogViewModel::new(sales_logs); 41 | 42 | self.view.index(vm); 43 | } 44 | 45 | #[rocal::action] 46 | pub async fn show(&self, id: u32) { 47 | let sales_repo = SalesRepository::new(CONFIG.database.clone()); 48 | 49 | let sales_items = if let Ok(items) = sales_repo.get_all_items(id).await { 50 | items 51 | } else { 52 | self.router 53 | .borrow() 54 | .resolve(RequestMethod::Get, "/sales", None) 55 | .await; 56 | return; 57 | }; 58 | 59 | let vm = SalesItemViewModel::new(sales_items); 60 | 61 | self.view.show(vm); 62 | } 63 | 64 | #[rocal::action] 65 | pub async fn checkout(&self) { 66 | let sales_repo = SalesRepository::new(CONFIG.database.clone()); 67 | let cart_repo = CartRepository::new(CONFIG.database.clone()); 68 | 69 | let items = if let Ok(items) = cart_repo.get_all_items().await { 70 | items 71 | .into_iter() 72 | .map(|item| { 73 | Sales::new( 74 | *item.get_product_id(), 75 | item.get_product_name(), 76 | item.get_product_price(), 77 | *item.get_number_of_items(), 78 | ) 79 | }) 80 | .collect() 81 | } else { 82 | if let Ok(mut flash) = FLASH_MEMORY.lock() { 83 | let _ = flash.set("get_all_cart_items_error", "Could not get all cart items"); 84 | } 85 | return; 86 | }; 87 | 88 | if let Err(Some(err)) = sales_repo.create(items).await { 89 | if let Ok(mut flash) = FLASH_MEMORY.lock() { 90 | let _ = flash.set("sales_repo.create", &err); 91 | } 92 | return; 93 | } 94 | 95 | if let Err(Some(err)) = cart_repo.remove_all_items().await { 96 | if let Ok(mut flash) = FLASH_MEMORY.lock() { 97 | let _ = flash.set("cart_repo.remove_all_items", &err); 98 | } 99 | return; 100 | } 101 | 102 | self.router 103 | .borrow() 104 | .resolve(RequestMethod::Get, "/", None) 105 | .await; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /examples/self_checkout/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | sync::{Arc, LazyLock, Mutex}, 4 | }; 5 | 6 | use models::flash_memory::FlashMemory; 7 | use rocal::{config, migrate, route}; 8 | 9 | mod controllers; 10 | mod models; 11 | mod repositories; 12 | mod templates; 13 | mod view_models; 14 | mod views; 15 | 16 | // app_id and sync_server_endpoint should be set to utilize a sync server. 17 | // You can get them by $ rocal sync-servers list 18 | config! { 19 | app_id: "", 20 | sync_server_endpoint: "", 21 | database_directory_name: "local", 22 | database_file_name: "local.sqlite3" 23 | } 24 | 25 | static FLASH_MEMORY: LazyLock> = 26 | LazyLock::new(|| Mutex::new(FlashMemory::new(Arc::new(Mutex::new(HashMap::new()))))); 27 | 28 | #[rocal::main] 29 | fn app() { 30 | migrate!("db/migrations"); 31 | 32 | route! { 33 | get "/" => { controller: RootController, action: index, view: RootView }, 34 | post "/carts/" => { controller: CartsController, action: add, view: EmptyView }, 35 | delete "/carts/" => { controller: CartsController, action: delete, view: EmptyView }, 36 | get "/sales" => { controller: SalesController, action: index, view: SalesView }, 37 | get "/sales/" => { controller: SalesController, action: show, view: SalesView }, 38 | post "/sales/checkout" => { controller: SalesController, action: checkout, view: SalesView } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/self_checkout/src/models.rs: -------------------------------------------------------------------------------- 1 | pub mod cart_item; 2 | pub mod flash_memory; 3 | pub mod product; 4 | pub mod sales; 5 | pub mod sales_item; 6 | pub mod sales_log; 7 | -------------------------------------------------------------------------------- /examples/self_checkout/src/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocal-dev/rocal/ac1ad999f29e80a2cd7e4be135f14c7e2433063b/examples/self_checkout/src/models/.keep -------------------------------------------------------------------------------- /examples/self_checkout/src/models/cart_item.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize)] 4 | pub struct CartItem { 5 | id: u32, 6 | product_id: u32, 7 | product_name: String, 8 | product_price: f64, 9 | number_of_items: u32, 10 | } 11 | 12 | impl CartItem { 13 | pub fn get_product_id(&self) -> &u32 { 14 | &self.product_id 15 | } 16 | 17 | pub fn get_product_name(&self) -> &str { 18 | &self.product_name 19 | } 20 | 21 | pub fn get_product_price(&self) -> f64 { 22 | self.product_price * self.number_of_items as f64 23 | } 24 | 25 | pub fn get_number_of_items(&self) -> &u32 { 26 | &self.number_of_items 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/self_checkout/src/models/flash_memory.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | sync::{Arc, Mutex}, 4 | }; 5 | 6 | pub struct FlashMemory { 7 | data: Arc>>, 8 | } 9 | 10 | impl FlashMemory { 11 | pub fn new(data: Arc>>) -> Self { 12 | Self { data } 13 | } 14 | 15 | pub fn set(&mut self, key: &str, value: &str) -> Result<(), String> { 16 | let cloned = self.data.clone(); 17 | 18 | { 19 | let mut guard = cloned.lock().map_err(|err| err.to_string())?; 20 | guard.insert(key.to_string(), value.to_string()); 21 | } 22 | 23 | Ok(()) 24 | } 25 | 26 | pub fn get(&self, key: &str) -> Result { 27 | let cloned = self.data.clone(); 28 | let result = { 29 | let mut guard = cloned.lock().map_err(|err| err.to_string())?; 30 | let value = guard.get(key).cloned(); 31 | guard.remove(key); 32 | value 33 | }; 34 | Ok(result.unwrap_or(String::from(""))) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/self_checkout/src/models/product.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize)] 4 | pub struct Product { 5 | id: u32, 6 | name: String, 7 | price: f64, 8 | } 9 | 10 | impl Product { 11 | pub fn get_id(&self) -> &u32 { 12 | &self.id 13 | } 14 | 15 | pub fn get_name(&self) -> &str { 16 | &self.name 17 | } 18 | 19 | pub fn get_price(&self) -> &f64 { 20 | &self.price 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/self_checkout/src/models/sales.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize)] 4 | pub struct Sales { 5 | product_id: u32, 6 | product_name: String, 7 | product_price: f64, 8 | number_of_items: u32, 9 | } 10 | 11 | impl Sales { 12 | pub fn new( 13 | product_id: u32, 14 | product_name: &str, 15 | product_price: f64, 16 | number_of_items: u32, 17 | ) -> Self { 18 | Self { 19 | product_id, 20 | product_name: product_name.to_string(), 21 | product_price, 22 | number_of_items, 23 | } 24 | } 25 | 26 | pub fn get_product_id(&self) -> &u32 { 27 | &self.product_id 28 | } 29 | 30 | pub fn get_product_name(&self) -> &str { 31 | &self.product_name 32 | } 33 | 34 | pub fn get_product_price(&self) -> &f64 { 35 | &self.product_price 36 | } 37 | 38 | pub fn get_number_of_items(&self) -> &u32 { 39 | &self.number_of_items 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/self_checkout/src/models/sales_item.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize)] 4 | pub struct SalesItem { 5 | product_name: String, 6 | product_price: f64, 7 | number_of_items: u32, 8 | } 9 | 10 | impl SalesItem { 11 | pub fn get_product_name(&self) -> &str { 12 | &self.product_name 13 | } 14 | 15 | pub fn get_product_price(&self) -> &f64 { 16 | &self.product_price 17 | } 18 | 19 | pub fn get_number_of_items(&self) -> &u32 { 20 | &self.number_of_items 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/self_checkout/src/models/sales_log.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize)] 4 | pub struct SalesLog { 5 | id: u32, 6 | created_at: String, 7 | } 8 | 9 | impl SalesLog { 10 | pub fn get_id(&self) -> &u32 { 11 | &self.id 12 | } 13 | 14 | pub fn get_created_at(&self) -> &str { 15 | &self.created_at 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/self_checkout/src/repositories.rs: -------------------------------------------------------------------------------- 1 | pub mod cart_repository; 2 | pub mod product_repository; 3 | pub mod sales_repository; 4 | -------------------------------------------------------------------------------- /examples/self_checkout/src/repositories/cart_repository.rs: -------------------------------------------------------------------------------- 1 | use crate::{models::cart_item::CartItem, Database}; 2 | use std::sync::Arc; 3 | 4 | pub struct CartRepository { 5 | database: Arc, 6 | } 7 | 8 | impl CartRepository { 9 | pub fn new(database: Arc) -> Self { 10 | Self { database } 11 | } 12 | 13 | pub async fn get_all_items(&self) -> Result, Option> { 14 | let result: Vec = self 15 | .database 16 | .query( 17 | r#" 18 | select 19 | c.id as id, 20 | c.product_id as product_id, 21 | p.name as product_name, 22 | p.price as product_price, 23 | c.number_of_items as number_of_items 24 | from cart_items as c 25 | inner join products as p on c.product_id = p.id; 26 | "#, 27 | ) 28 | .fetch() 29 | .await 30 | .map_err(|err| err.as_string())?; 31 | 32 | Ok(result) 33 | } 34 | 35 | pub async fn add_item(&self, product_id: u32) -> Result<(), Option> { 36 | let mut items: Vec = self 37 | .database 38 | .query(&format!( 39 | r#" 40 | select 41 | c.id as id, 42 | c.product_id as product_id, 43 | p.name as product_name, 44 | p.price as product_price, 45 | c.number_of_items as number_of_items 46 | from cart_items as c 47 | inner join products as p on c.product_id = p.id 48 | where p.id = {} limit 1;"#, 49 | product_id 50 | )) 51 | .fetch() 52 | .await 53 | .map_err(|err| err.as_string())?; 54 | 55 | match items.pop() { 56 | Some(item) => { 57 | let number_of_items = item.get_number_of_items() + 1; 58 | self.database 59 | .query(&format!( 60 | "update cart_items set number_of_items = {} where product_id = {}", 61 | number_of_items, product_id 62 | )) 63 | .execute() 64 | .await 65 | .map_err(|err| err.as_string())?; 66 | } 67 | None => { 68 | let number_of_items = 1; 69 | self.database 70 | .query(&format!( 71 | "insert into cart_items (product_id, number_of_items) values ({}, {})", 72 | product_id, number_of_items 73 | )) 74 | .execute() 75 | .await 76 | .map_err(|err| err.as_string())?; 77 | } 78 | }; 79 | 80 | Ok(()) 81 | } 82 | 83 | pub async fn remove_item(&self, product_id: u32) -> Result<(), Option> { 84 | self.database 85 | .query(&format!( 86 | "delete from cart_items where product_id = {}", 87 | product_id 88 | )) 89 | .execute() 90 | .await 91 | .map_err(|err| err.as_string())?; 92 | 93 | Ok(()) 94 | } 95 | 96 | pub async fn remove_all_items(&self) -> Result<(), Option> { 97 | self.database 98 | .query("delete from cart_items;") 99 | .execute() 100 | .await 101 | .map_err(|err| err.as_string())?; 102 | 103 | Ok(()) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /examples/self_checkout/src/repositories/product_repository.rs: -------------------------------------------------------------------------------- 1 | use crate::{models::product::Product, Database}; 2 | use std::sync::Arc; 3 | 4 | pub struct ProductRepository { 5 | database: Arc, 6 | } 7 | 8 | impl ProductRepository { 9 | pub fn new(database: Arc) -> Self { 10 | Self { database } 11 | } 12 | 13 | pub async fn get_all(&self) -> Result, Option> { 14 | let result: Vec = self 15 | .database 16 | .query("select id, name, price from products;") 17 | .fetch() 18 | .await 19 | .map_err(|err| err.as_string())?; 20 | 21 | Ok(result) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/self_checkout/src/repositories/sales_repository.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | models::{sales::Sales, sales_item::SalesItem, sales_log::SalesLog}, 3 | Database, 4 | }; 5 | use std::sync::Arc; 6 | 7 | pub struct SalesRepository { 8 | database: Arc, 9 | } 10 | 11 | impl SalesRepository { 12 | pub fn new(database: Arc) -> Self { 13 | Self { database } 14 | } 15 | 16 | pub async fn get_all(&self) -> Result, Option> { 17 | let result: Vec = self 18 | .database 19 | .query("select id, created_at from sales order by created_at desc;") 20 | .fetch() 21 | .await 22 | .map_err(|err| err.as_string())?; 23 | 24 | Ok(result) 25 | } 26 | 27 | pub async fn get_all_items(&self, id: u32) -> Result, Option> { 28 | let result: Vec = self 29 | .database 30 | .query(&format!( 31 | r#" 32 | select product_name, product_price, number_of_items from sales_items where sales_id = {} 33 | "#, 34 | id 35 | )) 36 | .fetch() 37 | .await 38 | .map_err(|err| err.as_string())?; 39 | 40 | Ok(result) 41 | } 42 | 43 | pub async fn create(&self, sales_list: Vec) -> Result<(), Option> { 44 | let mut values: Vec = vec![]; 45 | 46 | for sales in sales_list { 47 | values.push(format!( 48 | "((select sales_id from sid), {}, '{}', {}, {})", 49 | sales.get_product_id(), 50 | sales.get_product_name(), 51 | sales.get_product_price(), 52 | sales.get_number_of_items() 53 | )) 54 | } 55 | 56 | let values = values.join(","); 57 | 58 | self.database 59 | .query(&format!( 60 | r#" 61 | begin immediate; 62 | insert into sales default values; 63 | 64 | with sid as ( 65 | select last_insert_rowid() as sales_id 66 | ) 67 | insert into 68 | sales_items (sales_id, product_id, product_name, product_price, number_of_items) 69 | values 70 | {}; 71 | commit; 72 | "#, 73 | values 74 | )) 75 | .execute() 76 | .await 77 | .map_err(|err| err.as_string())?; 78 | 79 | Ok(()) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /examples/self_checkout/src/templates.rs: -------------------------------------------------------------------------------- 1 | pub mod root_template; 2 | pub mod sales_item_template; 3 | pub mod sales_log_template; 4 | -------------------------------------------------------------------------------- /examples/self_checkout/src/templates/root_template.rs: -------------------------------------------------------------------------------- 1 | use crate::view_models::root_view_model::RootViewModel; 2 | use rocal::{ 3 | rocal_core::traits::{SharedRouter, Template}, 4 | view, 5 | }; 6 | 7 | pub struct RootTemplate { 8 | router: SharedRouter, 9 | } 10 | 11 | impl Template for RootTemplate { 12 | type Data = RootViewModel; 13 | 14 | fn new(router: SharedRouter) -> Self { 15 | RootTemplate { router } 16 | } 17 | 18 | fn body(&self, data: Self::Data) -> String { 19 | view! { 20 | 24 | 25 |
26 |
27 |
28 | for product in data.get_products() { 29 |
30 | 34 |
35 | } 36 |
37 | 38 |
39 | if data.get_cart_items().is_empty() { 40 | { "-" } 41 | } else { 42 | for item in data.get_cart_items() { 43 |
44 |
45 | {{ item.get_product_name() }} 46 | {{ &format!("x{}", item.get_number_of_items()) }} 47 | {{ &format!("${:.2}", item.get_product_price()) }} 48 |
49 |
50 | 51 |
52 |
53 | } 54 | } 55 |
56 |
57 |
58 | { "Total" } 59 | {{ &format!("${:.2}", data.get_total_price() )}} 60 |
61 |
62 |
63 | 64 |
65 |
66 |
67 | } 68 | } 69 | 70 | fn router(&self) -> SharedRouter { 71 | self.router.clone() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /examples/self_checkout/src/templates/sales_item_template.rs: -------------------------------------------------------------------------------- 1 | use rocal::{ 2 | rocal_core::traits::{SharedRouter, Template}, 3 | view, 4 | }; 5 | 6 | use crate::view_models::sales_item_view_model::SalesItemViewModel; 7 | 8 | pub struct SalesItemTemplate { 9 | router: SharedRouter, 10 | } 11 | 12 | impl Template for SalesItemTemplate { 13 | type Data = SalesItemViewModel; 14 | 15 | fn new(router: SharedRouter) -> Self { 16 | Self { router } 17 | } 18 | 19 | fn body(&self, data: Self::Data) -> String { 20 | view! { 21 | 25 |
26 |
27 |
28 |
29 | for item in data.get_sales_items() { 30 |
31 | {{ item.get_product_name() }} 32 | {{ &format!("x{}", item.get_number_of_items()) }} 33 | {{ &format!("${:.2}", item.get_product_price()) }} 34 |
35 | } 36 |
37 | {"Total"} 38 | {{ &format!("${:.2}", data.get_total_price()) }} 39 |
40 |
41 |
42 |
43 |
44 | } 45 | } 46 | 47 | fn router(&self) -> SharedRouter { 48 | self.router.clone() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/self_checkout/src/templates/sales_log_template.rs: -------------------------------------------------------------------------------- 1 | use rocal::{ 2 | rocal_core::traits::{SharedRouter, Template}, 3 | view, 4 | }; 5 | 6 | use crate::view_models::sales_log_view_model::SalesLogViewModel; 7 | 8 | pub struct SalesLogTemplate { 9 | router: SharedRouter, 10 | } 11 | 12 | impl Template for SalesLogTemplate { 13 | type Data = SalesLogViewModel; 14 | 15 | fn new(router: SharedRouter) -> Self { 16 | Self { router } 17 | } 18 | 19 | fn body(&self, data: Self::Data) -> String { 20 | view! { 21 | 25 |
26 |
27 |
28 | 35 |
36 |
37 |
38 | } 39 | } 40 | 41 | fn router(&self) -> SharedRouter { 42 | self.router.clone() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/self_checkout/src/view_models.rs: -------------------------------------------------------------------------------- 1 | pub mod root_view_model; 2 | pub mod sales_item_view_model; 3 | pub mod sales_log_view_model; 4 | -------------------------------------------------------------------------------- /examples/self_checkout/src/view_models/root_view_model.rs: -------------------------------------------------------------------------------- 1 | use crate::models::{cart_item::CartItem, product::Product}; 2 | 3 | pub struct RootViewModel { 4 | products: Vec, 5 | cart_items: Vec, 6 | } 7 | 8 | impl RootViewModel { 9 | pub fn new(products: Vec, cart_items: Vec) -> Self { 10 | Self { 11 | products, 12 | cart_items, 13 | } 14 | } 15 | 16 | pub fn get_products(&self) -> &Vec { 17 | &self.products 18 | } 19 | 20 | pub fn get_cart_items(&self) -> &Vec { 21 | &self.cart_items 22 | } 23 | 24 | pub fn get_total_price(&self) -> f64 { 25 | let mut total: f64 = 0.0; 26 | 27 | for item in &self.cart_items { 28 | total += item.get_product_price(); 29 | } 30 | 31 | total 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/self_checkout/src/view_models/sales_item_view_model.rs: -------------------------------------------------------------------------------- 1 | use crate::models::sales_item::SalesItem; 2 | 3 | pub struct SalesItemViewModel { 4 | sales_items: Vec, 5 | } 6 | 7 | impl SalesItemViewModel { 8 | pub fn new(sales_items: Vec) -> Self { 9 | Self { sales_items } 10 | } 11 | 12 | pub fn get_sales_items(&self) -> &Vec { 13 | &self.sales_items 14 | } 15 | 16 | pub fn get_total_price(&self) -> f64 { 17 | let mut total: f64 = 0.0; 18 | 19 | for item in &self.sales_items { 20 | total += item.get_product_price(); 21 | } 22 | 23 | total 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/self_checkout/src/view_models/sales_log_view_model.rs: -------------------------------------------------------------------------------- 1 | use crate::models::sales_log::SalesLog; 2 | 3 | pub struct SalesLogViewModel { 4 | sales_logs: Vec, 5 | } 6 | 7 | impl SalesLogViewModel { 8 | pub fn new(sales_logs: Vec) -> Self { 9 | Self { sales_logs } 10 | } 11 | 12 | pub fn get_sales_logs(&self) -> &Vec { 13 | &self.sales_logs 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/self_checkout/src/views.rs: -------------------------------------------------------------------------------- 1 | pub mod empty_view; 2 | pub mod root_view; 3 | pub mod sales_view; 4 | -------------------------------------------------------------------------------- /examples/self_checkout/src/views/empty_view.rs: -------------------------------------------------------------------------------- 1 | use rocal::rocal_core::traits::{SharedRouter, View}; 2 | 3 | pub struct EmptyView { 4 | router: SharedRouter, 5 | } 6 | 7 | impl View for EmptyView { 8 | fn new(router: SharedRouter) -> Self { 9 | Self { router } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/self_checkout/src/views/root_view.rs: -------------------------------------------------------------------------------- 1 | use crate::{templates::root_template::RootTemplate, view_models::root_view_model::RootViewModel}; 2 | use rocal::rocal_core::traits::{SharedRouter, Template, View}; 3 | 4 | pub struct RootView { 5 | router: SharedRouter, 6 | } 7 | 8 | impl View for RootView { 9 | fn new(router: SharedRouter) -> Self { 10 | RootView { router } 11 | } 12 | } 13 | 14 | impl RootView { 15 | pub fn index(&self, view_model: RootViewModel) { 16 | let template = RootTemplate::new(self.router.clone()); 17 | template.render(view_model); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/self_checkout/src/views/sales_view.rs: -------------------------------------------------------------------------------- 1 | use rocal::rocal_core::traits::{SharedRouter, Template, View}; 2 | 3 | use crate::{ 4 | templates::{sales_item_template::SalesItemTemplate, sales_log_template::SalesLogTemplate}, 5 | view_models::{ 6 | sales_item_view_model::SalesItemViewModel, sales_log_view_model::SalesLogViewModel, 7 | }, 8 | }; 9 | 10 | pub struct SalesView { 11 | router: SharedRouter, 12 | } 13 | 14 | impl View for SalesView { 15 | fn new(router: SharedRouter) -> Self { 16 | Self { router } 17 | } 18 | } 19 | 20 | impl SalesView { 21 | pub fn index(&self, view_model: SalesLogViewModel) { 22 | let template = SalesLogTemplate::new(self.router.clone()); 23 | template.render(view_model); 24 | } 25 | 26 | pub fn show(&self, view_model: SalesItemViewModel) { 27 | let template = SalesItemTemplate::new(self.router.clone()); 28 | template.render(view_model); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/self_checkout/sw.js: -------------------------------------------------------------------------------- 1 | const version = "v1"; 2 | const assets = [ 3 | "./", 4 | "./index.html", 5 | "./js/db_query_worker.js", 6 | "./js/db_sync_worker.js", 7 | "./js/global.js", 8 | "./js/sqlite3-opfs-async-proxy.js", 9 | "./js/sqlite3.mjs", 10 | "./js/sqlite3.wasm", 11 | ]; 12 | 13 | self.addEventListener('install', (e) => { 14 | // Do precache assets 15 | e.waitUntil( 16 | caches 17 | .open(version) 18 | .then((cache) => { 19 | cache.addAll(assets); 20 | }) 21 | .then(() => self.skipWaiting()) 22 | ); 23 | }); 24 | 25 | self.addEventListener('activate', (e) => { 26 | // Delete old versions of the cache 27 | e.waitUntil( 28 | caches.keys().then((keys) => { 29 | return Promise.all( 30 | keys.filter((key) => key != version).map((name) => caches.delete(name)) 31 | ); 32 | }) 33 | ); 34 | }); 35 | 36 | self.addEventListener('fetch', (e) => { 37 | if (e.request.method !== "GET") { 38 | return; 39 | } 40 | 41 | const isOnline = self.navigator.onLine; 42 | 43 | const url = new URL(e.request.url); 44 | 45 | if (isOnline) { 46 | e.respondWith(staleWhileRevalidate(e)); 47 | } else { 48 | e.respondWith(cacheOnly(e)); 49 | } 50 | }); 51 | 52 | function cacheOnly(e) { 53 | return caches.match(e.request); 54 | } 55 | 56 | function staleWhileRevalidate(ev) { 57 | return caches.match(ev.request).then((cacheResponse) => { 58 | let fetchResponse = fetch(ev.request).then((response) => { 59 | if (response.ok) { 60 | return caches.open(version).then((cache) => { 61 | cache.put(ev.request, response.clone()); 62 | return response; 63 | }); 64 | } 65 | 66 | return cacheResponse; 67 | }); 68 | return cacheResponse || fetchResponse; 69 | }); 70 | } 71 | 72 | function networkRevalidateAndCache(ev) { 73 | return fetch(ev.request, { mode: 'cors', credentials: 'omit' }).then( 74 | (fetchResponse) => { 75 | if (fetchResponse.ok) { 76 | return caches.open(version).then((cache) => { 77 | cache.put(ev.request, fetchResponse.clone()); 78 | return fetchResponse; 79 | }); 80 | } else { 81 | return caches.match(ev.request); 82 | } 83 | } 84 | ); 85 | } 86 | 87 | 88 | -------------------------------------------------------------------------------- /examples/simple_note/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | pkg 4 | release 5 | release.tar.gz 6 | Cargo.lock 7 | -------------------------------------------------------------------------------- /examples/simple_note/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "simple_note" 4 | version = "0.1.0" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | rocal = { path = "../../rocal" } 12 | wasm-bindgen = "0.2" 13 | wasm-bindgen-futures = "0.4" 14 | web-sys = { version = "0.3", features = [ 15 | "Window", 16 | "History", 17 | "console", 18 | "Location", 19 | "Document", 20 | "DocumentFragment", 21 | "Element", 22 | "HtmlElement", 23 | "Node", 24 | "NodeList", 25 | "Event", 26 | "FormData", 27 | "HtmlFormElement", 28 | "Worker", 29 | "WorkerOptions", 30 | "WorkerType" 31 | ]} 32 | js-sys = "0.3" 33 | serde = { version = "1.0", features = ["derive"] } 34 | serde-wasm-bindgen = "0.6" 35 | -------------------------------------------------------------------------------- /examples/simple_note/db/migrations/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocal-dev/rocal/ac1ad999f29e80a2cd7e4be135f14c7e2433063b/examples/simple_note/db/migrations/.keep -------------------------------------------------------------------------------- /examples/simple_note/db/migrations/20250506055848-create-notes-table.sql: -------------------------------------------------------------------------------- 1 | create table if not exists notes ( 2 | id integer primary key, 3 | title text, 4 | body text 5 | ); 6 | -------------------------------------------------------------------------------- /examples/simple_note/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Note – Offline-First, Privacy-Always. Your notes never touch the cloud. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |

24 | Simple Note... 25 |

26 |
27 | 34 | 35 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/simple_note/js/db_query_worker.js: -------------------------------------------------------------------------------- 1 | import sqlite3InitModule from './sqlite3.mjs'; 2 | 3 | const dbCache = Object.create(null); 4 | 5 | self.onmessage = function (message) { 6 | const db_name = message.data.db; 7 | 8 | self.sqlite3InitModule().then((sqlite3) => { 9 | if (sqlite3.capi.sqlite3_vfs_find("opfs")) { 10 | const db = dbCache[db_name] ??= new sqlite3.oo1.OpfsDb(db_name, "ct"); 11 | if (!!message.data.query) { 12 | self.postMessage(db.exec(message.data.query, { bind: message.data.bindings, rowMode: 'object' })); 13 | } 14 | } else { 15 | console.error("OPFS not available because of your browser capability."); 16 | } 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /examples/simple_note/js/db_sync_worker.js: -------------------------------------------------------------------------------- 1 | import sqlite3InitModule from './sqlite3.mjs'; 2 | 3 | self.onmessage = async function (message) { 4 | const { app_id, directory_name, file_name, endpoint, force } = message.data; 5 | 6 | self.sqlite3InitModule().then((sqlite3) => { 7 | if (sqlite3.capi.sqlite3_vfs_find("opfs")) { 8 | const db = new sqlite3.oo1.OpfsDb(`${directory_name}/${file_name}`, "ct"); 9 | const query = "select id, password from sync_connections order by created_at asc limit 1;"; 10 | 11 | try { 12 | const result = db.exec(query, { rowMode: 'array' }); 13 | 14 | if (0 < result.length && 1 < result[0].length) { 15 | const user_id = result[0][0]; 16 | const password = result[0][1]; 17 | 18 | if (force !== "none") { 19 | sync(app_id, user_id, password, directory_name, file_name, endpoint, force); 20 | setInterval(sync, 30000, app_id, user_id, password, directory_name, file_name, endpoint, null); 21 | } else { 22 | setInterval(sync, 30000, app_id, user_id, password, directory_name, file_name, endpoint, force); 23 | } 24 | } 25 | } catch {} 26 | } else { 27 | console.error("OPFS not available because of your browser capability."); 28 | } 29 | }); 30 | }; 31 | 32 | async function sync(app_id, user_id, password, directory_name, file_name, endpoint, force) { 33 | console.log('Syncing..'); 34 | 35 | try { 36 | const file = await getFile(directory_name, file_name); 37 | 38 | const last_modified = file === null || force === "remote" ? 0 : Math.floor(file.lastModified / 1000); 39 | 40 | const response = await fetch(endpoint, { 41 | method: "POST", 42 | headers: { "Content-Type": "application/json" }, 43 | body: JSON.stringify({ app_id, user_id, password, file_name, unix_timestamp: last_modified }), 44 | credentials: "include" 45 | }); 46 | 47 | if (!response.ok) { 48 | console.error("Sync API is not working now"); 49 | return; 50 | } 51 | 52 | const json = await response.json(); 53 | 54 | const obj = JSON.parse(json); 55 | 56 | if (obj.presigned_url === null || obj.last_modified_url === null || obj.action === null) { 57 | console.log("No need to sync your database"); 58 | return; 59 | } 60 | 61 | if (obj.action === "get_object") { 62 | const res = await fetch(obj.presigned_url, { method: "GET" }); 63 | 64 | const fileHandler = await getFileHandler(directory_name, file_name, file === null); 65 | 66 | if (fileHandler === null) { 67 | return; 68 | } 69 | 70 | const fileAccessHandler = await fileHandler.createSyncAccessHandle(); 71 | 72 | const arrayBuffer = await res.arrayBuffer(); 73 | const uint8Array = new Uint8Array(arrayBuffer); 74 | 75 | fileAccessHandler.write(uint8Array, { at: 0 }); 76 | fileAccessHandler.flush(); 77 | 78 | fileAccessHandler.close(); 79 | } else if (obj.action === "put_object") { 80 | const arrayBuffer = await file.arrayBuffer(); 81 | await Promise.all([ 82 | fetch(obj.presigned_url, { method: "PUT", headers: { "Content-Type": "application/vnd.sqlite3" }, body: arrayBuffer }), 83 | fetch(obj.last_modified_url, { method: "PUT", headers: { "Content-Type": "text/plain" }, body: new File([last_modified], "LASTMODIFIED", { type: "text/plain" }) }) 84 | ]); 85 | } 86 | 87 | console.log('Synced'); 88 | } catch (err) { 89 | console.error(err.message); 90 | } 91 | } 92 | 93 | async function getFile(directory_name, file_name) { 94 | try { 95 | const fileHandler = await getFileHandler(directory_name, file_name); 96 | return await fileHandler.getFile(); 97 | } catch (err) { 98 | console.error(err.message, ": Cannot find the file"); 99 | return null; 100 | } 101 | } 102 | 103 | async function getFileHandler(directory_name, file_name, create = false) { 104 | try { 105 | const root = await navigator.storage.getDirectory(); 106 | const dirHandler = await root.getDirectoryHandle(directory_name, { create: create }); 107 | return await dirHandler.getFileHandle(file_name, { create: create }); 108 | } catch (err) { 109 | console.error(err.message, ": Cannot get file handler"); 110 | return null; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /examples/simple_note/js/global.js: -------------------------------------------------------------------------------- 1 | function execSQL(db, query, bindings) { 2 | return new Promise((resolve, reject) => { 3 | const worker = new Worker("./js/db_query_worker.js", { type: 'module' }); 4 | worker.postMessage({ db: db, query: query, bindings: bindings }); 5 | 6 | worker.onmessage = function (message) { 7 | resolve(message.data); 8 | worker.terminate(); 9 | }; 10 | 11 | worker.onerror = function (err) { 12 | reject(err); 13 | worker.terminate(); 14 | }; 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /examples/simple_note/js/sqlite3.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocal-dev/rocal/ac1ad999f29e80a2cd7e4be135f14c7e2433063b/examples/simple_note/js/sqlite3.wasm -------------------------------------------------------------------------------- /examples/simple_note/public/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocal-dev/rocal/ac1ad999f29e80a2cd7e4be135f14c7e2433063b/examples/simple_note/public/.keep -------------------------------------------------------------------------------- /examples/simple_note/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocal-dev/rocal/ac1ad999f29e80a2cd7e4be135f14c7e2433063b/examples/simple_note/public/favicon.ico -------------------------------------------------------------------------------- /examples/simple_note/src/controllers.rs: -------------------------------------------------------------------------------- 1 | pub mod notes_controller; 2 | pub mod root_controller; 3 | -------------------------------------------------------------------------------- /examples/simple_note/src/controllers/notes_controller.rs: -------------------------------------------------------------------------------- 1 | use rocal::rocal_core::traits::{Controller, SharedRouter}; 2 | use wasm_bindgen::JsValue; 3 | use web_sys::console; 4 | 5 | use crate::{models::note_id::NoteId, views::notes_view::NotesView, CONFIG}; 6 | 7 | pub struct NotesController { 8 | router: SharedRouter, 9 | view: NotesView, 10 | } 11 | 12 | impl Controller for NotesController { 13 | type View = NotesView; 14 | 15 | fn new(router: SharedRouter, view: Self::View) -> Self { 16 | Self { router, view } 17 | } 18 | } 19 | 20 | impl NotesController { 21 | #[rocal::action] 22 | pub fn create(&self, title: Option, body: Option) { 23 | let db = CONFIG.get_database().clone(); 24 | 25 | let result: Result, JsValue> = if let (Some(title), Some(body)) = (title, body) 26 | { 27 | db.query("insert into notes(title, body) values ($1, $2) returning id;") 28 | .bind(title) 29 | .bind(body) 30 | .fetch() 31 | .await 32 | } else { 33 | db.query("insert into notes(title, body) values (null, null) returning id;") 34 | .fetch() 35 | .await 36 | }; 37 | 38 | let result = match result { 39 | Ok(result) => result, 40 | Err(err) => { 41 | console::error_1(&err); 42 | return; 43 | } 44 | }; 45 | 46 | if let Some(note_id) = result.get(0) { 47 | self.router 48 | .borrow() 49 | .redirect(&format!("/?note_id={}", ¬e_id.id)) 50 | .await; 51 | } else { 52 | console::error_1(&"Could not add a new note".into()); 53 | } 54 | } 55 | 56 | #[rocal::action] 57 | pub fn update(&self, note_id: i64, title: String, body: String) { 58 | let db = CONFIG.get_database().clone(); 59 | 60 | let result = db 61 | .query("update notes set title = $1, body = $2 where id = $3;") 62 | .bind(title) 63 | .bind(body) 64 | .bind(note_id) 65 | .execute() 66 | .await; 67 | 68 | match result { 69 | Ok(_) => { 70 | self.router 71 | .borrow() 72 | .redirect(&format!("/?note_id={}", ¬e_id)) 73 | .await; 74 | } 75 | Err(err) => { 76 | console::error_1(&err); 77 | return; 78 | } 79 | }; 80 | } 81 | 82 | #[rocal::action] 83 | pub fn delete(&self, note_id: i64) { 84 | let db = CONFIG.get_database().clone(); 85 | 86 | let result = db 87 | .query("delete from notes where id = $1;") 88 | .bind(note_id) 89 | .execute() 90 | .await; 91 | 92 | match result { 93 | Ok(_) => { 94 | self.router.borrow().redirect("/").await; 95 | } 96 | Err(err) => { 97 | console::error_1(&err); 98 | } 99 | }; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /examples/simple_note/src/controllers/root_controller.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | models::note::Note, view_models::root_view_model::RootViewModel, views::root_view::RootView, 3 | CONFIG, 4 | }; 5 | use rocal::rocal_core::traits::{Controller, SharedRouter}; 6 | use wasm_bindgen::JsValue; 7 | 8 | pub struct RootController { 9 | router: SharedRouter, 10 | view: RootView, 11 | } 12 | 13 | impl Controller for RootController { 14 | type View = RootView; 15 | fn new(router: SharedRouter, view: Self::View) -> Self { 16 | RootController { router, view } 17 | } 18 | } 19 | 20 | impl RootController { 21 | #[rocal::action] 22 | pub fn index(&self, note_id: Option) { 23 | let db = CONFIG.get_database().clone(); 24 | let result: Result, JsValue> = 25 | db.query("select id, title, body from notes").fetch().await; 26 | 27 | let notes = if let Ok(notes) = result { 28 | notes 29 | } else { 30 | vec![] 31 | }; 32 | 33 | let note: Option = if let Some(note_id) = note_id { 34 | notes.iter().find(|note| note.id == note_id).cloned() 35 | } else { 36 | None 37 | }; 38 | 39 | let vm = RootViewModel::new(note, notes); 40 | 41 | self.view.index(vm); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/simple_note/src/lib.rs: -------------------------------------------------------------------------------- 1 | use rocal::{config, migrate, route}; 2 | 3 | mod controllers; 4 | mod models; 5 | mod templates; 6 | mod view_models; 7 | mod views; 8 | 9 | // app_id and sync_server_endpoint should be set to utilize a sync server. 10 | // You can get them by $ rocal sync-servers list 11 | config! { 12 | app_id: "", 13 | sync_server_endpoint: "", 14 | database_directory_name: "local", 15 | database_file_name: "local.sqlite3" 16 | } 17 | 18 | #[rocal::main] 19 | fn app() { 20 | migrate!("db/migrations"); 21 | 22 | route! { 23 | get "/" => { controller: RootController, action: index, view: RootView }, 24 | post "/notes" => { controller: NotesController, action: create, view: NotesView }, 25 | patch "/notes/" => { controller: NotesController, action: update, view: NotesView }, 26 | delete "/notes/" => { controller: NotesController, action: delete, view: NotesView } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/simple_note/src/models.rs: -------------------------------------------------------------------------------- 1 | pub mod note; 2 | pub mod note_id; 3 | -------------------------------------------------------------------------------- /examples/simple_note/src/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocal-dev/rocal/ac1ad999f29e80a2cd7e4be135f14c7e2433063b/examples/simple_note/src/models/.keep -------------------------------------------------------------------------------- /examples/simple_note/src/models/note.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize, Clone)] 4 | pub struct Note { 5 | pub id: i64, 6 | pub title: Option, 7 | pub body: Option, 8 | } 9 | 10 | impl Note { 11 | pub fn get_title(&self) -> &Option { 12 | if let Some(title) = &self.title { 13 | if title.is_empty() { 14 | &None 15 | } else { 16 | &self.title 17 | } 18 | } else { 19 | &self.title 20 | } 21 | } 22 | 23 | pub fn get_body(&self) -> &Option { 24 | &self.body 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/simple_note/src/models/note_id.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize)] 4 | pub struct NoteId { 5 | pub id: i64, 6 | } 7 | -------------------------------------------------------------------------------- /examples/simple_note/src/templates.rs: -------------------------------------------------------------------------------- 1 | pub mod root_template; 2 | -------------------------------------------------------------------------------- /examples/simple_note/src/templates/root_template.rs: -------------------------------------------------------------------------------- 1 | use rocal::{ 2 | rocal_core::{ 3 | router::link_to, 4 | traits::{SharedRouter, Template}, 5 | }, 6 | view, 7 | }; 8 | 9 | use crate::view_models::root_view_model::RootViewModel; 10 | 11 | pub struct RootTemplate { 12 | router: SharedRouter, 13 | } 14 | 15 | impl Template for RootTemplate { 16 | type Data = RootViewModel; 17 | 18 | fn new(router: SharedRouter) -> Self { 19 | RootTemplate { router } 20 | } 21 | 22 | fn body(&self, data: Self::Data) -> String { 23 | view! { 24 |
25 |
26 |

{"Simple Note"}

27 |

{"powered by"}{"Rocal"}

28 |
29 |
30 |
31 |
32 | 33 |
34 | 47 |
48 |
49 | if let Some(note) = data.get_note() { 50 |
51 | if let Some(title) = note.get_title() { 52 | 53 | } else { 54 | 55 | } 56 | 61 | 62 |
63 |
64 | 65 |
66 | } else { 67 |
68 | 69 | 70 | 71 |
72 | } 73 |
74 |
75 |
76 | } 77 | } 78 | 79 | fn router(&self) -> SharedRouter { 80 | self.router.clone() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /examples/simple_note/src/view_models.rs: -------------------------------------------------------------------------------- 1 | pub mod root_view_model; 2 | -------------------------------------------------------------------------------- /examples/simple_note/src/view_models/root_view_model.rs: -------------------------------------------------------------------------------- 1 | use crate::models::note::Note; 2 | 3 | pub struct RootViewModel { 4 | note: Option, 5 | notes: Vec, 6 | } 7 | 8 | impl RootViewModel { 9 | pub fn new(note: Option, notes: Vec) -> Self { 10 | Self { note, notes } 11 | } 12 | 13 | pub fn get_note(&self) -> &Option { 14 | &self.note 15 | } 16 | 17 | pub fn get_notes(&self) -> &Vec { 18 | &self.notes 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/simple_note/src/views.rs: -------------------------------------------------------------------------------- 1 | pub mod notes_view; 2 | pub mod root_view; 3 | -------------------------------------------------------------------------------- /examples/simple_note/src/views/notes_view.rs: -------------------------------------------------------------------------------- 1 | use rocal::rocal_core::traits::{SharedRouter, View}; 2 | 3 | pub struct NotesView { 4 | router: SharedRouter, 5 | } 6 | 7 | impl View for NotesView { 8 | fn new(router: SharedRouter) -> Self { 9 | Self { router } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/simple_note/src/views/root_view.rs: -------------------------------------------------------------------------------- 1 | use crate::{templates::root_template::RootTemplate, view_models::root_view_model::RootViewModel}; 2 | use rocal::rocal_core::traits::{SharedRouter, Template, View}; 3 | pub struct RootView { 4 | router: SharedRouter, 5 | } 6 | impl View for RootView { 7 | fn new(router: SharedRouter) -> Self { 8 | RootView { router } 9 | } 10 | } 11 | impl RootView { 12 | pub fn index(&self, vm: RootViewModel) { 13 | let template = RootTemplate::new(self.router.clone()); 14 | template.render(vm); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/simple_note/sw.js: -------------------------------------------------------------------------------- 1 | const version = "v1"; 2 | const assets = [ 3 | "./", 4 | "./index.html", 5 | "./js/db_query_worker.js", 6 | "./js/db_sync_worker.js", 7 | "./js/global.js", 8 | "./js/sqlite3-opfs-async-proxy.js", 9 | "./js/sqlite3.mjs", 10 | "./js/sqlite3.wasm", 11 | ]; 12 | 13 | self.addEventListener('install', (e) => { 14 | // Do precache assets 15 | e.waitUntil( 16 | caches 17 | .open(version) 18 | .then((cache) => { 19 | cache.addAll(assets); 20 | }) 21 | .then(() => self.skipWaiting()) 22 | ); 23 | }); 24 | 25 | self.addEventListener('activate', (e) => { 26 | // Delete old versions of the cache 27 | e.waitUntil( 28 | caches.keys().then((keys) => { 29 | return Promise.all( 30 | keys.filter((key) => key != version).map((name) => caches.delete(name)) 31 | ); 32 | }) 33 | ); 34 | }); 35 | 36 | self.addEventListener('fetch', (e) => { 37 | if (e.request.method !== "GET") { 38 | return; 39 | } 40 | 41 | const isOnline = self.navigator.onLine; 42 | 43 | const url = new URL(e.request.url); 44 | 45 | if (isOnline) { 46 | e.respondWith(staleWhileRevalidate(e)); 47 | } else { 48 | e.respondWith(cacheOnly(e)); 49 | } 50 | }); 51 | 52 | function cacheOnly(e) { 53 | return caches.match(e.request); 54 | } 55 | 56 | function staleWhileRevalidate(ev) { 57 | return caches.match(ev.request).then((cacheResponse) => { 58 | let fetchResponse = fetch(ev.request).then((response) => { 59 | if (response.ok) { 60 | return caches.open(version).then((cache) => { 61 | cache.put(ev.request, response.clone()); 62 | return response; 63 | }); 64 | } 65 | 66 | return cacheResponse; 67 | }); 68 | return cacheResponse || fetchResponse; 69 | }); 70 | } 71 | 72 | function networkRevalidateAndCache(ev) { 73 | return fetch(ev.request, { mode: 'cors', credentials: 'omit' }).then( 74 | (fetchResponse) => { 75 | if (fetchResponse.ok) { 76 | return caches.open(version).then((cache) => { 77 | cache.put(ev.request, fetchResponse.clone()); 78 | return fetchResponse; 79 | }); 80 | } else { 81 | return caches.match(ev.request); 82 | } 83 | } 84 | ); 85 | } 86 | 87 | 88 | -------------------------------------------------------------------------------- /rocal/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rocal" 3 | version = "0.3.0" 4 | edition = "2021" 5 | 6 | authors = ["Yoshiki Sashiyama "] 7 | description = "Full-Stack WASM framework" 8 | license = "MIT" 9 | homepage = "https://github.com/rocal-dev/rocal" 10 | repository = "https://github.com/rocal-dev/rocal" 11 | readme = "README.md" 12 | keywords = ["local-first", "web-framework", "macro", "wasm", "web"] 13 | 14 | [dependencies] 15 | rocal-macro = "0.3" 16 | rocal-core = "0.3" 17 | rocal-cli = { version = "0.3", optional = true } 18 | rocal-ui = "0.1" 19 | tokio = { version = "1", features = ["full"], optional = true } 20 | 21 | [lib] 22 | name = "rocal" 23 | path = "src/lib.rs" 24 | 25 | [[bin]] 26 | name = "rocal" 27 | path = "src/main.rs" 28 | required-features = ["cli"] 29 | 30 | [features] 31 | cli = ["rocal-cli", "tokio"] 32 | default = [] 33 | -------------------------------------------------------------------------------- /rocal/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /rocal/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | pub use rocal_core; 4 | pub use rocal_macro::action; 5 | pub use rocal_macro::config; 6 | pub use rocal_macro::main; 7 | pub use rocal_macro::migrate; 8 | pub use rocal_macro::route; 9 | pub use rocal_macro::view; 10 | -------------------------------------------------------------------------------- /rocal/src/main.rs: -------------------------------------------------------------------------------- 1 | #[tokio::main] 2 | async fn main() { 3 | rocal_cli::run().await; 4 | } 5 | -------------------------------------------------------------------------------- /rocal_cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rocal-cli" 3 | version = "0.3.0" 4 | edition = "2021" 5 | build = "build.rs" 6 | 7 | authors = ["Yoshiki Sashiyama "] 8 | description = "CLI tool for Rocal - Full-Stack WASM framework" 9 | license = "MIT" 10 | homepage = "https://github.com/rocal-dev/rocal" 11 | repository = "https://github.com/rocal-dev/rocal" 12 | readme = "README.md" 13 | keywords = ["local-first", "web-framework", "wasm", "web"] 14 | 15 | [dependencies] 16 | rocal-dev-server = "0.1" 17 | quote = "1.0" 18 | syn = { version = "2.0", features = ["extra-traits"] } 19 | clap = { version = "4.5.28", features = ["cargo"] } 20 | tar = "0.4" 21 | flate2 = "1.0" 22 | reqwest = { version = "0.12", default-features= false, features = ["json", "rustls-tls"] } 23 | tokio = { version = "1", features = ["full"] } 24 | serde = { version = "1.0", features = ["derive"] } 25 | keyring = { version = "3", features = ["apple-native", "windows-native", "linux-native"] } 26 | rpassword = "7.3.1" 27 | chrono = "0.4" 28 | 29 | [dependencies.uuid] 30 | version = "1.13.1" 31 | features = [ 32 | "v4", 33 | "fast-rng", 34 | "macro-diagnostics", 35 | ] -------------------------------------------------------------------------------- /rocal_cli/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /rocal_cli/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | fn main() { 4 | let profile = env::var("PROFILE").unwrap_or_else(|_| "debug".into()); 5 | 6 | println!("cargo:rustc-env=BUILD_PROFILE={}", profile); 7 | } 8 | -------------------------------------------------------------------------------- /rocal_cli/js/db_query_worker.js: -------------------------------------------------------------------------------- 1 | import sqlite3InitModule from './sqlite3.mjs'; 2 | 3 | const dbCache = Object.create(null); 4 | 5 | self.onmessage = function (message) { 6 | const db_name = message.data.db; 7 | 8 | self.sqlite3InitModule().then((sqlite3) => { 9 | if (sqlite3.capi.sqlite3_vfs_find("opfs")) { 10 | const db = dbCache[db_name] ??= new sqlite3.oo1.OpfsDb(db_name, "ct"); 11 | if (!!message.data.query) { 12 | self.postMessage(db.exec(message.data.query, { bind: message.data.bindings, rowMode: 'object' })); 13 | } 14 | } else { 15 | console.error("OPFS not available because of your browser capability."); 16 | } 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /rocal_cli/js/db_sync_worker.js: -------------------------------------------------------------------------------- 1 | import sqlite3InitModule from './sqlite3.mjs'; 2 | 3 | self.onmessage = async function (message) { 4 | const { app_id, directory_name, file_name, endpoint, force } = message.data; 5 | 6 | self.sqlite3InitModule().then((sqlite3) => { 7 | if (sqlite3.capi.sqlite3_vfs_find("opfs")) { 8 | const db = new sqlite3.oo1.OpfsDb(`${directory_name}/${file_name}`, "ct"); 9 | const query = "select id, password from sync_connections order by created_at asc limit 1;"; 10 | 11 | try { 12 | const result = db.exec(query, { rowMode: 'array' }); 13 | 14 | if (0 < result.length && 1 < result[0].length) { 15 | const user_id = result[0][0]; 16 | const password = result[0][1]; 17 | 18 | if (force !== "none") { 19 | sync(app_id, user_id, password, directory_name, file_name, endpoint, force); 20 | setInterval(sync, 30000, app_id, user_id, password, directory_name, file_name, endpoint, null); 21 | } else { 22 | setInterval(sync, 30000, app_id, user_id, password, directory_name, file_name, endpoint, force); 23 | } 24 | } 25 | } catch {} 26 | } else { 27 | console.error("OPFS not available because of your browser capability."); 28 | } 29 | }); 30 | }; 31 | 32 | async function sync(app_id, user_id, password, directory_name, file_name, endpoint, force) { 33 | console.log('Syncing..'); 34 | 35 | try { 36 | const file = await getFile(directory_name, file_name); 37 | 38 | const last_modified = file === null || force === "remote" ? 0 : Math.floor(file.lastModified / 1000); 39 | 40 | const response = await fetch(endpoint, { 41 | method: "POST", 42 | headers: { "Content-Type": "application/json" }, 43 | body: JSON.stringify({ app_id, user_id, password, file_name, unix_timestamp: last_modified }), 44 | credentials: "include" 45 | }); 46 | 47 | if (!response.ok) { 48 | console.error("Sync API is not working now"); 49 | return; 50 | } 51 | 52 | const json = await response.json(); 53 | 54 | const obj = JSON.parse(json); 55 | 56 | if (obj.presigned_url === null || obj.last_modified_url === null || obj.action === null) { 57 | console.log("No need to sync your database"); 58 | return; 59 | } 60 | 61 | if (obj.action === "get_object") { 62 | const res = await fetch(obj.presigned_url, { method: "GET" }); 63 | 64 | const fileHandler = await getFileHandler(directory_name, file_name, file === null); 65 | 66 | if (fileHandler === null) { 67 | return; 68 | } 69 | 70 | const fileAccessHandler = await fileHandler.createSyncAccessHandle(); 71 | 72 | const arrayBuffer = await res.arrayBuffer(); 73 | const uint8Array = new Uint8Array(arrayBuffer); 74 | 75 | fileAccessHandler.write(uint8Array, { at: 0 }); 76 | fileAccessHandler.flush(); 77 | 78 | fileAccessHandler.close(); 79 | } else if (obj.action === "put_object") { 80 | const arrayBuffer = await file.arrayBuffer(); 81 | await Promise.all([ 82 | fetch(obj.presigned_url, { method: "PUT", headers: { "Content-Type": "application/vnd.sqlite3" }, body: arrayBuffer }), 83 | fetch(obj.last_modified_url, { method: "PUT", headers: { "Content-Type": "text/plain" }, body: new File([last_modified], "LASTMODIFIED", { type: "text/plain" }) }) 84 | ]); 85 | } 86 | 87 | console.log('Synced'); 88 | } catch (err) { 89 | console.error(err.message); 90 | } 91 | } 92 | 93 | async function getFile(directory_name, file_name) { 94 | try { 95 | const fileHandler = await getFileHandler(directory_name, file_name); 96 | return await fileHandler.getFile(); 97 | } catch (err) { 98 | console.error(err.message, ": Cannot find the file"); 99 | return null; 100 | } 101 | } 102 | 103 | async function getFileHandler(directory_name, file_name, create = false) { 104 | try { 105 | const root = await navigator.storage.getDirectory(); 106 | const dirHandler = await root.getDirectoryHandle(directory_name, { create: create }); 107 | return await dirHandler.getFileHandle(file_name, { create: create }); 108 | } catch (err) { 109 | console.error(err.message, ": Cannot get file handler"); 110 | return null; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /rocal_cli/js/global.js: -------------------------------------------------------------------------------- 1 | function execSQL(db, query, bindings) { 2 | return new Promise((resolve, reject) => { 3 | const worker = new Worker("./js/db_query_worker.js", { type: 'module' }); 4 | worker.postMessage({ db: db, query: query, bindings: bindings }); 5 | 6 | worker.onmessage = function (message) { 7 | resolve(message.data); 8 | worker.terminate(); 9 | }; 10 | 11 | worker.onerror = function (err) { 12 | reject(err); 13 | worker.terminate(); 14 | }; 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /rocal_cli/js/sqlite3.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocal-dev/rocal/ac1ad999f29e80a2cd7e4be135f14c7e2433063b/rocal_cli/js/sqlite3.wasm -------------------------------------------------------------------------------- /rocal_cli/js/sw.js: -------------------------------------------------------------------------------- 1 | const version = "v1"; 2 | const assets = [ 3 | "./", 4 | "./index.html", 5 | "./js/db_query_worker.js", 6 | "./js/db_sync_worker.js", 7 | "./js/global.js", 8 | "./js/sqlite3-opfs-async-proxy.js", 9 | "./js/sqlite3.mjs", 10 | "./js/sqlite3.wasm", 11 | ]; 12 | 13 | self.addEventListener('install', (e) => { 14 | // Do precache assets 15 | e.waitUntil( 16 | caches 17 | .open(version) 18 | .then((cache) => { 19 | cache.addAll(assets); 20 | }) 21 | .then(() => self.skipWaiting()) 22 | ); 23 | }); 24 | 25 | self.addEventListener('activate', (e) => { 26 | // Delete old versions of the cache 27 | e.waitUntil( 28 | caches.keys().then((keys) => { 29 | return Promise.all( 30 | keys.filter((key) => key != version).map((name) => caches.delete(name)) 31 | ); 32 | }) 33 | ); 34 | }); 35 | 36 | self.addEventListener('fetch', (e) => { 37 | if (e.request.method !== "GET") { 38 | return; 39 | } 40 | 41 | const isOnline = self.navigator.onLine; 42 | 43 | const url = new URL(e.request.url); 44 | 45 | if (isOnline) { 46 | e.respondWith(staleWhileRevalidate(e)); 47 | } else { 48 | e.respondWith(cacheOnly(e)); 49 | } 50 | }); 51 | 52 | function cacheOnly(e) { 53 | return caches.match(e.request); 54 | } 55 | 56 | function staleWhileRevalidate(ev) { 57 | return caches.match(ev.request).then((cacheResponse) => { 58 | let fetchResponse = fetch(ev.request).then((response) => { 59 | if (response.ok) { 60 | return caches.open(version).then((cache) => { 61 | cache.put(ev.request, response.clone()); 62 | return response; 63 | }); 64 | } 65 | 66 | return cacheResponse; 67 | }); 68 | return cacheResponse || fetchResponse; 69 | }); 70 | } 71 | 72 | function networkRevalidateAndCache(ev) { 73 | return fetch(ev.request, { mode: 'cors', credentials: 'omit' }).then( 74 | (fetchResponse) => { 75 | if (fetchResponse.ok) { 76 | return caches.open(version).then((cache) => { 77 | cache.put(ev.request, fetchResponse.clone()); 78 | return fetchResponse; 79 | }); 80 | } else { 81 | return caches.match(ev.request); 82 | } 83 | } 84 | ); 85 | } 86 | 87 | 88 | -------------------------------------------------------------------------------- /rocal_cli/seeds/root_template.rs: -------------------------------------------------------------------------------- 1 | use rocal::{ 2 | rocal_core::traits::{SharedRouter, Template}, 3 | view, 4 | }; 5 | 6 | pub struct RootTemplate { 7 | router: SharedRouter, 8 | } 9 | 10 | impl Template for RootTemplate { 11 | type Data = String; 12 | 13 | fn new(router: SharedRouter) -> Self { 14 | RootTemplate { router } 15 | } 16 | 17 | fn body(&self, data: Self::Data) -> String { 18 | view! { 19 |

{"Welcome to rocal world!"}

20 |

{{ &data }}

21 | } 22 | } 23 | 24 | fn router(&self) -> SharedRouter { 25 | self.router.clone() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /rocal_cli/src/commands.rs: -------------------------------------------------------------------------------- 1 | pub mod build; 2 | pub mod init; 3 | pub mod login; 4 | pub mod migrate; 5 | pub mod password; 6 | pub mod publish; 7 | pub mod register; 8 | pub mod subscribe; 9 | pub mod sync_servers; 10 | pub mod unsubscribe; 11 | pub mod utils; 12 | -------------------------------------------------------------------------------- /rocal_cli/src/commands/build.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | use super::utils::{ 4 | color::Color, 5 | indicator::{IndicatorLauncher, Kind}, 6 | }; 7 | 8 | pub fn build() { 9 | let mut indicator = IndicatorLauncher::new() 10 | .kind(Kind::Dots) 11 | .interval(100) 12 | .text("Building...") 13 | .color(Color::White) 14 | .start(); 15 | 16 | let output = Command::new("wasm-pack") 17 | .arg("build") 18 | .arg("--target") 19 | .arg("web") 20 | .arg("--dev") 21 | .output() 22 | .expect("Confirm you run this command in a rocal project or you've installed wasm-pack"); 23 | 24 | let _ = indicator.stop(); 25 | 26 | if !output.status.success() { 27 | eprintln!( 28 | "rocal build failed: {}", 29 | String::from_utf8_lossy(&output.stderr) 30 | ); 31 | } else { 32 | println!("Done."); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /rocal_cli/src/commands/login.rs: -------------------------------------------------------------------------------- 1 | use super::utils::{get_user_input, get_user_secure_input}; 2 | use crate::rocal_api_client::{login_user::LoginUser, RocalAPIClient}; 3 | 4 | pub async fn login() { 5 | let email = get_user_input("your email"); 6 | let password = get_user_secure_input("your password"); 7 | 8 | let client = RocalAPIClient::new(); 9 | let user = LoginUser::new(&email, &password); 10 | 11 | if let Err(err) = client.sign_in(user).await { 12 | match err.as_str() { 13 | "INVALID_LOGIN_CREDENTIALS" => eprintln!("Your email address or password is wrong"), 14 | "INVALID_EMAIL" => eprintln!("An email address you entered is invalid"), 15 | _ => eprintln!("{}", err), 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /rocal_cli/src/commands/migrate.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | 3 | use chrono::Utc; 4 | 5 | use crate::commands::utils::project::find_project_root; 6 | 7 | pub fn add(name: &str) { 8 | let now = Utc::now(); 9 | let stamp = now.format("%Y%m%d%H%M%S").to_string(); 10 | let file_name = &format!("{stamp}-{name}.sql"); 11 | 12 | let root_path = find_project_root().expect("Failed to find the project root"); 13 | 14 | File::create(root_path.join(&format!("db/migrations/{file_name}"))) 15 | .expect(&format!("Failed to create db/migrations/{file_name}")); 16 | 17 | println!("{file_name}"); 18 | } 19 | -------------------------------------------------------------------------------- /rocal_cli/src/commands/password.rs: -------------------------------------------------------------------------------- 1 | use crate::rocal_api_client::{send_password_reset_email::SendPasswordResetEmail, RocalAPIClient}; 2 | 3 | use super::utils::get_user_input; 4 | 5 | pub async fn reset() { 6 | let email = get_user_input("your email"); 7 | 8 | let client = RocalAPIClient::new(); 9 | let req = SendPasswordResetEmail::new(&email); 10 | 11 | if let Err(err) = client.send_password_reset_email(req).await { 12 | match err.as_str() { 13 | "INVALID_LOGIN_CREDENTIALS" => eprintln!("Your email address or password is wrong"), 14 | "INVALID_EMAIL" => eprintln!("An email address you entered is invalid"), 15 | _ => eprintln!("{}", err), 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /rocal_cli/src/commands/register.rs: -------------------------------------------------------------------------------- 1 | use super::utils::{get_user_input, get_user_secure_input}; 2 | use crate::{ 3 | rocal_api_client::{ 4 | create_user::CreateUser, send_email_verification::SendEmailVerification, RocalAPIClient, 5 | }, 6 | token_manager::{Kind, TokenManager}, 7 | }; 8 | 9 | pub async fn register() { 10 | let email = get_user_input("your email"); 11 | let mut password = get_user_secure_input("password"); 12 | let mut confirm_password = get_user_secure_input("confirm password"); 13 | 14 | while password != confirm_password { 15 | println!("The password should be same as the confirm password"); 16 | 17 | password = get_user_secure_input("password"); 18 | confirm_password = get_user_secure_input("confirm password"); 19 | } 20 | 21 | let workspace = get_user_input("a workspace name"); 22 | 23 | let client = RocalAPIClient::new(); 24 | let user = CreateUser::new(&email, &password, &workspace); 25 | 26 | if let Err(err) = client.sign_up(user).await { 27 | eprintln!("{}", err); 28 | return; 29 | } 30 | 31 | if let Ok(token) = TokenManager::get_token(Kind::RocalAccessToken) { 32 | let req = SendEmailVerification::new(&token); 33 | client.send_email_verification(req).await; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rocal_cli/src/commands/subscribe.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | commands::{ 3 | unsubscribe::get_subscription_status, 4 | utils::{color::Color, list::List, open_link::open_link}, 5 | }, 6 | rocal_api_client::{create_payment_link::CreatePaymentLink, RocalAPIClient}, 7 | }; 8 | 9 | use super::utils::{get_user_input, refresh_user_token::refresh_user_token}; 10 | 11 | pub async fn subscribe() -> Result<(), std::io::Error> { 12 | refresh_user_token().await; 13 | 14 | if let Ok(status) = get_subscription_status().await { 15 | println!("Your plan is {}", status.get_plan()); 16 | 17 | if !status.is_free_plan() { 18 | show_plans()?; 19 | return Ok(()); 20 | } 21 | } 22 | 23 | println!( 24 | "Choose your plan from these options ({} or {})", 25 | &Color::Green.text("basic"), 26 | &Color::Blue.text("developer") 27 | ); 28 | 29 | show_plans()?; 30 | 31 | let plan = get_user_input("a plan (basic or developer)"); 32 | let plan = plan.to_lowercase(); 33 | 34 | if !(plan == "basic" || plan == "developer") { 35 | println!( 36 | "{}", 37 | &Color::Red.text("Enter a plan you want to subscribe from basic or developer") 38 | ); 39 | return Ok(()); 40 | } 41 | 42 | create_payment_link(&plan).await; 43 | 44 | Ok(()) 45 | } 46 | 47 | async fn create_payment_link(plan: &str) { 48 | let client = RocalAPIClient::new(); 49 | let create_payment_link = CreatePaymentLink::new(&plan); 50 | 51 | match client.create_payment_link(create_payment_link).await { 52 | Ok(link) => { 53 | println!( 54 | "{}", 55 | Color::Green.text( 56 | "Here is your payment link. Open the link with your browser to subscribe." 57 | ) 58 | ); 59 | 60 | println!("{}", Color::Green.text(&link)); 61 | 62 | if let Err(err) = open_link(&link) { 63 | println!("{}", err.to_string()); 64 | } 65 | } 66 | Err(err) => { 67 | println!("{}", Color::Red.text(&err)); 68 | } 69 | } 70 | } 71 | 72 | fn show_plans() -> Result<(), std::io::Error> { 73 | let mut list = List::new(); 74 | 75 | // Basic 76 | let mut plan = List::new(); 77 | plan.add_text(&Color::Green.text("Basic")); 78 | 79 | let mut plan_cap = List::new(); 80 | plan_cap.add_text("Deploy your application + compression, basic hosting, and versioning"); 81 | 82 | let mut plan_price = List::new(); 83 | plan_price.add_text("$10/month"); 84 | 85 | plan.add_list(plan_cap); 86 | plan.add_list(plan_price); 87 | 88 | list.add_list(plan); 89 | 90 | // Developer 91 | let mut plan = List::new(); 92 | plan.add_text(&Color::Blue.text("Developer")); 93 | 94 | let mut plan_cap = List::new(); 95 | plan_cap.add_text("Including all Basic plan's capabilities plus CDN and Sync server support"); 96 | 97 | let mut plan_price = List::new(); 98 | plan_price.add_text("$20/month"); 99 | 100 | plan.add_list(plan_cap); 101 | plan.add_list(plan_price); 102 | 103 | list.add_list(plan); 104 | 105 | // Pro 106 | let mut plan = List::new(); 107 | plan.add_text(&Color::Red.text("Pro (coming soon, stay tuned...)")); 108 | 109 | let mut plan_cap = List::new(); 110 | plan_cap.add_text("Including all Developer plan's capabilities plus custom domain, team collboration, and customer supports"); 111 | 112 | let mut plan_price = List::new(); 113 | plan_price.add_text("$40/month"); 114 | 115 | plan.add_list(plan_cap); 116 | plan.add_list(plan_price); 117 | 118 | list.add_list(plan); 119 | 120 | println!("{}", list); 121 | 122 | Ok(()) 123 | } 124 | -------------------------------------------------------------------------------- /rocal_cli/src/commands/sync_servers.rs: -------------------------------------------------------------------------------- 1 | use crate::rocal_api_client::RocalAPIClient; 2 | 3 | use super::{ 4 | unsubscribe::get_subscription_status, 5 | utils::{ 6 | project::{find_project_root, get_app_name}, 7 | refresh_user_token::refresh_user_token, 8 | }, 9 | }; 10 | 11 | pub async fn list() { 12 | refresh_user_token().await; 13 | 14 | let subscription_status = if let Ok(status) = get_subscription_status().await { 15 | status 16 | } else { 17 | eprintln!("Could not find your subscription."); 18 | return; 19 | }; 20 | 21 | if subscription_status.get_plan() != "developer" && subscription_status.get_plan() != "pro" { 22 | eprintln!("You must subscribe Developer or Pro plan to use sync servers."); 23 | return; 24 | } 25 | 26 | get_sync_server_info().await; 27 | } 28 | 29 | async fn get_sync_server_info() { 30 | let root_path = find_project_root().expect( 31 | "Failed to find a project root. Please run the command in a project built by Cargo", 32 | ); 33 | 34 | let app_name = get_app_name(&root_path); 35 | 36 | let client = RocalAPIClient::new(); 37 | 38 | match client.get_sync_server(&app_name).await { 39 | Ok(sync_server) => { 40 | println!("App ID: {}", sync_server.get_app_id()); 41 | println!("Sync server: {}", sync_server.get_endpoint()); 42 | println!("To use the sync server, set the App ID and the endpoint on your config."); 43 | } 44 | Err(err) => { 45 | eprintln!("{}", err); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /rocal_cli/src/commands/unsubscribe.rs: -------------------------------------------------------------------------------- 1 | use super::utils::refresh_user_token::refresh_user_token; 2 | use crate::{ 3 | commands::utils::{color::Color, get_user_input}, 4 | rocal_api_client::{ 5 | cancel_subscription::CancelSubscription, subscription_status::SubscriptionStatus, 6 | RocalAPIClient, 7 | }, 8 | }; 9 | use std::io::Write; 10 | 11 | pub async fn unsubscribe() { 12 | refresh_user_token().await; 13 | 14 | let status = if let Ok(status) = get_subscription_status().await { 15 | status 16 | } else { 17 | println!("You have not subscribed yet."); 18 | return; 19 | }; 20 | 21 | println!("Your plan is {}", status.get_plan()); 22 | 23 | if !status.is_free_plan() { 24 | if *status.get_cancel_at_period_end() { 25 | println!( 26 | "Your subscription has been scheduled to cancel at the end of the current period." 27 | ); 28 | println!("If you want to continue your subscription next period, please wait for the end so that you could subscribe again by `rocal subscribe` command."); 29 | return; 30 | } 31 | 32 | println!("Are you sure to unsubscribe? (yes/no)"); 33 | 34 | std::io::stdout().flush().expect("Failed to flush stdout"); 35 | 36 | let mut proceed = String::new(); 37 | 38 | std::io::stdin() 39 | .read_line(&mut proceed) 40 | .expect("Enter yes or no"); 41 | 42 | let proceed = proceed.trim().to_lowercase(); 43 | 44 | if proceed == "yes" { 45 | handle_unsubscribe().await; 46 | } else if proceed != "no" { 47 | println!("{}", Color::Red.text("Answer yes/no")); 48 | } 49 | } else { 50 | println!("You have not subscribed yet."); 51 | } 52 | } 53 | 54 | async fn handle_unsubscribe() { 55 | println!("Tell us a reason why you want to leave."); 56 | 57 | let reasons = CancelSubscription::get_reasons(); 58 | 59 | for n in 1..(reasons.len() + 1) { 60 | let reason = reasons.get(&(n as u32)).unwrap(); 61 | println!("{}. {}", n, reason); 62 | } 63 | 64 | let reason = get_user_input("a reason (1 to 8)"); 65 | 66 | if let Ok(reason) = reason.parse::() { 67 | if 1 <= reason && reason <= 8 { 68 | let cancel_subscription = match CancelSubscription::new(reason) { 69 | Ok(sub) => sub, 70 | Err(err) => { 71 | println!("{}", Color::Red.text(&err)); 72 | return; 73 | } 74 | }; 75 | 76 | let client = RocalAPIClient::new(); 77 | 78 | if let Err(err) = client.unsubscribe(cancel_subscription).await { 79 | println!("{}", Color::Red.text(&err)); 80 | } else { 81 | println!( 82 | "Done. Your subscription is scheduled to cancel at the end of the current period." 83 | ) 84 | } 85 | } else { 86 | println!("{}", Color::Red.text("Answer 1 to 8")); 87 | } 88 | } else { 89 | println!("{}", Color::Red.text("Answer 1 to 8")); 90 | } 91 | } 92 | 93 | pub async fn get_subscription_status() -> Result { 94 | let client = RocalAPIClient::new(); 95 | 96 | match client.get_subscription_status().await { 97 | Ok(status) => { 98 | if status.get_plan() == "N/A" { 99 | return Err(()); 100 | } 101 | 102 | Ok(status) 103 | } 104 | Err(_) => Err(()), 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /rocal_cli/src/commands/utils.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | pub mod color; 4 | pub mod indicator; 5 | pub mod list; 6 | pub mod open_link; 7 | pub mod project; 8 | pub mod refresh_user_token; 9 | 10 | pub fn get_user_input(label: &str) -> String { 11 | print!("Enter {}: ", label); 12 | 13 | std::io::stdout().flush().expect("Failed to flush stdout"); 14 | 15 | let mut input = String::new(); 16 | 17 | std::io::stdin() 18 | .read_line(&mut input) 19 | .expect(&format!("Failed to read {}", label)); 20 | 21 | let input = input.trim(); 22 | 23 | input.to_string() 24 | } 25 | 26 | pub fn get_user_secure_input(label: &str) -> String { 27 | let secure_string = rpassword::prompt_password(&format!("Enter {}: ", label)) 28 | .expect(&format!("Failed to read {}", label)); 29 | secure_string 30 | } 31 | -------------------------------------------------------------------------------- /rocal_cli/src/commands/utils/color.rs: -------------------------------------------------------------------------------- 1 | #[allow(dead_code)] 2 | #[derive(Clone, Copy)] 3 | pub enum Color { 4 | Black, 5 | Red, 6 | Green, 7 | Yellow, 8 | Blue, 9 | Magenta, 10 | Cyan, 11 | White, 12 | Gray, 13 | BrightRed, 14 | BrightGreen, 15 | BrigthYellow, 16 | BrightBlue, 17 | BrightMagenta, 18 | BrightCyan, 19 | BrightWhite, 20 | } 21 | 22 | impl Color { 23 | pub fn reset() -> &'static str { 24 | "\x1b[0m" 25 | } 26 | 27 | pub fn text(&self, t: &str) -> String { 28 | format!("{}{}\x1b[0m", self.code(), t) 29 | } 30 | 31 | pub fn code(&self) -> &str { 32 | match self { 33 | Color::Black => "\x1b[30m", 34 | Color::Red => "\x1b[31m", 35 | Color::Green => "\x1b[32m", 36 | Color::Yellow => "\x1b[33m", 37 | Color::Blue => "\x1b[34m", 38 | Color::Magenta => "\x1b[35m", 39 | Color::Cyan => "\x1b[36m", 40 | Color::White => "\x1b[37m", 41 | Color::Gray => "\x1b[90m", 42 | Color::BrightRed => "\x1b[91m", 43 | Color::BrightGreen => "\x1b[92m", 44 | Color::BrigthYellow => "\x1b[93m", 45 | Color::BrightBlue => "\x1b[94m", 46 | Color::BrightMagenta => "\x1b[95m", 47 | Color::BrightCyan => "\x1b[96m", 48 | Color::BrightWhite => "\x1b[97m", 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rocal_cli/src/commands/utils/indicator.rs: -------------------------------------------------------------------------------- 1 | use core::time; 2 | use std::{ 3 | io::Write, 4 | sync::{ 5 | atomic::{AtomicBool, Ordering}, 6 | Arc, 7 | }, 8 | thread, 9 | time::Duration, 10 | }; 11 | 12 | use super::color::Color; 13 | 14 | /// 15 | /// Usage 16 | /// 17 | /// ```rust 18 | /// let mut indicator = IndicatorLauncher::new() 19 | /// .kind(Kind::Dots) 20 | /// .interval(100) 21 | /// .text("Processing...") 22 | /// .color(Color::White) 23 | /// .start(); 24 | /// 25 | /// thread::sleep(Duration::from_millis(1000)); 26 | /// 27 | /// indicator.stop()?; 28 | /// 29 | /// let mut f = std::io::stdout(); 30 | /// 31 | /// writeln!(f, "{}", Color::Green.text("Done"))?; 32 | /// f.flush()?; 33 | /// ``` 34 | 35 | #[derive(Clone, Copy)] 36 | pub enum Kind { 37 | Spinner, 38 | Dots, 39 | } 40 | 41 | pub struct Indicator { 42 | is_processing: Arc, 43 | } 44 | 45 | pub struct IndicatorLauncher { 46 | kind: Option, 47 | interval_millis: Option, 48 | text: Option, 49 | color: Option, 50 | } 51 | 52 | impl IndicatorLauncher { 53 | pub fn new() -> Self { 54 | Self { 55 | kind: None, 56 | interval_millis: None, 57 | text: None, 58 | color: None, 59 | } 60 | } 61 | 62 | pub fn kind(&mut self, kind: Kind) -> &mut Self { 63 | self.kind = Some(kind); 64 | self 65 | } 66 | 67 | pub fn interval(&mut self, millis: u64) -> &mut Self { 68 | self.interval_millis = Some(millis); 69 | self 70 | } 71 | 72 | pub fn text(&mut self, text: &str) -> &mut Self { 73 | self.text = Some(text.to_string()); 74 | self 75 | } 76 | 77 | pub fn color(&mut self, color: Color) -> &mut Self { 78 | self.color = Some(color); 79 | self 80 | } 81 | 82 | pub fn start(&mut self) -> Indicator { 83 | let kind = if let Some(kind) = self.kind { 84 | kind 85 | } else { 86 | Kind::Spinner 87 | }; 88 | 89 | let interval = if let Some(interval) = self.interval_millis { 90 | interval 91 | } else { 92 | 100 93 | }; 94 | 95 | let text = if let Some(text) = &self.text { 96 | text 97 | } else { 98 | "" 99 | }; 100 | 101 | let color = if let Some(color) = self.color { 102 | color 103 | } else { 104 | Color::White 105 | }; 106 | 107 | Indicator::start(kind, interval, text, color) 108 | } 109 | } 110 | 111 | impl Indicator { 112 | pub fn start(kind: Kind, interval_millis: u64, text: &str, color: Color) -> Self { 113 | let is_processing = Arc::new(AtomicBool::new(true)); 114 | let is_processing_cloned = Arc::clone(&is_processing); 115 | let text = text.to_string(); 116 | let interval = time::Duration::from_millis(interval_millis); 117 | let check_interval = Duration::from_millis(10); 118 | 119 | thread::spawn(move || { 120 | let mut f = std::io::stdout(); 121 | 122 | while is_processing_cloned.load(Ordering::SeqCst) { 123 | for i in kind.symbols().iter() { 124 | if !is_processing_cloned.load(Ordering::SeqCst) { 125 | break; 126 | } 127 | 128 | write!(f, "\r{}{} {}{}", color.code(), i, text, Color::reset()).unwrap(); 129 | f.flush().unwrap(); 130 | 131 | let mut elapsed = Duration::from_millis(0); 132 | while elapsed < interval { 133 | if !is_processing_cloned.load(Ordering::SeqCst) { 134 | break; 135 | } 136 | thread::sleep(check_interval); 137 | elapsed += check_interval; 138 | } 139 | } 140 | } 141 | }); 142 | 143 | Self { is_processing } 144 | } 145 | 146 | pub fn stop(&mut self) -> Result<(), std::io::Error> { 147 | self.is_processing.store(false, Ordering::SeqCst); 148 | 149 | let mut f = std::io::stdout(); 150 | write!(f, "\r\x1b[2K")?; 151 | f.flush()?; 152 | 153 | Ok(()) 154 | } 155 | } 156 | 157 | impl Kind { 158 | fn symbols(&self) -> Vec<&str> { 159 | match self { 160 | Self::Spinner => vec!["|", "/", "-", "\\"], 161 | Self::Dots => vec!["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"], 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /rocal_cli/src/commands/utils/list.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | pub struct List { 4 | text: Option, 5 | items: Vec, 6 | } 7 | 8 | #[derive(Clone)] 9 | enum BulletStyle { 10 | Dot, 11 | Asterisk, 12 | Plus, 13 | Dash, 14 | } 15 | 16 | impl BulletStyle { 17 | pub fn to_symbol(&self) -> char { 18 | match self { 19 | Self::Dot => '.', 20 | Self::Asterisk => '*', 21 | Self::Plus => '+', 22 | Self::Dash => '-', 23 | } 24 | } 25 | 26 | pub fn len() -> usize { 27 | 4 28 | } 29 | 30 | pub fn list() -> Vec { 31 | vec![Self::Dot, Self::Asterisk, Self::Plus, Self::Dash] 32 | } 33 | } 34 | 35 | impl List { 36 | pub fn new() -> Self { 37 | Self { 38 | text: None, 39 | items: vec![], 40 | } 41 | } 42 | 43 | pub fn add_text(&mut self, text: &str) { 44 | self.text = Some(text.to_string()); 45 | } 46 | 47 | pub fn add_list(&mut self, list: List) { 48 | self.items.push(list); 49 | } 50 | 51 | fn print(list: &List, indent: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result { 52 | let mut i = indent.clone(); 53 | 54 | while 0 < i { 55 | write!(f, " ")?; 56 | i -= 1; 57 | } 58 | 59 | if let Some(text) = &list.text { 60 | let bullet_i: usize = indent % BulletStyle::len(); 61 | 62 | let bullet = BulletStyle::list().get(bullet_i).unwrap().clone(); 63 | 64 | writeln!(f, "{} {}", bullet.to_symbol(), text)?; 65 | } 66 | 67 | if list.items.len() < 1 { 68 | return Ok(()); 69 | } 70 | 71 | for l in list.items.iter() { 72 | List::print(l, indent + 1, f)?; 73 | } 74 | 75 | Ok(()) 76 | } 77 | } 78 | 79 | impl fmt::Display for List { 80 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 81 | List::print(self, 0, f) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /rocal_cli/src/commands/utils/open_link.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::process::{Command, Stdio}; 3 | 4 | #[cfg(target_os = "macos")] 5 | pub fn open_link(url: &str) -> io::Result<()> { 6 | Command::new("open") 7 | .arg(url) 8 | .stdout(Stdio::null()) 9 | .stderr(Stdio::null()) 10 | .spawn()?; 11 | Ok(()) 12 | } 13 | 14 | #[cfg(target_os = "linux")] 15 | pub fn open_link(url: &str) -> io::Result<()> { 16 | Command::new("xdg-open") 17 | .arg(url) 18 | .stdout(Stdio::null()) 19 | .stderr(Stdio::null()) 20 | .spawn()?; 21 | Ok(()) 22 | } 23 | 24 | #[cfg(target_os = "windows")] 25 | pub fn open_link(url: &str) -> io::Result<()> { 26 | Command::new("cmd") 27 | .args(&["/C", "start", "", url]) 28 | .stdout(Stdio::null()) 29 | .stderr(Stdio::null()) 30 | .spawn()?; 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /rocal_cli/src/commands/utils/project.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, env, path::PathBuf}; 2 | 3 | pub fn find_project_root() -> Option { 4 | let mut current_dir = env::current_dir().ok()?; 5 | loop { 6 | if current_dir.join("Cargo.toml").exists() { 7 | return Some(current_dir); 8 | } 9 | if !current_dir.pop() { 10 | break; 11 | } 12 | } 13 | None 14 | } 15 | 16 | pub fn get_app_name(root_path: &PathBuf) -> Cow { 17 | root_path 18 | .file_name() 19 | .expect("Failed to find your app name") 20 | .to_string_lossy() 21 | } 22 | -------------------------------------------------------------------------------- /rocal_cli/src/commands/utils/refresh_user_token.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | commands::utils::color::Color, 3 | rocal_api_client::RocalAPIClient, 4 | token_manager::{Kind, TokenManager}, 5 | }; 6 | 7 | pub async fn refresh_user_token() { 8 | if let Ok(refresh_token) = TokenManager::get_token(Kind::RocalRefreshToken) { 9 | let client = RocalAPIClient::new(); 10 | if let Err(err) = client.refresh_user_login_token(&refresh_token).await { 11 | println!("{}", Color::Red.text(&err)); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /rocal_cli/src/generators.rs: -------------------------------------------------------------------------------- 1 | pub mod cargo_file_generator; 2 | pub mod controller_generator; 3 | pub mod entrypoint_generator; 4 | pub mod gitignore_generator; 5 | pub mod js_generator; 6 | pub mod lib_generator; 7 | pub mod migration_generator; 8 | pub mod model_generator; 9 | pub mod public_generator; 10 | pub mod template_generator; 11 | pub mod view_generator; 12 | -------------------------------------------------------------------------------- /rocal_cli/src/generators/cargo_file_generator.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::Write}; 2 | 3 | pub fn create_cargo_file(project_name: &str) { 4 | let content = format!( 5 | r#" 6 | [package] 7 | name = "{}" 8 | version = "0.1.0" 9 | edition = "2021" 10 | 11 | [lib] 12 | crate-type = ["cdylib"] 13 | 14 | [dependencies] 15 | rocal = "0.3" 16 | wasm-bindgen = "0.2" 17 | wasm-bindgen-futures = "0.4" 18 | web-sys = {{ version = "0.3", features = [ 19 | "Window", 20 | "History", 21 | "console", 22 | "Location", 23 | "Document", 24 | "DocumentFragment", 25 | "Element", 26 | "HtmlElement", 27 | "Node", 28 | "NodeList", 29 | "Event", 30 | "FormData", 31 | "HtmlFormElement", 32 | "Worker", 33 | "WorkerOptions", 34 | "WorkerType" 35 | ]}} 36 | js-sys = "0.3" 37 | serde = {{ version = "1.0", features = ["derive"] }} 38 | serde-wasm-bindgen = "0.6" 39 | "#, 40 | project_name 41 | ); 42 | 43 | let mut file = File::create("Cargo.toml").expect("Failed to create Cargo.toml"); 44 | file.write_all(content.to_string().as_bytes()) 45 | .expect("Failed to create Cargo.toml"); 46 | file.flush().expect("Failed to create Cargo.toml"); 47 | } 48 | -------------------------------------------------------------------------------- /rocal_cli/src/generators/controller_generator.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{self, File}, 3 | io::Write, 4 | }; 5 | 6 | use quote::quote; 7 | 8 | pub fn create_controller_file() { 9 | let root_controller_content = quote! { 10 | use rocal::rocal_core::traits::{Controller, SharedRouter}; 11 | use crate::views::root_view::RootView; 12 | 13 | pub struct RootController { 14 | router: SharedRouter, 15 | view: RootView, 16 | } 17 | 18 | impl Controller for RootController { 19 | type View = RootView; 20 | 21 | fn new(router: SharedRouter, view: Self::View) -> Self { 22 | RootController { router, view } 23 | } 24 | } 25 | 26 | impl RootController { 27 | #[rocal::action] 28 | pub fn index(&self) { 29 | self.view.index(); 30 | } 31 | } 32 | }; 33 | 34 | let controller_content = quote! { 35 | pub mod root_controller; 36 | }; 37 | 38 | fs::create_dir_all("src/controllers").expect("Failed to create src/controllers"); 39 | 40 | let mut root_controller_file = File::create("src/controllers/root_controller.rs") 41 | .expect("Failed to create src/controllers/root_controller.rs"); 42 | 43 | root_controller_file 44 | .write_all(root_controller_content.to_string().as_bytes()) 45 | .expect("Failed to create src/controllers/root_controller.rs"); 46 | root_controller_file 47 | .flush() 48 | .expect("Failed to create src/controllers/root_controller.rs"); 49 | 50 | let mut controller_file = 51 | File::create("src/controllers.rs").expect("Failed to create src/controllers.rs"); 52 | 53 | controller_file 54 | .write_all(controller_content.to_string().as_bytes()) 55 | .expect("Failed to create src/controllers.rs"); 56 | controller_file 57 | .flush() 58 | .expect("Failed to create src/controllers.rs"); 59 | } 60 | -------------------------------------------------------------------------------- /rocal_cli/src/generators/entrypoint_generator.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::Write}; 2 | 3 | pub fn create_entrypoint(project_name: &str) { 4 | let html = format!( 5 | r#" 6 | 7 | 8 | 9 | 10 | {} 11 | 12 | 13 | 20 | 21 | 30 | 31 | 32 | "#, 33 | project_name, project_name 34 | ); 35 | 36 | let mut file = File::create("index.html").expect("Failed to create index.html"); 37 | file.write_all(html.as_bytes()) 38 | .expect("Failed to create index.html"); 39 | file.flush().expect("Failed to create index.html"); 40 | } 41 | -------------------------------------------------------------------------------- /rocal_cli/src/generators/gitignore_generator.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::Write}; 2 | 3 | pub fn create_gitignore() { 4 | let content = r#" 5 | target 6 | pkg 7 | release 8 | release.tar.gz 9 | Cargo.lock 10 | "#; 11 | 12 | let mut file = File::create(".gitignore").expect("Failed to create .gitignore"); 13 | 14 | file.write_all(content.to_string().as_bytes()) 15 | .expect("Failed to create .gitignore"); 16 | file.flush().expect("Failed to create .gitignore"); 17 | } 18 | -------------------------------------------------------------------------------- /rocal_cli/src/generators/js_generator.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf}; 2 | 3 | macro_rules! copy_files { 4 | ( $( $filename:literal ),* $(,)? ) => {{ 5 | $( 6 | let src_file = include_bytes!(concat!("../../js/", $filename)); 7 | let dst_file = std::path::PathBuf::from(&format!("js/{}", $filename)); 8 | std::fs::write(&dst_file, src_file).expect(&format!("Failed to copy {}", $filename)); 9 | )* 10 | 11 | }}; 12 | } 13 | 14 | pub fn create_js_files() { 15 | let src_sw_file = include_bytes!("../../js/sw.js"); 16 | let dst_sw_file = PathBuf::from("sw.js"); 17 | fs::write(&dst_sw_file, src_sw_file).expect("Failed to copy js/sw.js"); 18 | 19 | fs::create_dir_all("js").expect("Failed to create js/"); 20 | 21 | copy_files![ 22 | "db_query_worker.js", 23 | "db_sync_worker.js", 24 | "global.js", 25 | "sqlite3-opfs-async-proxy.js", 26 | "sqlite3.mjs", 27 | "sqlite3.wasm", 28 | ]; 29 | } 30 | -------------------------------------------------------------------------------- /rocal_cli/src/generators/lib_generator.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::Write}; 2 | 3 | pub fn create_lib_file() { 4 | let content = r#" 5 | use rocal::{config, migrate, route}; 6 | 7 | mod controllers; 8 | mod models; 9 | mod templates; 10 | mod views; 11 | 12 | // app_id and sync_server_endpoint should be set to utilize a sync server. 13 | // You can get them by $ rocal sync-servers list 14 | config! { 15 | app_id: "", 16 | sync_server_endpoint: "", 17 | database_directory_name: "local", 18 | database_file_name: "local.sqlite3" 19 | } 20 | 21 | #[rocal::main] 22 | fn app() { 23 | migrate!("db/migrations"); 24 | 25 | route! { 26 | get "/" => { controller: RootController, action: index, view: RootView } 27 | } 28 | } 29 | "#; 30 | 31 | let mut file = File::create("src/lib.rs").expect("Failed to create src/lib.rs"); 32 | 33 | file.write_all(content.as_bytes()) 34 | .expect("Failed to create src/lib.rs"); 35 | file.flush().expect("Failed to create src/lib.rs"); 36 | } 37 | -------------------------------------------------------------------------------- /rocal_cli/src/generators/migration_generator.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{self, File}; 2 | 3 | pub fn create_migration_dir() { 4 | fs::create_dir_all("db/migrations").expect("Failed to create db/migrations"); 5 | File::create("db/migrations/.keep").expect("Failed to create db/migrations/.keep"); 6 | } 7 | -------------------------------------------------------------------------------- /rocal_cli/src/generators/model_generator.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{self, File}; 2 | 3 | pub fn create_model_file() { 4 | fs::create_dir_all("src/models").expect("Failed to create src/models"); 5 | File::create("src/models/.keep").expect("Failed to create src/models/.keep"); 6 | File::create("src/models.rs").expect("Failed to create src/models.rs"); 7 | } 8 | -------------------------------------------------------------------------------- /rocal_cli/src/generators/public_generator.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{self, File}; 2 | 3 | pub fn create_public_dir() { 4 | fs::create_dir("public").expect("Failed to create public/"); 5 | File::create("public/.keep").expect("Failed to create public/.keep"); 6 | } 7 | -------------------------------------------------------------------------------- /rocal_cli/src/generators/template_generator.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{self, File}, 3 | io::Write, 4 | path::PathBuf, 5 | }; 6 | 7 | use quote::quote; 8 | 9 | pub fn create_template_file() { 10 | let template_content = quote! { 11 | pub mod root_template; 12 | }; 13 | 14 | fs::create_dir_all("src/templates").expect("Failed to create src/templates"); 15 | 16 | let src_file = include_bytes!("../../seeds/root_template.rs"); 17 | let dst_file = PathBuf::from("src/templates/root_template.rs"); 18 | fs::write(&dst_file, src_file).expect("Failed to copy root_template.rs"); 19 | 20 | let mut template_file = 21 | File::create("src/templates.rs").expect("Failed to create src/templates.rs"); 22 | 23 | template_file 24 | .write_all(template_content.to_string().as_bytes()) 25 | .expect("Failed to create src/templates.rs"); 26 | template_file 27 | .flush() 28 | .expect("Failed to create src/templates.rs"); 29 | } 30 | -------------------------------------------------------------------------------- /rocal_cli/src/generators/view_generator.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{self, File}, 3 | io::Write, 4 | }; 5 | 6 | use quote::quote; 7 | 8 | pub fn create_view_file() { 9 | let root_view_content = quote! { 10 | use rocal::rocal_core::traits::{SharedRouter, Template, View}; 11 | 12 | use crate::templates::root_template::RootTemplate; 13 | 14 | pub struct RootView { 15 | router: SharedRouter, 16 | } 17 | 18 | impl View for RootView { 19 | fn new(router: SharedRouter) -> Self { 20 | RootView { router } 21 | } 22 | } 23 | 24 | impl RootView { 25 | pub fn index(&self) { 26 | let template = RootTemplate::new(self.router.clone()); 27 | template.render(String::new()); 28 | } 29 | } 30 | }; 31 | 32 | let view_content = quote! { 33 | pub mod root_view; 34 | }; 35 | 36 | fs::create_dir_all("src/views").expect("Failed to create src/views"); 37 | 38 | let mut root_view_file = 39 | File::create("src/views/root_view.rs").expect("Failed to create src/views/root_view.rs"); 40 | 41 | root_view_file 42 | .write_all(root_view_content.to_string().as_bytes()) 43 | .expect("Failed to create src/views/root_view.rs"); 44 | root_view_file 45 | .flush() 46 | .expect("Failed to create src/views/root_view.rs"); 47 | 48 | let mut view_file = File::create("src/views.rs").expect("Failed to create src/views.rs"); 49 | 50 | view_file 51 | .write_all(view_content.to_string().as_bytes()) 52 | .expect("Failed to create src/views.rs"); 53 | view_file.flush().expect("Failed to create src/views.rs"); 54 | } 55 | -------------------------------------------------------------------------------- /rocal_cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | mod commands; 4 | mod generators; 5 | mod response; 6 | mod rocal_api_client; 7 | mod runner; 8 | mod token_manager; 9 | 10 | pub use runner::run; 11 | -------------------------------------------------------------------------------- /rocal_cli/src/response.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize)] 4 | pub struct ResponseWithMessage 5 | where 6 | T: Clone, 7 | { 8 | data: Option, 9 | message: String, 10 | } 11 | 12 | impl ResponseWithMessage 13 | where 14 | T: Clone, 15 | { 16 | pub fn get_data(&self) -> &Option { 17 | &self.data 18 | } 19 | 20 | pub fn get_message(&self) -> &str { 21 | &self.message 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /rocal_cli/src/rocal_api_client/cancel_subscription.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::Serialize; 4 | 5 | #[derive(Serialize)] 6 | pub struct CancelSubscription { 7 | reason: String, 8 | } 9 | 10 | impl CancelSubscription { 11 | pub fn new(reason: u32) -> Result { 12 | let reasons = Self::get_reasons(); 13 | 14 | if let Some(reason) = reasons.get(&reason) { 15 | Ok(Self { 16 | reason: reason.to_string(), 17 | }) 18 | } else { 19 | Err("The reason number is out of options".to_string()) 20 | } 21 | } 22 | 23 | pub fn get_reasons() -> HashMap { 24 | let mut reasons = HashMap::new(); 25 | 26 | reasons.insert(1, "Custormer service was less than expected".to_string()); 27 | reasons.insert(2, "Quality was less than expected".to_string()); 28 | reasons.insert(3, "Some features are missing".to_string()); 29 | reasons.insert(4, "I'm switching to a different service".to_string()); 30 | reasons.insert(5, "Ease of use was less than expected".to_string()); 31 | reasons.insert(6, "It's too expensive".to_string()); 32 | reasons.insert(7, "I don't use the service enough".to_string()); 33 | reasons.insert(8, "Other reason".to_string()); 34 | 35 | reasons 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /rocal_cli/src/rocal_api_client/create_app.rs: -------------------------------------------------------------------------------- 1 | use std::time::SystemTime; 2 | 3 | use serde::Serialize; 4 | 5 | #[derive(Serialize)] 6 | pub struct CreateApp { 7 | app_name: String, 8 | subdomain: String, 9 | version: String, 10 | } 11 | 12 | impl CreateApp { 13 | pub fn new(app_name: &str, subdomain: &str) -> Self { 14 | Self { 15 | app_name: app_name.to_string(), 16 | subdomain: subdomain.to_string(), 17 | version: Self::default_version().to_string(), 18 | } 19 | } 20 | 21 | fn default_version() -> u64 { 22 | SystemTime::now() 23 | .duration_since(SystemTime::UNIX_EPOCH) 24 | .unwrap() 25 | .as_secs() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /rocal_cli/src/rocal_api_client/create_payment_link.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | #[derive(Serialize)] 4 | pub struct CreatePaymentLink { 5 | plan: String, 6 | } 7 | 8 | impl CreatePaymentLink { 9 | pub fn new(plan: &str) -> Self { 10 | Self { 11 | plan: plan.to_string(), 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /rocal_cli/src/rocal_api_client/create_user.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | #[derive(Serialize)] 4 | pub struct CreateUser { 5 | email: String, 6 | password: String, 7 | workspace: String, 8 | } 9 | 10 | impl CreateUser { 11 | pub fn new(email: &str, password: &str, workspace: &str) -> Self { 12 | Self { 13 | email: email.to_string(), 14 | password: password.to_string(), 15 | workspace: workspace.to_string(), 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /rocal_cli/src/rocal_api_client/login_user.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | #[derive(Serialize)] 4 | pub struct LoginUser { 5 | email: String, 6 | password: String, 7 | } 8 | 9 | impl LoginUser { 10 | pub fn new(email: &str, password: &str) -> Self { 11 | Self { 12 | email: email.to_string(), 13 | password: password.to_string(), 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /rocal_cli/src/rocal_api_client/oob_code_response.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize, Clone)] 4 | pub struct OobCodeResponse { 5 | email: String, 6 | } 7 | 8 | impl OobCodeResponse { 9 | pub fn get_email(&self) -> &str { 10 | &self.email 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /rocal_cli/src/rocal_api_client/payment_link.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize, Clone)] 4 | pub struct PaymentLink { 5 | url: String, 6 | } 7 | 8 | impl PaymentLink { 9 | pub fn get_url(&self) -> &str { 10 | &self.url 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /rocal_cli/src/rocal_api_client/registered_sync_server.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize, Clone)] 4 | pub struct RegisteredSyncServer { 5 | app_id: String, 6 | endpoint: String, 7 | } 8 | 9 | impl RegisteredSyncServer { 10 | pub fn get_app_id(&self) -> &str { 11 | &self.app_id 12 | } 13 | 14 | pub fn get_endpoint(&self) -> &str { 15 | &self.endpoint 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /rocal_cli/src/rocal_api_client/send_email_verification.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | #[derive(Serialize)] 4 | #[serde(rename_all = "camelCase")] 5 | pub struct SendEmailVerification { 6 | id_token: String, 7 | } 8 | 9 | impl SendEmailVerification { 10 | pub fn new(id_token: &str) -> Self { 11 | Self { 12 | id_token: id_token.to_string(), 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /rocal_cli/src/rocal_api_client/send_password_reset_email.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | #[derive(Serialize)] 4 | pub struct SendPasswordResetEmail { 5 | email: String, 6 | } 7 | 8 | impl SendPasswordResetEmail { 9 | pub fn new(email: &str) -> Self { 10 | Self { 11 | email: email.to_string(), 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /rocal_cli/src/rocal_api_client/subdomain.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[allow(dead_code)] 4 | #[derive(Deserialize, Clone)] 5 | pub struct Subdomain { 6 | app_name: String, 7 | subdomain: String, 8 | } 9 | 10 | impl Subdomain { 11 | pub fn get_subdomain(&self) -> &str { 12 | &self.subdomain 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /rocal_cli/src/rocal_api_client/subscription_status.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize, Clone)] 4 | pub struct SubscriptionStatus { 5 | plan: String, 6 | cancel_at_period_end: bool, 7 | } 8 | 9 | impl SubscriptionStatus { 10 | pub fn is_free_plan(&self) -> bool { 11 | self.plan == "free" 12 | } 13 | 14 | pub fn get_plan(&self) -> &str { 15 | &self.plan 16 | } 17 | 18 | pub fn get_cancel_at_period_end(&self) -> &bool { 19 | &self.cancel_at_period_end 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /rocal_cli/src/rocal_api_client/user_login_token.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[allow(dead_code)] 4 | #[derive(Deserialize, Clone)] 5 | pub struct UserLoginToken { 6 | id_token: String, 7 | refresh_token: String, 8 | expires_in: String, 9 | local_id: String, 10 | } 11 | 12 | #[allow(dead_code)] 13 | impl UserLoginToken { 14 | pub fn get_id_token(&self) -> &str { 15 | &self.id_token 16 | } 17 | 18 | pub fn get_refresh_token(&self) -> &str { 19 | &self.refresh_token 20 | } 21 | 22 | pub fn get_expires_in(&self) -> &str { 23 | &self.expires_in 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /rocal_cli/src/rocal_api_client/user_refresh_token.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | #[derive(Serialize)] 4 | pub struct UserRefreshToken { 5 | refresh_token: String, 6 | } 7 | 8 | impl UserRefreshToken { 9 | pub fn new(token: &str) -> Self { 10 | Self { 11 | refresh_token: token.to_string(), 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /rocal_cli/src/token_manager.rs: -------------------------------------------------------------------------------- 1 | use keyring::{Entry, Error}; 2 | 3 | const DEFAULT_KEY: &'static str = "default"; 4 | 5 | pub struct TokenManager; 6 | 7 | #[allow(dead_code)] 8 | impl TokenManager { 9 | pub fn set_token(kind: Kind, token: &str) -> Result<(), Error> { 10 | let entry = Entry::new(&kind.to_string(), DEFAULT_KEY)?; 11 | entry.set_password(token)?; 12 | Ok(()) 13 | } 14 | 15 | pub fn get_token(kind: Kind) -> Result { 16 | let entry = Entry::new(&kind.to_string(), DEFAULT_KEY)?; 17 | let token = entry.get_password()?; 18 | Ok(token) 19 | } 20 | 21 | pub fn delete_token(kind: Kind) -> Result<(), Error> { 22 | let entry = Entry::new(&kind.to_string(), DEFAULT_KEY)?; 23 | entry.delete_credential()?; 24 | Ok(()) 25 | } 26 | } 27 | 28 | pub enum Kind { 29 | RocalAccessToken, 30 | RocalRefreshToken, 31 | } 32 | 33 | impl Kind { 34 | pub fn to_string(&self) -> String { 35 | match self { 36 | Kind::RocalAccessToken => { 37 | format!("{}:rocal_access_token", env!("BUILD_PROFILE")) 38 | } 39 | Kind::RocalRefreshToken => { 40 | format!("{}:rocal_refresh_token", env!("BUILD_PROFILE")) 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rocal_core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rocal-core" 3 | version = "0.3.0" 4 | edition = "2021" 5 | 6 | authors = ["Yoshiki Sashiyama "] 7 | description = "Core for Rocal - Full-Stack WASM framework" 8 | license = "MIT" 9 | homepage = "https://github.com/rocal-dev/rocal" 10 | repository = "https://github.com/rocal-dev/rocal" 11 | readme = "README.md" 12 | keywords = ["local-first", "web-framework", "wasm", "web"] 13 | 14 | [dependencies] 15 | quote = "1.0" 16 | syn = { version = "2.0", features = ["full", "extra-traits"] } 17 | proc-macro2 = "1.0" 18 | url = "2" 19 | regex = "1.11" 20 | wasm-bindgen = "0.2" 21 | js-sys = "0.3" 22 | wasm-bindgen-futures = "0.4" 23 | web-sys = { version = "0.3", features = [ 24 | "Window", 25 | "History", 26 | "console", 27 | "Location", 28 | "Document", 29 | "DocumentFragment", 30 | "Element", 31 | "HtmlElement", 32 | "Node", 33 | "NodeList", 34 | "Event", 35 | "FormData", 36 | "HtmlFormElement", 37 | "Worker", 38 | "WorkerOptions", 39 | "WorkerType" 40 | ]} 41 | serde = { version = "1.0", features = ["derive"] } 42 | serde-wasm-bindgen = "0.6" -------------------------------------------------------------------------------- /rocal_core/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /rocal_core/src/configuration.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use syn::{ 4 | parse::{Parse, ParseStream}, 5 | punctuated::Punctuated, 6 | Ident, LitStr, Token, 7 | }; 8 | 9 | pub fn build_config_struct() -> TokenStream { 10 | quote! { 11 | pub struct Configuration { 12 | app_id: String, 13 | sync_server_endpoint: String, 14 | database: std::sync::Arc, 15 | } 16 | 17 | impl Configuration { 18 | pub fn new(app_id: String, sync_server_endpoint: String, database: std::sync::Arc) -> Self { 19 | Configuration { 20 | app_id, 21 | sync_server_endpoint, 22 | database, 23 | } 24 | } 25 | 26 | pub fn get_app_id(&self) -> &str { 27 | &self.app_id 28 | } 29 | 30 | pub fn get_sync_server_endpoint(&self) -> &str { 31 | &self.sync_server_endpoint 32 | } 33 | 34 | pub fn get_database(&self) -> std::sync::Arc { 35 | self.database.clone() 36 | } 37 | } 38 | } 39 | } 40 | 41 | pub fn parse_config(item: TokenStream) -> Result { 42 | let parsed_config: ParsedConfig = syn::parse(item.into())?; 43 | 44 | Ok(parsed_config) 45 | } 46 | 47 | #[derive(Debug, Default)] 48 | pub struct ParsedConfig { 49 | app_id: Option, 50 | sync_server_endpoint: Option, 51 | database_directory_name: Option, 52 | database_file_name: Option, 53 | } 54 | 55 | impl ParsedConfig { 56 | pub fn set_app_id(&mut self, app_id: String) { 57 | self.app_id = Some(app_id); 58 | } 59 | 60 | pub fn set_sync_server_endpoint(&mut self, endpoint: String) { 61 | self.sync_server_endpoint = Some(endpoint); 62 | } 63 | 64 | pub fn set_database_directory_name(&mut self, directory_name: String) { 65 | self.database_directory_name = Some(directory_name); 66 | } 67 | 68 | pub fn set_database_file_name(&mut self, file_name: String) { 69 | self.database_file_name = Some(file_name); 70 | } 71 | 72 | pub fn get_app_id(&self) -> &Option { 73 | &self.app_id 74 | } 75 | 76 | pub fn get_sync_server_endpoint(&self) -> &Option { 77 | &self.sync_server_endpoint 78 | } 79 | 80 | pub fn get_database_directory_name(&self) -> &Option { 81 | &self.database_directory_name 82 | } 83 | 84 | pub fn get_database_file_name(&self) -> &Option { 85 | &self.database_file_name 86 | } 87 | } 88 | 89 | impl Parse for ParsedConfig { 90 | fn parse(input: ParseStream) -> Result { 91 | let mut config = ParsedConfig::default(); 92 | 93 | let kvs = Punctuated::::parse_terminated(&input)?; 94 | let mut has_error_attribute = false; 95 | 96 | kvs.into_iter().for_each(|kv| match kv.key.as_str() { 97 | "app_id" => config.set_app_id(kv.value), 98 | "sync_server_endpoint" => config.set_sync_server_endpoint(kv.value), 99 | "database_directory_name" => config.set_database_directory_name(kv.value), 100 | "database_file_name" => config.set_database_file_name(kv.value), 101 | _ => has_error_attribute = true, 102 | }); 103 | 104 | if has_error_attribute { 105 | return Err(syn::Error::new( 106 | input.span(), 107 | "You put (an) invalid attribute(s)", 108 | )); 109 | } 110 | 111 | Ok(config) 112 | } 113 | } 114 | 115 | struct KeyValue { 116 | key: String, 117 | value: String, 118 | } 119 | 120 | impl Parse for KeyValue { 121 | fn parse(input: ParseStream) -> Result { 122 | let key = input 123 | .parse() 124 | .map(|v: Ident| v.to_string()) 125 | .map_err(|_| syn::Error::new(input.span(), "should have property keys"))?; 126 | 127 | let _: Token!(:) = input.parse().map_err(|_| { 128 | syn::Error::new(input.span(), "prop name and value should be separated by :") 129 | })?; 130 | 131 | let value = input 132 | .parse() 133 | .map(|v: LitStr| v.value()) 134 | .map_err(|_| syn::Error::new(input.span(), "Value should be here"))?; 135 | 136 | Ok(KeyValue { key, value }) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /rocal_core/src/database.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | 4 | pub fn build_database_struct() -> TokenStream { 5 | quote! { 6 | use js_sys::Promise; 7 | use wasm_bindgen::{JsCast, JsValue}; 8 | use serde::de::DeserializeOwned; 9 | use serde_wasm_bindgen::from_value; 10 | 11 | pub struct Database { 12 | directory_name: String, 13 | file_name: String, 14 | } 15 | 16 | impl Database { 17 | pub fn new(directory_name: String, file_name: String) -> Self { 18 | Database { 19 | directory_name, 20 | file_name, 21 | } 22 | } 23 | 24 | pub fn get_directory_name(&self) -> &str { 25 | &self.directory_name 26 | } 27 | 28 | pub fn get_file_name(&self) -> &str { 29 | &self.file_name 30 | } 31 | 32 | pub fn get_name(&self) -> String { 33 | format!("{}/{}", self.directory_name, self.file_name) 34 | } 35 | 36 | pub fn query(&self, query: &str) -> Query { 37 | Query::new(&self.get_name(), query) 38 | } 39 | } 40 | 41 | struct Query { 42 | db: String, 43 | query: String, 44 | bindings: Vec, 45 | } 46 | 47 | impl Query { 48 | fn new(db: &str, query: &str) -> Self { 49 | Self { 50 | db: db.to_string(), 51 | query: query.to_string(), 52 | bindings: vec![], 53 | } 54 | } 55 | 56 | fn bind(&mut self, bind: T) -> &mut Self 57 | where 58 | T: Into, 59 | { 60 | self.bindings.push(bind.into()); 61 | self 62 | } 63 | 64 | pub async fn fetch(&self) -> Result, JsValue> 65 | where 66 | T: DeserializeOwned, 67 | { 68 | let promise = crate::exec_sql(&self.db, &self.query, self.bindings.clone().into_boxed_slice()) 69 | .dyn_into::()?; 70 | let result = wasm_bindgen_futures::JsFuture::from(promise).await?; 71 | let result: Vec = from_value(result)?; 72 | Ok(result) 73 | } 74 | 75 | pub async fn execute(&self) -> Result { 76 | let promise = crate::exec_sql(&self.db, &self.query, self.bindings.clone().into_boxed_slice()) 77 | .dyn_into::()?; 78 | let result = wasm_bindgen_futures::JsFuture::from(promise).await?; 79 | Ok(result) 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /rocal_core/src/enums.rs: -------------------------------------------------------------------------------- 1 | pub mod request_method; 2 | -------------------------------------------------------------------------------- /rocal_core/src/enums/request_method.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | 3 | #[derive(Debug)] 4 | pub enum RequestMethod { 5 | Get, 6 | Post, 7 | Put, 8 | Patch, 9 | Delete, 10 | } 11 | 12 | impl RequestMethod { 13 | pub fn from(method: &str) -> Self { 14 | match method.to_uppercase().as_str() { 15 | "GET" => RequestMethod::Get, 16 | "POST" => RequestMethod::Post, 17 | "PUT" => RequestMethod::Put, 18 | "PATCH" => RequestMethod::Patch, 19 | "DELETE" => RequestMethod::Delete, 20 | _ => RequestMethod::Post, 21 | } 22 | } 23 | } 24 | 25 | impl fmt::Display for RequestMethod { 26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 27 | match self { 28 | RequestMethod::Get => write!(f, "GET"), 29 | RequestMethod::Post => write!(f, "POST"), 30 | RequestMethod::Put => write!(f, "PUT"), 31 | RequestMethod::Patch => write!(f, "PATCH"), 32 | RequestMethod::Delete => write!(f, "DELETE"), 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /rocal_core/src/migrator.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use std::{ 3 | env, fs, 4 | path::{Path, PathBuf}, 5 | }; 6 | use syn::{spanned::Spanned, LitStr}; 7 | 8 | pub fn get_migrations(item: &TokenStream) -> Result { 9 | let path_name: LitStr = if item.is_empty() { 10 | LitStr::new("db/migrations", item.span()) 11 | } else { 12 | syn::parse2(item.clone())? 13 | }; 14 | 15 | let path = resolve_path(&path_name.value(), item.span())?; 16 | 17 | let mut result = String::new(); 18 | 19 | if let Ok(dir) = fs::read_dir(&path) { 20 | let mut entries: Vec<_> = dir.filter_map(Result::ok).collect(); 21 | 22 | entries.sort_by_key(|entry| entry.path()); 23 | 24 | for entry in entries { 25 | let path = entry.path(); 26 | 27 | if path.is_file() { 28 | if let Ok(contents) = fs::read_to_string(&path) { 29 | result += &contents; 30 | } else { 31 | return Err(syn::Error::new( 32 | item.span(), 33 | format!("{} cannot be opened", entry.file_name().to_str().unwrap()), 34 | )); 35 | } 36 | } 37 | } 38 | 39 | Ok(result) 40 | } else { 41 | Err(syn::Error::new( 42 | item.span(), 43 | format!("{} not found", path_name.value()), 44 | )) 45 | } 46 | } 47 | 48 | fn resolve_path(path: impl AsRef, span: Span) -> syn::Result { 49 | let path = path.as_ref(); 50 | 51 | if path.is_absolute() { 52 | return Err(syn::Error::new( 53 | span, 54 | "absolute paths will only work on the current machine", 55 | )); 56 | } 57 | 58 | if path.is_relative() 59 | && !path 60 | .parent() 61 | .map_or(false, |parent| !parent.as_os_str().is_empty()) 62 | { 63 | return Err(syn::Error::new( 64 | span, 65 | "paths relative to the current file's directory are not currently supported", 66 | )); 67 | } 68 | 69 | let base_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| { 70 | syn::Error::new( 71 | span, 72 | "CARGO_MANIFEST_DIR is not set; please use Cargo to build", 73 | ) 74 | })?; 75 | 76 | let base_dir_path = Path::new(&base_dir); 77 | 78 | Ok(base_dir_path.join(path)) 79 | } 80 | -------------------------------------------------------------------------------- /rocal_core/src/parsed_action.rs: -------------------------------------------------------------------------------- 1 | use syn::{ 2 | FnArg, GenericArgument, Ident, ItemFn, Pat, PatIdent, PatType, PathArguments, Type, TypePath, 3 | }; 4 | 5 | #[derive(Debug)] 6 | pub struct ParsedAction { 7 | name: Ident, 8 | args: Vec, 9 | } 10 | 11 | impl ParsedAction { 12 | pub fn new(name: Ident, args: Vec) -> Self { 13 | ParsedAction { name, args } 14 | } 15 | 16 | pub fn get_name(&self) -> &Ident { 17 | &self.name 18 | } 19 | 20 | pub fn get_args(&self) -> &Vec { 21 | &self.args 22 | } 23 | } 24 | 25 | #[derive(Debug)] 26 | pub struct Arg { 27 | name: Ident, 28 | ty: Ident, 29 | is_optional: bool, 30 | } 31 | 32 | impl Arg { 33 | pub fn get_name(&self) -> &Ident { 34 | &self.name 35 | } 36 | 37 | pub fn get_ty(&self) -> &Ident { 38 | &self.ty 39 | } 40 | 41 | pub fn get_is_optional(&self) -> &bool { 42 | &self.is_optional 43 | } 44 | } 45 | 46 | pub fn parse_action(ast: &ItemFn) -> Result { 47 | let fn_name = ast.sig.ident.clone(); 48 | let args = extract_args(ast); 49 | 50 | Ok(ParsedAction::new(fn_name, args)) 51 | } 52 | 53 | fn extract_args(item_fn: &ItemFn) -> Vec { 54 | let mut args = Vec::new(); 55 | 56 | for input in item_fn.sig.inputs.iter() { 57 | if let FnArg::Typed(PatType { pat, ty, .. }) = input { 58 | if let Pat::Ident(PatIdent { ident, .. }) = &**pat { 59 | if let Some((type_ident, is_optional)) = extract_type_ident(&**ty) { 60 | args.push(Arg { 61 | name: ident.clone(), 62 | ty: type_ident, 63 | is_optional, 64 | }); 65 | } 66 | } 67 | } 68 | } 69 | 70 | args 71 | } 72 | 73 | fn extract_type_ident(ty: &Type) -> Option<(Ident, bool)> { 74 | match ty { 75 | Type::Reference(type_ref) => extract_type_ident(&*type_ref.elem), 76 | Type::Path(TypePath { path, .. }) => { 77 | let segment = path.segments.last()?; 78 | if segment.ident == "Option" { 79 | if let PathArguments::AngleBracketed(angle_bracketed) = &segment.arguments { 80 | if let Some(GenericArgument::Type(inner_ty)) = angle_bracketed.args.first() { 81 | return extract_type_ident(inner_ty) 82 | .map(|(inner_ident, _)| (inner_ident, true)); 83 | } 84 | } 85 | None 86 | } else { 87 | Some((segment.ident.clone(), false)) 88 | } 89 | } 90 | _ => None, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /rocal_core/src/route_handler.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | use url::Url; 5 | use web_sys::window; 6 | 7 | use crate::enums::request_method::RequestMethod; 8 | use crate::router::Router; 9 | 10 | pub struct RouteHandler { 11 | router: Rc>, 12 | not_found: Box, 13 | } 14 | 15 | impl RouteHandler { 16 | pub fn new(router: Rc>, not_found: Option>) -> Self { 17 | let not_found = match not_found { 18 | Some(nf) => nf, 19 | None => Box::new(Self::default_not_found_page), 20 | }; 21 | 22 | RouteHandler { router, not_found } 23 | } 24 | 25 | pub async fn handle_route(&self) { 26 | let href = if let Some(w) = window() { 27 | if let Ok(href) = w.location().href() { 28 | href 29 | } else { 30 | (self.not_found)(); 31 | return; 32 | } 33 | } else { 34 | (self.not_found)(); 35 | return; 36 | }; 37 | 38 | let url = match Url::parse(&href) { 39 | Ok(u) => u, 40 | Err(_) => { 41 | (self.not_found)(); 42 | return; 43 | } 44 | }; 45 | 46 | let path = url.fragment().unwrap_or_else(|| "/"); 47 | 48 | if !self 49 | .router 50 | .borrow() 51 | .resolve(RequestMethod::Get, path, None) 52 | .await 53 | { 54 | (self.not_found)(); 55 | } 56 | } 57 | 58 | fn default_not_found_page() { 59 | web_sys::window() 60 | .unwrap() 61 | .document() 62 | .unwrap() 63 | .body() 64 | .unwrap() 65 | .set_inner_html("

404 - Page Not Found

"); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /rocal_core/src/router.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, future::Future, pin::Pin}; 2 | 3 | use regex::Regex; 4 | use url::Url; 5 | use wasm_bindgen::JsValue; 6 | use web_sys::{console, window}; 7 | 8 | use crate::enums::request_method::RequestMethod; 9 | 10 | type Action = Box) -> Pin>>>; 11 | 12 | struct Node { 13 | children: HashMap, 14 | action: Option, 15 | } 16 | 17 | pub struct Router { 18 | root: Node, 19 | } 20 | 21 | impl Router { 22 | const HOST: &str = "https://www.example.com"; 23 | 24 | pub fn new() -> Self { 25 | Router { 26 | root: Node { 27 | children: HashMap::new(), 28 | action: None, 29 | }, 30 | } 31 | } 32 | 33 | pub fn register(&mut self, method: RequestMethod, route: &str, action: Action) { 34 | let mut ptr = &mut self.root; 35 | 36 | if !ptr.children.contains_key(&method.to_string()) { 37 | ptr.children.insert( 38 | method.to_string(), 39 | Node { 40 | children: HashMap::new(), 41 | action: None, 42 | }, 43 | ); 44 | } 45 | 46 | ptr = ptr.children.get_mut(&method.to_string()).unwrap(); 47 | 48 | for s in route.split("/") { 49 | if !ptr.children.contains_key(s) { 50 | ptr.children.insert( 51 | s.to_string(), 52 | Node { 53 | children: HashMap::new(), 54 | action: None, 55 | }, 56 | ); 57 | } 58 | 59 | ptr = ptr.children.get_mut(s).unwrap(); 60 | } 61 | 62 | ptr.action = Some(action); 63 | } 64 | 65 | pub async fn resolve( 66 | &self, 67 | method: RequestMethod, 68 | route: &str, 69 | action_args: Option>, 70 | ) -> bool { 71 | let mut route = route.to_string(); 72 | let path_param_regex: Regex = Regex::new(r"^<(?.+)>$").unwrap(); 73 | 74 | let mut action_args: HashMap = action_args.unwrap_or(HashMap::new()); 75 | 76 | if let Ok(url) = Url::parse(&format!("{}{}", Self::HOST, route)) { 77 | for (k, v) in url.query_pairs() { 78 | action_args.insert(k.to_string(), v.to_string()); 79 | } 80 | route = url.path().to_string(); 81 | } 82 | 83 | let mut ptr = &self.root; 84 | 85 | if !ptr.children.contains_key(&method.to_string()) { 86 | return false; 87 | } 88 | 89 | ptr = ptr.children.get(&method.to_string()).unwrap(); 90 | 91 | for s in route.split("/") { 92 | if !ptr.children.contains_key(s) { 93 | if let Some(param) = ptr 94 | .children 95 | .keys() 96 | .find(|key| path_param_regex.is_match(key)) 97 | { 98 | let caps = path_param_regex.captures(¶m).unwrap(); 99 | action_args.insert(caps["key"].to_string(), s.to_string()); 100 | ptr = ptr.children.get(param).unwrap(); 101 | continue; 102 | } else { 103 | return false; 104 | } 105 | } 106 | 107 | ptr = ptr.children.get(s).unwrap(); 108 | } 109 | 110 | if let Some(action) = &ptr.action { 111 | action(action_args).await; 112 | true 113 | } else { 114 | false 115 | } 116 | } 117 | 118 | pub async fn redirect(&self, path: &str) -> bool { 119 | let result = self.resolve(RequestMethod::Get, path, None).await; 120 | 121 | if !result { 122 | return false; 123 | } 124 | 125 | let win = if let Some(win) = window() { 126 | win 127 | } else { 128 | return false; 129 | }; 130 | 131 | let (history, origin) = 132 | if let (Ok(history), Ok(origin)) = (win.history(), win.location().origin()) { 133 | (history, origin) 134 | } else { 135 | return false; 136 | }; 137 | 138 | let abs = format!("{}/#{}", origin, path); 139 | 140 | if let Err(err) = history.push_state_with_url(&JsValue::NULL, "", Some(&abs)) { 141 | console::error_1(&err); 142 | return false; 143 | } 144 | 145 | true 146 | } 147 | } 148 | 149 | pub fn link_to(path: &str, remote: bool) -> String { 150 | if remote { 151 | format!("{}", path) 152 | } else { 153 | format!("/#{}", path) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /rocal_core/src/traits.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, collections::HashMap, rc::Rc}; 2 | use url::Url; 3 | use wasm_bindgen::{closure::Closure, JsCast}; 4 | use wasm_bindgen_futures::spawn_local; 5 | use web_sys::{window, Document, Event, FormData, HtmlFormElement}; 6 | 7 | use crate::{enums::request_method::RequestMethod, router::Router}; 8 | 9 | pub type SharedRouter = Rc>; 10 | 11 | pub trait Controller { 12 | type View; 13 | fn new(router: SharedRouter, view: Self::View) -> Self; 14 | } 15 | 16 | pub trait View { 17 | fn new(router: SharedRouter) -> Self; 18 | } 19 | 20 | pub trait Template { 21 | type Data; 22 | 23 | fn new(router: SharedRouter) -> Self; 24 | fn router(&self) -> SharedRouter; 25 | fn body(&self, data: Self::Data) -> String; 26 | 27 | fn render(&self, data: Self::Data) { 28 | self.render_html(&self.body(data)); 29 | self.register_forms(); 30 | } 31 | 32 | fn render_html(&self, html: &str) { 33 | let doc = match self.get_document() { 34 | Some(doc) => doc, 35 | None => return, 36 | }; 37 | 38 | let body = match doc.body() { 39 | Some(body) => body, 40 | None => return, 41 | }; 42 | 43 | body.set_inner_html(html); 44 | } 45 | 46 | fn register_forms(&self) { 47 | let doc = match self.get_document() { 48 | Some(doc) => doc, 49 | None => return, 50 | }; 51 | 52 | let forms = match self.get_all_forms(&doc) { 53 | Some(forms) => forms, 54 | None => return, 55 | }; 56 | 57 | for i in 0..forms.length() { 58 | if let Some(form_node) = forms.get(i) { 59 | if let Some(form) = self.reset_form(form_node) { 60 | self.attach_form_listener(&form); 61 | } 62 | } 63 | } 64 | } 65 | 66 | fn get_document(&self) -> Option { 67 | window()?.document() 68 | } 69 | 70 | fn get_all_forms(&self, doc: &Document) -> Option { 71 | doc.query_selector_all("form").ok() 72 | } 73 | 74 | fn reset_form(&self, form_node: web_sys::Node) -> Option { 75 | let parent = form_node.parent_node()?; 76 | let new_node = form_node.clone_node_with_deep(true).ok()?; 77 | parent.replace_child(&new_node, &form_node).ok()?; 78 | new_node.dyn_into::().ok() 79 | } 80 | 81 | fn attach_form_listener(&self, form: &HtmlFormElement) { 82 | let router_for_closure = self.router().clone(); 83 | 84 | let closure = Closure::wrap(Box::new(move |e: Event| { 85 | e.prevent_default(); 86 | 87 | let mut args: HashMap = HashMap::new(); 88 | 89 | let element: HtmlFormElement = match e 90 | .current_target() 91 | .and_then(|t| t.dyn_into::().ok()) 92 | { 93 | Some(el) => el, 94 | None => return, 95 | }; 96 | 97 | let form_data = match FormData::new_with_form(&element) { 98 | Ok(data) => data, 99 | Err(_) => return, 100 | }; 101 | 102 | let entries = form_data.entries(); 103 | 104 | for entry in entries { 105 | if let Ok(entry) = entry { 106 | let entry_array = js_sys::Array::from(&entry); 107 | if entry_array.length() == 2 { 108 | let key = entry_array.get(0).as_string().unwrap_or_default(); 109 | let value = entry_array.get(1).as_string().unwrap_or_default(); 110 | args.insert(key, value); 111 | } 112 | } 113 | } 114 | 115 | if let Ok(url) = Url::parse(&element.action()) { 116 | let router = router_for_closure.clone(); 117 | spawn_local(async move { 118 | let method = RequestMethod::from( 119 | &element 120 | .get_attribute("method") 121 | .unwrap_or(String::from("post")), 122 | ); 123 | 124 | router 125 | .borrow() 126 | .resolve(method, url.path(), Some(args)) 127 | .await; 128 | }); 129 | } 130 | }) as Box); 131 | 132 | form.add_event_listener_with_callback("submit", closure.as_ref().unchecked_ref()) 133 | .expect("Failed to add submit event listeners"); 134 | closure.forget(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /rocal_core/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn to_snake_case(input: &str) -> String { 2 | let mut result = String::new(); 3 | 4 | for (i, c) in input.chars().enumerate() { 5 | if c.is_uppercase() { 6 | if i > 0 { 7 | result.push('_'); 8 | } 9 | result.push(c.to_ascii_lowercase()); 10 | } else { 11 | result.push(c); 12 | } 13 | } 14 | 15 | result 16 | } 17 | -------------------------------------------------------------------------------- /rocal_core/src/workers.rs: -------------------------------------------------------------------------------- 1 | pub mod db_sync_worker; 2 | -------------------------------------------------------------------------------- /rocal_core/src/workers/db_sync_worker.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | 4 | pub fn build_db_sync_worker_struct() -> TokenStream { 5 | quote! { 6 | use serde::{Deserialize, Serialize}; 7 | use web_sys::{Worker, WorkerOptions, WorkerType}; 8 | 9 | pub struct DbSyncWorker<'a> { 10 | worker_path: &'a str, 11 | force: ForceType, 12 | } 13 | 14 | pub enum ForceType { 15 | #[allow(dead_code)] 16 | Local, 17 | Remote, 18 | None, 19 | } 20 | 21 | #[derive(Serialize, Deserialize)] 22 | struct Message<'a> { 23 | app_id: &'a str, 24 | directory_name: &'a str, 25 | file_name: &'a str, 26 | endpoint: &'a str, 27 | force: &'a str, 28 | } 29 | 30 | impl<'a> DbSyncWorker<'a> { 31 | pub fn new(worker_path: &'a str, force: ForceType) -> Self { 32 | DbSyncWorker { worker_path, force } 33 | } 34 | 35 | pub fn run(&self) { 36 | let options = WorkerOptions::new(); 37 | options.set_type(WorkerType::Module); 38 | 39 | if let Ok(worker) = Worker::new_with_options(&self.worker_path, &options) { 40 | let config = &crate::CONFIG; 41 | 42 | let db = config.get_database().clone(); 43 | 44 | let force = match self.force { 45 | ForceType::Local => "local", 46 | ForceType::Remote => "remote", 47 | ForceType::None => "none", 48 | }; 49 | 50 | let message = Message { 51 | app_id: config.get_app_id(), 52 | directory_name: db.get_directory_name(), 53 | file_name: db.get_file_name(), 54 | endpoint: config.get_sync_server_endpoint(), 55 | force, 56 | }; 57 | 58 | if let Ok(value) = serde_wasm_bindgen::to_value(&message) { 59 | let _ = worker.post_message(&value); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /rocal_dev_server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rocal-dev-server" 3 | version = "0.1.1" 4 | edition = "2021" 5 | 6 | authors = ["Yoshiki Sashiyama "] 7 | description = "Dev server for Rocal - Full-Stack WASM framework" 8 | license = "MIT" 9 | homepage = "https://github.com/rocal-dev/rocal" 10 | repository = "https://github.com/rocal-dev/rocal" 11 | readme = "README.md" 12 | keywords = ["local-first", "web-framework", "wasm", "web"] 13 | 14 | [dependencies] 15 | -------------------------------------------------------------------------------- /rocal_dev_server/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /rocal_dev_server/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::io::prelude::*; 2 | use std::{env, fs}; 3 | use std::{ 4 | net::{TcpListener, TcpStream}, 5 | thread, 6 | }; 7 | 8 | use models::content_type::ContentType; 9 | use utils::color::Color; 10 | 11 | mod models; 12 | mod utils; 13 | 14 | pub fn run(port: Option<&str>) { 15 | let port = if let Some(port) = port { port } else { "3000" }; 16 | 17 | let listener = TcpListener::bind(&format!("127.0.0.1:{}", &port)).expect(&format!( 18 | "{}", 19 | Color::Red.text(&format!("Failed to listen on 127.0.0.1:{}.", &port)) 20 | )); 21 | 22 | let current_dir = match env::current_dir() { 23 | Ok(path) => path, 24 | Err(e) => { 25 | eprintln!( 26 | "{}", 27 | Color::Red.text(&format!( 28 | "[ERROR] the current directory could not be found: {}", 29 | e 30 | )) 31 | ); 32 | return; 33 | } 34 | }; 35 | println!( 36 | "Serving path {}", 37 | Color::Cyan.text(¤t_dir.to_string_lossy()) 38 | ); 39 | println!("Available at:"); 40 | println!( 41 | " {}", 42 | Color::Green.text(&format!("http://127.0.0.1:{}", &port)) 43 | ); 44 | println!("\nQuit by pressing CTRL-C"); 45 | 46 | for stream in listener.incoming() { 47 | match stream { 48 | Ok(stream) => { 49 | thread::spawn(|| { 50 | handle_connection(stream); 51 | }); 52 | } 53 | Err(e) => { 54 | eprintln!( 55 | "{}", 56 | Color::Red.text(&format!("[ERROR] Connection failed: {}", e)) 57 | ); 58 | } 59 | } 60 | } 61 | } 62 | 63 | fn handle_connection(mut stream: TcpStream) { 64 | let mut buffer = [0; 1024]; 65 | 66 | match stream.read(&mut buffer) { 67 | Ok(_) => { 68 | let request = String::from_utf8_lossy(&buffer[..]); 69 | 70 | if request.is_empty() { 71 | let res = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n"; 72 | stream.write_all(res.as_bytes()).expect(&format!( 73 | "{}", 74 | Color::Red.text("[ERROR] Failed to return 400 Bad Request") 75 | )); 76 | return; 77 | } 78 | 79 | let request: Vec<&str> = request.lines().collect(); 80 | let request = if let Some(request) = request.first() { 81 | request 82 | } else { 83 | let res = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n"; 84 | stream.write_all(res.as_bytes()).expect(&format!( 85 | "{}", 86 | Color::Red.text("[ERROR] Failed to return 400 Bad Request") 87 | )); 88 | return; 89 | }; 90 | let request: Vec<&str> = request.split(' ').collect(); 91 | let resource = if let Some(resource) = request.get(1) { 92 | resource 93 | } else { 94 | let res = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n"; 95 | stream.write_all(res.as_bytes()).expect(&format!( 96 | "{}", 97 | Color::Red.text("[ERROR] Failed to return 400 Bad Request") 98 | )); 99 | return; 100 | }; 101 | let resource: Vec<&str> = resource.split('?').collect(); 102 | let resource = if let Some(resource) = resource.get(0) { 103 | resource 104 | } else { 105 | let res = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n"; 106 | stream.write_all(res.as_bytes()).expect(&format!( 107 | "{}", 108 | Color::Red.text("[ERROR] Failed to return 400 Bad Request") 109 | )); 110 | return; 111 | }; 112 | 113 | let file_path = if 1 < resource.len() { 114 | let resource = &resource[1..]; 115 | resource 116 | } else { 117 | "index.html" 118 | }; 119 | 120 | let contents = if let Ok(contents) = fs::read(&format!("./{}", file_path)) { 121 | println!("[INFO] {} could be found", resource); 122 | contents 123 | } else { 124 | eprintln!( 125 | "{}", 126 | Color::Red.text(&format!("[ERROR] {} could not be found", resource)) 127 | ); 128 | let res = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n"; 129 | stream.write_all(res.as_bytes()).expect(&format!( 130 | "{}", 131 | Color::Red.text("[ERROR] Failed to return 400 Bad Request") 132 | )); 133 | return; 134 | }; 135 | 136 | let content_type = ContentType::new(file_path); 137 | 138 | let response_header = format!( 139 | "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: {}\r\nCross-Origin-Opener-Policy: same-origin\r\nCross-Origin-Embedder-Policy: require-corp\r\n\r\n", 140 | contents.len(), 141 | content_type.get_content_type() 142 | ); 143 | 144 | stream 145 | .write_all(response_header.as_bytes()) 146 | .expect(&format!( 147 | "{}", 148 | Color::Red.text("[ERROR] Failed to send header") 149 | )); 150 | 151 | stream.write_all(&contents).expect(&format!( 152 | "{}", 153 | Color::Red.text("[ERROR] Failed to send file contents") 154 | )); 155 | } 156 | Err(e) => { 157 | eprintln!( 158 | "{}", 159 | Color::Red.text(&format!("[ERROR] Failed to read from connection: {}", e)) 160 | ); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /rocal_dev_server/src/models.rs: -------------------------------------------------------------------------------- 1 | pub mod content_type; 2 | -------------------------------------------------------------------------------- /rocal_dev_server/src/models/content_type.rs: -------------------------------------------------------------------------------- 1 | pub struct ContentType { 2 | file_name: String, 3 | } 4 | 5 | impl ContentType { 6 | pub fn new(file_name: &str) -> Self { 7 | Self { 8 | file_name: file_name.to_string(), 9 | } 10 | } 11 | 12 | pub fn get_content_type(&self) -> &str { 13 | if self.file_name.contains(".js") || self.file_name.contains(".mjs") { 14 | "application/javascript; charset=UTF-8" 15 | } else if self.file_name.contains(".html") { 16 | "text/html; charset=UTF-8" 17 | } else if self.file_name.contains(".wasm") { 18 | "application/wasm" 19 | } else if self.file_name.contains(".css") { 20 | "text/css; charset=UTF-8" 21 | } else if self.file_name.contains(".jpg") || self.file_name.contains(".jpeg") { 22 | "image/jpeg" 23 | } else if self.file_name.contains(".png") { 24 | "image/png" 25 | } else if self.file_name.contains(".gif") { 26 | "image/gif" 27 | } else if self.file_name.contains(".ico") { 28 | "image/x-icon" 29 | } else if self.file_name.contains(".svg") { 30 | "image/svg+xml" 31 | } else if self.file_name.contains(".webp") { 32 | "image/webp" 33 | } else if self.file_name.contains(".avif") { 34 | "image/avif" 35 | } else if self.file_name.contains(".apng") { 36 | "image/apng" 37 | } else if self.file_name.contains(".bmp") { 38 | "image/bmp" 39 | } else if self.file_name.contains(".heic") { 40 | "image/heic" 41 | } else { 42 | "application/octet-stream" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /rocal_dev_server/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub mod color; 2 | -------------------------------------------------------------------------------- /rocal_dev_server/src/utils/color.rs: -------------------------------------------------------------------------------- 1 | #[allow(dead_code)] 2 | #[derive(Clone, Copy)] 3 | pub enum Color { 4 | Black, 5 | Red, 6 | Green, 7 | Yellow, 8 | Blue, 9 | Magenta, 10 | Cyan, 11 | White, 12 | Gray, 13 | BrightRed, 14 | BrightGreen, 15 | BrigthYellow, 16 | BrightBlue, 17 | BrightMagenta, 18 | BrightCyan, 19 | BrightWhite, 20 | } 21 | 22 | #[allow(dead_code)] 23 | impl Color { 24 | pub fn reset() -> &'static str { 25 | "\x1b[0m" 26 | } 27 | 28 | pub fn text(&self, t: &str) -> String { 29 | format!("{}{}\x1b[0m", self.code(), t) 30 | } 31 | 32 | pub fn code(&self) -> &str { 33 | match self { 34 | Color::Black => "\x1b[30m", 35 | Color::Red => "\x1b[31m", 36 | Color::Green => "\x1b[32m", 37 | Color::Yellow => "\x1b[33m", 38 | Color::Blue => "\x1b[34m", 39 | Color::Magenta => "\x1b[35m", 40 | Color::Cyan => "\x1b[36m", 41 | Color::White => "\x1b[37m", 42 | Color::Gray => "\x1b[90m", 43 | Color::BrightRed => "\x1b[91m", 44 | Color::BrightGreen => "\x1b[92m", 45 | Color::BrigthYellow => "\x1b[93m", 46 | Color::BrightBlue => "\x1b[94m", 47 | Color::BrightMagenta => "\x1b[95m", 48 | Color::BrightCyan => "\x1b[96m", 49 | Color::BrightWhite => "\x1b[97m", 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /rocal_macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rocal-macro" 3 | version = "0.3.4" 4 | edition = "2021" 5 | 6 | authors = ["Yoshiki Sashiyama "] 7 | description = "Macros for Rocal - Full-Stack WASM framework" 8 | license = "MIT" 9 | homepage = "https://github.com/rocal-dev/rocal" 10 | repository = "https://github.com/rocal-dev/rocal" 11 | readme = "README.md" 12 | keywords = ["local-first", "web-framework", "macro", "wasm", "web"] 13 | 14 | [dependencies] 15 | quote = "1.0" 16 | syn = { version = "2.0", features = ["extra-traits"] } 17 | proc-macro2 = "1.0" 18 | rocal-core = { version = "0.3", optional = true } 19 | rocal-ui = { version = "0.1", optional = true } 20 | 21 | [lib] 22 | proc-macro = true 23 | 24 | [features] 25 | default = ["full"] 26 | full = ["rocal-core", "rocal-ui"] 27 | ui = ["rocal-ui"] -------------------------------------------------------------------------------- /rocal_macro/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /rocal_macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | use proc_macro::TokenStream; 4 | use rocal_ui::build_ui; 5 | 6 | #[cfg(feature = "full")] 7 | use rocal_core::{build_action, build_config, build_route, run_migration, start_app}; 8 | 9 | /// This attribute macro should be used when you create an entrypoint of a Rocal application. 10 | /// 11 | /// ```rust 12 | /// use rocal::config; 13 | /// 14 | /// #[rocal::main] 15 | /// fn app() {} 16 | /// ``` 17 | /// 18 | #[cfg(feature = "full")] 19 | #[proc_macro_attribute] 20 | pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { 21 | start_app(item.into()).into() 22 | } 23 | 24 | /// This attribute macro should be used when you create an action of a controller. 25 | /// 26 | /// ```rust 27 | /// use crate::views::root_view::RootView; 28 | /// use rocal::rocal_core::traits::{Controller, SharedRouter}; 29 | /// 30 | /// pub struct RootController { 31 | /// router: SharedRouter, 32 | /// view: RootView, 33 | /// } 34 | /// 35 | /// impl Controller for RootController { 36 | /// type View = RootView; 37 | /// fn new(router: SharedRouter, view: Self::View) -> Self { 38 | /// RootController { router, view } 39 | /// } 40 | /// } 41 | /// 42 | /// impl RootController { 43 | /// #[rocal::action] 44 | /// pub fn index(&self) { 45 | /// self.view.index(); 46 | /// } 47 | /// } 48 | /// ``` 49 | /// 50 | #[cfg(feature = "full")] 51 | #[proc_macro_attribute] 52 | pub fn action(_: TokenStream, item: TokenStream) -> TokenStream { 53 | build_action(item.into()).into() 54 | } 55 | 56 | /// This function-like macro sets up application routing. 57 | /// 58 | /// ```rust 59 | /// route! { 60 | /// get "/" => { controller: RootController , action: index , view: RootView }, 61 | /// post "/users" => { controller: UsersController, action: create, view: UserView} 62 | /// } 63 | /// 64 | /// ``` 65 | #[cfg(feature = "full")] 66 | #[proc_macro] 67 | pub fn route(item: TokenStream) -> TokenStream { 68 | build_route(item.into()).into() 69 | } 70 | 71 | /// This function-like macro makes `static CONFIG` which contains app_id, a connection of an embedded database, and sync server endpoint URL. 72 | /// 73 | /// ```rust 74 | /// config! { 75 | /// app_id: "a917e367-3484-424d-9302-f09bdaf647ae" , 76 | /// sync_server_endpoint: "http://127.0.0.1:3000/presigned-url" , 77 | /// database_directory_name: "local" , 78 | /// database_file_name: "local.sqlite3" 79 | /// } 80 | /// ``` 81 | #[cfg(feature = "full")] 82 | #[proc_macro] 83 | pub fn config(item: TokenStream) -> TokenStream { 84 | build_config(item.into()).into() 85 | } 86 | 87 | /// This function-like macro allows users to set a path where migration files are supposed to be. 88 | /// 89 | /// ```rust 90 | /// migrate!("db/migrations"); 91 | /// ``` 92 | #[cfg(feature = "full")] 93 | #[proc_macro] 94 | pub fn migrate(item: TokenStream) -> TokenStream { 95 | run_migration(item.into()).into() 96 | } 97 | 98 | /// This function-like macro generates code to produce HTML string. 99 | /// 100 | /// ```rust 101 | /// view! { 102 | ///
103 | ///

{"Hello, World!"}

104 | /// if true { 105 | ///

{"This is how you can use this macro"}

106 | /// } else { 107 | ///

{"Even you can use if-else condition control"}

108 | /// } 109 | /// for item in items { 110 | ///

{{ item.id }}{"Maybe, you also want to use for-loop."}

111 | /// } 112 | ///
113 | /// } 114 | /// ``` 115 | #[cfg(any(feature = "full", feature = "ui"))] 116 | #[proc_macro] 117 | pub fn view(item: TokenStream) -> TokenStream { 118 | build_ui(item.into()).into() 119 | } 120 | -------------------------------------------------------------------------------- /rocal_ui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rocal-ui" 3 | version = "0.1.9" 4 | edition = "2021" 5 | 6 | authors = ["Yoshiki Sashiyama "] 7 | description = "UI for Rocal - Full-Stack WASM framework" 8 | license = "MIT" 9 | homepage = "https://github.com/rocal-dev/rocal" 10 | repository = "https://github.com/rocal-dev/rocal" 11 | readme = "README.md" 12 | keywords = ["template-engine", "web-framework", "macro", "wasm", "web"] 13 | 14 | [dependencies] 15 | quote = "1.0" 16 | syn = { version = "2.0", features = ["full", "extra-traits"] } 17 | proc-macro2 = "1.0" -------------------------------------------------------------------------------- /rocal_ui/README.md: -------------------------------------------------------------------------------- 1 | # Rocal UI - A simple template engine with Rust 2 | 3 | ![logo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a2eofyw92dwrvbuorvik.png) 4 | 5 | Although this template engine is basically intended to use with Rocal framework to craft views, it can be used anywhere with Rust. 6 | 7 | Let's begin with syntax of Rocal UI. Here is a simple example including variables, if-else control, and for-loop control. 8 | 9 | ```rust ,ignore 10 | view! { 11 |
12 |

{ title }

13 | 14 | if user.id <= 10 { 15 |

{ "You are an early user!" }

16 | { "Click here to get rewards!" } 17 | } else if user.id <= 20 { 18 |

{ "You are kind of an early user." }

19 | { "Check it out for your reward." } 20 | } else { 21 |

{ "You are a regular user." }

22 | } 23 | 24 |
25 | 26 | 31 |
32 | } 33 | ``` 34 | 35 | It's straight forward, isn't it? 36 | - `{ variable }`: You can set a variable that returns `&str` and it will be sanitized HTML safe. 37 | - `{{ variable }}` : You can set a variable that returns `&str` but it will NOT sanitized HTML safe. So maybe you could use it to embed a safe HTML. 38 | - `if-else` : you can utilize `if-else` even `else-if` as below 39 | ```rust ,ignore 40 | if user.id <= 10 { 41 |

{ "You are an early user!" }

42 | { "Click here to get rewards!" } 43 | } else if user.id <= 20 { 44 |

{ "You are kind of an early user." }

45 | { "Check it out for your reward." } 46 | } else { 47 |

{ "You are a regular user." }

48 | } 49 | ``` 50 | - `for-in`: This can be used as same as Rust syntax 51 | ```rust,ignore 52 | for article in articles { 53 |
  • {{ article.title }}
  • 54 | } 55 | ``` 56 | 57 | ## Advanced use 58 | `view! {}` produces HTML string technically, so you can embed view! in another view! like using it as a partial template. 59 | 60 | ```rust ,ignore 61 | let button = view! { 62 | 65 | }; 66 | 67 | view! { 68 |
    69 | 70 | {{ &button }} 71 |
    72 | } 73 | ``` 74 | 75 | On top of that, so `{{ variable }}` can take any expression that emits `&str` of Rust, if you want to do string interpolation, you can write like `{{ &format!("Hi, {}", name) }}`. 76 | 77 | ## How to install 78 | 79 | ```toml 80 | // Cargo.toml 81 | rocal-macro = { version = [LATEST_VERSION], default-features = false, features = ["ui"] } 82 | ``` 83 | 84 | OR 85 | 86 | 87 | (if you have not had `cargo` yet, follow [this link](https://doc.rust-lang.org/cargo/getting-started/installation.html) first) 88 | ```bash 89 | $ cargo install rocal --features="cli" 90 | $ rocal new -n yourapp 91 | ``` 92 | Then in `yourapp/src/templates/root_template.rs`, you could see an example of usage of Rocal UI 93 | 94 | ## Links 95 | - [GitHub](https://github.com/rocal-dev/rocal) I'd appreciate it if you could star it. 96 | - [Download](https://crates.io/crates/rocal-ui) 97 | -------------------------------------------------------------------------------- /rocal_ui/src/data_types.rs: -------------------------------------------------------------------------------- 1 | pub mod queue; 2 | pub mod stack; 3 | -------------------------------------------------------------------------------- /rocal_ui/src/data_types/queue.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | pub struct Queue 5 | where 6 | T: Clone, 7 | { 8 | start: Option>>>, 9 | end: Option>>>, 10 | pub len: u64, 11 | } 12 | 13 | impl Queue 14 | where 15 | T: Clone, 16 | { 17 | pub fn new() -> Self { 18 | Self { 19 | start: None, 20 | end: None, 21 | len: 0, 22 | } 23 | } 24 | 25 | pub fn enqueue(&mut self, node: T) { 26 | let node = Rc::new(RefCell::new(LinkedList::new(node))); 27 | 28 | if self.end.is_some() { 29 | self.end.clone().unwrap().borrow_mut().right = Some(node.clone()); 30 | node.clone().borrow_mut().left = self.end.clone(); 31 | } else { 32 | self.start = Some(node.clone()); 33 | } 34 | 35 | self.end = Some(node.clone()); 36 | self.len += 1; 37 | } 38 | 39 | pub fn dequeue(&mut self) -> Option { 40 | let mut node: Option>>> = None; 41 | 42 | if self.len > 0 { 43 | if let Some(start) = self.start.clone() { 44 | node = Some(start); 45 | self.start = node.clone().unwrap().borrow().right.clone(); 46 | node.clone().unwrap().borrow_mut().left = None; 47 | node.clone().unwrap().borrow_mut().right = None; 48 | self.len -= 1; 49 | } 50 | } else { 51 | self.start = None; 52 | self.end = None; 53 | } 54 | 55 | match node { 56 | Some(node) => Some(node.borrow().get_value().to_owned()), 57 | None => None, 58 | } 59 | } 60 | 61 | pub fn peek(&self) -> Option { 62 | if let Some(end) = self.end.clone() { 63 | Some(end.borrow().get_value().to_owned()) 64 | } else { 65 | None 66 | } 67 | } 68 | } 69 | 70 | struct LinkedList 71 | where 72 | T: Clone, 73 | { 74 | right: Option>>>, 75 | left: Option>>>, 76 | value: T, 77 | } 78 | 79 | impl LinkedList 80 | where 81 | T: Clone, 82 | { 83 | pub fn new(value: T) -> Self { 84 | Self { 85 | right: None, 86 | left: None, 87 | value, 88 | } 89 | } 90 | 91 | pub fn get_value(&self) -> &T { 92 | &self.value 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /rocal_ui/src/data_types/stack.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | #[derive(Debug)] 4 | pub struct Stack 5 | where 6 | T: Clone, 7 | { 8 | top: Option>>>, 9 | pub len: u64, 10 | } 11 | 12 | impl Stack 13 | where 14 | T: Clone, 15 | { 16 | pub fn new() -> Self { 17 | Self { top: None, len: 0 } 18 | } 19 | 20 | pub fn push(&mut self, node: T) { 21 | let node = Rc::new(RefCell::new(LinkedList { 22 | next: self.top.clone(), 23 | value: node, 24 | })); 25 | 26 | self.top = Some(node.clone()); 27 | self.len += 1; 28 | } 29 | 30 | pub fn pop(&mut self) -> Option { 31 | if let Some(top) = self.top.clone() { 32 | self.top = top.borrow().next.clone(); 33 | self.len -= 1; 34 | Some(top.borrow().get_value().to_owned()) 35 | } else { 36 | None 37 | } 38 | } 39 | 40 | pub fn peek(&self) -> Option { 41 | if let Some(top) = self.top.clone() { 42 | Some(top.borrow().get_value().to_owned()) 43 | } else { 44 | None 45 | } 46 | } 47 | } 48 | 49 | #[derive(Debug)] 50 | struct LinkedList 51 | where 52 | T: Clone, 53 | { 54 | next: Option>>>, 55 | value: T, 56 | } 57 | 58 | impl LinkedList 59 | where 60 | T: Clone, 61 | { 62 | pub fn get_value(&self) -> &T { 63 | &self.value 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /rocal_ui/src/enums.rs: -------------------------------------------------------------------------------- 1 | pub mod html_element; 2 | -------------------------------------------------------------------------------- /rocal_ui/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | pub mod data_types; 4 | pub mod enums; 5 | pub mod html; 6 | 7 | use html::to_tokens::ToTokens; 8 | use proc_macro2::TokenStream; 9 | 10 | pub fn build_ui(item: TokenStream) -> TokenStream { 11 | match html::parse(item.into()) { 12 | Ok(html) => html.to_token_stream().into(), 13 | Err(err) => err.into_compile_error().into(), 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /rocal_ui/tests/test_html_to_tokens.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | //! Integration tests for the Html → TokenStream “tokenizer”. 4 | //! 5 | //! These tests 6 | //! 1. parse a DSL snippet into an `Html` AST with `parse()` 7 | //! 2. turn that AST into Rust tokens via `Tokenizer::to_token_stream()` 8 | //! 3. assert that the generated token text contains the expected bits 9 | //! (opening/closing tags, attributes, flow-control keywords, …) 10 | 11 | use quote::quote; 12 | use rocal_ui::html::{parse, to_tokens::ToTokens}; 13 | 14 | /// Convenience: parse and immediately stringify the generated tokens. 15 | fn gen(src: proc_macro2::TokenStream) -> String { 16 | let ast = parse(src).expect("parser should succeed"); 17 | ast.to_token_stream().to_string() 18 | } 19 | 20 | #[test] 21 | fn simple_div_and_text() { 22 | let out = gen(quote! {
    { "Hi" }
    }); 23 | 24 | assert!(out.contains("let mut html")); 25 | assert!(out.contains("div")); 26 | assert!(out.contains("Hi")); 27 | assert!(out.contains("")); 28 | } 29 | 30 | #[test] 31 | fn simple_button_and_text() { 32 | let out = gen( 33 | quote! { }, 34 | ); 35 | 36 | assert!(out.contains("Submit")); 37 | } 38 | 39 | #[test] 40 | fn void_tag_br_inside_paragraph() { 41 | let out = gen(quote! {

    { "Break" }
    { "next" }

    }); 42 | 43 | assert!(out.contains("p")); 44 | assert!(out.contains("br")); 45 | assert!(out.contains("next")); 46 | assert!(out.contains("

    ")); 47 | } 48 | 49 | #[test] 50 | fn attributes_render_correctly() { 51 | let out = gen(quote! {
    }); 52 | 53 | assert!(out.contains(r#""section""#)); 54 | assert!(out.contains(r#"main"#)); 55 | assert!(out.contains("")); 56 | } 57 | 58 | #[test] 59 | fn nested_headers_and_paragraph() { 60 | let out = gen(quote! { 61 |
    62 |

    { "Hello, world!" }

    63 |

    64 |

    { "Hey, mate!" }

    65 |

    66 |
    67 | }); 68 | 69 | for needle in [ 70 | r#"div"#, 71 | r#"class"#, 72 | r#""section""#, 73 | r#"h1"#, 74 | r#"class"#, 75 | r#""title""#, 76 | r#"h2"#, 77 | r#"class"#, 78 | r#""body""#, 79 | r#"p"#, 80 | r#"id"#, 81 | r#""item""#, 82 | "", 83 | "", 84 | "", 85 | ] { 86 | assert!( 87 | out.contains(needle), 88 | "generated tokens should contain `{needle}`" 89 | ); 90 | } 91 | } 92 | 93 | #[test] 94 | fn if_else_chain_in_html() { 95 | let out = gen(quote! { 96 |
    97 | if x == 1 || x == 2 { 98 | { "x is 1 or 2" } 99 | } else if x == 3 { 100 | { "x is 3" } 101 | } else { 102 | if y == 1 { 103 | { "y is 1 but x is unknown" } 104 | } else { 105 | { "x and y are unknown" } 106 | } 107 | } 108 |
    109 | }); 110 | 111 | assert!(out.contains("if x == 1 || x == 2")); 112 | assert!(out.contains("else if x == 3")); 113 | assert!(out.contains("else {")); 114 | assert!(out.contains("span")); 115 | } 116 | 117 | #[test] 118 | fn variable_interpolation_emits_plain_ident() { 119 | let out = gen(quote! {

    {{ name }}

    }); 120 | 121 | assert!(out.contains("push_str")); 122 | assert!(out.contains("(name)")); 123 | } 124 | 125 | #[test] 126 | fn for_loop_generates_rust_for() { 127 | let out = gen(quote! { for item in items {
  • {{ item }}
  • } }); 128 | 129 | assert!(out.contains("for item in items")); 130 | assert!(out.contains("li")); 131 | assert!(out.contains("")); 132 | } 133 | 134 | #[test] 135 | fn doc_type_declaration() { 136 | let out = gen(quote! { }); 137 | 138 | assert!(out.contains("")); 139 | } 140 | 141 | #[test] 142 | fn async_and_defer_in_script_tag() { 143 | let out = gen( 144 | quote! { }, 145 | ); 146 | 147 | assert!(out.contains("async")); 148 | assert!(out.contains("defer")); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /rocal_ui/tests/test_queue.rs: -------------------------------------------------------------------------------- 1 | mod tests { 2 | use rocal_ui::data_types::queue::Queue; 3 | 4 | #[test] 5 | fn test_queue_with_primitive_type() { 6 | let mut queue: Queue = Queue::new(); 7 | 8 | queue.enqueue(1); 9 | queue.enqueue(2); 10 | queue.enqueue(3); 11 | 12 | assert_eq!(queue.dequeue(), Some(1)); 13 | assert_eq!(queue.dequeue(), Some(2)); 14 | 15 | queue.enqueue(4); 16 | 17 | assert_eq!(queue.dequeue(), Some(3)); 18 | assert_eq!(queue.dequeue(), Some(4)); 19 | assert_eq!(queue.dequeue(), None); 20 | 21 | queue.enqueue(5); 22 | assert_eq!(queue.dequeue(), Some(5)); 23 | 24 | assert_eq!(queue.dequeue(), None); 25 | } 26 | 27 | #[test] 28 | fn test_queue_with_obj() { 29 | let mut queue: Queue = Queue::new(); 30 | 31 | queue.enqueue(Obj { id: 1 }); 32 | queue.enqueue(Obj { id: 2 }); 33 | queue.enqueue(Obj { id: 3 }); 34 | 35 | let obj1 = queue.dequeue(); 36 | let obj2 = queue.dequeue(); 37 | let obj3 = queue.dequeue(); 38 | let obj4 = queue.dequeue(); 39 | 40 | assert!(obj1.is_some()); 41 | assert!(obj2.is_some()); 42 | assert!(obj3.is_some()); 43 | assert_ne!(obj4.is_some(), true); 44 | } 45 | 46 | #[derive(Clone)] 47 | struct Obj { 48 | #[allow(dead_code)] 49 | id: u32, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rocal_ui/tests/test_stack.rs: -------------------------------------------------------------------------------- 1 | mod tests { 2 | use rocal_ui::data_types::stack::Stack; 3 | 4 | #[test] 5 | fn test_stack_with_primitive_type() { 6 | let mut stack: Stack = Stack::new(); 7 | 8 | stack.push(1); 9 | stack.push(2); 10 | stack.push(3); 11 | 12 | assert_eq!(stack.peek(), Some(3)); 13 | assert_eq!(stack.pop(), Some(3)); 14 | assert_eq!(stack.pop(), Some(2)); 15 | assert_eq!(stack.pop(), Some(1)); 16 | assert_eq!(stack.pop(), None); 17 | assert_eq!(stack.pop(), None); 18 | 19 | stack.push(4); 20 | 21 | assert_eq!(stack.pop(), Some(4)); 22 | } 23 | 24 | #[test] 25 | fn test_stack_with_obj() { 26 | let mut stack: Stack = Stack::new(); 27 | 28 | stack.push(Obj(1)); 29 | stack.push(Obj(2)); 30 | stack.push(Obj(3)); 31 | 32 | if let Some(Obj(n)) = stack.pop() { 33 | assert_eq!(n, 3) 34 | } 35 | 36 | if let Some(Obj(n)) = stack.pop() { 37 | assert_eq!(n, 2) 38 | } 39 | 40 | if let Some(Obj(n)) = stack.pop() { 41 | assert_eq!(n, 1) 42 | } 43 | 44 | assert_eq!(stack.pop().is_none(), true); 45 | } 46 | 47 | #[derive(Clone)] 48 | struct Obj(u64); 49 | } 50 | --------------------------------------------------------------------------------