rust rhai ui components wip
This commit is contained in:
parent
5764950949
commit
1ea37e2e7f
11
devtools/reloadd/Cargo.toml
Normal file
11
devtools/reloadd/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "reloadd"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-tungstenite = "0.21"
|
||||
notify = "6"
|
||||
futures-util = "0.3"
|
97
devtools/reloadd/bin/reloadd.rs
Normal file
97
devtools/reloadd/bin/reloadd.rs
Normal file
@ -0,0 +1,97 @@
|
||||
use clap::{Parser};
|
||||
use notify::{RecursiveMode, Watcher, EventKind, recommended_watcher};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::process::{Command, Child, Stdio};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio_tungstenite::accept_async;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about)]
|
||||
struct Args {
|
||||
/// Paths to watch (like src/, templates/, scripts/)
|
||||
#[arg(short, long, value_name = "PATH", num_args = 1.., required = true)]
|
||||
watch: Vec<String>,
|
||||
|
||||
/// Command to run on change (like: -- run --example server)
|
||||
#[arg(last = true)]
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
let (tx, _) = broadcast::channel::<()>(10);
|
||||
let tx_ws = tx.clone();
|
||||
let server_process = Arc::new(Mutex::new(None::<Child>));
|
||||
|
||||
// Start WebSocket reload server
|
||||
tokio::spawn(start_websocket_server(tx_ws.clone()));
|
||||
|
||||
// Start watching files and restarting command
|
||||
let server_process_clone = Arc::clone(&server_process);
|
||||
let watch_paths = args.watch.clone();
|
||||
let cmd_args = args.command.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let mut watcher = recommended_watcher(move |res| {
|
||||
if let Ok(event) = res {
|
||||
if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)) {
|
||||
println!("📦 Change detected, restarting...");
|
||||
|
||||
// Kill previous process
|
||||
if let Some(mut child) = server_process_clone.lock().unwrap().take() {
|
||||
let _ = child.kill();
|
||||
}
|
||||
|
||||
// Run new process
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.args(&cmd_args);
|
||||
cmd.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit());
|
||||
let child = cmd.spawn().expect("Failed to spawn");
|
||||
|
||||
*server_process_clone.lock().unwrap() = Some(child);
|
||||
|
||||
// Notify browser
|
||||
let _ = tx.send(());
|
||||
}
|
||||
}
|
||||
}).expect("watcher failed");
|
||||
|
||||
// Add watches
|
||||
for path in &watch_paths {
|
||||
watcher.watch(path, RecursiveMode::Recursive).unwrap();
|
||||
}
|
||||
|
||||
loop {
|
||||
std::thread::sleep(std::time::Duration::from_secs(3600));
|
||||
}
|
||||
});
|
||||
|
||||
println!("🔁 Watching paths: {:?}", args.watch);
|
||||
println!("🌐 Connect browser to ws://localhost:35729");
|
||||
loop {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_websocket_server(tx: broadcast::Sender<()>) {
|
||||
let listener = TcpListener::bind("127.0.0.1:35729").await.unwrap();
|
||||
|
||||
while let Ok((stream, _)) = listener.accept().await {
|
||||
let tx = tx.clone();
|
||||
let mut rx = tx.subscribe();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let ws_stream = accept_async(stream).await.unwrap();
|
||||
let (mut write, _) = ws_stream.split();
|
||||
|
||||
while rx.recv().await.is_ok() {
|
||||
let _ = write.send(tokio_tungstenite::tungstenite::Message::Text("reload".into())).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
240
devtools/reloadd/src/main.rs
Normal file
240
devtools/reloadd/src/main.rs
Normal file
@ -0,0 +1,240 @@
|
||||
use clap::{Parser};
|
||||
use notify::{RecursiveMode, Watcher, EventKind, recommended_watcher};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::process::{Command, Child, Stdio};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio_tungstenite::accept_async;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use tokio::time::{sleep, Duration};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use std::net::TcpListener as StdTcpListener;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about)]
|
||||
struct Args {
|
||||
/// Paths to watch (like src/, templates/, scripts/)
|
||||
#[arg(short, long, value_name = "PATH", num_args = 1.., required = true)]
|
||||
watch: Vec<String>,
|
||||
|
||||
/// Command to run on change (like: -- run --example server)
|
||||
#[arg(last = true)]
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
async fn wait_for_server(port: u16) -> bool {
|
||||
let address = format!("localhost:{}", port);
|
||||
let mut retries = 0;
|
||||
|
||||
while retries < 10 {
|
||||
if let Ok(mut stream) = TcpStream::connect(&address).await {
|
||||
let _ = stream.write_all(b"GET / HTTP/1.1\r\n\r\n").await; // A dummy GET request
|
||||
println!("✅ Server is ready on {}!", address);
|
||||
return true;
|
||||
} else {
|
||||
retries += 1;
|
||||
println!("⏳ Waiting for server to be ready (Attempt {}/10)...", retries);
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!("❌ Server not ready after 10 attempts.");
|
||||
false
|
||||
}
|
||||
|
||||
// Check if a port is already in use
|
||||
fn is_port_in_use(port: u16) -> bool {
|
||||
StdTcpListener::bind(format!("127.0.0.1:{}", port)).is_err()
|
||||
}
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = Args::parse();
|
||||
println!("Command: {:?}", args.command);
|
||||
|
||||
// Check if server port is already in use
|
||||
let server_port = 8080; // Adjust as needed
|
||||
if is_port_in_use(server_port) {
|
||||
eprintln!("❌ Error: Port {} is already in use. Stop any running instances before starting a new one.", server_port);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Check if WebSocket port is already in use
|
||||
let ws_port = 35729;
|
||||
if is_port_in_use(ws_port) {
|
||||
eprintln!("❌ Error: WebSocket port {} is already in use. Stop any running instances before starting a new one.", ws_port);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let (tx, _) = broadcast::channel::<()>(10);
|
||||
let tx_ws = tx.clone();
|
||||
let server_process = Arc::new(Mutex::new(None::<Child>));
|
||||
|
||||
// Validate paths before starting the watcher
|
||||
let mut invalid_paths = Vec::new();
|
||||
for path in &args.watch {
|
||||
if !std::path::Path::new(path).exists() {
|
||||
invalid_paths.push(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if !invalid_paths.is_empty() {
|
||||
eprintln!("❌ Error: The following watch paths do not exist:");
|
||||
for path in invalid_paths {
|
||||
eprintln!(" - {}", path);
|
||||
}
|
||||
eprintln!("Please provide valid paths to watch.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Start WebSocket reload server
|
||||
match start_websocket_server(tx_ws.clone()).await {
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
eprintln!("❌ Failed to start WebSocket server: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 🚀 Run the server immediately
|
||||
{
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.args(&args.command);
|
||||
cmd.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit());
|
||||
match cmd.spawn() {
|
||||
Ok(child) => {
|
||||
*server_process.lock().unwrap() = Some(child);
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("❌ Failed to start initial process: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the server to be ready before triggering reloads
|
||||
if !wait_for_server(server_port).await {
|
||||
eprintln!("❌ Server failed to start properly.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
println!("🔁 Watching paths: {:?}", args.watch);
|
||||
println!("🌐 Connect browser to ws://localhost:{}", ws_port);
|
||||
|
||||
// Create a runtime handle for the watcher to use
|
||||
let rt_handle = tokio::runtime::Handle::current();
|
||||
|
||||
// Clone necessary values before moving into the watcher thread
|
||||
let watch_paths = args.watch.clone();
|
||||
let cmd_args = args.command.clone();
|
||||
let server_process_clone = Arc::clone(&server_process);
|
||||
let tx_clone = tx.clone();
|
||||
|
||||
// Create a dedicated thread for the file watcher
|
||||
let watcher_thread = std::thread::spawn(move || {
|
||||
// Create a new watcher
|
||||
let mut watcher = match recommended_watcher(move |res: notify::Result<notify::Event>| {
|
||||
if let Ok(event) = res {
|
||||
if matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)) {
|
||||
println!("📦 Change detected, restarting...");
|
||||
|
||||
// Kill previous process
|
||||
if let Some(mut child) = server_process_clone.lock().unwrap().take() {
|
||||
let _ = child.kill();
|
||||
}
|
||||
|
||||
// Run new process
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.args(&cmd_args);
|
||||
cmd.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit());
|
||||
|
||||
match cmd.spawn() {
|
||||
Ok(child) => {
|
||||
*server_process_clone.lock().unwrap() = Some(child);
|
||||
|
||||
// Use the runtime handle to spawn a task
|
||||
let tx = tx_clone.clone();
|
||||
rt_handle.spawn(async move {
|
||||
if wait_for_server(server_port).await {
|
||||
// Notify browser to reload
|
||||
let _ = tx.send(());
|
||||
}
|
||||
});
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("❌ Failed to spawn process: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Err(e) = res {
|
||||
eprintln!("❌ Watch error: {}", e);
|
||||
}
|
||||
}) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
eprintln!("❌ Failed to create watcher: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Add watches
|
||||
for path in &watch_paths {
|
||||
match watcher.watch(path.as_ref(), RecursiveMode::Recursive) {
|
||||
Ok(_) => println!("👁️ Watching path: {}", path),
|
||||
Err(e) => {
|
||||
eprintln!("❌ Failed to watch path '{}': {}", path, e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the thread alive
|
||||
loop {
|
||||
std::thread::sleep(std::time::Duration::from_secs(3600));
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for the watcher thread to finish (it shouldn't unless there's an error)
|
||||
match watcher_thread.join() {
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
eprintln!("❌ Watcher thread panicked: {:?}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_websocket_server(tx: broadcast::Sender<()>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:35729").await?;
|
||||
println!("WebSocket server started on ws://localhost:35729");
|
||||
|
||||
// Spawn a task to handle WebSocket connections
|
||||
tokio::spawn(async move {
|
||||
while let Ok((stream, addr)) = listener.accept().await {
|
||||
println!("New WebSocket connection from: {}", addr);
|
||||
let tx = tx.clone();
|
||||
let mut rx = tx.subscribe();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match accept_async(stream).await {
|
||||
Ok(ws_stream) => {
|
||||
let (mut write, _) = ws_stream.split();
|
||||
|
||||
while rx.recv().await.is_ok() {
|
||||
if let Err(e) = write.send(tokio_tungstenite::tungstenite::Message::Text("reload".into())).await {
|
||||
eprintln!("❌ Error sending reload message to client {}: {}", addr, e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("❌ Failed to accept WebSocket connection from {}: {}", addr, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
251
examples/calendar/backend/Cargo.lock
generated
Normal file
251
examples/calendar/backend/Cargo.lock
generated
Normal file
@ -0,0 +1,251 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"const-random",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
|
||||
|
||||
[[package]]
|
||||
name = "calendar_backend"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rhai",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||
dependencies = [
|
||||
"const-random-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random-macro"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.171"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bitflags",
|
||||
"instant",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"rhai_codegen",
|
||||
"smallvec",
|
||||
"smartstring",
|
||||
"thin-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_codegen"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
|
||||
|
||||
[[package]]
|
||||
name = "smartstring"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"static_assertions",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thin-vec"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
11
examples/calendar/backend/Cargo.toml
Normal file
11
examples/calendar/backend/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "calendar_backend"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "rhai_engine"
|
||||
path = "main.rs"
|
||||
|
||||
[dependencies]
|
||||
rhai = "1.12.0"
|
31
examples/calendar/backend/data/calendars.json
Normal file
31
examples/calendar/backend/data/calendars.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"calendars": [
|
||||
{
|
||||
"id": "cal1",
|
||||
"name": "Work Calendar",
|
||||
"owner_id": "user1",
|
||||
"description": "Main work calendar for team coordination",
|
||||
"color": "#4285F4",
|
||||
"shared_with": ["user2", "user3", "user4"],
|
||||
"visibility": "team"
|
||||
},
|
||||
{
|
||||
"id": "cal2",
|
||||
"name": "Personal Calendar",
|
||||
"owner_id": "user1",
|
||||
"description": "Personal appointments and reminders",
|
||||
"color": "#0F9D58",
|
||||
"shared_with": ["user5"],
|
||||
"visibility": "private"
|
||||
},
|
||||
{
|
||||
"id": "cal3",
|
||||
"name": "Project Calendar",
|
||||
"owner_id": "user2",
|
||||
"description": "Project-specific deadlines and milestones",
|
||||
"color": "#DB4437",
|
||||
"shared_with": ["user1", "user3", "user4"],
|
||||
"visibility": "public"
|
||||
}
|
||||
]
|
||||
}
|
57
examples/calendar/backend/data/events.json
Normal file
57
examples/calendar/backend/data/events.json
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"events": [
|
||||
{
|
||||
"id": "event1",
|
||||
"title": "Team Meeting",
|
||||
"description": "Weekly team sync meeting",
|
||||
"start_time": "2025-04-04T10:00:00",
|
||||
"end_time": "2025-04-04T11:00:00",
|
||||
"location": "Conference Room A",
|
||||
"calendar_id": "cal1",
|
||||
"organizer_id": "user1",
|
||||
"attendees": ["user1", "user2", "user3"],
|
||||
"recurring": false,
|
||||
"status": "confirmed"
|
||||
},
|
||||
{
|
||||
"id": "event2",
|
||||
"title": "Project Deadline",
|
||||
"description": "Final submission for Q2 project",
|
||||
"start_time": "2025-04-15T17:00:00",
|
||||
"end_time": "2025-04-15T18:00:00",
|
||||
"location": "Virtual",
|
||||
"calendar_id": "cal1",
|
||||
"organizer_id": "user2",
|
||||
"attendees": ["user1", "user2", "user4"],
|
||||
"recurring": false,
|
||||
"status": "confirmed"
|
||||
},
|
||||
{
|
||||
"id": "event3",
|
||||
"title": "Lunch with Client",
|
||||
"description": "Discuss upcoming partnership",
|
||||
"start_time": "2025-04-10T12:30:00",
|
||||
"end_time": "2025-04-10T14:00:00",
|
||||
"location": "Downtown Cafe",
|
||||
"calendar_id": "cal2",
|
||||
"organizer_id": "user1",
|
||||
"attendees": ["user1", "user5"],
|
||||
"recurring": false,
|
||||
"status": "tentative"
|
||||
},
|
||||
{
|
||||
"id": "event4",
|
||||
"title": "Weekly Status Update",
|
||||
"description": "Regular status update meeting",
|
||||
"start_time": "2025-04-05T09:00:00",
|
||||
"end_time": "2025-04-05T09:30:00",
|
||||
"location": "Conference Room B",
|
||||
"calendar_id": "cal1",
|
||||
"organizer_id": "user3",
|
||||
"attendees": ["user1", "user2", "user3", "user4"],
|
||||
"recurring": true,
|
||||
"recurrence_pattern": "weekly",
|
||||
"status": "confirmed"
|
||||
}
|
||||
]
|
||||
}
|
54
examples/calendar/backend/data/users.json
Normal file
54
examples/calendar/backend/data/users.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"users": [
|
||||
{
|
||||
"id": "user1",
|
||||
"name": "John Doe",
|
||||
"email": "john.doe@example.com",
|
||||
"timezone": "UTC+2",
|
||||
"preferences": {
|
||||
"notification_time": 15,
|
||||
"default_calendar_id": "cal1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "user2",
|
||||
"name": "Jane Smith",
|
||||
"email": "jane.smith@example.com",
|
||||
"timezone": "UTC+1",
|
||||
"preferences": {
|
||||
"notification_time": 30,
|
||||
"default_calendar_id": "cal1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "user3",
|
||||
"name": "Bob Johnson",
|
||||
"email": "bob.johnson@example.com",
|
||||
"timezone": "UTC",
|
||||
"preferences": {
|
||||
"notification_time": 10,
|
||||
"default_calendar_id": "cal2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "user4",
|
||||
"name": "Alice Brown",
|
||||
"email": "alice.brown@example.com",
|
||||
"timezone": "UTC-5",
|
||||
"preferences": {
|
||||
"notification_time": 20,
|
||||
"default_calendar_id": "cal1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "user5",
|
||||
"name": "Charlie Davis",
|
||||
"email": "charlie.davis@example.com",
|
||||
"timezone": "UTC+3",
|
||||
"preferences": {
|
||||
"notification_time": 15,
|
||||
"default_calendar_id": "cal2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
118
examples/calendar/backend/main.rhai
Normal file
118
examples/calendar/backend/main.rhai
Normal file
@ -0,0 +1,118 @@
|
||||
// main.rhai - Main script for the calendar backend
|
||||
|
||||
// Import all modules
|
||||
import "scripts/events" as events;
|
||||
import "scripts/users" as users;
|
||||
import "scripts/calendars" as calendars;
|
||||
import "scripts/utils" as utils;
|
||||
|
||||
// Example function to get a user's upcoming events
|
||||
fn get_user_upcoming_events(user_id, days_ahead) {
|
||||
let user = users::get_user(user_id);
|
||||
|
||||
if utils::is_empty(user) {
|
||||
return "User not found";
|
||||
}
|
||||
|
||||
// Get current date
|
||||
let current_date = utils::get_current_date();
|
||||
|
||||
// Calculate end date
|
||||
let end_date = utils::add_days_to_date(current_date, days_ahead);
|
||||
|
||||
// Get events in the date range
|
||||
let upcoming_events = events::get_events_by_date_range(current_date, end_date);
|
||||
|
||||
// Filter to only include events where the user is an attendee
|
||||
let user_events = [];
|
||||
for event in upcoming_events {
|
||||
if utils::array_contains(event.attendees, user_id) {
|
||||
user_events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
return user_events;
|
||||
}
|
||||
|
||||
// Example function to get a user's calendar summary
|
||||
fn get_user_calendar_summary(user_id) {
|
||||
let user = users::get_user(user_id);
|
||||
|
||||
if utils::is_empty(user) {
|
||||
return "User not found";
|
||||
}
|
||||
|
||||
let user_calendars = calendars::get_accessible_calendars(user_id);
|
||||
let calendar_count = user_calendars.len();
|
||||
|
||||
let user_events = events::get_events_by_attendee(user_id);
|
||||
let event_count = user_events.len();
|
||||
|
||||
let organized_events = events::get_events_by_organizer(user_id);
|
||||
let organized_count = organized_events.len();
|
||||
|
||||
return #{
|
||||
user_name: user.name,
|
||||
calendar_count: calendar_count,
|
||||
event_count: event_count,
|
||||
organized_count: organized_count
|
||||
};
|
||||
}
|
||||
|
||||
// Example function to get details of a specific event
|
||||
fn get_event_details(event_id) {
|
||||
let event = events::get_event(event_id);
|
||||
|
||||
if utils::is_empty(event) {
|
||||
return "Event not found";
|
||||
}
|
||||
|
||||
let organizer = users::get_user(event.organizer_id);
|
||||
let calendar = calendars::get_calendar(event.calendar_id);
|
||||
|
||||
let attendees_info = [];
|
||||
for attendee_id in event.attendees {
|
||||
let attendee = users::get_user(attendee_id);
|
||||
if !utils::is_empty(attendee) {
|
||||
attendees_info.push(attendee.name);
|
||||
}
|
||||
}
|
||||
|
||||
return #{
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
start_time: event.start_time,
|
||||
end_time: event.end_time,
|
||||
location: event.location,
|
||||
organizer: organizer.name,
|
||||
calendar: calendar.name,
|
||||
attendees: attendees_info,
|
||||
status: event.status
|
||||
};
|
||||
}
|
||||
|
||||
// Example function to create a new event
|
||||
fn create_new_event(title, description, start_time, end_time, location, calendar_id, organizer_id, attendees) {
|
||||
// Validate inputs
|
||||
let calendar = calendars::get_calendar(calendar_id);
|
||||
if utils::is_empty(calendar) {
|
||||
return "Calendar not found";
|
||||
}
|
||||
|
||||
let organizer = users::get_user(organizer_id);
|
||||
if utils::is_empty(organizer) {
|
||||
return "Organizer not found";
|
||||
}
|
||||
|
||||
// Create the event
|
||||
let new_event = events::create_event(
|
||||
title, description, start_time, end_time, location,
|
||||
calendar_id, organizer_id, attendees, false, "confirmed"
|
||||
);
|
||||
|
||||
return #{
|
||||
message: "Event created successfully",
|
||||
event_id: new_event.id,
|
||||
title: new_event.title
|
||||
};
|
||||
}
|
11
examples/calendar/backend/main.rs
Normal file
11
examples/calendar/backend/main.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use rhai::{Engine};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
let engine = Engine::new();
|
||||
|
||||
// Rhai's import system works relative to current working dir
|
||||
engine.eval_file::<()>(PathBuf::from("main.rhai"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
54
examples/calendar/backend/models/calendar_model.rhai
Normal file
54
examples/calendar/backend/models/calendar_model.rhai
Normal file
@ -0,0 +1,54 @@
|
||||
// calendar_model.rhai - Calendar data model
|
||||
|
||||
// Create a new calendar object
|
||||
fn create_calendar(id, name, owner_id, description, color, shared_with, visibility) {
|
||||
return #{
|
||||
id: id,
|
||||
name: name,
|
||||
owner_id: owner_id,
|
||||
description: description,
|
||||
color: color,
|
||||
shared_with: shared_with,
|
||||
visibility: visibility
|
||||
};
|
||||
}
|
||||
|
||||
// Sample calendars data
|
||||
fn get_sample_calendars() {
|
||||
let calendars = [];
|
||||
|
||||
// Calendar 1: Work Calendar
|
||||
calendars.push(create_calendar(
|
||||
"cal1",
|
||||
"Work Calendar",
|
||||
"user1",
|
||||
"Main work calendar for team coordination",
|
||||
"#4285F4",
|
||||
["user2", "user3", "user4"],
|
||||
"team"
|
||||
));
|
||||
|
||||
// Calendar 2: Personal Calendar
|
||||
calendars.push(create_calendar(
|
||||
"cal2",
|
||||
"Personal Calendar",
|
||||
"user1",
|
||||
"Personal appointments and reminders",
|
||||
"#0F9D58",
|
||||
["user5"],
|
||||
"private"
|
||||
));
|
||||
|
||||
// Calendar 3: Project Calendar
|
||||
calendars.push(create_calendar(
|
||||
"cal3",
|
||||
"Project Calendar",
|
||||
"user2",
|
||||
"Project-specific deadlines and milestones",
|
||||
"#DB4437",
|
||||
["user1", "user3", "user4"],
|
||||
"public"
|
||||
));
|
||||
|
||||
return calendars;
|
||||
}
|
92
examples/calendar/backend/models/event_model.rhai
Normal file
92
examples/calendar/backend/models/event_model.rhai
Normal file
@ -0,0 +1,92 @@
|
||||
// event_model.rhai - Event data model
|
||||
|
||||
// Create a new event object
|
||||
fn create_event(id, title, description, start_time, end_time, location, calendar_id, organizer_id, attendees, recurring, status) {
|
||||
return #{
|
||||
id: id,
|
||||
title: title,
|
||||
description: description,
|
||||
start_time: start_time,
|
||||
end_time: end_time,
|
||||
location: location,
|
||||
calendar_id: calendar_id,
|
||||
organizer_id: organizer_id,
|
||||
attendees: attendees,
|
||||
recurring: recurring,
|
||||
status: status
|
||||
};
|
||||
}
|
||||
|
||||
// Create a recurring event object (extends regular event)
|
||||
fn create_recurring_event(id, title, description, start_time, end_time, location, calendar_id, organizer_id, attendees, recurrence_pattern, status) {
|
||||
let event = create_event(id, title, description, start_time, end_time, location, calendar_id, organizer_id, attendees, true, status);
|
||||
event.recurrence_pattern = recurrence_pattern;
|
||||
return event;
|
||||
}
|
||||
|
||||
// Sample events data
|
||||
fn get_sample_events() {
|
||||
let events = [];
|
||||
|
||||
// Event 1: Team Meeting
|
||||
events.push(create_event(
|
||||
"event1",
|
||||
"Team Meeting",
|
||||
"Weekly team sync meeting",
|
||||
"2025-04-04T10:00:00",
|
||||
"2025-04-04T11:00:00",
|
||||
"Conference Room A",
|
||||
"cal1",
|
||||
"user1",
|
||||
["user1", "user2", "user3"],
|
||||
false,
|
||||
"confirmed"
|
||||
));
|
||||
|
||||
// Event 2: Project Deadline
|
||||
events.push(create_event(
|
||||
"event2",
|
||||
"Project Deadline",
|
||||
"Final submission for Q2 project",
|
||||
"2025-04-15T17:00:00",
|
||||
"2025-04-15T18:00:00",
|
||||
"Virtual",
|
||||
"cal1",
|
||||
"user2",
|
||||
["user1", "user2", "user4"],
|
||||
false,
|
||||
"confirmed"
|
||||
));
|
||||
|
||||
// Event 3: Lunch with Client
|
||||
events.push(create_event(
|
||||
"event3",
|
||||
"Lunch with Client",
|
||||
"Discuss upcoming partnership",
|
||||
"2025-04-10T12:30:00",
|
||||
"2025-04-10T14:00:00",
|
||||
"Downtown Cafe",
|
||||
"cal2",
|
||||
"user1",
|
||||
["user1", "user5"],
|
||||
false,
|
||||
"tentative"
|
||||
));
|
||||
|
||||
// Event 4: Weekly Status Update (recurring)
|
||||
events.push(create_recurring_event(
|
||||
"event4",
|
||||
"Weekly Status Update",
|
||||
"Regular status update meeting",
|
||||
"2025-04-05T09:00:00",
|
||||
"2025-04-05T09:30:00",
|
||||
"Conference Room B",
|
||||
"cal1",
|
||||
"user3",
|
||||
["user1", "user2", "user3", "user4"],
|
||||
"weekly",
|
||||
"confirmed"
|
||||
));
|
||||
|
||||
return events;
|
||||
}
|
72
examples/calendar/backend/models/user_model.rhai
Normal file
72
examples/calendar/backend/models/user_model.rhai
Normal file
@ -0,0 +1,72 @@
|
||||
// user_model.rhai - User data model
|
||||
|
||||
// Create user preferences object
|
||||
fn create_user_preferences(notification_time, default_calendar_id) {
|
||||
return #{
|
||||
notification_time: notification_time,
|
||||
default_calendar_id: default_calendar_id
|
||||
};
|
||||
}
|
||||
|
||||
// Create a new user object
|
||||
fn create_user(id, name, email, timezone, preferences) {
|
||||
return #{
|
||||
id: id,
|
||||
name: name,
|
||||
email: email,
|
||||
timezone: timezone,
|
||||
preferences: preferences
|
||||
};
|
||||
}
|
||||
|
||||
// Sample users data
|
||||
fn get_sample_users() {
|
||||
let users = [];
|
||||
|
||||
// User 1: John Doe
|
||||
users.push(create_user(
|
||||
"user1",
|
||||
"John Doe",
|
||||
"john.doe@example.com",
|
||||
"UTC+2",
|
||||
create_user_preferences(15, "cal1")
|
||||
));
|
||||
|
||||
// User 2: Jane Smith
|
||||
users.push(create_user(
|
||||
"user2",
|
||||
"Jane Smith",
|
||||
"jane.smith@example.com",
|
||||
"UTC+1",
|
||||
create_user_preferences(30, "cal1")
|
||||
));
|
||||
|
||||
// User 3: Bob Johnson
|
||||
users.push(create_user(
|
||||
"user3",
|
||||
"Bob Johnson",
|
||||
"bob.johnson@example.com",
|
||||
"UTC",
|
||||
create_user_preferences(10, "cal2")
|
||||
));
|
||||
|
||||
// User 4: Alice Brown
|
||||
users.push(create_user(
|
||||
"user4",
|
||||
"Alice Brown",
|
||||
"alice.brown@example.com",
|
||||
"UTC-5",
|
||||
create_user_preferences(20, "cal1")
|
||||
));
|
||||
|
||||
// User 5: Charlie Davis
|
||||
users.push(create_user(
|
||||
"user5",
|
||||
"Charlie Davis",
|
||||
"charlie.davis@example.com",
|
||||
"UTC+3",
|
||||
create_user_preferences(15, "cal2")
|
||||
));
|
||||
|
||||
return users;
|
||||
}
|
73
examples/calendar/backend/scripts/calendars.rhai
Normal file
73
examples/calendar/backend/scripts/calendars.rhai
Normal file
@ -0,0 +1,73 @@
|
||||
// calendars.rhai - Functions for handling calendars
|
||||
|
||||
import "utils" as utils;
|
||||
import "../models/calendar_model" as calendar_model;
|
||||
|
||||
// Get all calendars
|
||||
fn get_calendars() {
|
||||
return calendar_model::get_sample_calendars();
|
||||
}
|
||||
|
||||
// Get a specific calendar by ID
|
||||
fn get_calendar(calendar_id) {
|
||||
let calendars = get_all_calendars();
|
||||
return utils::find_by_id(calendars, calendar_id);
|
||||
}
|
||||
|
||||
// Get calendars owned by a specific user
|
||||
fn get_calendars_by_owner(user_id) {
|
||||
let calendars = get_all_calendars();
|
||||
return utils::filter_by_property(calendars, "owner_id", user_id);
|
||||
}
|
||||
|
||||
// Get calendars shared with a specific user
|
||||
fn get_calendars_shared_with(user_id) {
|
||||
let calendars = get_all_calendars();
|
||||
let shared_calendars = [];
|
||||
|
||||
for calendar in calendars {
|
||||
if utils::array_contains(calendar.shared_with, user_id) {
|
||||
shared_calendars.push(calendar);
|
||||
}
|
||||
}
|
||||
|
||||
return shared_calendars;
|
||||
}
|
||||
|
||||
// Get all calendars accessible by a user (owned + shared)
|
||||
fn get_accessible_calendars(user_id) {
|
||||
let owned = get_calendars_by_owner(user_id);
|
||||
let shared_calendars = get_calendars_shared_with(user_id);
|
||||
|
||||
// Combine the two arrays
|
||||
for calendar in shared_calendars {
|
||||
owned.push(calendar);
|
||||
}
|
||||
|
||||
return owned;
|
||||
}
|
||||
|
||||
// Create a new calendar
|
||||
fn create_calendar(name, owner_id, description, color, shared_with, visibility) {
|
||||
// Generate a simple ID (in a real app, we would use a proper ID generation method)
|
||||
let id = "cal" + (get_all_calendars().len() + 1).to_string();
|
||||
|
||||
return calendar_model::create_calendar(
|
||||
id, name, owner_id, description, color, shared_with, visibility
|
||||
);
|
||||
}
|
||||
|
||||
// Format calendar details for display
|
||||
fn format_calendar(calendar) {
|
||||
if utils::is_empty(calendar) {
|
||||
return "Calendar not found";
|
||||
}
|
||||
|
||||
let shared_with_count = calendar.shared_with.len();
|
||||
|
||||
return `${calendar.name} (${calendar.color})
|
||||
Description: ${calendar.description}
|
||||
Owner: ${calendar.owner_id}
|
||||
Visibility: ${calendar.visibility}
|
||||
Shared with: ${shared_with_count} users`;
|
||||
}
|
85
examples/calendar/backend/scripts/events.rhai
Normal file
85
examples/calendar/backend/scripts/events.rhai
Normal file
@ -0,0 +1,85 @@
|
||||
// events.rhai - Functions for handling calendar events
|
||||
|
||||
import "utils" as utils;
|
||||
import "../models/event_model" as event_model;
|
||||
|
||||
// Get all events
|
||||
fn get_all_events() {
|
||||
return event_model::get_sample_events();
|
||||
}
|
||||
|
||||
// Get a specific event by ID
|
||||
fn get_event(event_id) {
|
||||
let events = get_all_events();
|
||||
return utils::find_by_id(events, event_id);
|
||||
}
|
||||
|
||||
// Get events for a specific calendar
|
||||
fn get_events_by_calendar(calendar_id) {
|
||||
let events = get_all_events();
|
||||
return utils::filter_by_property(events, "calendar_id", calendar_id);
|
||||
}
|
||||
|
||||
// Get events for a specific user (as attendee)
|
||||
fn get_events_by_attendee(user_id) {
|
||||
let events = get_all_events();
|
||||
let user_events = [];
|
||||
|
||||
for event in events {
|
||||
if utils::array_contains(event.attendees, user_id) {
|
||||
user_events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
return user_events;
|
||||
}
|
||||
|
||||
// Get events organized by a specific user
|
||||
fn get_events_by_organizer(user_id) {
|
||||
let events = get_all_events();
|
||||
return utils::filter_by_property(events, "organizer_id", user_id);
|
||||
}
|
||||
|
||||
// Get events for a specific date range
|
||||
fn get_events_by_date_range(start_date, end_date) {
|
||||
let events = get_all_events();
|
||||
let filtered_events = [];
|
||||
|
||||
for event in events {
|
||||
let event_start = event.start_time;
|
||||
|
||||
// Simple string comparison - in a real app, we would use proper date comparison
|
||||
if event_start >= start_date && event_start <= end_date {
|
||||
filtered_events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
return filtered_events;
|
||||
}
|
||||
|
||||
// Create a new event
|
||||
fn create_event(title, description, start_time, end_time, location, calendar_id, organizer_id, attendees, recurring, status) {
|
||||
// Generate a simple ID (in a real app, we would use a proper ID generation method)
|
||||
let id = "event" + (get_all_events().len() + 1).to_string();
|
||||
|
||||
return event_model::create_event(
|
||||
id, title, description, start_time, end_time, location,
|
||||
calendar_id, organizer_id, attendees, recurring, status
|
||||
);
|
||||
}
|
||||
|
||||
// Format event details for display
|
||||
fn format_event(event) {
|
||||
if utils::is_empty(event) {
|
||||
return "Event not found";
|
||||
}
|
||||
|
||||
let start_date = utils::format_date(event.start_time);
|
||||
let start_time = utils::format_time(event.start_time);
|
||||
let end_time = utils::format_time(event.end_time);
|
||||
|
||||
return `${event.title} (${start_date}, ${start_time} - ${end_time})
|
||||
Location: ${event.location}
|
||||
Description: ${event.description}
|
||||
Status: ${event.status}`;
|
||||
}
|
60
examples/calendar/backend/scripts/users.rhai
Normal file
60
examples/calendar/backend/scripts/users.rhai
Normal file
@ -0,0 +1,60 @@
|
||||
// users.rhai - Functions for handling calendar users
|
||||
|
||||
import "utils" as utils;
|
||||
import "../models/user_model" as user_model;
|
||||
|
||||
// Get all users
|
||||
fn get_all_users() {
|
||||
return user_model::get_sample_users();
|
||||
}
|
||||
|
||||
// Get a specific user by ID
|
||||
fn get_user(user_id) {
|
||||
let users = get_all_users();
|
||||
return utils::find_by_id(users, user_id);
|
||||
}
|
||||
|
||||
// Get user by email
|
||||
fn get_user_by_email(email) {
|
||||
let users = get_all_users();
|
||||
|
||||
for user in users {
|
||||
if user.email == email {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
return #{}; // Return empty object if not found
|
||||
}
|
||||
|
||||
// Get users by timezone
|
||||
fn get_users_by_timezone(timezone) {
|
||||
let users = get_all_users();
|
||||
return utils::filter_by_property(users, "timezone", timezone);
|
||||
}
|
||||
|
||||
// Create a new user
|
||||
fn create_user(name, email, timezone, notification_time, default_calendar_id) {
|
||||
// Generate a simple ID (in a real app, we would use a proper ID generation method)
|
||||
let id = "user" + (get_all_users().len() + 1).to_string();
|
||||
|
||||
let preferences = user_model::create_user_preferences(
|
||||
notification_time, default_calendar_id
|
||||
);
|
||||
|
||||
return user_model::create_user(
|
||||
id, name, email, timezone, preferences
|
||||
);
|
||||
}
|
||||
|
||||
// Format user details for display
|
||||
fn format_user(user) {
|
||||
if utils::is_empty(user) {
|
||||
return "User not found";
|
||||
}
|
||||
|
||||
return `${user.name} (${user.email})
|
||||
Timezone: ${user.timezone}
|
||||
Default Calendar: ${user.preferences.default_calendar_id}
|
||||
Notification Time: ${user.preferences.notification_time} minutes before`;
|
||||
}
|
84
examples/calendar/backend/scripts/utils.rhai
Normal file
84
examples/calendar/backend/scripts/utils.rhai
Normal file
@ -0,0 +1,84 @@
|
||||
// utils.rhai - Utility functions for the calendar backend
|
||||
|
||||
// Function to find an item by ID in an array
|
||||
fn find_by_id(array, id) {
|
||||
for item in array {
|
||||
if item.id == id {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return #{}; // Return empty object if not found
|
||||
}
|
||||
|
||||
// Function to filter array by a property value
|
||||
fn filter_by_property(array, property, value) {
|
||||
let result = [];
|
||||
|
||||
for item in array {
|
||||
if item[property] == value {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Function to check if an object is empty
|
||||
fn is_empty(obj) {
|
||||
if obj.type_of() == "object" {
|
||||
return obj.keys().len() == 0;
|
||||
}
|
||||
if obj.type_of() == "array" {
|
||||
return obj.len() == 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Function to format date string
|
||||
fn format_date(date_str) {
|
||||
// Simple formatting - in a real app, we would use proper date formatting
|
||||
return date_str.substr(0, 10);
|
||||
}
|
||||
|
||||
// Function to format time string
|
||||
fn format_time(date_str) {
|
||||
// Simple formatting - in a real app, we would use proper time formatting
|
||||
if date_str.len() >= 16 {
|
||||
return date_str.substr(11, 5);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Function to check if an array contains a value
|
||||
fn array_contains(array, value) {
|
||||
for item in array {
|
||||
if item == value {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Function to get current date (simplified for demo)
|
||||
fn get_current_date() {
|
||||
return "2025-04-03";
|
||||
}
|
||||
|
||||
// Function to calculate a future date (simplified for demo)
|
||||
fn add_days_to_date(date_str, days) {
|
||||
let year = date_str.substr(0, 4).parse_int();
|
||||
let month = date_str.substr(5, 2).parse_int();
|
||||
let day = date_str.substr(8, 2).parse_int() + days;
|
||||
|
||||
// Very simplified date calculation - in a real app, we would handle month/year boundaries
|
||||
if day > 30 {
|
||||
day = day - 30;
|
||||
month += 1;
|
||||
}
|
||||
if month > 12 {
|
||||
month = 1;
|
||||
year += 1;
|
||||
}
|
||||
|
||||
return `${year}-${month.to_string().pad_left(2, '0')}-${day.to_string().pad_left(2, '0')}`;
|
||||
}
|
2
examples/calendar/components/calendar/.cargo/config.toml
Normal file
2
examples/calendar/components/calendar/.cargo/config.toml
Normal file
@ -0,0 +1,2 @@
|
||||
[alias]
|
||||
dev = "watch -w src -w examples -w src/templates -w src/scripts -x 'run --example server'"
|
1524
examples/calendar/components/calendar/Cargo.lock
generated
Normal file
1524
examples/calendar/components/calendar/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
examples/calendar/components/calendar/Cargo.toml
Normal file
15
examples/calendar/components/calendar/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "calendar"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
hyper = { version = "0.14", features = ["full"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
http = "0.2"
|
||||
httparse = "1"
|
||||
rhai = { version = "1.21", features = ["serde"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tera = "1.0"
|
||||
once_cell = "1"
|
51
examples/calendar/components/calendar/README.md
Normal file
51
examples/calendar/components/calendar/README.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Calendar Server Example
|
||||
|
||||
A simple Rust web server using Hyper that exposes an `/all_calendars` endpoint.
|
||||
|
||||
## Running the server
|
||||
|
||||
```bash
|
||||
# Navigate to the examples directory
|
||||
cd /path/to/examples
|
||||
|
||||
# Build and run the server
|
||||
cargo run
|
||||
```
|
||||
|
||||
Once the server is running, you can access the endpoint at:
|
||||
- http://127.0.0.1:8080/all_calendars
|
||||
|
||||
## Features
|
||||
- Simple HTTP server using Hyper
|
||||
- Single endpoint that returns "Hello World"
|
||||
|
||||
|
||||
Sure thing! Here’s the Markdown version you can copy-paste directly into your README.md:
|
||||
|
||||
## 🔁 Live Reload (Hot Reload for Development)
|
||||
|
||||
To automatically recompile and restart your example server on file changes (e.g. Rust code, templates, Rhai scripts), you can use [`cargo-watch`](https://github.com/watchexec/cargo-watch):
|
||||
|
||||
### ✅ Step 1: Install `cargo-watch`
|
||||
|
||||
```bash
|
||||
cargo install cargo-watch
|
||||
```
|
||||
|
||||
### ✅ Step 2: Run the server with live reload
|
||||
|
||||
cargo watch -x 'run --example server'
|
||||
|
||||
This will:
|
||||
• Watch for file changes in your project
|
||||
• Rebuild and re-run examples/server.rs whenever you make a change
|
||||
|
||||
### 🧠 Bonus: Watch additional folders
|
||||
|
||||
To also reload when .tera templates or .rhai scripts change:
|
||||
|
||||
cargo watch -w src -w examples -w src/templates -w src/scripts -x 'run --example server'
|
||||
|
||||
### 💡 Optional: Clear terminal on each reload
|
||||
|
||||
cargo watch -c -x 'run --example server'
|
9
examples/calendar/components/calendar/develop.sh
Executable file
9
examples/calendar/components/calendar/develop.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# 🚀 Start dev server with file watching and browser reload
|
||||
reloadd \
|
||||
--watch src \
|
||||
--watch src/templates \
|
||||
--watch examples \
|
||||
-- run --example server
|
114
examples/calendar/components/calendar/examples/server.rs
Normal file
114
examples/calendar/components/calendar/examples/server.rs
Normal file
@ -0,0 +1,114 @@
|
||||
use hyper::{Body, Request, Response, Server};
|
||||
use hyper::service::{make_service_fn, service_fn};
|
||||
use hyper::{Method, StatusCode};
|
||||
use std::convert::Infallible;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::process::Command;
|
||||
use httparse;
|
||||
|
||||
async fn handle_request(req: Request<Body>) -> Result<Response<Body>, Infallible> {
|
||||
// Serialize request into raw HTTP format
|
||||
let raw_request = build_raw_http_request(req).await;
|
||||
|
||||
// Spawn the binary
|
||||
let mut child = Command::new("./target/debug/calendar")
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.expect("failed to spawn binary");
|
||||
|
||||
// Feed raw HTTP request to binary's stdin
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
let _ = stdin.write_all(&raw_request).await;
|
||||
}
|
||||
|
||||
// Read response from binary's stdout
|
||||
let mut stdout = child.stdout.take().expect("no stdout");
|
||||
let mut output = Vec::new();
|
||||
let _ = stdout.read_to_end(&mut output).await;
|
||||
|
||||
let _ = child.wait().await;
|
||||
|
||||
// Parse raw HTTP response
|
||||
match parse_raw_http_response(&output) {
|
||||
Ok(res) => Ok(res),
|
||||
Err(e) => {
|
||||
let mut resp = Response::new(Body::from(format!("Failed to parse response: {}", e)));
|
||||
*resp.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
Ok(resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build raw HTTP request string
|
||||
async fn build_raw_http_request(req: Request<Body>) -> Vec<u8> {
|
||||
let mut raw = String::new();
|
||||
|
||||
let method = req.method();
|
||||
let path = req.uri().path();
|
||||
let version = match req.version() {
|
||||
hyper::Version::HTTP_11 => "HTTP/1.1",
|
||||
_ => "HTTP/1.1",
|
||||
};
|
||||
|
||||
raw.push_str(&format!("{method} {path} {version}\r\n"));
|
||||
|
||||
for (key, value) in req.headers() {
|
||||
if let Ok(val_str) = value.to_str() {
|
||||
raw.push_str(&format!("{}: {}\r\n", key, val_str));
|
||||
}
|
||||
}
|
||||
|
||||
raw.push_str("\r\n");
|
||||
|
||||
let body_bytes = hyper::body::to_bytes(req.into_body()).await.unwrap_or_default();
|
||||
let mut full = raw.into_bytes();
|
||||
full.extend_from_slice(&body_bytes);
|
||||
|
||||
full
|
||||
}
|
||||
|
||||
// Parse raw HTTP response into hyper::Response<Body>
|
||||
fn parse_raw_http_response(bytes: &[u8]) -> Result<Response<Body>, Box<dyn std::error::Error>> {
|
||||
let mut headers = [httparse::EMPTY_HEADER; 64];
|
||||
let mut res = httparse::Response::new(&mut headers);
|
||||
let parsed_len = res.parse(bytes)?.unwrap(); // returns offset
|
||||
|
||||
let status = res.code.unwrap_or(200);
|
||||
let mut builder = Response::builder().status(status);
|
||||
|
||||
for h in res.headers.iter() {
|
||||
builder = builder.header(h.name, std::str::from_utf8(h.value)?);
|
||||
}
|
||||
|
||||
// Get body and append LiveReload script
|
||||
let mut body = bytes[parsed_len..].to_vec();
|
||||
let livereload_snippet = br#"<script>
|
||||
const ws = new WebSocket("ws://localhost:35729");
|
||||
ws.onmessage = (msg) => {
|
||||
if (msg.data === "reload") location.reload();
|
||||
};
|
||||
</script>"#;
|
||||
body.extend_from_slice(b"\n");
|
||||
body.extend_from_slice(livereload_snippet);
|
||||
|
||||
Ok(builder.body(Body::from(body))?)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
|
||||
let make_svc = make_service_fn(|_conn| async {
|
||||
Ok::<_, Infallible>(service_fn(handle_request))
|
||||
});
|
||||
|
||||
println!("Proxy server running at http://{}", addr);
|
||||
|
||||
if let Err(e) = Server::bind(&addr).serve(make_svc).await {
|
||||
eprintln!("server error: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
121
examples/calendar/components/calendar/src/controller/calendar.rs
Normal file
121
examples/calendar/components/calendar/src/controller/calendar.rs
Normal file
@ -0,0 +1,121 @@
|
||||
use rhai::{Engine, Scope, Array, Dynamic, AST};
|
||||
use hyper::{Request, Body, Response, StatusCode};
|
||||
use std::collections::HashMap;
|
||||
use tera::{Function as TeraFunction, Value, to_value, Result as TeraResult};
|
||||
use tera::Tera;
|
||||
use std::path::PathBuf;
|
||||
use serde_json;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
pub fn get_calendars(_req: Request<Body>) -> Response<Body> {
|
||||
let engine = Engine::new();
|
||||
|
||||
// Compile the script
|
||||
let ast = match engine.compile_file(
|
||||
"/Users/timurgordon/code/git.ourworld.tf/herocode/rhaj/examples/calendar/backend/scripts/calendars.rhai".into()
|
||||
) {
|
||||
Ok(ast) => ast,
|
||||
Err(e) => {
|
||||
let mut res = Response::new(Body::from(format!("Rhai compile error: {}", e)));
|
||||
*res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
let mut scope = Scope::new();
|
||||
|
||||
|
||||
let tera = setup_tera_with_rhai();
|
||||
|
||||
let context = tera::Context::new(); // no need to pass data — we're calling the Rhai function inside
|
||||
|
||||
let rendered = match tera.render("get_calendars.html.tera", &context) {
|
||||
Ok(html) => Response::new(Body::from(html)),
|
||||
Err(err) => {
|
||||
// 🔍 Print the full error with source
|
||||
eprintln!("Template render error: {:?}", err); // or use err.to_string() for cleaner message
|
||||
|
||||
let mut res = Response::new(Body::from(format!("Template error: {}", err)));
|
||||
*res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
res
|
||||
}
|
||||
};
|
||||
|
||||
rendered
|
||||
}
|
||||
|
||||
// pub fn get_calendars(_req: Request<Body>) -> Response<Body> {
|
||||
// let engine = Engine::new();
|
||||
|
||||
// // Compile the script
|
||||
// let ast = match engine.compile_file(
|
||||
// "/Users/timurgordon/code/git.ourworld.tf/herocode/rhaj/examples/calendar/backend/scripts/calendars.rhai".into()
|
||||
// ) {
|
||||
// Ok(ast) => ast,
|
||||
// Err(e) => {
|
||||
// let mut res = Response::new(Body::from(format!("Rhai compile error: {}", e)));
|
||||
// *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
// return res;
|
||||
// }
|
||||
// };
|
||||
|
||||
// let mut scope = Scope::new();
|
||||
|
||||
// // Call the function in the Rhai script
|
||||
// match engine.call_fn::<Array>(&mut scope, &ast, "get_calendars", ()) {
|
||||
// Ok(result) => {
|
||||
// // Convert array to string representation
|
||||
// let response_text = format!("{:?}", result);
|
||||
// Response::new(Body::from(response_text))
|
||||
// },
|
||||
// Err(e) => {
|
||||
// let mut res = Response::new(Body::from(format!("Rhai call error: {}", e)));
|
||||
// *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
|
||||
// res
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
pub struct RhaiFunctionAdapter {
|
||||
fn_name: String,
|
||||
script_path: PathBuf,
|
||||
}
|
||||
|
||||
impl TeraFunction for RhaiFunctionAdapter {
|
||||
fn call(&self, args: &HashMap<String, Value>) -> TeraResult<Value> {
|
||||
let mut scope = Scope::new();
|
||||
|
||||
// Convert args from Tera into Rhai's Dynamic
|
||||
for (key, value) in args {
|
||||
let json_str = serde_json::to_string(value).unwrap();
|
||||
let json_value: serde_json::Value = serde_json::from_str(&json_str).unwrap();
|
||||
let dynamic = rhai::serde::to_dynamic(json_value).unwrap();
|
||||
scope.push_dynamic(key.clone(), dynamic);
|
||||
}
|
||||
|
||||
// Build engine and compile AST inside call (safe)
|
||||
let engine = Engine::new();
|
||||
let ast = engine
|
||||
.compile_file(self.script_path.clone())
|
||||
.map_err(|e| tera::Error::msg(format!("Rhai compile error: {}", e)))?;
|
||||
|
||||
let result = engine
|
||||
.call_fn::<Dynamic>(&mut scope, &ast, &self.fn_name, ())
|
||||
.map_err(|e| tera::Error::msg(format!("Rhai error: {}", e)))?;
|
||||
|
||||
let tera_value = rhai::serde::from_dynamic(&result).unwrap();
|
||||
Ok(tera_value)
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_tera_with_rhai() -> Tera {
|
||||
let mut tera = Tera::new("src/templates/**/*").unwrap();
|
||||
|
||||
let adapter = RhaiFunctionAdapter {
|
||||
fn_name: "get_calendars".into(),
|
||||
script_path: PathBuf::from("../../backend/scripts/calendars.rhai"),
|
||||
};
|
||||
|
||||
tera.register_function("get_calendars", adapter);
|
||||
tera
|
||||
}
|
69
examples/calendar/components/calendar/src/main.rs
Normal file
69
examples/calendar/components/calendar/src/main.rs
Normal file
@ -0,0 +1,69 @@
|
||||
mod controller {
|
||||
pub mod calendar;
|
||||
}
|
||||
|
||||
use controller::calendar;
|
||||
|
||||
use hyper::{Body, Method, Request, Response, StatusCode};
|
||||
use std::convert::Infallible;
|
||||
use std::io::{self, Read};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let input = read_request_from_stdin()?;
|
||||
let request = parse_http_request(&input)?;
|
||||
let response = handle_request(request).await?;
|
||||
print_http_response(response).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_request_from_stdin() -> io::Result<String> {
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_to_string(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
fn parse_http_request(input: &str) -> Result<Request<Body>, Box<dyn std::error::Error>> {
|
||||
let mut headers = [httparse::EMPTY_HEADER; 32];
|
||||
let mut req = httparse::Request::new(&mut headers);
|
||||
|
||||
let bytes = input.as_bytes();
|
||||
let parsed_len = req.parse(bytes)?.unwrap();
|
||||
|
||||
let method = req.method.ok_or("missing method")?.parse::<Method>()?;
|
||||
let path = req.path.ok_or("missing path")?;
|
||||
let uri = path.parse::<hyper::Uri>()?;
|
||||
|
||||
let body = &bytes[parsed_len..];
|
||||
let request = Request::builder()
|
||||
.method(method)
|
||||
.uri(uri)
|
||||
.body(Body::from(body.to_vec()))?;
|
||||
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
async fn handle_request(req: Request<Body>) -> Result<Response<Body>, Infallible> {
|
||||
match (req.method(), req.uri().path()) {
|
||||
(&Method::GET, "/calendars") => Ok(calendar::get_calendars(req)),
|
||||
_ => {
|
||||
let mut res = Response::new(Body::from("Not Found"));
|
||||
*res.status_mut() = StatusCode::NOT_FOUND;
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn print_http_response(res: Response<Body>) {
|
||||
println!("HTTP/1.1 {}", res.status());
|
||||
|
||||
for (key, value) in res.headers() {
|
||||
println!("{}: {}", key, value.to_str().unwrap_or_default());
|
||||
}
|
||||
|
||||
println!(); // blank line before body
|
||||
|
||||
let body_bytes = hyper::body::to_bytes(res.into_body()).await.unwrap();
|
||||
let body = String::from_utf8_lossy(&body_bytes);
|
||||
println!("{}", body);
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
<h2>Calendars</h2>
|
||||
<ul>
|
||||
{% for cal in get_calendars() %}
|
||||
<li>
|
||||
<article>
|
||||
<header>
|
||||
<h3>{{ cal.name }}</h3>
|
||||
<p>{{ cal.description }}</p>
|
||||
</header>
|
||||
<footer>
|
||||
<p>{{ cal.id }}</p>
|
||||
</footer>
|
||||
</article>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
Reference in New Issue
Block a user