add engine and rpc client, archive old code

This commit is contained in:
timurgordon
2025-06-03 21:47:36 +03:00
parent 061aee6f1d
commit b20140785e
42 changed files with 10310 additions and 0 deletions

1320
_archive/devtools/reloadd/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View 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"

View File

@@ -0,0 +1,124 @@
# Reloadd
A powerful development tool for automatically restarting your application and reloading your browser when files change.
## Features
- 🔄 **File Watching**: Monitor multiple directories for changes
- 🚀 **Auto-Restart**: Automatically restart your application when files change
- 🌐 **Browser Reload**: Automatically reload connected browsers
- 🔌 **WebSocket Integration**: Uses WebSockets for instant browser reloading
- 📊 **Sequential Commands**: Run multiple commands in sequence
- 🔧 **Configurable Ports**: Customize web server and WebSocket ports
- 🛠️ **Robust Error Handling**: Clear error messages and graceful recovery
## Installation
### From Source
```bash
# Clone the repository
git clone https://github.com/yourusername/reloadd.git
cd reloadd
# Build and install
cargo install --path .
```
## Usage
### Basic Usage
```bash
reloadd --watch src --watch templates -- run --example server
```
### With Sequential Commands
```bash
reloadd --watch src --run "cargo build" --run "cargo test" --run "cargo run --example server"
```
Commands will run in the order specified. All commands except the last one will run to completion. The last command is treated as the long-running server process.
### With Custom Port
```bash
reloadd --watch src --port 3000 --run "cargo run --example server"
```
### In a Shell Script
Create a `develop.sh` script for your project:
```bash
#!/usr/bin/env bash
set -e
# Start dev server with file watching and browser reload
reloadd \
--watch src \
--watch templates \
--port 8080 \
--run "cargo run --example server"
```
Make it executable and run it:
```bash
chmod +x develop.sh
./develop.sh
```
## Command Line Options
```
Usage: reloadd [OPTIONS] --watch <PATH>... [-- <COMMAND>...]
Arguments:
[COMMAND]... Command to run on change (legacy format)
Options:
-w, --watch <PATH>... Paths to watch (like src/, templates/, scripts/)
-p, --port <PORT> Port for the web server [default: 8080]
-r, --run <COMMAND>... Multiple commands to run
-h, --help Print help
-V, --version Print version
```
## LiveReload Integration
When you start the tool, it will output a script tag that you can add to your HTML files:
```html
<script>
const ws = new WebSocket("ws://localhost:35729");
ws.onmessage = (msg) => {
if (msg.data === "reload") location.reload();
};
</script>
```
Add this script to your HTML templates to enable automatic browser reloading.
## How It Works
1. Reloadd watches specified directories for file changes
2. When a change is detected, it runs your commands in sequence
3. Build commands (all except the last) run to completion before proceeding
4. The last command (typically a server) runs and stays active
5. After a brief delay, it sends a reload signal to connected browsers
6. Browsers with the LiveReload script will automatically refresh
## Error Handling
Reloadd includes robust error handling:
- Validates watch paths before starting
- Checks for port availability
- Provides clear error messages
- Gracefully exits with error codes when critical errors occur
## License
MIT

View 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;
}
});
}
}

View File

@@ -0,0 +1,333 @@
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;
use std::net::SocketAddr;
#[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>,
/// Port for the web server (default: 8080)
#[arg(short, long, default_value = "8080")]
port: u16,
/// Multiple commands to run (format: --run "cargo build" --run "cargo test")
#[arg(short, long, value_name = "COMMAND", num_args = 1..)]
run: Vec<String>,
/// Command to run on change (like: -- run --example server)
/// This is kept for backward compatibility
#[arg(last = true)]
command: Vec<String>,
}
// Run commands in sequence, with the last one potentially being a long-running server
async fn run_commands_in_sequence(commands: &[Vec<String>], server_process: Arc<Mutex<Option<Child>>>) -> bool {
// Run all commands in sequence
for (i, command) in commands.iter().enumerate() {
let is_last = i == commands.len() - 1;
let program = if command[0] == "cargo" || command[0].ends_with("/cargo") {
"cargo".to_string()
} else {
command[0].clone()
};
let args = if command[0] == "cargo" || command[0].ends_with("/cargo") {
command.iter().skip(1).cloned().collect::<Vec<String>>()
} else {
command.iter().skip(1).cloned().collect::<Vec<String>>()
};
let mut cmd = Command::new(&program);
cmd.args(&args);
cmd.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
if is_last {
// Last command might be a long-running server
match cmd.spawn() {
Ok(child) => {
*server_process.lock().unwrap() = Some(child);
println!("🚀 Started server process: {} {}", program, args.join(" "));
},
Err(e) => {
eprintln!("❌ Failed to start server process: {}", e);
return false;
}
}
} else {
// For non-last commands, wait for them to complete
println!("🔄 Running build step: {} {}", program, args.join(" "));
match cmd.output() {
Ok(output) => {
if !output.status.success() {
eprintln!("❌ Command failed: {} {}", program, args.join(" "));
return false;
}
println!("✅ Command completed successfully");
},
Err(e) => {
eprintln!("❌ Failed to execute command: {}", e);
return false;
}
}
}
}
true
}
// 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()
}
// Find an available port starting from the given port
fn find_available_port(start_port: u16) -> Option<u16> {
let mut port = start_port;
// Try up to 10 ports (start_port through start_port+9)
for _ in 0..10 {
if !is_port_in_use(port) {
return Some(port);
}
port += 1;
}
None
}
// Generate LiveReload script with the given WebSocket port
fn generate_livereload_script(ws_port: u16) -> String {
format!(
r#"<script>
const ws = new WebSocket("ws://localhost:{}");
ws.onmessage = (msg) => {{
if (msg.data === "reload") location.reload();
}};
</script>"#,
ws_port
)
}
#[tokio::main]
async fn main() {
let args = Args::parse();
// Handle both the new --run and legacy command format
let commands = if !args.run.is_empty() {
// New format: each --run is a separate command
args.run.iter().map(|cmd| {
// Split the command string into arguments
cmd.split_whitespace().map(String::from).collect::<Vec<String>>()
}).collect::<Vec<Vec<String>>>()
} else if !args.command.is_empty() {
// Legacy format: single command from the trailing arguments
println!("Command: {:?}", args.command);
vec![args.command.clone()]
} else {
// No commands provided
eprintln!("❌ Error: No commands provided. Use --run or trailing arguments.");
std::process::exit(1);
};
// Check if server port is already in use
let server_port = args.port;
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);
}
// Find an available WebSocket port
let ws_port = match find_available_port(35729) {
Some(port) => port,
None => {
eprintln!("❌ Error: Could not find an available WebSocket port. Please free up some ports and try again.");
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(), ws_port).await {
Ok(_) => {},
Err(e) => {
eprintln!("❌ Failed to start WebSocket server: {}", e);
std::process::exit(1);
}
}
// Output the LiveReload script for users to add to their projects
println!("📋 Add this script to your HTML for live reloading:");
println!("{}", generate_livereload_script(ws_port));
// 🚀 Run all commands in sequence
if !run_commands_in_sequence(&commands, Arc::clone(&server_process)).await {
eprintln!("❌ Command execution failed.");
std::process::exit(1);
}
println!("🔁 Watching paths: {:?}", args.watch);
println!("🌐 Connect browser to ws://localhost:{}", ws_port);
println!("🖥️ Web server running on http://localhost:{}", server_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 commands_clone = commands.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 (only the primary command gets restarted)
if let Some(first_command) = commands.first() {
let program = if first_command[0] == "cargo" || first_command[0].ends_with("/cargo") {
"cargo".to_string()
} else {
first_command[0].clone()
};
let args = if first_command[0] == "cargo" || first_command[0].ends_with("/cargo") {
first_command.iter().skip(1).cloned().collect::<Vec<String>>()
} else {
first_command.iter().skip(1).cloned().collect::<Vec<String>>()
};
let mut cmd = Command::new(&program);
cmd.args(&args);
cmd.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
match cmd.spawn() {
Ok(child) => {
*server_process_clone.lock().unwrap() = Some(child);
// Immediately send reload signal without waiting for server
let tx = tx_clone.clone();
rt_handle.spawn(async move {
// Small delay to give the server a moment to start
sleep(Duration::from_millis(500)).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<()>, ws_port: u16) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = format!("127.0.0.1:{}", ws_port);
let listener = tokio::net::TcpListener::bind(&addr).await?;
println!("WebSocket server started on ws://localhost:{}", ws_port);
// 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(())
}

BIN
_archive/listen/.DS_Store vendored Normal file

Binary file not shown.

2620
_archive/listen/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
[package]
name = "server"
version = "0.1.0"
edition = "2021"
[dependencies]
hyper = { version = "0.14", features = ["full"] }
tokio = { version = "1", features = ["full"] }
rhai = { version = "1.15.0", features = ["sync"] }
rhai_system = { path = "../rhai_system" }
bytes = "1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tera = "1.0"
once_cell = "1"
rhai_tera = { path = "../rhai_tera" }
calendar = { path = "../components/calendar" }
[[example]]
name = "send_rhai_script"
path = "examples/send_rhai_script.rs"
required-features = []
[dev-dependencies] # Examples often use dev-dependencies, but reqwest is more like a direct dep for the example's purpose.
reqwest = { version = "0.11", features = ["json", "rustls-tls"] } # Updated for async example client
# tokio is already a main dependency, so the example can use it.

51
_archive/listen/README.md Normal file
View 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! Heres 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'

11
_archive/listen/develop.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -e
# 🚀 Start dev server with file watching and browser reload
reloadd \
--watch src \
--watch src/templates \
--watch examples \
--port 8080 \
--run "cargo build" \
--run "cargo run --example server"

View File

@@ -0,0 +1,38 @@
use reqwest::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let server_url = "http://127.0.0.1:8000";
// Simple Rhai script that creates a map
let rhai_script = r#"
let message = "Hello from Rhai script!";
let number = 40 + 2;
#{
greeting: message,
calculation_result: number
}
"#;
println!("Sending Rhai script to server:\n{}", rhai_script);
let response = client
.post(server_url)
.header("Content-Type", "text/plain") // Or application/rhai, but plain text is fine
.body(rhai_script.to_string())
.send()
.await?;
let status = response.status();
let response_text = response.text().await?;
println!("\nServer responded with status: {}", status);
println!("Response body:\n{}", response_text);
if !status.is_success() {
return Err(format!("Server returned error: {} - {}", status, response_text).into());
}
Ok(())
}

View File

@@ -0,0 +1,3 @@
// This script is used to initialize the Rhai system.
// It can be left empty or used to define globally available functions/variables.
// print("init.rhai loaded!");

111
_archive/listen/src/main.rs Normal file
View File

@@ -0,0 +1,111 @@
use hyper::{
service::{make_service_fn, service_fn},
Body,
Request,
Response,
Server,
StatusCode,
Method,
};
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;
use std::path::Path;
use rhai_system::{create_hot_reloadable_system, System};
use rhai::Dynamic;
use hyper::body::to_bytes;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = SocketAddr::from(([127, 0, 0, 1], 8000));
let init_script_path = Path::new("scripts/init.rhai");
let rhai_sys = match create_hot_reloadable_system(&[init_script_path], Some(0)) {
Ok(system) => Arc::new(system),
Err(e) => {
eprintln!("Failed to create Rhai system: {}", e);
return Err(e.into());
}
};
let rhai_sys_clone = Arc::clone(&rhai_sys);
let make_svc = make_service_fn(move |_conn| {
let rhai_system_for_service = Arc::clone(&rhai_sys_clone);
async {
Ok::<_, Infallible>(service_fn(move |req: Request<Body>| {
let rhai_system_for_request = Arc::clone(&rhai_system_for_service);
handle_request(rhai_system_for_request, req)
}))
}
});
println!("Rhai script server running at http://{}", addr);
println!("Send POST requests with Rhai script in the body to execute.");
Server::bind(&addr).serve(make_svc).await?;
Ok(())
}
async fn handle_request(rhai_sys: Arc<System>, req: Request<Body>) -> Result<Response<Body>, Infallible> {
match *req.method() {
Method::POST => {
let body_bytes = match to_bytes(req.into_body()).await {
Ok(bytes) => bytes,
Err(e) => {
eprintln!("Error reading request body: {}", e);
return Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(format!("Error reading request body: {}", e)))
.unwrap());
}
};
let script_string = match String::from_utf8(body_bytes.to_vec()) {
Ok(s) => s,
Err(e) => {
eprintln!("Request body is not valid UTF-8: {}", e);
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Body::from(format!("Request body is not valid UTF-8: {}", e)))
.unwrap());
}
};
if script_string.trim().is_empty() {
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Body::from("Rhai script body cannot be empty."))
.unwrap());
}
println!("Executing Rhai script: \n{}", script_string);
match rhai_sys.engine.eval::<Dynamic>(&script_string) {
Ok(result) => {
let response_body = format!("{}", result);
println!("Script result: {}", response_body);
Ok(Response::new(Body::from(response_body)))
}
Err(e) => {
let error_msg = format!("Rhai script execution error: {}", e);
eprintln!("{}", error_msg);
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Body::from(error_msg))
.unwrap())
}
}
}
_ => {
Ok(Response::builder()
.status(StatusCode::METHOD_NOT_ALLOWED)
.header("Allow", "POST")
.body(Body::from("Method Not Allowed. Please use POST with Rhai script in the body."))
.unwrap())
}
}
}

282
_archive/rhai_autobind_macros/Cargo.lock generated Normal file
View File

@@ -0,0 +1,282 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ahash"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b79b82693f705137f8fb9b37871d99e4f9a7df12b917eed79c3d3954830a60b"
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.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[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.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[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.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[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.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
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.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1acc213aa1e33611a4b20b31b738af675113e1c9944d6e3d79e3e7318ce0205"
dependencies = [
"ahash",
"bitflags",
"instant",
"num-traits",
"once_cell",
"rhai_codegen",
"smallvec",
"smartstring",
"thin-vec",
]
[[package]]
name = "rhai_autobind_macros"
version = "0.1.0"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rhai",
"serde",
"syn",
]
[[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 = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "smallvec"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[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.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
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",
]

View File

@@ -0,0 +1,17 @@
[package]
name = "rhai_autobind_macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
syn = { version = "2.0", features = ["full", "extra-traits"] }
quote = "1.0"
proc-macro2 = "1.0"
heck = "0.4"
[dev-dependencies]
rhai = "1.18.0"
serde = { version = "1.0", features = ["derive"] }

View File

@@ -0,0 +1,33 @@
// calculator.rhai
// This script demonstrates using the Calculator struct from Rust in Rhai
// Create a new calculator
let calc = new_calculator();
println("Created a new calculator with value: " + calc.value);
// Perform some calculations
calc.add(5);
println("After adding 5: " + calc.value);
calc.multiply(2);
println("After multiplying by 2: " + calc.value);
calc.subtract(3);
println("After subtracting 3: " + calc.value);
calc.divide(2);
println("After dividing by 2: " + calc.value);
// Set value directly
calc.value = 100;
println("After setting value to 100: " + calc.value);
// Clear the calculator
calc.clear();
println("After clearing: " + calc.value);
// Chain operations
let result = calc.add(10).multiply(2).subtract(5).divide(3);
println("Result of chained operations: " + result);
println("Final calculator value: " + calc.value);

View File

@@ -0,0 +1,97 @@
use rhai::{Engine, EvalAltResult, CustomType, TypeBuilder};
use rhai_autobind_macros::rhai_model_export;
use serde::{Deserialize, Serialize};
use std::path::Path;
// Dummy DB type for the example, as rhai_model_export requires a db_type
struct DummyDb;
impl DummyDb {
fn new() -> Self { DummyDb }
}
// Define a simple Calculator struct with the rhai_autobind attribute
#[derive(Debug, Clone, Serialize, Deserialize, rhai::CustomType)]
#[rhai_model_export(db_type = "DummyDb")] // Provide the required db_type
pub struct Calculator {
pub value: f64,
}
impl Calculator {
// Constructor
pub fn new() -> Self {
Calculator { value: 0.0 }
}
// Add method
pub fn add(&mut self, x: f64) -> f64 {
self.value += x;
self.value
}
// Subtract method
pub fn subtract(&mut self, x: f64) -> f64 {
self.value -= x;
self.value
}
// Multiply method
pub fn multiply(&mut self, x: f64) -> f64 {
self.value *= x;
self.value
}
// Divide method
pub fn divide(&mut self, x: f64) -> f64 {
if x == 0.0 {
println!("Error: Division by zero!");
return self.value;
}
self.value /= x;
self.value
}
// Clear method
pub fn clear(&mut self) -> f64 {
self.value = 0.0;
self.value
}
// Get value method
pub fn get_value(&self) -> f64 {
self.value
}
}
fn main() -> Result<(), Box<EvalAltResult>> {
println!("Rhai Calculator Example");
println!("======================");
// Create a new Rhai engine
let mut engine = Engine::new();
// Register the Calculator type with the engine using the generated function
let dummy_db = DummyDb::new(); // Create an instance of the dummy DB
Calculator::register_rhai_bindings_for_calculator(&mut engine, dummy_db);
// Register print function for output
engine.register_fn("println", |s: &str| println!("{}", s));
// Create a calculator instance to demonstrate it works
let calc = Calculator::new();
println!("Initial value: {}", calc.value);
// Load and run the Rhai script
let script_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples")
.join("rhai_autobind_example")
.join("calculator.rhai");
println!("Loading Rhai script from: {}", script_path.display());
match engine.eval_file::<()>(script_path) {
Ok(_) => println!("Script executed successfully"),
Err(e) => eprintln!("Error executing script: {}\nAt: {:?}", e, e.position()),
}
Ok(())
}

View File

@@ -0,0 +1,152 @@
use proc_macro::TokenStream;
use quote::{quote, format_ident};
use syn::{parse_macro_input, Ident, Fields, Visibility, ItemStruct, LitStr, ExprArray, Expr, Lit};
use syn::spanned::Spanned; // Add the Spanned trait for span() method
use heck::ToSnakeCase; // For converting struct name to snake_case for function names
#[derive(Debug)]
struct MacroArgs {
db_type: syn::Type,
collection_name: Option<String>,
methods: Vec<String>, // To store method names to be registered
}
impl syn::parse::Parse for MacroArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut db_type_str: Option<LitStr> = None;
let mut collection_name_str: Option<LitStr> = None;
let mut methods_array: Option<ExprArray> = None;
while !input.is_empty() {
let ident: Ident = input.parse()?;
let _eq_token: syn::token::Eq = input.parse()?;
if ident == "db_type" {
db_type_str = Some(input.parse()?);
} else if ident == "collection_name" {
collection_name_str = Some(input.parse()?);
} else if ident == "methods" {
methods_array = Some(input.parse()?);
} else {
return Err(syn::Error::new(ident.span(), "Unknown attribute argument"));
}
if !input.is_empty() {
let _comma: syn::token::Comma = input.parse()?;
}
}
let db_type_str = db_type_str.ok_or_else(|| syn::Error::new(input.span(), "Missing required attribute `db_type`"))?;
let db_type: syn::Type = syn::parse_str(&db_type_str.value())?;
let collection_name = collection_name_str.map(|s| s.value());
let mut methods = Vec::new();
if let Some(array_expr) = methods_array {
for expr in array_expr.elems {
if let Expr::Lit(expr_lit) = expr {
if let Lit::Str(lit_str) = expr_lit.lit {
methods.push(lit_str.value());
} else {
return Err(syn::Error::new(expr_lit.lit.span(), "Method name must be a string literal"));
}
} else {
return Err(syn::Error::new(expr.span(), "Method name must be a string literal"));
}
}
}
Ok(MacroArgs { db_type, collection_name, methods })
}
}
#[proc_macro_attribute]
pub fn rhai_model_export(attr: TokenStream, item: TokenStream) -> TokenStream {
let macro_args = parse_macro_input!(attr as MacroArgs);
let input_struct = parse_macro_input!(item as ItemStruct);
let struct_name = &input_struct.ident;
let struct_name_str = struct_name.to_string();
let struct_name_snake_case = struct_name_str.to_snake_case(); // Use heck for snake_case
let db_type = macro_args.db_type;
let collection_name = macro_args.collection_name.unwrap_or_else(|| {
// Basic pluralization
if struct_name_snake_case.ends_with('y') {
format!("{}ies", &struct_name_snake_case[..struct_name_snake_case.len()-1])
} else if struct_name_snake_case.ends_with('s') {
format!("{}es", struct_name_snake_case)
} else {
format!("{}s", struct_name_snake_case)
}
});
let generated_registration_fn_name = format_ident!("register_rhai_bindings_for_{}", struct_name_snake_case);
let constructor_fn_name = format!("new_{}", struct_name_snake_case);
let mut field_registrations = Vec::new();
if let Fields::Named(fields) = &input_struct.fields {
for field in fields.named.iter() {
if let (Some(field_name), Visibility::Public(_)) = (&field.ident, &field.vis) {
let field_name_str = field_name.to_string();
let field_type = &field.ty;
// Register getter
field_registrations.push(quote! {
engine.register_get(#field_name_str, |s: &mut #struct_name| s.#field_name.clone());
});
// Register setter
field_registrations.push(quote! {
engine.register_set(#field_name_str, |s: &mut #struct_name, val: #field_type| s.#field_name = val);
});
}
}
}
let mut method_registrations = Vec::new();
for method_name_str in &macro_args.methods {
let method_ident = format_ident!("{}", method_name_str);
let rhai_method_name = method_name_str.clone(); // Rhai function name can be the same as Rust method name
method_registrations.push(quote! {
engine.register_fn(#rhai_method_name, #struct_name::#method_ident);
});
}
// Create a formatted string of method names for printing
let methods_str = format!("{:?}", macro_args.methods);
let output = quote! {
#input_struct // Re-emit the original struct definition
impl #struct_name {
pub fn #generated_registration_fn_name(
engine: &mut rhai::Engine,
db: #db_type // db is now passed but not used by default for non-DB methods
) {
// For rhai_wrapper if/when DB operations are added
// use rhai_wrapper::*;
engine.build_type::<#struct_name>();
// Register constructor (e.g., new_calculator for Calculator::new())
engine.register_fn(#constructor_fn_name, #struct_name::new);
// Register field getters and setters
#(#field_registrations)*
// Register specified instance methods
#(#method_registrations)*
// Placeholder for DB function registrations
// e.g., get_by_id, get_all, save, delete
println!("Registered {} with Rhai (collection: '{}'). Constructor: '{}'. Methods: {}. Fields accessible.",
#struct_name_str,
#collection_name,
#constructor_fn_name,
#methods_str
);
}
}
};
TokenStream::from(output)
}

BIN
_archive/talk/.DS_Store vendored Normal file

Binary file not shown.

2146
_archive/talk/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

14
_archive/talk/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "talk"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.4.0"
actix-files = "0.6.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rhai = "1.18.0"
heromodels = { path = "../../db/heromodels" }
rhai_wrapper = { path = "../rhai_wrapper" }
chrono = { version = "0.4", features = ["serde"] }

331
_archive/talk/src/main.rs Normal file
View File

@@ -0,0 +1,331 @@
use actix_files::Files;
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use heromodels::db::hero::OurDB;
use heromodels::models::calendar::Calendar;
use heromodels::models::governance::{Proposal, Ballot, VoteOption, ProposalStatus, VoteEventStatus};
use rhai::Engine;
use rhai_wrapper::wrap_vec_return;
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use chrono::{Utc, Duration};
#[derive(Deserialize)]
struct ScriptRequest {
script: String,
model_type: String,
}
#[derive(Serialize)]
struct ScriptResponse {
output: String,
success: bool,
}
// Function to set up the calendar model in the Rhai engine
fn setup_calendar_engine(engine: &mut Engine, db: Arc<OurDB>) {
// Register the Calendar type with Rhai
Calendar::register_rhai_bindings_for_calendar(engine, db.clone());
// Register a function to get the database instance
engine.register_fn("get_db", move || db.clone());
// Register a calendar builder function
engine.register_fn("calendar__builder", |id: i64| {
Calendar::new(id as u32)
});
// Register setter methods for Calendar properties
engine.register_fn("set_description", |calendar: &mut Calendar, desc: String| {
calendar.description = Some(desc);
});
// Register getter methods for Calendar properties
engine.register_fn("get_description", |calendar: Calendar| -> String {
calendar.description.clone().unwrap_or_default()
});
// Register getter for base_data.id
engine.register_fn("get_id", |calendar: Calendar| -> i64 {
calendar.base_data.id as i64
});
// Register additional functions needed by the script
engine.register_fn("set_calendar", |_db: Arc<OurDB>, _calendar: Calendar| {
// In a real implementation, this would save the calendar to the database
println!("Calendar saved: {}", _calendar.name);
});
engine.register_fn("get_calendar_by_id", |_db: Arc<OurDB>, id: i64| -> Calendar {
// In a real implementation, this would retrieve the calendar from the database
Calendar::new(id as u32)
});
// Register a function to check if a calendar exists
engine.register_fn("calendar_exists", |_db: Arc<OurDB>, id: i64| -> bool {
// In a real implementation, this would check if the calendar exists in the database
id == 1 || id == 2
});
// Define the function separately to use with the wrap_vec_return macro
fn get_all_calendars(_db: Arc<OurDB>) -> Vec<Calendar> {
// In a real implementation, this would retrieve all calendars from the database
vec![Calendar::new(1), Calendar::new(2)]
}
// Register the function with the wrap_vec_return macro
engine.register_fn("get_all_calendars", wrap_vec_return!(get_all_calendars, Arc<OurDB> => Calendar));
engine.register_fn("delete_calendar_by_id", |_db: Arc<OurDB>, _id: i64| {
// In a real implementation, this would delete the calendar from the database
println!("Calendar deleted with ID: {}", _id);
});
}
// Function to set up the governance model in the Rhai engine
fn setup_governance_engine(engine: &mut Engine, db: Arc<OurDB>) {
// Register the Proposal type with Rhai
Proposal::register_rhai_bindings_for_proposal(engine, db.clone());
// Register the Ballot type with Rhai
Ballot::register_rhai_bindings_for_ballot(engine, db.clone());
// Register a function to get the database instance
engine.register_fn("get_db", move || db.clone());
// Register builder functions for Proposal and related types
engine.register_fn("create_proposal", |id: i64, creator_id: String, title: String, description: String| {
let start_date = Utc::now();
let end_date = start_date + Duration::days(14);
Proposal::new(id as u32, creator_id, title, description, start_date, end_date)
});
engine.register_fn("create_vote_option", |id: i64, text: String| {
VoteOption::new(id as u8, text)
});
engine.register_fn("create_ballot", |id: i64, user_id: i64, vote_option_id: i64, shares_count: i64| {
Ballot::new(id as u32, user_id as u32, vote_option_id as u8, shares_count)
});
// Register getter and setter methods for Proposal properties
engine.register_fn("get_title", |proposal: Proposal| -> String {
proposal.title.clone()
});
engine.register_fn("get_description", |proposal: Proposal| -> String {
proposal.description.clone()
});
engine.register_fn("get_creator_id", |proposal: Proposal| -> String {
proposal.creator_id.clone()
});
engine.register_fn("get_id", |proposal: Proposal| -> i64 {
proposal.base_data.id as i64
});
engine.register_fn("get_status", |proposal: Proposal| -> String {
format!("{:?}", proposal.status)
});
engine.register_fn("get_vote_status", |proposal: Proposal| -> String {
format!("{:?}", proposal.vote_status)
});
// Register methods for proposal operations
engine.register_fn("add_option_to_proposal", |proposal: Proposal, option_id: i64, option_text: String| -> Proposal {
proposal.add_option(option_id as u8, option_text)
});
engine.register_fn("cast_vote_on_proposal", |proposal: Proposal, ballot_id: i64, user_id: i64, option_id: i64, shares: i64| -> Proposal {
proposal.cast_vote(ballot_id as u32, user_id as u32, option_id as u8, shares)
});
engine.register_fn("change_proposal_status", |proposal: Proposal, status_str: String| -> Proposal {
let new_status = match status_str.as_str() {
"Draft" => ProposalStatus::Draft,
"Active" => ProposalStatus::Active,
"Approved" => ProposalStatus::Approved,
"Rejected" => ProposalStatus::Rejected,
"Cancelled" => ProposalStatus::Cancelled,
_ => ProposalStatus::Draft,
};
proposal.change_proposal_status(new_status)
});
engine.register_fn("change_vote_event_status", |proposal: Proposal, status_str: String| -> Proposal {
let new_status = match status_str.as_str() {
"Open" => VoteEventStatus::Open,
"Closed" => VoteEventStatus::Closed,
"Cancelled" => VoteEventStatus::Cancelled,
_ => VoteEventStatus::Open,
};
proposal.change_vote_event_status(new_status)
});
// Register functions for database operations
engine.register_fn("save_proposal", |_db: Arc<OurDB>, proposal: Proposal| {
println!("Proposal saved: {}", proposal.title);
});
engine.register_fn("get_proposal_by_id", |_db: Arc<OurDB>, id: i64| -> Proposal {
// In a real implementation, this would retrieve the proposal from the database
let start_date = Utc::now();
let end_date = start_date + Duration::days(14);
Proposal::new(id as u32, "Retrieved Creator", "Retrieved Proposal", "Retrieved Description", start_date, end_date)
});
// Register a function to check if a proposal exists
engine.register_fn("proposal_exists", |_db: Arc<OurDB>, id: i64| -> bool {
// In a real implementation, this would check if the proposal exists in the database
id == 1 || id == 2
});
// Define the function for get_all_proposals
fn get_all_proposals(_db: Arc<OurDB>) -> Vec<Proposal> {
// In a real implementation, this would retrieve all proposals from the database
let start_date = Utc::now();
let end_date = start_date + Duration::days(14);
vec![
Proposal::new(1, "Creator 1", "Proposal 1", "Description 1", start_date, end_date),
Proposal::new(2, "Creator 2", "Proposal 2", "Description 2", start_date, end_date)
]
}
// Register the function with the wrap_vec_return macro
engine.register_fn("get_all_proposals", wrap_vec_return!(get_all_proposals, Arc<OurDB> => Proposal));
engine.register_fn("delete_proposal_by_id", |_db: Arc<OurDB>, _id: i64| {
// In a real implementation, this would delete the proposal from the database
println!("Proposal deleted with ID: {}", _id);
});
// Register helper functions for accessing proposal options and ballots
engine.register_fn("get_option_count", |proposal: Proposal| -> i64 {
proposal.options.len() as i64
});
engine.register_fn("get_option_at", |proposal: Proposal, index: i64| -> VoteOption {
if index >= 0 && index < proposal.options.len() as i64 {
proposal.options[index as usize].clone()
} else {
VoteOption::new(0, "Invalid Option")
}
});
engine.register_fn("get_option_text", |option: VoteOption| -> String {
option.text.clone()
});
engine.register_fn("get_option_votes", |option: VoteOption| -> i64 {
option.count
});
engine.register_fn("get_ballot_count", |proposal: Proposal| -> i64 {
proposal.ballots.len() as i64
});
engine.register_fn("get_ballot_at", |proposal: Proposal, index: i64| -> Ballot {
if index >= 0 && index < proposal.ballots.len() as i64 {
proposal.ballots[index as usize].clone()
} else {
Ballot::new(0, 0, 0, 0)
}
});
engine.register_fn("get_ballot_user_id", |ballot: Ballot| -> i64 {
ballot.user_id as i64
});
engine.register_fn("get_ballot_option_id", |ballot: Ballot| -> i64 {
ballot.vote_option_id as i64
});
engine.register_fn("get_ballot_shares", |ballot: Ballot| -> i64 {
ballot.shares_count
});
}
// Endpoint to execute Rhai scripts
async fn execute_script(req: web::Json<ScriptRequest>) -> impl Responder {
// Create a string to capture stdout
let output = Arc::new(Mutex::new(String::new()));
// Initialize Rhai engine
let mut engine = Engine::new();
// Register print function to capture output
let output_clone = output.clone();
engine.register_fn("print", move |text: String| {
if let Ok(mut output_guard) = output_clone.lock() {
output_guard.push_str(&text);
output_guard.push('\n');
}
});
// Initialize database
let db = Arc::new(OurDB::new("temp_rhai_playground_db", true).expect("Failed to create database"));
// Set up the engine based on the model type
match req.model_type.as_str() {
"calendar" => setup_calendar_engine(&mut engine, db),
"governance" => setup_governance_engine(&mut engine, db),
_ => {
return HttpResponse::BadRequest().json(ScriptResponse {
output: "Invalid model type. Supported types: 'calendar', 'governance'".to_string(),
success: false,
});
}
}
// Execute the script
match engine.eval::<()>(&req.script) {
Ok(_) => {
let output_str = output.lock().unwrap_or_else(|_| panic!("Failed to lock output")).clone();
HttpResponse::Ok().json(ScriptResponse {
output: output_str,
success: true,
})
}
Err(e) => {
HttpResponse::Ok().json(ScriptResponse {
output: format!("Script execution failed: {}", e),
success: false,
})
}
}
}
// Endpoint to get example scripts
async fn get_example_script(path: web::Path<String>) -> impl Responder {
let script_type = path.into_inner();
let script_content = match script_type.as_str() {
"calendar" => {
std::fs::read_to_string("../../db/heromodels/examples/calendar_rhai/calendar.rhai")
.unwrap_or_else(|_| "// Failed to load calendar example".to_string())
}
"governance" => {
std::fs::read_to_string("../../db/heromodels/examples/governance_rhai/governance.rhai")
.unwrap_or_else(|_| "// Failed to load governance example".to_string())
}
_ => "// Invalid example type".to_string(),
};
HttpResponse::Ok().body(script_content)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("Starting Rhai Web Playground server at http://localhost:8080");
HttpServer::new(|| {
App::new()
.service(web::resource("/api/execute").route(web::post().to(execute_script)))
.service(web::resource("/api/example/{script_type}").route(web::get().to(get_example_script)))
.service(Files::new("/", "./static").index_file("index.html"))
})
.bind("127.0.0.1:8080")?
.run()
.await
}

View File

@@ -0,0 +1,235 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rhai Script Playground</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.2/lib/codemirror.css">
<link rel="stylesheet" href="style.css">
<style>
.btn-success { background-color: #20c997 !important; border-color: #20c997 !important; color: white !important; }
.btn-danger { background-color: #f87171 !important; border-color: #f87171 !important; color: white !important; }
.btn-warning { background-color: #ffc107 !important; border-color: #ffc107 !important; color: black !important; }
/* Toast styling */
.toast-container {
z-index: 1050;
}
.toast {
min-width: 300px;
opacity: 1 !important;
}
</style>
</head>
<body>
<div class="container-fluid">
<header class="py-3 mb-3 border-bottom">
<h1 class="text-center">Rhai Script Talk</h1>
</header>
<div class="row">
<div class="col-md-6 pe-md-2">
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Script Input</h5>
<div>
<select id="modelSelect" class="form-select form-select-sm">
<option value="calendar">Calendar Model</option>
<option value="governance">Governance Model</option>
</select>
</div>
</div>
<div class="card-body p-0">
<textarea id="scriptInput"></textarea>
</div>
<div class="card-footer d-flex justify-content-between">
<button id="loadExampleBtn" class="btn btn-outline-secondary">Load Example</button>
<button id="runScriptBtn" class="btn btn-primary">Run Script</button>
</div>
</div>
</div>
<div class="col-md-6 ps-md-2">
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Output</h5>
<div class="d-flex align-items-center">
<div class="input-group me-2" style="width: 250px;">
<select id="listenerSelect" class="form-select form-select-sm">
<option value="">Custom URL...</option>
<option value="http://localhost:3000/governance">Governance Listener</option>
<option value="http://localhost:3000/finance">Finance Listener</option>
<option value="http://localhost:3000/calendar">Calendar Listener</option>
</select>
<input type="text" id="listenerUrl" class="form-control form-control-sm" placeholder="Listener URL" style="display: none;">
</div>
<button id="connectListenerBtn" class="btn btn-sm btn-outline-secondary d-flex align-items-center">
<span id="statusCircle" class="me-2" style="width: 8px; height: 8px; border-radius: 50%; background-color: #6c757d; display: inline-block;"></span>
<span id="connectBtnText">Connect</span>
</button>
</div>
</div>
<div class="card-body p-0">
<pre id="outputArea" class="p-3"></pre>
</div>
</div>
</div>
</div>
</div>
<!-- Toast container for notifications -->
<div class="toast-container position-fixed bottom-0 end-0 p-3"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const listenerUrlInput = document.getElementById('listenerUrl');
const listenerSelect = document.getElementById('listenerSelect');
const connectListenerBtn = document.getElementById('connectListenerBtn');
const statusCircle = document.getElementById('statusCircle');
const toastContainer = document.querySelector('.toast-container');
// Handle listener selection dropdown
listenerSelect.addEventListener('change', () => {
const selectedValue = listenerSelect.value;
if (selectedValue === '') {
// Show custom URL input when "Custom URL..." is selected
listenerUrlInput.style.display = 'block';
listenerUrlInput.focus();
} else {
// Hide custom URL input when a predefined option is selected
listenerUrlInput.style.display = 'none';
// Clear any previous custom URL
listenerUrlInput.value = '';
}
});
// Function to show toast notifications
function showToast(message, type = 'error') {
const toastId = 'toast-' + Date.now();
const bgClass = type === 'error' ? 'bg-danger' : (type === 'warning' ? 'bg-warning' : 'bg-info');
const headerText = type === 'error' ? 'Error' : (type === 'warning' ? 'Warning' : 'Info');
// Create the toast element
const toastDiv = document.createElement('div');
toastDiv.className = 'toast mb-3';
toastDiv.id = toastId;
toastDiv.setAttribute('role', 'alert');
toastDiv.setAttribute('aria-live', 'assertive');
toastDiv.setAttribute('aria-atomic', 'true');
toastDiv.innerHTML = `
<div class="toast-header ${bgClass} text-white">
<strong class="me-auto">${headerText}</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
${message}
</div>
`;
// Add to container
toastContainer.appendChild(toastDiv);
// Initialize and show the toast
const toast = new bootstrap.Toast(toastDiv, {
autohide: true,
delay: 10000
});
toast.show();
// Remove the toast element after it's hidden
toastDiv.addEventListener('hidden.bs.toast', () => {
toastDiv.remove();
});
}
connectListenerBtn.addEventListener('click', async () => {
// Get URL from either dropdown or text input
let url = listenerSelect.value;
// If custom URL option is selected, use the text input value
if (url === '') {
url = listenerUrlInput.value.trim();
if (!url) {
showToast('Please enter a listener URL.');
return;
}
}
const connectBtnText = document.getElementById('connectBtnText');
const statusCircle = document.getElementById('statusCircle');
// Set to connecting state
connectBtnText.textContent = 'Connecting...';
connectListenerBtn.classList.remove('btn-outline-secondary', 'btn-success', 'btn-danger');
connectListenerBtn.classList.add('btn-warning');
statusCircle.style.backgroundColor = '#e6a800'; // Darker yellow for the circle
// Disable the button during connection attempts
connectListenerBtn.disabled = true;
// Function to attempt a ping
const attemptPing = async () => {
try {
const response = await fetch(url, {
method: 'HEAD',
mode: 'no-cors',
// Set a short timeout to fail faster
signal: AbortSignal.timeout(800)
});
return true; // Connection successful
} catch (error) {
console.error('Ping attempt failed:', error);
return false; // Connection failed
}
};
// Try pinging multiple times with 1-second intervals
let isConnected = false;
const maxAttempts = 5;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
connectBtnText.textContent = `Connecting... (${attempt}/${maxAttempts})`;
isConnected = await attemptPing();
if (isConnected) {
break; // Exit the loop if connection is successful
}
if (attempt < maxAttempts) {
// Wait 1 second before the next attempt
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
// Re-enable the button
connectListenerBtn.disabled = false;
if (isConnected) {
// Set to connected state
connectBtnText.textContent = 'Connected';
connectListenerBtn.classList.remove('btn-outline-secondary', 'btn-warning', 'btn-danger');
connectListenerBtn.classList.add('btn-success');
statusCircle.style.backgroundColor = '#198754'; // Darker green for the circle
} else {
// Set to offline state
connectBtnText.textContent = 'Connect';
connectListenerBtn.classList.remove('btn-outline-secondary', 'btn-warning', 'btn-success');
connectListenerBtn.classList.add('btn-danger');
statusCircle.style.backgroundColor = '#dc3545'; // Darker red for the circle
// Show error toast
showToast(`Could not connect to ${url}. Please check the URL and try again.`);
}
});
});
</script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.2/lib/codemirror.js"></script>
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.2/mode/javascript/javascript.js"></script>
<script src="script.js"></script>
</body>
</html>

View File

@@ -0,0 +1,131 @@
document.addEventListener('DOMContentLoaded', () => {
// Initialize CodeMirror editor
const editor = CodeMirror.fromTextArea(document.getElementById('scriptInput'), {
mode: 'javascript',
lineNumbers: true,
theme: 'default',
indentUnit: 4,
tabSize: 4,
lineWrapping: true,
autoCloseBrackets: true,
matchBrackets: true
});
// Get DOM elements
const runScriptBtn = document.getElementById('runScriptBtn');
const loadExampleBtn = document.getElementById('loadExampleBtn');
const modelSelect = document.getElementById('modelSelect');
const outputArea = document.getElementById('outputArea');
const toastContainer = document.querySelector('.toast-container');
// Function to show toast notifications
function showToast(message, type = 'error') {
const toastId = 'toast-' + Date.now();
const bgClass = type === 'error' ? 'bg-danger' : (type === 'warning' ? 'bg-warning' : 'bg-info');
const headerText = type === 'error' ? 'Error' : (type === 'warning' ? 'Warning' : 'Info');
// Create the toast element
const toastDiv = document.createElement('div');
toastDiv.className = 'toast mb-3';
toastDiv.id = toastId;
toastDiv.setAttribute('role', 'alert');
toastDiv.setAttribute('aria-live', 'assertive');
toastDiv.setAttribute('aria-atomic', 'true');
toastDiv.innerHTML = `
<div class="toast-header ${bgClass} text-white">
<strong class="me-auto">${headerText}</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
${message}
</div>
`;
// Add to container
toastContainer.appendChild(toastDiv);
// Initialize and show the toast
const toast = new bootstrap.Toast(toastDiv, {
autohide: true,
delay: 10000
});
toast.show();
// Remove the toast element after it's hidden
toastDiv.addEventListener('hidden.bs.toast', () => {
toastDiv.remove();
});
}
// Function to run the script
async function runScript() {
const script = editor.getValue();
const modelType = modelSelect.value;
if (!script.trim()) {
showToast('Please enter a script to run.');
return;
}
outputArea.textContent = 'Running script...';
try {
const response = await fetch('/api/execute', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
script,
model_type: modelType
})
});
const result = await response.json();
if (result.success) {
outputArea.textContent = result.output || 'Script executed successfully with no output.';
} else {
outputArea.textContent = result.output || 'Script execution failed.';
showToast(result.error || 'Script execution failed.', 'error');
}
} catch (error) {
outputArea.textContent = 'Script execution failed. See error details.';
showToast(`Error: ${error.message}`, 'error');
}
}
// Function to load example script
async function loadExample() {
const modelType = modelSelect.value;
try {
const response = await fetch(`/api/example/${modelType}`);
if (!response.ok) {
throw new Error(`Server returned ${response.status}: ${response.statusText}`);
}
const exampleScript = await response.text();
editor.setValue(exampleScript);
outputArea.textContent = `Loaded ${modelType} example script. Click "Run Script" to execute.`;
} catch (error) {
// Don't show error in output area, use toast instead
showToast(`Failed to load ${modelType} example: ${error.message}`, 'error');
// Clear the editor if it was a new load (not if there was already content)
if (!editor.getValue().trim()) {
editor.setValue('// Write your Rhai script here');
}
}
}
// Event listeners
runScriptBtn.addEventListener('click', runScript);
loadExampleBtn.addEventListener('click', loadExample);
// Load default example on page load
loadExample();
});

View File

@@ -0,0 +1,49 @@
.card {
height: calc(100vh - 150px);
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card-header {
background-color: #f1f3f5;
border-bottom: 1px solid #dee2e6;
}
.card-body {
overflow: hidden;
height: calc(100% - 110px);
}
.card-footer {
background-color: #f8f9fa;
border-top: 1px solid #dee2e6;
padding: 10px 15px;
}
.CodeMirror {
height: 100%;
font-size: 14px;
font-family: 'Courier New', monospace;
}
#outputArea {
height: 100%;
overflow: auto;
margin: 0;
font-family: 'Courier New', monospace;
font-size: 14px;
white-space: pre-wrap;
background-color: #f8f9fa;
color: #212529;
}
#modelSelect {
width: auto;
}
@media (max-width: 767.98px) {
.card {
height: 400px;
margin-bottom: 20px;
}
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
2