├── rocal
├── README.md
├── src
│ ├── main.rs
│ └── lib.rs
└── Cargo.toml
├── examples
├── simple_note
│ ├── public
│ │ ├── .keep
│ │ └── favicon.ico
│ ├── src
│ │ ├── models
│ │ │ ├── .keep
│ │ │ ├── note_id.rs
│ │ │ └── note.rs
│ │ ├── templates.rs
│ │ ├── view_models.rs
│ │ ├── models.rs
│ │ ├── views.rs
│ │ ├── controllers.rs
│ │ ├── views
│ │ │ ├── notes_view.rs
│ │ │ └── root_view.rs
│ │ ├── view_models
│ │ │ └── root_view_model.rs
│ │ ├── lib.rs
│ │ ├── controllers
│ │ │ ├── root_controller.rs
│ │ │ └── notes_controller.rs
│ │ └── templates
│ │ │ └── root_template.rs
│ ├── db
│ │ └── migrations
│ │ │ ├── .keep
│ │ │ └── 20250506055848-create-notes-table.sql
│ ├── .gitignore
│ ├── js
│ │ ├── sqlite3.wasm
│ │ ├── global.js
│ │ ├── db_query_worker.js
│ │ └── db_sync_worker.js
│ ├── Cargo.toml
│ ├── sw.js
│ └── index.html
├── hello_world
│ ├── src
│ │ ├── models
│ │ │ ├── .keep
│ │ │ └── sync_connection.rs
│ │ ├── models.rs
│ │ ├── repositories.rs
│ │ ├── views.rs
│ │ ├── templates.rs
│ │ ├── controllers.rs
│ │ ├── views
│ │ │ ├── root_view.rs
│ │ │ └── sync_connection_view.rs
│ │ ├── controllers
│ │ │ ├── root_controller.rs
│ │ │ └── sync_connections_controller.rs
│ │ ├── templates
│ │ │ ├── root_template.rs
│ │ │ └── sync_connection_edit_template.rs
│ │ ├── lib.rs
│ │ └── repositories
│ │ │ └── sync_connection_repository.rs
│ ├── db
│ │ └── migrations
│ │ │ ├── .keep
│ │ │ └── 202402090340_create_sync_connection_table.sql
│ ├── js
│ │ ├── sqlite3.wasm
│ │ ├── global.js
│ │ ├── db_query_worker.js
│ │ └── db_sync_worker.js
│ ├── index.html
│ ├── Cargo.toml
│ └── sw.js
└── self_checkout
│ ├── db
│ └── migrations
│ │ ├── .keep
│ │ ├── 202504121026_create_cart_table.sql
│ │ ├── 202504130331_create_sales_table.sql
│ │ └── 202504120810_create_product_table.sql
│ ├── src
│ ├── models
│ │ ├── .keep
│ │ ├── sales_log.rs
│ │ ├── product.rs
│ │ ├── sales_item.rs
│ │ ├── cart_item.rs
│ │ ├── sales.rs
│ │ └── flash_memory.rs
│ ├── views.rs
│ ├── controllers.rs
│ ├── repositories.rs
│ ├── templates.rs
│ ├── view_models.rs
│ ├── models.rs
│ ├── views
│ │ ├── empty_view.rs
│ │ ├── root_view.rs
│ │ └── sales_view.rs
│ ├── view_models
│ │ ├── sales_log_view_model.rs
│ │ ├── sales_item_view_model.rs
│ │ └── root_view_model.rs
│ ├── repositories
│ │ ├── product_repository.rs
│ │ ├── sales_repository.rs
│ │ └── cart_repository.rs
│ ├── controllers
│ │ ├── root_controller.rs
│ │ ├── carts_controller.rs
│ │ └── sales_controller.rs
│ ├── lib.rs
│ └── templates
│ │ ├── sales_log_template.rs
│ │ ├── sales_item_template.rs
│ │ └── root_template.rs
│ ├── js
│ ├── sqlite3.wasm
│ ├── global.js
│ ├── db_query_worker.js
│ └── db_sync_worker.js
│ ├── public
│ ├── favicon.ico
│ └── images
│ │ └── loading.gif
│ ├── Cargo.toml
│ ├── index.html
│ └── sw.js
├── rocal_cli
├── README.md
├── js
│ ├── sqlite3.wasm
│ ├── global.js
│ ├── db_query_worker.js
│ ├── sw.js
│ └── db_sync_worker.js
├── build.rs
├── src
│ ├── lib.rs
│ ├── generators
│ │ ├── public_generator.rs
│ │ ├── migration_generator.rs
│ │ ├── model_generator.rs
│ │ ├── gitignore_generator.rs
│ │ ├── template_generator.rs
│ │ ├── js_generator.rs
│ │ ├── lib_generator.rs
│ │ ├── entrypoint_generator.rs
│ │ ├── cargo_file_generator.rs
│ │ ├── view_generator.rs
│ │ └── controller_generator.rs
│ ├── commands.rs
│ ├── rocal_api_client
│ │ ├── payment_link.rs
│ │ ├── oob_code_response.rs
│ │ ├── create_payment_link.rs
│ │ ├── subdomain.rs
│ │ ├── user_refresh_token.rs
│ │ ├── send_password_reset_email.rs
│ │ ├── login_user.rs
│ │ ├── send_email_verification.rs
│ │ ├── registered_sync_server.rs
│ │ ├── create_user.rs
│ │ ├── subscription_status.rs
│ │ ├── user_login_token.rs
│ │ ├── create_app.rs
│ │ └── cancel_subscription.rs
│ ├── generators.rs
│ ├── response.rs
│ ├── commands
│ │ ├── utils
│ │ │ ├── refresh_user_token.rs
│ │ │ ├── project.rs
│ │ │ ├── open_link.rs
│ │ │ ├── color.rs
│ │ │ ├── list.rs
│ │ │ └── indicator.rs
│ │ ├── migrate.rs
│ │ ├── password.rs
│ │ ├── login.rs
│ │ ├── utils.rs
│ │ ├── build.rs
│ │ ├── register.rs
│ │ ├── sync_servers.rs
│ │ ├── unsubscribe.rs
│ │ └── subscribe.rs
│ └── token_manager.rs
├── seeds
│ └── root_template.rs
└── Cargo.toml
├── rocal_core
├── README.md
├── src
│ ├── enums.rs
│ ├── workers.rs
│ ├── utils.rs
│ ├── enums
│ │ └── request_method.rs
│ ├── route_handler.rs
│ ├── workers
│ │ └── db_sync_worker.rs
│ ├── migrator.rs
│ ├── parsed_action.rs
│ ├── database.rs
│ ├── configuration.rs
│ ├── traits.rs
│ └── router.rs
└── Cargo.toml
├── rocal_macro
├── README.md
├── Cargo.toml
└── src
│ └── lib.rs
├── rocal_dev_server
├── README.md
├── src
│ ├── utils.rs
│ ├── models.rs
│ ├── utils
│ │ └── color.rs
│ ├── models
│ │ └── content_type.rs
│ └── lib.rs
└── Cargo.toml
├── rocal_ui
├── src
│ ├── enums.rs
│ ├── data_types.rs
│ ├── lib.rs
│ └── data_types
│ │ ├── stack.rs
│ │ └── queue.rs
├── Cargo.toml
├── tests
│ ├── test_stack.rs
│ ├── test_queue.rs
│ └── test_html_to_tokens.rs
└── README.md
├── .gitattributes
├── Cargo.toml
├── .gitignore
├── MIT-LICENSE
└── README.md
/rocal/README.md:
--------------------------------------------------------------------------------
1 | ../README.md
--------------------------------------------------------------------------------
/examples/simple_note/public/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rocal_cli/README.md:
--------------------------------------------------------------------------------
1 | ../README.md
--------------------------------------------------------------------------------
/rocal_core/README.md:
--------------------------------------------------------------------------------
1 | ../README.md
--------------------------------------------------------------------------------
/rocal_macro/README.md:
--------------------------------------------------------------------------------
1 | ../README.md
--------------------------------------------------------------------------------
/examples/hello_world/src/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/simple_note/src/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rocal_dev_server/README.md:
--------------------------------------------------------------------------------
1 | ../README.md
--------------------------------------------------------------------------------
/examples/hello_world/db/migrations/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/self_checkout/db/migrations/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/self_checkout/src/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/simple_note/db/migrations/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/rocal_dev_server/src/utils.rs:
--------------------------------------------------------------------------------
1 | pub mod color;
2 |
--------------------------------------------------------------------------------
/rocal_ui/src/enums.rs:
--------------------------------------------------------------------------------
1 | pub mod html_element;
2 |
--------------------------------------------------------------------------------
/rocal_core/src/enums.rs:
--------------------------------------------------------------------------------
1 | pub mod request_method;
2 |
--------------------------------------------------------------------------------
/rocal_core/src/workers.rs:
--------------------------------------------------------------------------------
1 | pub mod db_sync_worker;
2 |
--------------------------------------------------------------------------------
/rocal_dev_server/src/models.rs:
--------------------------------------------------------------------------------
1 | pub mod content_type;
2 |
--------------------------------------------------------------------------------
/examples/hello_world/src/models.rs:
--------------------------------------------------------------------------------
1 | pub mod sync_connection;
2 |
--------------------------------------------------------------------------------
/examples/simple_note/src/templates.rs:
--------------------------------------------------------------------------------
1 | pub mod root_template;
2 |
--------------------------------------------------------------------------------
/examples/simple_note/src/view_models.rs:
--------------------------------------------------------------------------------
1 | pub mod root_view_model;
2 |
--------------------------------------------------------------------------------
/rocal_ui/src/data_types.rs:
--------------------------------------------------------------------------------
1 | pub mod queue;
2 | pub mod stack;
3 |
--------------------------------------------------------------------------------
/examples/simple_note/src/models.rs:
--------------------------------------------------------------------------------
1 | pub mod note;
2 | pub mod note_id;
3 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.js linguist-detectable=false
2 | *.mjs linguist-detectable=false
--------------------------------------------------------------------------------
/examples/hello_world/src/repositories.rs:
--------------------------------------------------------------------------------
1 | pub mod sync_connection_repository;
2 |
--------------------------------------------------------------------------------
/examples/simple_note/src/views.rs:
--------------------------------------------------------------------------------
1 | pub mod notes_view;
2 | pub mod root_view;
3 |
--------------------------------------------------------------------------------
/examples/hello_world/src/views.rs:
--------------------------------------------------------------------------------
1 | pub mod root_view;
2 | pub mod sync_connection_view;
3 |
--------------------------------------------------------------------------------
/examples/simple_note/src/controllers.rs:
--------------------------------------------------------------------------------
1 | pub mod notes_controller;
2 | pub mod root_controller;
3 |
--------------------------------------------------------------------------------
/rocal/src/main.rs:
--------------------------------------------------------------------------------
1 | #[tokio::main]
2 | async fn main() {
3 | rocal_cli::run().await;
4 | }
5 |
--------------------------------------------------------------------------------
/examples/simple_note/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | target
3 | pkg
4 | release
5 | release.tar.gz
6 | Cargo.lock
7 |
--------------------------------------------------------------------------------
/rocal_cli/js/sqlite3.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocal-dev/rocal/HEAD/rocal_cli/js/sqlite3.wasm
--------------------------------------------------------------------------------
/examples/hello_world/src/templates.rs:
--------------------------------------------------------------------------------
1 | pub mod root_template;
2 | pub mod sync_connection_edit_template;
3 |
--------------------------------------------------------------------------------
/examples/self_checkout/src/views.rs:
--------------------------------------------------------------------------------
1 | pub mod empty_view;
2 | pub mod root_view;
3 | pub mod sales_view;
4 |
--------------------------------------------------------------------------------
/examples/hello_world/src/controllers.rs:
--------------------------------------------------------------------------------
1 | pub mod root_controller;
2 | pub mod sync_connections_controller;
3 |
--------------------------------------------------------------------------------
/examples/hello_world/js/sqlite3.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocal-dev/rocal/HEAD/examples/hello_world/js/sqlite3.wasm
--------------------------------------------------------------------------------
/examples/simple_note/js/sqlite3.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocal-dev/rocal/HEAD/examples/simple_note/js/sqlite3.wasm
--------------------------------------------------------------------------------
/examples/self_checkout/js/sqlite3.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocal-dev/rocal/HEAD/examples/self_checkout/js/sqlite3.wasm
--------------------------------------------------------------------------------
/examples/self_checkout/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocal-dev/rocal/HEAD/examples/self_checkout/public/favicon.ico
--------------------------------------------------------------------------------
/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/repositories.rs:
--------------------------------------------------------------------------------
1 | pub mod cart_repository;
2 | pub mod product_repository;
3 | pub mod sales_repository;
4 |
--------------------------------------------------------------------------------
/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/simple_note/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocal-dev/rocal/HEAD/examples/simple_note/public/favicon.ico
--------------------------------------------------------------------------------
/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/public/images/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rocal-dev/rocal/HEAD/examples/self_checkout/public/images/loading.gif
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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_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_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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_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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/rocal_ui/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "rocal-ui"
3 | version = "0.1.11"
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"
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/hello_world/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | hello_world
7 |
8 |
9 |
16 |
17 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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_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"]
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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"
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
30 | }
31 |
32 | }
33 | }
34 |
35 | fn router(&self) -> SharedRouter {
36 | self.router.clone()
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/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_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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_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_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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_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 |
--------------------------------------------------------------------------------
/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 |
38 | }
39 | }
40 |
41 | fn router(&self) -> SharedRouter {
42 | self.router.clone()
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/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_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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/rocal_ui/README.md:
--------------------------------------------------------------------------------
1 | # Rocal UI - A simple template engine with Rust
2 |
3 | 
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 |
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 |
63 | Submit
64 |
65 | };
66 |
67 | view! {
68 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
52 |
53 | }
54 | }
55 |
56 |
57 |
58 | { "Total" }
59 | {{ &format!("${:.2}", data.get_total_price() )}}
60 |
61 |
62 |
65 |
66 |
67 | }
68 | }
69 |
70 | fn router(&self) -> SharedRouter {
71 | self.router.clone()
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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_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 |
--------------------------------------------------------------------------------
/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 |
28 |
29 |
30 |
48 |
49 | if let Some(note) = data.get_note() {
50 |
63 |
64 | {"Delete"}
65 |
66 | } else {
67 |
68 |
69 |
70 | {"Save changes"}
71 |
72 | }
73 |
74 |
75 |
76 | }
77 | }
78 |
79 | fn router(&self) -> SharedRouter {
80 | self.router.clone()
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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_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_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/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 |
--------------------------------------------------------------------------------
/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 |
57 | {{ label }}
58 |
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 |
--------------------------------------------------------------------------------
/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! { { "Submit" } },
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 | #[test]
152 | fn svg_image() {
153 | let out = gen(quote! {
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | });
165 |
166 | assert!(out.contains("svg"));
167 | assert!(out.contains("path"));
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------