Skip to content
Closed
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
49 changes: 49 additions & 0 deletions src/build/caller_utils_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> =
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 {
Expand Down
224 changes: 224 additions & 0 deletions src/build/wit/hyperware.wit
Original file line number Diff line number Diff line change
@@ -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<u8>;

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<string>,
bytes: list<u8>,
}

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<u64>,
body: list<u8>,
metadata: option<json>,
capabilities: list<capability>,
// to grab lazy-load-blob, use get_blob()
}

record response {
inherit: bool,
body: list<u8>,
metadata: option<json>,
capabilities: list<capability>,
// 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<response, option<context>>),
}

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<tuple<address, request, option<lazy-load-blob>>>),
}

/// 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<lazy-load-blob>,
}

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<list<u8>>;

set-state: func(bytes: list<u8>);

clear-state: func();

spawn: func(
// name is optional. if not provided, name will be a random u64.
name: option<string>,
// 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<capability>,
// granted capabilities will be generated by the child process
// and handed out to the indicated process-id.
grant-capabilities: list<tuple<process-id, json>>,
public: bool
) -> result<process-id, spawn-error>;

// ˗ˏˋ ♡ ˎˊ˗
// Capabilities Management
// ˗ˏˋ ♡ ˎˊ˗

/// Saves the capabilities to persisted process state.
save-capabilities: func(caps: list<capability>);

/// Deletes the capabilities from persisted process state.
drop-capabilities: func(caps: list<capability>);

/// Gets all capabilities from persisted process state.
our-capabilities: func() -> list<capability>;

// ˗ˏˋ ♡ ˎˊ˗
// 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<address, message>, tuple<send-error, option<context>>>;

/// 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<lazy-load-blob>;

/// Returns the last blob this process received.
last-blob: func() -> option<lazy-load-blob>;

/// Send request to target.
send-request: func(
target: address,
request: request,
context: option<context>,
lazy-load-blob: option<lazy-load-blob>
);

/// Send requests to targets.
send-requests: func(
requests: list<tuple<address,
request,
option<context>,
option<lazy-load-blob>>>
);

/// Send response to the request currently being handled.
send-response: func(
response: response,
lazy-load-blob: option<lazy-load-blob>
);

/// 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<lazy-load-blob>
) -> result<tuple<address, message>, send-error>;
}

world lib {
import standard;
}

world process-v1 {
include lib;

export init: func(our: string);
}
4 changes: 4 additions & 0 deletions src/build/wit/id-sys-v0.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
world id-sys-v0 {
import id;
include process-v1;
}
26 changes: 26 additions & 0 deletions src/build/wit/id.wit
Original file line number Diff line number Diff line change
@@ -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<u8>,
returning: result<list<u8>, string>
}

// Function signature for: verify (http)
record verify-signature-http {
target: string,
message: list<u8>,
signature: list<u8>,
returning: result<bool, string>
}
}
4 changes: 4 additions & 0 deletions src/build/wit/sign-sys-v0.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
world sign-sys-v0 {
import sign;
include process-v1;
}
26 changes: 26 additions & 0 deletions src/build/wit/sign.wit
Original file line number Diff line number Diff line change
@@ -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<u8>,
returning: result<list<u8>, string>
}

// Function signature for: verify (local)
record verify-signature-local {
target: address,
message: list<u8>,
signature: list<u8>,
returning: result<bool, string>
}
}
4 changes: 4 additions & 0 deletions src/build/wit/types-id-sys-v0.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
world types-id-sys-v0 {
import id;
include lib;
}
4 changes: 4 additions & 0 deletions src/build/wit/types-sign-sys-v0.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
world types-sign-sys-v0 {
import sign;
include lib;
}
4 changes: 4 additions & 0 deletions src/build/wit/types.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
world types {
include types-sign-sys-v0;
include types-id-sys-v0;
}
4 changes: 3 additions & 1 deletion src/build/wit_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,23 +520,25 @@ fn find_rust_projects(base_dir: &Path) -> Vec<PathBuf> {
.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;
};
let Ok(cargo_data) = content.parse::<Value>() else {
continue;
};

// Check for the specific metadata
let Some(metadata) = cargo_data
.get("package")
Expand Down