diff --git a/src/main.rs b/src/main.rs index a49c9139..6a1b6dc1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1066,7 +1066,7 @@ async fn make_app(current_dir: &std::ffi::OsString) -> Result { .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-echo"]) .default_value("chat") ) .arg(Arg::new("UI") diff --git a/src/new/mod.rs b/src/new/mod.rs index 1b5f1e82..497b4440 100644 --- a/src/new/mod.rs +++ b/src/new/mod.rs @@ -23,6 +23,7 @@ pub enum Template { Echo, Fibonacci, FileTransfer, + HyperappEcho, } impl Language { @@ -44,6 +45,7 @@ impl Template { Template::Echo => "echo", Template::Fibonacci => "fibonacci", Template::FileTransfer => "file-transfer", + Template::HyperappEcho => "hyperapp-echo", } .to_string() } @@ -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-echo" => Template::HyperappEcho, + _ => panic!("kit: template must be 'blank', 'chat', 'echo', 'fibonacci', or 'hyperapp-echo'; not '{s}'"), } } } diff --git a/src/new/templates/rust/no-ui/hyperapp-echo/.gitignore b/src/new/templates/rust/no-ui/hyperapp-echo/.gitignore new file mode 100644 index 00000000..e0f310c9 --- /dev/null +++ b/src/new/templates/rust/no-ui/hyperapp-echo/.gitignore @@ -0,0 +1,10 @@ +api +!test/hyperapp-echo-test/api +*swp +**/target +pkg/*.wasm +pkg/*.zip +pkg/ui +crates/ +caller-utils/ +**/.DS_Store \ No newline at end of file diff --git a/src/new/templates/rust/no-ui/hyperapp-echo/Cargo.toml b/src/new/templates/rust/no-ui/hyperapp-echo/Cargo.toml new file mode 100644 index 00000000..614cee7d --- /dev/null +++ b/src/new/templates/rust/no-ui/hyperapp-echo/Cargo.toml @@ -0,0 +1,11 @@ +[profile.release] +lto = true +opt-level = "s" +panic = "abort" + +[workspace] +members = [ + "hyperapp-echo", + "test/hyperapp-echo-test/hyperapp-echo-test", +] +resolver = "2" diff --git a/src/new/templates/rust/no-ui/hyperapp-echo/hyperapp-echo/Cargo.toml b/src/new/templates/rust/no-ui/hyperapp-echo/hyperapp-echo/Cargo.toml new file mode 100644 index 00000000..81b1ddaf --- /dev/null +++ b/src/new/templates/rust/no-ui/hyperapp-echo/hyperapp-echo/Cargo.toml @@ -0,0 +1,31 @@ +[dependencies] +process_macros = "0.1" +serde_json = "1.0" +wit-bindgen = "0.36.0" + + +[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-echo" +version = "0.1.0" + +[package.metadata.component] +package = "hyperware:process" diff --git a/src/new/templates/rust/no-ui/hyperapp-echo/hyperapp-echo/src/lib.rs b/src/new/templates/rust/no-ui/hyperapp-echo/hyperapp-echo/src/lib.rs new file mode 100644 index 00000000..7369d5df --- /dev/null +++ b/src/new/templates/rust/no-ui/hyperapp-echo/hyperapp-echo/src/lib.rs @@ -0,0 +1,53 @@ +use serde::{Serialize, Deserialize}; +use hyperprocess_macro::hyperprocess; +use hyperware_process_lib::println; + +#[derive(Default, Debug, Serialize, Deserialize)] +pub struct HyperappEchoState {} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Argument { + header: String, + body: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ReturnValue { + response: String, +} + +#[hyperprocess( + name = "HyperappEcho", + ui = None, + 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::Never, + wit_world = "hyperapp-echo-template-dot-os-v0" +)] + +impl HyperappEchoState { + // Initialize the process, every application needs an init function + #[init] + async fn initialize(&mut self) { + println!("init HyperappEcho"); + } + + // Endpoint accepting both local, remote Hyperware requests, and HTTP requests + #[local] + #[remote] + #[http] + async fn echo(&self, arg: Argument) -> ReturnValue { + println!("header: {:?}, body: {:?}", arg.header, arg.body); + + ReturnValue { response: "Ack".to_string() } + } + +} diff --git a/src/new/templates/rust/no-ui/hyperapp-echo/metadata.json b/src/new/templates/rust/no-ui/hyperapp-echo/metadata.json new file mode 100644 index 00000000..b63ff4c2 --- /dev/null +++ b/src/new/templates/rust/no-ui/hyperapp-echo/metadata.json @@ -0,0 +1,20 @@ +{ + "name": "Hyperapp Echo", + "description": "Echo process for Hyperapp framework.", + "image": "", + "properties": { + "package_name": "hyperapp-echo", + "current_version": "0.1.0", + "publisher": "template.os", + "mirrors": [], + "code_hashes": { + "0.1.0": "" + }, + "wit_version": 1, + "dependencies": [ + "hyperapp-echo:template.os" + ] + }, + "external_url": "https://hyperware.ai", + "animation_url": "" +} diff --git a/src/new/templates/rust/no-ui/hyperapp-echo/pkg/manifest.json b/src/new/templates/rust/no-ui/hyperapp-echo/pkg/manifest.json new file mode 100644 index 00000000..98f17d2a --- /dev/null +++ b/src/new/templates/rust/no-ui/hyperapp-echo/pkg/manifest.json @@ -0,0 +1,23 @@ +[ + { + "process_name": "hyperapp-echo", + "process_wasm_path": "/hyperapp-echo.wasm", + "on_exit": "Restart", + "request_networking": true, + "request_capabilities": [ + "homepage:homepage:sys", + "http-client:distro:sys", + "http-server:distro:sys", + "terminal:terminal:sys", + "vfs:distro:sys" + ], + "grant_capabilities": [ + "homepage:homepage:sys", + "http-client:distro:sys", + "http-server:distro:sys", + "terminal:terminal:sys", + "vfs:distro:sys" + ], + "public": false + } +] diff --git a/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/Cargo.toml b/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/Cargo.toml new file mode 100644 index 00000000..06be6286 --- /dev/null +++ b/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] +resolver = "2" +members = [ + "hyperapp-echo-test", +] + +[profile.release] +panic = "abort" +opt-level = "s" +lto = true \ No newline at end of file diff --git a/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/api/hyperapp-echo-test-template-dot-os-v0.wit b/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/api/hyperapp-echo-test-template-dot-os-v0.wit new file mode 100644 index 00000000..ff00f125 --- /dev/null +++ b/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/api/hyperapp-echo-test-template-dot-os-v0.wit @@ -0,0 +1,5 @@ +world hyperapp-echo-test-template-dot-os-v0 { + import hyperapp-echo; + import tester; + include process-v1; +} \ No newline at end of file diff --git a/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/hyperapp-echo-test/Cargo.toml b/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/hyperapp-echo-test/Cargo.toml new file mode 100644 index 00000000..5b0b6481 --- /dev/null +++ b/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/hyperapp-echo-test/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "hyperapp-echo-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" +hyperware_process_lib = { git = "https://github.com/hyperware-ai/process_lib", features = ["logging"], rev = "b7c9d27" } +process_macros = "0.1.0" +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" diff --git a/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/hyperapp-echo-test/src/lib.rs b/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/hyperapp-echo-test/src/lib.rs new file mode 100644 index 00000000..be99297f --- /dev/null +++ b/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/hyperapp-echo-test/src/lib.rs @@ -0,0 +1,88 @@ +// Import necessary types and functions explicitly from caller_utils +use caller_utils::{Argument, ReturnValue}; // Import types directly (via path dependency) +use caller_utils::hyperapp_echo::{echo_local_rpc, echo_remote_rpc}; // Import RPC functions + +// Add this import here, as fail! is expanded in this file +use crate::hyperware::process::tester::{FailResponse, Response as TesterResponse}; + +mod tester_lib; + +async_test_suite!( + "hyperapp-echo-test-template-dot-os-v0", + + test_basic_math: async { + if 2 + 2 != 4 { + fail!("wrong result"); + } + Ok(()) + }, + + // Test local echo RPC call + test_echo_local_rpc: async { + // Define the target process address + let address: Address = ("hyperapp-echo.os", "hyperapp-echo", "hyperapp-echo", "template.os").into(); + // Define the argument for the echo function + let arg = Argument { + header: "LocalTestHeader".to_string(), + body: "LocalTestBody".to_string(), + }; + // Define the expected return value + let expected_return = ReturnValue { + response: "Ack".to_string(), + }; + + match echo_local_rpc(&address, arg).await { + Ok(actual_value) => { + // Compare the 'response' field directly + if actual_value.response != expected_return.response { + // fail! macro uses FailResponse/TesterResponse imported above + fail!(format!( + "echo_local_rpc unexpected result: expected {:?}, got {:?}", + expected_return, actual_value // Keep original structs for error message + )); + } + // If the result matches, the test passes for this step + Ok(()) + } + Err(e) => { + // Use fail! macro if the RPC call itself returned an error + fail!(format!("echo_local_rpc failed: {:?}", e)); + } + } + }, + + // Test remote echo RPC call + test_echo_remote_rpc: async { + // Define the target process address + let address: Address = ("hyperapp-echo.os", "hyperapp-echo", "hyperapp-echo", "template.os").into(); + // Define the argument for the echo function + let arg = Argument { + header: "RemoteTestHeader".to_string(), + body: "RemoteTestBody".to_string(), + }; + // Define the expected return value + let expected_return = ReturnValue { + response: "Ack".to_string(), + }; + + // Call the remote echo RPC stub + match echo_remote_rpc(&address, arg).await { + Ok(actual_value) => { + // Compare the 'response' field directly + if actual_value.response != expected_return.response { + // fail! macro uses FailResponse/TesterResponse imported above + fail!(format!( + "echo_remote_rpc unexpected result: expected {:?}, got {:?}", + expected_return, actual_value // Keep original structs for error message + )); + } + // If the result matches, the test passes for this step + Ok(()) + } + Err(e) => { + // Use fail! macro if the RPC call itself returned an error + fail!(format!("echo_remote_rpc failed: {:?}", e)); + } + } + }, +); \ No newline at end of file diff --git a/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/hyperapp-echo-test/src/tester_lib.rs b/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/hyperapp-echo-test/src/tester_lib.rs new file mode 100644 index 00000000..1ce18437 --- /dev/null +++ b/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/hyperapp-echo-test/src/tester_lib.rs @@ -0,0 +1,200 @@ +#[macro_export] +macro_rules! fail { + ($test:expr) => { + // Use the unified hyperware_process_lib::Response now available via Cargo.toml + hyperware_process_lib::Response::new() + // Use the types from the macro invocation site (src/lib.rs) + .body(TesterResponse::Run(Err(FailResponse { + test: $test.into(), + file: file!().into(), + line: line!(), + column: column!(), + }))) + .send() + .unwrap(); + panic!("") + }; + ($test:expr, $file:expr, $line:expr, $column:expr) => { + // Use the unified hyperware_process_lib::Response now available via Cargo.toml + hyperware_process_lib::Response::new() + // Use the types from the macro invocation site (src/lib.rs) + .body(TesterResponse::Run(Err(FailResponse { + test: $test.into(), + file: $file.into(), + line: $line, + column: $column, + }))) + .send() + .unwrap(); + panic!("") + }; +} + +#[macro_export] +macro_rules! async_test_suite { + ($wit_world:expr, $($test_name:ident: async $test_body:block),* $(,)?) => { + wit_bindgen::generate!({ + path: "../target/wit", + world: $wit_world, + generate_unused_types: true, + additional_derives: [PartialEq, serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto], + }); + + // Use items from the unified hyperware_process_lib now available via Cargo.toml + use hyperware_process_lib::{ + await_message, call_init, print_to_terminal, Address, Message, Response, SendError + }; + // Use items from the hyperware_app_common now available via Cargo.toml + use hyperware_app_common::{APP_CONTEXT, RESPONSE_REGISTRY, hyper}; + + $( + async fn $test_name() -> anyhow::Result<()> { + $test_body + } + )* + + async fn run_all_tests() -> anyhow::Result<()> { + $( + print_to_terminal(0, concat!("Running test: ", stringify!($test_name))); + match $test_name().await { + Ok(()) => { + print_to_terminal(0, concat!("Test passed: ", stringify!($test_name))); + }, + Err(e) => { + print_to_terminal(0, &format!("Test failed: {} - {:?}", stringify!($test_name), e)); + return Err(e); + } + } + )* + + print_to_terminal(0, "All tests passed!"); + Ok(()) + } + + call_init!(init); + fn init(_our: Address) { + print_to_terminal(0, "Starting test suite..."); + + // Flag to track if tests have been triggered and started + let mut tests_triggered = false; + + // Main event loop + loop { + // Poll tasks to advance the executor + APP_CONTEXT.with(|ctx| { + ctx.borrow_mut().executor.poll_all_tasks(); + }); + + // First, process any messages to handle RPC responses + match await_message() { + Ok(message) => { + // Message should resolve from the `use` statement above + match message { + Message::Response {body, context, ..} => { + // Handle responses to unblock waiting futures + let correlation_id = context + .as_deref() + .map(|bytes| String::from_utf8_lossy(bytes).to_string()) + .unwrap_or_else(|| "no context".to_string()); + + print_to_terminal(0, &format!("Received response with ID: {}", correlation_id)); + + RESPONSE_REGISTRY.with(|registry| { + let mut registry_mut = registry.borrow_mut(); + registry_mut.insert(correlation_id, body); + }); + }, + hyperware_process_lib::Message::Request { .. } => { + // The first request triggers test execution + if !tests_triggered { + tests_triggered = true; + print_to_terminal(0, "Received initial request, starting tests..."); + + hyper! { + match run_all_tests().await { + Ok(()) => { + print_to_terminal(0, "Tests completed successfully!"); + // Response should resolve from the `use` statement above + // TesterResponse needs to resolve from the macro invocation site (src/lib.rs) + Response::new() + .body(TesterResponse::Run(Ok(()))) + .send() + .unwrap_or_else(|e| { + print_to_terminal(0, &format!("Failed to send success response: {:?}", e)); + }); + }, + Err(e) => { + print_to_terminal(0, &format!("Test suite failed: {:?}", e)); + // fail! macro uses types imported in src/lib.rs + crate::fail!(&format!("Test failure: {:?}", e)); } + } + } + } + // No response here - response is sent when tests complete + } + } + }, + Err(e) => { + // Handle send errors to unblock futures that are waiting for responses + // SendError should resolve from the `use` statement above + if let SendError { + kind, + context: Some(context), + .. + } = &e + { + if let Ok(correlation_id) = String::from_utf8(context.clone()) { + let error_response = serde_json::to_vec(kind).unwrap(); + + RESPONSE_REGISTRY.with(|registry| { + let mut registry_mut = registry.borrow_mut(); + registry_mut.insert(correlation_id, error_response); + }); + } + } + + print_to_terminal(0, &format!("Message error: {:?}", e)); + } + } + } + } + }; +} +// TODO: SendResult does not exist anymore +// Helper function to test remote RPC calls +// +// This function handles: +// 1. Checking if the call was successful +// 2. Validating the returned value against an expected value +// 3. Handling error cases with appropriate failure messages +// +// Returns the actual value if successful, allowing it to be used in subsequent operations +// pub async fn test_remote_call( +// call_future: F, +// expected_value: T, +// error_msg: &str, +// ) -> anyhow::Result +// where +// T: std::cmp::PartialEq + std::fmt::Debug + Clone, +// F: std::future::Future>, +// { +// let result = call_future.await; + +// match result { +// SendResult::Success(actual) => { +// if actual != expected_value { +// fail!(format!("{}: expected {:?}, got {:?}", error_msg, expected_value, actual)); +// } +// // Return the actual value +// Ok(actual) +// } +// _ => { +// fail!(match result { +// SendResult::Timeout => "timeout", +// SendResult::Offline => "offline", +// SendResult::DeserializationError(_) => "deserialization error", +// _ => "unknown error", +// }); +// } +// } +// } \ No newline at end of file diff --git a/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/metadata.json b/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/metadata.json new file mode 100644 index 00000000..e7bf7ba1 --- /dev/null +++ b/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/metadata.json @@ -0,0 +1,22 @@ +{ + "name": "Hyperapp Echo Test", + "description": "A test showcasing the hyperware app framework.", + "image": "", + "properties": { + "package_name": "hyperapp-echo-test", + "current_version": "0.1.0", + "publisher": "template.os", + "mirrors": [], + "code_hashes": { + "0.1.0": "" + }, + "wit_version": 1, + "dependencies": [ + "hyperapp-echo:template.os", + "tester:sys" + ] + }, + "external_url": "", + "animation_url": "" +} + diff --git a/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/pkg/manifest.json b/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/pkg/manifest.json new file mode 100644 index 00000000..5634e8c3 --- /dev/null +++ b/src/new/templates/rust/no-ui/hyperapp-echo/test/hyperapp-echo-test/pkg/manifest.json @@ -0,0 +1,17 @@ +[ + { + "process_name": "hyperapp-echo-test", + "process_wasm_path": "/hyperapp-echo-test.wasm", + "on_exit": "None", + "request_networking": true, + "request_capabilities": [ + "http-client:distro:sys", + "hyperapp-echo:hyperapp-echo:template.os" + ], + "grant_capabilities": [ + "http-client:distro:sys", + "hyperapp-echo:hyperapp-echo:template.os" + ], + "public": true + } +] \ No newline at end of file diff --git a/src/new/templates/rust/no-ui/hyperapp-echo/test/tests.toml b/src/new/templates/rust/no-ui/hyperapp-echo/test/tests.toml new file mode 100644 index 00000000..a193d864 --- /dev/null +++ b/src/new/templates/rust/no-ui/hyperapp-echo/test/tests.toml @@ -0,0 +1,28 @@ +runtime = { FetchVersion = "latest" } +persist_home = false +runtime_build_release = false +always_print_node_output = false + + +[[tests]] +dependency_package_paths = [".."] +setup_packages = [{ path = "..", run = true }] +setup_scripts = [] +test_package_paths = ["hyperapp-echo-test"] +test_scripts = [] +timeout_secs = 35 +fakechain_router = 8545 +hyperapp = true + + +[[tests.nodes]] +port = 8080 +home = "home/hyperapp-echo-test" +fake_node_name = "host.os" +runtime_verbosity = 2 + +[[tests.nodes]] +port = 8081 +home = "home/hyperapp-echo-test" +fake_node_name = "hyperapp-echo.os" +runtime_verbosity = 2