diff --git a/src/build/caller_utils_generator.rs b/src/build/caller_utils_generator.rs index c450252e..edce5bed 100644 --- a/src/build/caller_utils_generator.rs +++ b/src/build/caller_utils_generator.rs @@ -186,6 +186,55 @@ fn generate_default_value(rust_type: &str) -> String { } } +fn wit_type_to_ts(wit_type: &str) -> String { + match wit_type { + // Integer types - all become "number" in TypeScript + "s8" | "u8" | "s16" | "u16" | "s32" | "u32" | "s64" | "u64" => "number".to_string(), + // Floating point types - also "number" in TypeScript + "f32" | "f64" => "number".to_string(), + // Other primitive types + "string" => "string".to_string(), + "str" => "string".to_string(), + "char" => "string".to_string(), + "bool" => "boolean".to_string(), + "_" => "void".to_string(), + "unit" => "void".to_string(), + // Special types + "address" => "Address".to_string(), + // Collection types with generics + t if t.starts_with("list<") => { + let inner_type = &t[5..t.len() - 1]; + format!("{}[]", wit_type_to_ts(inner_type)) + } + t if t.starts_with("option<") => { + let inner_type = &t[7..t.len() - 1]; + format!("{} | null", wit_type_to_ts(inner_type)) + } + t if t.starts_with("result<") => { + let inner_part = &t[7..t.len() - 1]; + if let Some(comma_pos) = inner_part.find(',') { + let ok_type = &inner_part[..comma_pos].trim(); + let err_type = &inner_part[comma_pos + 1..].trim(); + format!( + "{{ Ok: {} }} | {{ Err: {} }}", + wit_type_to_ts(ok_type), + wit_type_to_ts(err_type) + ) + } else { + format!("{{ Ok: {} }} | {{ Err: null }}", wit_type_to_ts(inner_part)) + } + } + t if t.starts_with("tuple<") => { + let inner_types = &t[6..t.len() - 1]; + let ts_types: Vec = + inner_types.split(", ").map(|t| wit_type_to_ts(t)).collect(); + format!("[{}]", ts_types.join(", ")) + } + // Custom types (assumed to be in kebab-case) need to be converted to PascalCase + _ => to_pascal_case(wit_type).to_string(), + } +} + // Structure to represent a field in a WIT signature struct #[derive(Debug)] struct SignatureField { diff --git a/src/build/wit/hyperware.wit b/src/build/wit/hyperware.wit new file mode 100644 index 00000000..db96383b --- /dev/null +++ b/src/build/wit/hyperware.wit @@ -0,0 +1,224 @@ +package hyperware:process@1.0.0; + +interface standard { + + // ˗ˏˋ ♡ ˎˊ˗ + // System Types + // ˗ˏˋ ♡ ˎˊ˗ + + /// JSON is passed over Wasm boundary as a string. + type json = string; + + /// In types passed from kernel, node-id will be a valid Kimap entry. + type node-id = string; + + /// Context, like a message body, is a protocol-defined serialized byte + /// array. It is used when building a Request to save information that + /// will not be part of a Response, in order to more easily handle + /// ("contextualize") that Response. + type context = list; + + record process-id { + process-name: string, + package-name: string, + publisher-node: node-id, + } + + record package-id { + package-name: string, + publisher-node: node-id, + } + + record address { + node: node-id, + process: process-id, + } + + record lazy-load-blob { + mime: option, + bytes: list, + } + + record request { + // set in order to inherit lazy-load-blob from parent message, and if + // expects-response is none, direct response to source of parent. + // also carries forward certain aspects of parent message in kernel, + // see documentation for formal spec and examples: + // https://docs.rs/hyperware_process_lib/latest/hyperware_process_lib/struct.Request.html + inherit: bool, + // if some, request expects a response in the given number of seconds + expects-response: option, + body: list, + metadata: option, + capabilities: list, + // to grab lazy-load-blob, use get_blob() + } + + record response { + inherit: bool, + body: list, + metadata: option, + capabilities: list, + // to grab lazy-load-blob, use get_blob() + } + + /// A message can be a request or a response. Within a response, there is + /// a result which surfaces any error that happened because of a request. + /// A successful response will contain the context of the request it + /// matches, if any was set. + variant message { + request(request), + response(tuple>), + } + + record capability { + issuer: address, + params: json, + } + + /// On-exit is a setting that determines what happens when a process + /// panics, completes, or otherwise "ends". + /// NOTE: requests will always have expects-response set to false by kernel. + variant on-exit { + none, + restart, + requests(list>>), + } + + /// Send errors come from trying to send a message to another process, + /// either locally or on another node. + /// A message can fail by timing out, or by the node being entirely + /// unreachable (offline or can't be found in PKI). In either case, + /// the message is not delivered and the process that sent it receives + /// that message back along with any assigned context and/or lazy-load-blob, + /// and is free to handle it as it sees fit. + /// In the local case, only timeout errors are possible and also cover the case + /// in which a process is not running or does not exist. + record send-error { + kind: send-error-kind, + target: address, + message: message, + lazy-load-blob: option, + } + + enum send-error-kind { + offline, + timeout, + } + + enum spawn-error { + name-taken, + no-file-at-path, + } + + // ˗ˏˋ ♡ ˎˊ˗ + // System Utils + // ˗ˏˋ ♡ ˎˊ˗ + + /// Prints to the terminal at a given verbosity level. + /// Higher verbosity levels print more information. + /// Level 0 is always printed -- use sparingly. + print-to-terminal: func(verbosity: u8, message: string); + + /// Returns the address of the process. + our: func() -> address; + + // ˗ˏˋ ♡ ˎˊ˗ + // Process Management + // ˗ˏˋ ♡ ˎˊ˗ + + get-on-exit: func() -> on-exit; + + set-on-exit: func(on-exit: on-exit); + + get-state: func() -> option>; + + set-state: func(bytes: list); + + clear-state: func(); + + spawn: func( + // name is optional. if not provided, name will be a random u64. + name: option, + // wasm-path must be located within package's drive + wasm-path: string, + on-exit: on-exit, + // requested capabilities must be owned by the caller + request-capabilities: list, + // granted capabilities will be generated by the child process + // and handed out to the indicated process-id. + grant-capabilities: list>, + public: bool + ) -> result; + + // ˗ˏˋ ♡ ˎˊ˗ + // Capabilities Management + // ˗ˏˋ ♡ ˎˊ˗ + + /// Saves the capabilities to persisted process state. + save-capabilities: func(caps: list); + + /// Deletes the capabilities from persisted process state. + drop-capabilities: func(caps: list); + + /// Gets all capabilities from persisted process state. + our-capabilities: func() -> list; + + // ˗ˏˋ ♡ ˎˊ˗ + // Message I/O + // ˗ˏˋ ♡ ˎˊ˗ + + /// Ingest next message when it arrives along with its source. + /// Almost all long-running processes will call this in a loop. + receive: func() -> + result, tuple>>; + + /// Returns whether or not the current message has a blob. + has-blob: func() -> bool; + + /// Returns the blob of the current message, if any. + get-blob: func() -> option; + + /// Returns the last blob this process received. + last-blob: func() -> option; + + /// Send request to target. + send-request: func( + target: address, + request: request, + context: option, + lazy-load-blob: option + ); + + /// Send requests to targets. + send-requests: func( + requests: list, + option>> + ); + + /// Send response to the request currently being handled. + send-response: func( + response: response, + lazy-load-blob: option + ); + + /// Send a single request, then block (internally) until its response. The + /// type returned is Message but will always contain Response. + send-and-await-response: func( + target: address, + request: request, + lazy-load-blob: option + ) -> result, send-error>; +} + +world lib { + import standard; +} + +world process-v1 { + include lib; + + export init: func(our: string); +} diff --git a/src/build/wit/id-sys-v0.wit b/src/build/wit/id-sys-v0.wit new file mode 100644 index 00000000..ba8c83c8 --- /dev/null +++ b/src/build/wit/id-sys-v0.wit @@ -0,0 +1,4 @@ +world id-sys-v0 { + import id; + include process-v1; +} \ No newline at end of file diff --git a/src/build/wit/id.wit b/src/build/wit/id.wit new file mode 100644 index 00000000..33db3dab --- /dev/null +++ b/src/build/wit/id.wit @@ -0,0 +1,26 @@ +interface id { + // This interface contains function signature definitions that will be used + // by the hyper-bindgen macro to generate async function bindings. + // + // NOTE: This is currently a hacky workaround since WIT async functions are not + // available until WASI Preview 3. Once Preview 3 is integrated into Hyperware, + // we should switch to using proper async WIT function signatures instead of + // this struct-based approach with hyper-bindgen generating the async stubs. + + use standard.{address}; + + // Function signature for: sign (http) + record sign-signature-http { + target: string, + message: list, + returning: result, string> + } + + // Function signature for: verify (http) + record verify-signature-http { + target: string, + message: list, + signature: list, + returning: result + } +} diff --git a/src/build/wit/sign-sys-v0.wit b/src/build/wit/sign-sys-v0.wit new file mode 100644 index 00000000..9d10e6b0 --- /dev/null +++ b/src/build/wit/sign-sys-v0.wit @@ -0,0 +1,4 @@ +world sign-sys-v0 { + import sign; + include process-v1; +} \ No newline at end of file diff --git a/src/build/wit/sign.wit b/src/build/wit/sign.wit new file mode 100644 index 00000000..c7e65b04 --- /dev/null +++ b/src/build/wit/sign.wit @@ -0,0 +1,26 @@ +interface sign { + // This interface contains function signature definitions that will be used + // by the hyper-bindgen macro to generate async function bindings. + // + // NOTE: This is currently a hacky workaround since WIT async functions are not + // available until WASI Preview 3. Once Preview 3 is integrated into Hyperware, + // we should switch to using proper async WIT function signatures instead of + // this struct-based approach with hyper-bindgen generating the async stubs. + + use standard.{address}; + + // Function signature for: sign (local) + record sign-signature-local { + target: address, + message: list, + returning: result, string> + } + + // Function signature for: verify (local) + record verify-signature-local { + target: address, + message: list, + signature: list, + returning: result + } +} diff --git a/src/build/wit/types-id-sys-v0.wit b/src/build/wit/types-id-sys-v0.wit new file mode 100644 index 00000000..bacca4ae --- /dev/null +++ b/src/build/wit/types-id-sys-v0.wit @@ -0,0 +1,4 @@ +world types-id-sys-v0 { + import id; + include lib; +} \ No newline at end of file diff --git a/src/build/wit/types-sign-sys-v0.wit b/src/build/wit/types-sign-sys-v0.wit new file mode 100644 index 00000000..cfe7941b --- /dev/null +++ b/src/build/wit/types-sign-sys-v0.wit @@ -0,0 +1,4 @@ +world types-sign-sys-v0 { + import sign; + include lib; +} \ No newline at end of file diff --git a/src/build/wit/types.wit b/src/build/wit/types.wit new file mode 100644 index 00000000..41746f3c --- /dev/null +++ b/src/build/wit/types.wit @@ -0,0 +1,4 @@ +world types { + include types-sign-sys-v0; + include types-id-sys-v0; +} diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 39cdaebf..93d1e282 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -520,16 +520,17 @@ fn find_rust_projects(base_dir: &Path) -> Vec { .filter_map(Result::ok) { let path = entry.path(); - if !path.is_dir() || path == base_dir { continue; } + let cargo_toml = path.join("Cargo.toml"); debug!(path = %cargo_toml.display(), "Checking path"); if !cargo_toml.exists() { continue; } + // Try to read and parse Cargo.toml let Ok(content) = fs::read_to_string(&cargo_toml) else { continue; @@ -537,6 +538,7 @@ fn find_rust_projects(base_dir: &Path) -> Vec { let Ok(cargo_data) = content.parse::() else { continue; }; + // Check for the specific metadata let Some(metadata) = cargo_data .get("package")