Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1066,7 +1066,14 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result<Command> {
.short('t')
.long("template")
.help("Template to create")
.value_parser(["blank", "chat", "echo", "fibonacci", "file-transfer"])
.value_parser([
"blank",
"chat",
"echo",
"fibonacci",
"file-transfer",
"hyperapp-todo"
])
.default_value("chat")
)
.arg(Arg::new("UI")
Expand Down
5 changes: 4 additions & 1 deletion src/new/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub enum Template {
Echo,
Fibonacci,
FileTransfer,
HyperappTodo,
}

impl Language {
Expand All @@ -44,6 +45,7 @@ impl Template {
Template::Echo => "echo",
Template::Fibonacci => "fibonacci",
Template::FileTransfer => "file-transfer",
Template::HyperappTodo => "hyperapp-todo",
}
.to_string()
}
Expand All @@ -68,7 +70,8 @@ impl From<&String> for Template {
"echo" => Template::Echo,
"fibonacci" => Template::Fibonacci,
"file-transfer" => Template::FileTransfer,
_ => panic!("kit: template must be 'blank', 'chat', 'echo', or 'fibonacci'; not '{s}'"),
"hyperapp-todo" => Template::HyperappTodo,
_ => panic!("kit: template must be 'blank', 'chat', 'echo', 'fibonacci', 'file-transfer', or 'hyperapp-todo'; not '{s}'"),
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/new/templates/rust/ui/hyperapp-todo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
api
!test/hyperapp-todo-test/api
*swp
**/target
pkg/*.wasm
pkg/*.zip
pkg/ui
crates/
caller-utils/
**/.DS_Store
11 changes: 11 additions & 0 deletions src/new/templates/rust/ui/hyperapp-todo/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[profile.release]
lto = true
opt-level = "s"
panic = "abort"

[workspace]
members = [
"hyperapp-todo",
"test/hyperapp-todo-test/hyperapp-todo-test",
]
resolver = "2"
35 changes: 35 additions & 0 deletions src/new/templates/rust/ui/hyperapp-todo/hyperapp-todo/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[dependencies]
anyhow = "1.0.97"
process_macros = "0.1"
rmp-serde = "1.3.0"
serde_json = "1.0"
wit-bindgen = "0.36.0"
uuid = "1.4.1"



[dependencies.hyperprocess_macro]
git = "https://github.com/hyperware-ai/hyperprocess-macro"
rev = "47400ab"

[dependencies.hyperware_app_common]
git = "https://github.com/hyperware-ai/hyperprocess-macro"
rev = "47400ab"

[dependencies.serde]
features = ["derive"]
version = "1.0"

[features]
simulation-mode = []

[lib]
crate-type = ["cdylib"]

[package]
edition = "2021"
name = "hyperapp-todo"
version = "0.1.0"

[package.metadata.component]
package = "hyperware:process"
105 changes: 105 additions & 0 deletions src/new/templates/rust/ui/hyperapp-todo/hyperapp-todo/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use hyperprocess_macro::hyperprocess;
use serde::{Deserialize, Serialize};
use uuid::Uuid; // Keep Uuid

// --- Todo Item ---
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub struct TodoItem {
id: String,
text: String,
completed: bool,
}

// --- State ---
// Add Clone for potential use in export/internal logic if needed.
#[derive(PartialEq, Clone, Default, Debug, Serialize, Deserialize)]
pub struct HyperappTodoState {
tasks: Vec<TodoItem>,
}

// --- Hyperware Process ---
#[hyperprocess(
name = "hyperapp-todo",
ui = Some(HttpBindingConfig::default()),
endpoints = vec![
Binding::Http {
path: "/api",
config: HttpBindingConfig::new(false, false, false, None),
},
Binding::Ws {
path: "/ws",
config: WsBindingConfig::new(false, false, false),
}
],
save_config = SaveOptions::EveryMessage,
wit_world = "hyperapp-todo-template-dot-os-v0"
)]

// --- Hyperware Process API definitions ---
impl HyperappTodoState {
#[init]
async fn initialize(&mut self) {
println!("Initializing todo list state");
self.tasks = Vec::new();
}

// Add a new task
#[http]
async fn add_task(&mut self, text: String) -> Result<TodoItem, String> {
if text.trim().is_empty() {
return Err("Task text cannot be empty".to_string());
}
let new_task = TodoItem {
id: Uuid::new_v4().to_string(),
text,
completed: false,
};
self.tasks.push(new_task.clone());
println!("Added task: {:?}", new_task);
Ok(new_task)
}

// Get all tasks
#[http]
async fn get_tasks(&self, request: String) -> Result<Vec<TodoItem>, String> {
println!("Request: {:?}", request);
println!("Fetching tasks");
Ok(self.tasks.clone())
}


// Toggle completion status of a task
#[http]
async fn toggle_task(&mut self, id: String) -> Result<TodoItem, String> {
if let Some(task) = self.tasks.iter_mut().find(|t| t.id == id) {
task.completed = !task.completed;
println!("Toggled task {:?}: completed={}", task.id, task.completed);
Ok(task.clone())
} else {
Err(format!("Task with ID {} not found", id))
}
}

// Export the current state (all tasks) as JSON bytes
#[local]
#[remote]
async fn export_state(&self) -> Result<HyperappTodoState, String> {
println!("Exporting tasks request received");
// Return the state directly instead of serializing it
Ok(self.clone())
}

// Import tasks from JSON bytes, replacing the current tasks
#[local]
async fn import_state(&mut self, data: Vec<u8>) -> Result<bool, String> {
println!("Importing tasks request received");
// Deserialize the data into the state struct using from_slice for Vec<u8>
let imported_state: HyperappTodoState = serde_json::from_slice(&data)
.map_err(|e| format!("Failed to deserialize state data: {}", e))?;
// Replace the current tasks with the imported ones
self.tasks = imported_state.tasks;
println!("Tasks imported successfully");
Ok(true)
}

}
19 changes: 19 additions & 0 deletions src/new/templates/rust/ui/hyperapp-todo/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "Hyperapp Todo List",
"description": "Template process for hyperapp todo app.",
"image": "",
"properties": {
"package_name": "hyperapp-todo",
"current_version": "0.1.0",
"publisher": "template.os",
"mirrors": [],
"code_hashes": {
"0.1.0": ""
},
"wit_version": 1,
"dependencies": [
]
},
"external_url": "https://hyperware.ai",
"animation_url": ""
}
21 changes: 21 additions & 0 deletions src/new/templates/rust/ui/hyperapp-todo/pkg/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[
{
"process_name": "hyperapp-todo",
"process_wasm_path": "/hyperapp-todo.wasm",
"on_exit": "Restart",
"request_networking": true,
"request_capabilities": [
"homepage:homepage:sys",
"http-client:distro:sys",
"http-server:distro:sys",
"vfs:distro:sys"
],
"grant_capabilities": [
"homepage:homepage:sys",
"http-client:distro:sys",
"http-server:distro:sys",
"vfs:distro:sys"
],
"public": false
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[workspace]
resolver = "2"
members = [
"hyperapp-todo-test",
]

[profile.release]
panic = "abort"
opt-level = "s"
lto = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
world hyperapp-todo-test-template-dot-os-v0 {
import hyperapp-todo;
import tester;
include process-v1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "hyperapp-todo-test"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies.caller-utils]
path = "../../../target/caller-utils"

[dependencies.hyperware_app_common]
git = "https://github.com/hyperware-ai/hyperprocess-macro"
rev = "b6ad495"

[dependencies]
anyhow = "1.0"
process_macros = "0.1.0"
hyperware_process_lib = { git = "https://github.com/hyperware-ai/process_lib", features = ["logging"], rev = "b7c9d27" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
wit-bindgen = "0.36.0"

[lib]
crate-type = ["cdylib"]

[package.metadata.component]
package = "hyperware:process"
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use caller_utils::{HyperappTodoState, TodoItem};
use caller_utils::hyperapp_todo::{export_state_local_rpc, import_state_local_rpc};
// Add this import here, as fail! is expanded in this file
use crate::hyperware::process::tester::{FailResponse, Response as TesterResponse};

use serde_json;
mod tester_lib;


async_test_suite!(
"hyperapp-todo-test-template-dot-os-v0",

test_basic_math: async {
if 2 + 2 != 4 {
fail!("wrong result");
}
Ok(())
},

// Test importing and exporting state locally
test_import_export_state: async {
let address: Address = ("hyperapp-todo.os", "hyperapp-todo", "hyperapp-todo", "template.os").into();

// 1. Define initial state (dummy data)
let initial_state = HyperappTodoState {
tasks: vec![
TodoItem { id: "1".to_string(), text: "Task 1".to_string(), completed: false },
TodoItem { id: "2".to_string(), text: "Task 2".to_string(), completed: true },
],
};
print_to_terminal(0, &format!("Initial state: {:?}", initial_state));


// 2. Serialize the initial state for import
let import_data = serde_json::to_vec(&initial_state)
.map_err(|e| anyhow::anyhow!(format!("Failed to serialize initial state: {}", e)))?;
print_to_terminal(0, "Serialized initial state for import.");

// 3. Call import_state_local_rpc
let import_result = import_state_local_rpc(&address, import_data).await;
print_to_terminal(0, &format!("import_state_local_rpc result: {:?}", import_result));

// Assert import was successful
match import_result {
Ok(Ok(true)) => print_to_terminal(0, "Import successful (returned true)."),
Ok(Ok(false)) => {
fail!("import_state_local_rpc returned false");
}
Ok(Err(e)) => {
fail!(format!("import_state_local_rpc returned an error: {}", e));
}
Err(e) => {
fail!(format!("import_state_local_rpc failed (send error): {:?}", e));
}
}

// 4. Call export_state_local_rpc
let export_result = export_state_local_rpc(&address).await;
print_to_terminal(0, &format!("export_state_local_rpc result: {:?}", export_result));

// Assert export was successful and get data, handling errors first
let inner_result = match export_result {
Ok(res) => res,
Err(e) => {
fail!(format!("export_state_local_rpc failed (send error): {:?}", e));
}
};
let exported_data = match inner_result {
Ok(data) => data,
Err(e) => {
fail!(format!("export_state_local_rpc returned an error: {}", e));
}
};
print_to_terminal(0, "Exported state data received.");

// 5. Compare initial state with exported state manually
if initial_state.tasks.len() != exported_data.tasks.len() {
fail!(format!(
"Task list lengths differ. Expected: {}, Got: {}",
initial_state.tasks.len(),
exported_data.tasks.len()
));
}

// Iterate and compare each task field by field
for (initial_task, exported_task) in initial_state.tasks.iter().zip(exported_data.tasks.iter()) {
if initial_task.id != exported_task.id ||
initial_task.text != exported_task.text ||
initial_task.completed != exported_task.completed {
fail!(format!(
"Task mismatch detected.\nExpected Task: {:?}\nGot Task: {:?}",
initial_task, // Assumes TodoItem derives Debug
exported_task // Assumes TodoItem derives Debug
));
}
}
print_to_terminal(0, "Exported state matches initial state.");

Ok(())
},

);
Loading