├── .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 |
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