#![allow(warnings)]
use proc_macro::TokenStream;
use quote::{format_ident, quote, ToTokens};
use syn::{
parse_macro_input, punctuated::Punctuated, spanned::Spanned, token::Comma, Expr, ItemImpl,
Meta, ReturnType,
};
mod kw {
syn::custom_keyword!(name);
syn::custom_keyword!(icon);
syn::custom_keyword!(widget);
syn::custom_keyword!(ui);
syn::custom_keyword!(ui_path);
syn::custom_keyword!(endpoints);
syn::custom_keyword!(save_config);
syn::custom_keyword!(wit_world);
}
struct MetaList(Punctuated<Meta, Comma>);
struct HyperProcessArgs {
name: String,
icon: Option<String>,
widget: Option<String>,
ui: Option<Expr>,
ui_path: Option<String>,
endpoints: Expr,
save_config: Expr,
wit_world: String,
}
#[derive(Clone)]
struct FunctionMetadata {
name: syn::Ident, variant_name: String, params: Vec<syn::Type>, return_type: Option<syn::Type>, is_async: bool, is_local: bool, is_remote: bool, is_http: bool, is_eth: bool, http_methods: Vec<String>, http_path: Option<String>, }
#[derive(Copy, Clone)]
enum HandlerType {
Local,
Remote,
Http,
Eth,
}
struct HandlerGroups<'a> {
local: Vec<&'a FunctionMetadata>,
remote: Vec<&'a FunctionMetadata>,
http: Vec<&'a FunctionMetadata>,
local_and_remote: Vec<&'a FunctionMetadata>,
}
impl<'a> HandlerGroups<'a> {
fn from_function_metadata(metadata: &'a [FunctionMetadata]) -> Self {
let local: Vec<_> = metadata.iter().filter(|f| f.is_local).collect();
let remote: Vec<_> = metadata.iter().filter(|f| f.is_remote).collect();
let http: Vec<_> = metadata.iter().filter(|f| f.is_http).collect();
let mut local_and_remote = local.clone();
for handler in remote.iter() {
if !local_and_remote
.iter()
.any(|h| h.variant_name == handler.variant_name)
{
local_and_remote.push(handler);
}
}
HandlerGroups {
local,
remote,
http,
local_and_remote,
}
}
}
struct HandlerDispatch {
local: proc_macro2::TokenStream,
remote: proc_macro2::TokenStream,
http: proc_macro2::TokenStream,
local_and_remote: proc_macro2::TokenStream,
}
struct InitMethodDetails {
identifier: proc_macro2::TokenStream,
call: proc_macro2::TokenStream,
}
#[derive(Clone)]
struct WsMethodInfo {
name: syn::Ident,
is_async: bool,
}
#[derive(Clone)]
struct WsClientMethodInfo {
name: syn::Ident,
is_async: bool,
}
#[derive(Clone)]
struct EthMethodInfo {
name: syn::Ident,
is_async: bool,
}
struct WsMethodDetails {
identifier: proc_macro2::TokenStream,
call: proc_macro2::TokenStream,
}
struct WsClientMethodDetails {
identifier: proc_macro2::TokenStream,
call: proc_macro2::TokenStream,
}
struct EthMethodDetails {
identifier: proc_macro2::TokenStream,
call: proc_macro2::TokenStream,
}
impl syn::parse::Parse for MetaList {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut args = Punctuated::new();
while !input.is_empty() {
args.push_value(input.parse()?);
if input.is_empty() {
break;
}
args.push_punct(input.parse()?);
}
Ok(MetaList(args))
}
}
fn to_camel_case(snake: &str) -> String {
let mut camel = String::new();
let mut capitalize_next = true;
for c in snake.chars() {
if c == '_' {
capitalize_next = true;
} else if capitalize_next {
camel.push(c.to_ascii_uppercase());
capitalize_next = false;
} else {
camel.push(c);
}
}
camel
}
fn parse_string_literal(expr: &Expr, span: proc_macro2::Span) -> syn::Result<String> {
if let Expr::Lit(expr_lit) = expr {
if let syn::Lit::Str(lit) = &expr_lit.lit {
Ok(lit.value())
} else {
Err(syn::Error::new(span, "Expected string literal"))
}
} else {
Err(syn::Error::new(span, "Expected string literal"))
}
}
fn parse_ui_expr(expr: &Expr) -> syn::Result<Option<Expr>> {
if let Expr::Call(call) = expr {
if let Expr::Path(path) = &*call.func {
if path
.path
.segments
.last()
.map(|s| s.ident == "Some")
.unwrap_or(false)
{
if call.args.len() == 1 {
return Ok(Some(call.args[0].clone()));
} else {
return Err(syn::Error::new(
call.span(),
"Some must have exactly one argument",
));
}
}
}
}
Ok(Some(expr.clone()))
}
fn has_attribute(method: &syn::ImplItemFn, attr_name: &str) -> bool {
method
.attrs
.iter()
.any(|attr| attr.path().is_ident(attr_name))
}
fn parse_http_attributes(method: &syn::ImplItemFn) -> (Vec<String>, Option<String>) {
for attr in &method.attrs {
if attr.path().is_ident("http") {
if matches!(&attr.meta, syn::Meta::Path(_)) {
return (
vec![
"GET".to_string(),
"POST".to_string(),
"PUT".to_string(),
"DELETE".to_string(),
"PATCH".to_string(),
"HEAD".to_string(),
"OPTIONS".to_string(),
],
None,
);
}
if let syn::Meta::List(list) = &attr.meta {
let mut methods = None;
let mut path = None;
let tokens: Vec<_> = list.tokens.clone().into_iter().collect();
let mut i = 0;
while i < tokens.len() {
if let proc_macro2::TokenTree::Ident(ident) = &tokens[i] {
let ident_str = ident.to_string();
if i + 2 < tokens.len() {
if let proc_macro2::TokenTree::Punct(punct) = &tokens[i + 1] {
if punct.as_char() == '=' {
if let proc_macro2::TokenTree::Literal(lit) = &tokens[i + 2] {
let lit_str = lit.to_string();
let value = lit_str.trim_matches('"');
if ident_str == "method" {
let method = value.to_uppercase();
if matches!(
method.as_str(),
"GET"
| "POST"
| "PUT"
| "DELETE"
| "PATCH"
| "HEAD"
| "OPTIONS"
) {
methods = Some(vec![method]);
}
} else if ident_str == "path" {
path = Some(value.to_string());
}
}
i += 3;
if i < tokens.len() {
if let proc_macro2::TokenTree::Punct(punct) = &tokens[i] {
if punct.as_char() == ',' {
i += 1;
}
}
}
continue;
}
}
}
}
i += 1;
}
let final_methods = methods.unwrap_or_else(|| {
vec![
"GET".to_string(),
"POST".to_string(),
"PUT".to_string(),
"DELETE".to_string(),
"PATCH".to_string(),
"HEAD".to_string(),
"OPTIONS".to_string(),
]
});
return (final_methods, path);
}
return (
vec![
"GET".to_string(),
"POST".to_string(),
"PUT".to_string(),
"DELETE".to_string(),
"PATCH".to_string(),
"HEAD".to_string(),
"OPTIONS".to_string(),
],
None,
);
}
}
(Vec::new(), None)
}
fn clean_impl_block(impl_block: &ItemImpl) -> ItemImpl {
let mut cleaned_impl_block = impl_block.clone();
for item in &mut cleaned_impl_block.items {
if let syn::ImplItem::Fn(method) = item {
method.attrs.retain(|attr| {
!attr.path().is_ident("init")
&& !attr.path().is_ident("http")
&& !attr.path().is_ident("local")
&& !attr.path().is_ident("remote")
&& !attr.path().is_ident("eth")
&& !attr.path().is_ident("ws")
&& !attr.path().is_ident("ws_client")
});
}
}
cleaned_impl_block
}
fn has_valid_self_receiver(method: &syn::ImplItemFn) -> bool {
method
.sig
.inputs
.first()
.map_or(false, |arg| matches!(arg, syn::FnArg::Receiver(_)))
}
fn parse_args(attr_args: MetaList) -> syn::Result<HyperProcessArgs> {
let mut name = None;
let mut icon = None;
let mut widget = None;
let mut ui = None;
let mut ui_path = None;
let mut endpoints = None;
let mut save_config = None;
let mut wit_world = None;
let span = attr_args
.0
.first()
.map_or_else(|| proc_macro2::Span::call_site(), |arg| arg.span());
for arg in &attr_args.0 {
if let Meta::NameValue(nv) = arg {
let key = nv.path.get_ident().unwrap().to_string();
match key.as_str() {
"name" => {
name = Some(parse_string_literal(&nv.value, nv.value.span())?);
}
"icon" => {
icon = Some(parse_string_literal(&nv.value, nv.value.span())?);
}
"widget" => {
widget = Some(parse_string_literal(&nv.value, nv.value.span())?);
}
"ui" => {
ui = parse_ui_expr(&nv.value)?;
}
"ui_path" => {
ui_path = Some(parse_string_literal(&nv.value, nv.value.span())?);
}
"endpoints" => endpoints = Some(nv.value.clone()),
"save_config" => save_config = Some(nv.value.clone()),
"wit_world" => {
wit_world = Some(parse_string_literal(&nv.value, nv.value.span())?);
}
_ => return Err(syn::Error::new(nv.path.span(), "Unknown attribute")),
}
} else {
return Err(syn::Error::new(arg.span(), "Expected name-value pair"));
}
}
Ok(HyperProcessArgs {
name: name.ok_or_else(|| syn::Error::new(span, "Missing 'name'"))?,
icon,
widget,
ui,
ui_path,
endpoints: endpoints.ok_or_else(|| syn::Error::new(span, "Missing 'endpoints'"))?,
save_config: save_config.ok_or_else(|| syn::Error::new(span, "Missing 'save_config'"))?,
wit_world: wit_world.ok_or_else(|| syn::Error::new(span, "Missing 'wit_world'"))?,
})
}
fn validate_init_method(method: &syn::ImplItemFn) -> syn::Result<()> {
if method.sig.asyncness.is_none() {
return Err(syn::Error::new_spanned(
&method.sig,
"Init method must be declared as async",
));
}
if !has_valid_self_receiver(method) {
return Err(syn::Error::new_spanned(
&method.sig,
"Init method must take &mut self as first parameter",
));
}
if method.sig.inputs.len() > 1 {
return Err(syn::Error::new_spanned(
&method.sig,
"Init method must not take any parameters other than &mut self",
));
}
if !matches!(method.sig.output, ReturnType::Default) {
return Err(syn::Error::new_spanned(
&method.sig,
"Init method must not return a value",
));
}
Ok(())
}
fn validate_websocket_method(method: &syn::ImplItemFn) -> syn::Result<()> {
if !has_valid_self_receiver(method) {
return Err(syn::Error::new_spanned(
&method.sig,
"WebSocket method must take &mut self as first parameter",
));
}
if method.sig.inputs.len() != 4 {
return Err(syn::Error::new_spanned(
&method.sig,
"WebSocket method must take exactly 3 additional parameters: channel_id, message_type, and blob",
));
}
let params: Vec<_> = method.sig.inputs.iter().skip(1).collect();
let channel_id_param = ¶ms[0];
let message_type_param = ¶ms[1];
let blob_param = ¶ms[2];
if let syn::FnArg::Typed(pat_type) = channel_id_param {
if !pat_type.ty.to_token_stream().to_string().contains("u32") {
return Err(syn::Error::new_spanned(
pat_type,
"First parameter of WebSocket method must be channel_id: u32",
));
}
}
if let syn::FnArg::Typed(pat_type) = message_type_param {
let type_str = pat_type.ty.to_token_stream().to_string();
if !type_str.contains("WsMessageType") && !type_str.contains("MessageType") {
return Err(syn::Error::new_spanned(
pat_type,
"Second parameter of WebSocket method must be message_type: WsMessageType",
));
}
}
if let syn::FnArg::Typed(pat_type) = blob_param {
if !pat_type
.ty
.to_token_stream()
.to_string()
.contains("LazyLoadBlob")
{
return Err(syn::Error::new_spanned(
pat_type,
"Third parameter of WebSocket method must be blob: LazyLoadBlob",
));
}
}
if !matches!(method.sig.output, ReturnType::Default) {
return Err(syn::Error::new_spanned(
&method.sig.output,
"WebSocket method must not return a value",
));
}
Ok(())
}
fn validate_websocket_client_method(method: &syn::ImplItemFn) -> syn::Result<()> {
if !has_valid_self_receiver(method) {
return Err(syn::Error::new_spanned(
&method.sig,
"WebSocket client method must take &mut self as first parameter",
));
}
if method.sig.inputs.len() != 4 {
return Err(syn::Error::new_spanned(
&method.sig,
"WebSocket client method must take exactly 3 additional parameters: channel_id, message_type, and blob",
));
}
let params: Vec<_> = method.sig.inputs.iter().skip(1).collect();
let channel_id_param = ¶ms[0];
let message_type_param = ¶ms[1];
let blob_param = ¶ms[2];
if let syn::FnArg::Typed(pat_type) = channel_id_param {
if !pat_type.ty.to_token_stream().to_string().contains("u32") {
return Err(syn::Error::new_spanned(
pat_type,
"First parameter of WebSocket client method must be channel_id: u32",
));
}
}
if let syn::FnArg::Typed(pat_type) = message_type_param {
let type_str = pat_type.ty.to_token_stream().to_string();
if !type_str.contains("WsMessageType") && !type_str.contains("MessageType") {
return Err(syn::Error::new_spanned(
pat_type,
"Second parameter of WebSocket client method must be message_type: WsMessageType",
));
}
}
if let syn::FnArg::Typed(pat_type) = blob_param {
if !pat_type
.ty
.to_token_stream()
.to_string()
.contains("LazyLoadBlob")
{
return Err(syn::Error::new_spanned(
pat_type,
"Third parameter of WebSocket client method must be blob: LazyLoadBlob",
));
}
}
if !matches!(method.sig.output, ReturnType::Default) {
return Err(syn::Error::new_spanned(
&method.sig.output,
"WebSocket client method must not return a value",
));
}
Ok(())
}
fn validate_request_response_function(method: &syn::ImplItemFn) -> syn::Result<()> {
if !has_valid_self_receiver(method) {
return Err(syn::Error::new_spanned(
&method.sig,
"Request-response handlers must take &mut self as their first parameter",
));
}
Ok(())
}
fn validate_eth_handler(method: &syn::ImplItemFn) -> syn::Result<()> {
if !has_valid_self_receiver(method) {
return Err(syn::Error::new_spanned(
&method.sig,
"ETH handler must take &mut self as first parameter",
));
}
if method.sig.inputs.len() != 2 {
return Err(syn::Error::new_spanned(
&method.sig,
"ETH handler must take exactly one parameter: eth_sub_result: EthSubResult",
));
}
let params: Vec<_> = method.sig.inputs.iter().skip(1).collect();
let eth_param = ¶ms[0];
if let syn::FnArg::Typed(pat_type) = eth_param {
let type_str = pat_type.ty.to_token_stream().to_string();
if !type_str.contains("EthSubResult") {
return Err(syn::Error::new_spanned(
pat_type,
"ETH handler parameter must be eth_sub_result: EthSubResult",
));
}
} else {
return Err(syn::Error::new_spanned(
eth_param,
"ETH handler parameter must be eth_sub_result: EthSubResult",
));
}
Ok(())
}
fn analyze_methods(
impl_block: &ItemImpl,
) -> syn::Result<(
Option<syn::Ident>, // init method
Option<WsMethodInfo>, // ws method
Option<WsClientMethodInfo>, // ws_client method
Option<EthMethodInfo>, // eth method
Vec<FunctionMetadata>, // metadata for request/response methods
bool, // whether init method contains logging init
)> {
let mut init_method = None;
let mut ws_method = None;
let mut ws_client_method = None;
let mut eth_method = None;
let mut has_init_logging = false;
let mut function_metadata = Vec::new();
for item in &impl_block.items {
if let syn::ImplItem::Fn(method) = item {
let ident = method.sig.ident.clone();
let has_init = has_attribute(method, "init");
let has_http = has_attribute(method, "http");
let has_local = has_attribute(method, "local");
let has_remote = has_attribute(method, "remote");
let has_eth = has_attribute(method, "eth");
let has_ws = has_attribute(method, "ws");
let has_ws_client = has_attribute(method, "ws_client");
if has_init {
if has_http || has_local || has_remote || has_eth || has_ws || has_ws_client {
return Err(syn::Error::new_spanned(
method,
"#[init] cannot be combined with other attributes",
));
}
validate_init_method(method)?;
if init_method.is_some() {
return Err(syn::Error::new_spanned(
method,
"Multiple #[init] methods defined",
));
}
init_method = Some(ident);
has_init_logging = contains_init_logging(method);
continue;
}
if has_ws {
if has_http || has_local || has_remote || has_eth || has_init || has_ws_client {
return Err(syn::Error::new_spanned(
method,
"#[ws] cannot be combined with other attributes",
));
}
validate_websocket_method(method)?;
if ws_method.is_some() {
return Err(syn::Error::new_spanned(
method,
"Multiple #[ws] methods defined",
));
}
ws_method = Some(WsMethodInfo {
name: ident,
is_async: method.sig.asyncness.is_some(),
});
continue;
}
if has_ws_client {
if has_http || has_local || has_remote || has_eth || has_init || has_ws {
return Err(syn::Error::new_spanned(
method,
"#[ws_client] cannot be combined with other attributes",
));
}
validate_websocket_client_method(method)?;
if ws_client_method.is_some() {
return Err(syn::Error::new_spanned(
method,
"Multiple #[ws_client] methods defined",
));
}
ws_client_method = Some(WsClientMethodInfo {
name: ident,
is_async: method.sig.asyncness.is_some(),
});
continue;
}
if has_eth {
if has_http || has_local || has_remote || has_init || has_ws || has_ws_client {
return Err(syn::Error::new_spanned(
method,
"#[eth] cannot be combined with other attributes",
));
}
validate_eth_handler(method)?;
if eth_method.is_some() {
return Err(syn::Error::new_spanned(
method,
"Multiple #[eth] methods defined",
));
}
eth_method = Some(EthMethodInfo {
name: ident.clone(),
is_async: method.sig.asyncness.is_some(),
});
}
if has_http || has_local || has_remote {
validate_request_response_function(method)?;
let metadata =
extract_function_metadata(method, has_local, has_remote, has_http, false);
function_metadata.push(metadata);
}
}
}
if function_metadata.is_empty() {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"You must specify at least one handler with #[remote], #[local] or #[http] attribute. Without any handlers, this hyperprocess wouldn't respond to any requests.",
));
}
let mut http_routes = std::collections::HashMap::new();
for func in &function_metadata {
if func.is_http {
if let Some(path) = &func.http_path {
for method in &func.http_methods {
let route_key = (method.clone(), path.clone());
if let Some(existing_handler) = http_routes.get(&route_key) {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
format!(
"Duplicate HTTP route detected: {} {}\n\
First handler: {}\n\
Second handler: {}\n\
\n\
Each (method + specific path) combination must map to exactly one handler.\n\
Consider:\n\
- Using different paths for different handlers\n\
- Combining the logic into a single handler\n\
- Using method-only handlers with get_path() for dynamic routing",
method, path, existing_handler, func.name
),
));
}
http_routes.insert(route_key, &func.name);
}
}
}
}
Ok((
init_method,
ws_method,
ws_client_method,
eth_method,
function_metadata,
has_init_logging,
))
}
fn extract_function_metadata(
method: &syn::ImplItemFn,
is_local: bool,
is_remote: bool,
is_http: bool,
is_eth: bool,
) -> FunctionMetadata {
let ident = method.sig.ident.clone();
let params = method
.sig
.inputs
.iter()
.skip(1)
.filter_map(|input| {
if let syn::FnArg::Typed(pat_type) = input {
Some((*pat_type.ty).clone())
} else {
None
}
})
.collect();
let return_type = match &method.sig.output {
ReturnType::Default => None, ReturnType::Type(_, ty) => Some((**ty).clone()),
};
let variant_name = to_camel_case(&ident.to_string());
let (http_methods, http_path) = if is_http {
parse_http_attributes(method)
} else {
(Vec::new(), None)
};
FunctionMetadata {
name: ident,
variant_name,
params,
return_type,
is_async: method.sig.asyncness.is_some(),
is_local,
is_remote,
is_http,
is_eth,
http_methods,
http_path,
}
}
fn contains_init_logging(method: &syn::ImplItemFn) -> bool {
let mut contains_logging = false;
struct LoggingVisitor {
found: bool,
}
impl<'ast> syn::visit::Visit<'ast> for LoggingVisitor {
fn visit_expr_call(&mut self, call: &'ast syn::ExprCall) {
if let syn::Expr::Path(path) = &*call.func {
if path
.path
.segments
.last()
.map(|s| s.ident == "init_logging")
.unwrap_or(false)
{
self.found = true;
}
}
syn::visit::visit_expr_call(self, call);
}
}
let mut visitor = LoggingVisitor { found: false };
syn::visit::visit_block(&mut visitor, &method.block);
visitor.found
}
fn generate_request_response_enums(
function_metadata: &[FunctionMetadata],
) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) {
if function_metadata.is_empty() {
return (quote! {}, quote! {});
}
let request_variants = function_metadata
.iter()
.map(|func| {
let variant_name = format_ident!("{}", &func.variant_name);
generate_enum_variant(&variant_name, &func.params)
});
let response_variants = function_metadata.iter().map(|func| {
let variant_name = format_ident!("{}", &func.variant_name);
if let Some(return_type) = &func.return_type {
let type_str = return_type.to_token_stream().to_string();
if type_str == "()" {
quote! { #variant_name }
} else {
quote! { #variant_name(#return_type) }
}
} else {
quote! { #variant_name }
}
});
(
quote! {
#[derive(Debug, serde::Serialize, serde::Deserialize)]
enum HPMRequest {
#(#request_variants),*
}
},
quote! {
#[derive(Debug, serde::Serialize, serde::Deserialize)]
enum HPMResponse {
#(#response_variants),*
}
},
)
}
fn generate_enum_variant(
variant_name: &syn::Ident,
params: &[syn::Type],
) -> proc_macro2::TokenStream {
if params.is_empty() {
quote! { #variant_name }
} else if params.len() == 1 {
let param_type = ¶ms[0];
quote! { #variant_name(#param_type) }
} else {
quote! { #variant_name(#(#params),*) }
}
}
fn generate_handler_dispatch(
handlers: &[&FunctionMetadata],
self_ty: &Box<syn::Type>,
handler_type: HandlerType,
) -> proc_macro2::TokenStream {
if handlers.is_empty() {
let message = match handler_type {
HandlerType::Local => "No local handlers defined but received a local request",
HandlerType::Remote => "No remote handlers defined but received a remote request",
HandlerType::Http => "No HTTP handlers defined but received an HTTP request",
HandlerType::Eth => "No ETH handlers defined but received an ETH request",
};
return quote! {
hyperware_process_lib::logging::warn!(#message);
};
}
let type_name = match handler_type {
HandlerType::Local => "local",
HandlerType::Remote => "remote",
HandlerType::Http => "http",
HandlerType::Eth => "eth",
};
let dispatch_arms = handlers
.iter()
.map(|func| generate_handler_dispatch_arm(func, self_ty, handler_type, type_name));
let unreachable_arm = match handler_type {
HandlerType::Http => {
quote! {
unexpected => {
hyperware_process_lib::logging::error!(
"Unexpected {} request variant received in HTTP handler: {:?}",
#type_name,
unexpected
);
let error_body = serde_json::json!({
"error": format!("Unexpected {} request variant", #type_name),
"received": format!("{:?}", unexpected),
})
.to_string()
.into_bytes();
hyperware_process_lib::http::server::send_response(
hyperware_process_lib::http::StatusCode::BAD_REQUEST,
None,
error_body,
);
hyperware_process_lib::hyperapp::clear_http_request_context();
}
}
}
_ => {
quote! {
unexpected => {
hyperware_process_lib::logging::error!(
"Unexpected {} request variant received in {} handler: {:?}",
#type_name,
#type_name,
unexpected
);
let send_error_bytes = hyperware_process_lib::hyperapp::APP_HELPERS.with(|helpers| {
let helpers_ref = helpers.borrow();
helpers_ref.current_message.clone().map(|original_message| {
let send_error = hyperware_process_lib::SendError {
kind: hyperware_process_lib::SendErrorKind::Timeout,
target: original_message.source().clone(),
message: original_message,
lazy_load_blob: None,
context: None,
};
serde_json::to_vec(&send_error)
})
});
if let Some(Ok(payload_bytes)) = send_error_bytes {
if let Err(e) = hyperware_process_lib::Response::new().body(payload_bytes).send() {
hyperware_process_lib::logging::error!(
"Failed to send SendError for unexpected {} variant: {}",
#type_name,
e
);
}
} else {
hyperware_process_lib::logging::error!(
"Failed to construct a SendError for unexpected {} variant",
#type_name,
);
}
}
}
}
};
quote! {
match request {
#(#dispatch_arms)*
#unreachable_arm
}
}
}
fn generate_handler_dispatch_arm(
func: &FunctionMetadata,
self_ty: &Box<syn::Type>,
handler_type: HandlerType,
type_name: &str,
) -> proc_macro2::TokenStream {
let fn_name = &func.name;
let variant_name = format_ident!("{}", &func.variant_name);
let response_handling =
generate_response_handling(func, &variant_name, handler_type, type_name);
if func.is_async {
generate_async_handler_arm(func, self_ty, fn_name, &variant_name, response_handling)
} else {
generate_sync_handler_arm(func, fn_name, &variant_name, response_handling)
}
}
fn generate_response_handling(
_func: &FunctionMetadata,
variant_name: &syn::Ident,
handler_type: HandlerType,
type_name: &str,
) -> proc_macro2::TokenStream {
match handler_type {
HandlerType::Local | HandlerType::Remote => {
quote! {
let resp = hyperware_process_lib::Response::new()
.body(serde_json::to_vec(&result).unwrap());
resp.send().unwrap();
}
}
HandlerType::Eth => {
quote! {
let resp = hyperware_process_lib::Response::new()
.body(serde_json::to_vec(&result).unwrap());
resp.send().unwrap();
}
}
HandlerType::Http => {
quote! {
let response_bytes = serde_json::to_vec(&result).unwrap();
let headers_opt = hyperware_process_lib::hyperapp::APP_HELPERS.with(|helpers| {
helpers.borrow().current_http_context.as_ref().and_then(|ctx| {
if ctx.response_headers.is_empty() {
None
} else {
Some(ctx.response_headers.clone())
}
})
});
let response_status = hyperware_process_lib::hyperapp::APP_HELPERS.with(|helpers| {
helpers
.borrow()
.current_http_context
.as_ref()
.map(|ctx| ctx.response_status)
.unwrap_or(hyperware_process_lib::http::StatusCode::OK)
});
hyperware_process_lib::http::server::send_response(
response_status,
headers_opt,
response_bytes
);
hyperware_process_lib::hyperapp::clear_http_request_context();
}
}
}
}
fn generate_async_handler_arm(
func: &FunctionMetadata,
self_ty: &Box<syn::Type>,
fn_name: &syn::Ident,
variant_name: &syn::Ident,
response_handling: proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
if func.params.is_empty() {
quote! {
HPMRequest::#variant_name{} => {
let state_ptr: *mut #self_ty = state;
hyperware_process_lib::hyperapp::spawn(async move {
let result = unsafe { (*state_ptr).#fn_name().await };
#response_handling
});
}
}
} else if func.params.len() == 1 {
quote! {
HPMRequest::#variant_name(param) => {
let param_captured = param; let state_ptr: *mut #self_ty = state;
hyperware_process_lib::hyperapp::spawn(async move {
let result = unsafe { (*state_ptr).#fn_name(param_captured).await };
#response_handling
});
}
}
} else {
let param_count = func.params.len();
let param_names = (0..param_count).map(|i| format_ident!("param{}", i));
let capture_statements = (0..param_count).map(|i| {
let param = format_ident!("param{}", i);
let captured = format_ident!("param{}_captured", i);
quote! { let #captured = #param; }
});
let captured_names = (0..param_count).map(|i| format_ident!("param{}_captured", i));
quote! {
HPMRequest::#variant_name(#(#param_names),*) => {
#(#capture_statements)*
let state_ptr: *mut #self_ty = state;
hyperware_process_lib::hyperapp::spawn(async move {
let result = unsafe { (*state_ptr).#fn_name(#(#captured_names),*).await };
#response_handling
});
}
}
}
}
fn generate_sync_handler_arm(
func: &FunctionMetadata,
fn_name: &syn::Ident,
variant_name: &syn::Ident,
response_handling: proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
if func.params.is_empty() {
quote! {
HPMRequest::#variant_name{} => {
let result = unsafe { (*state).#fn_name() };
#response_handling
}
}
} else if func.params.len() == 1 {
quote! {
HPMRequest::#variant_name(param) => {
let result = unsafe { (*state).#fn_name(param) };
#response_handling
}
}
} else {
let param_count = func.params.len();
let param_names = (0..param_count).map(|i| format_ident!("param{}", i));
let param_names2 = param_names.clone();
quote! {
HPMRequest::#variant_name(#(#param_names),*) => {
let result = unsafe { (*state).#fn_name(#(#param_names2),*) };
#response_handling
}
}
}
}
fn init_method_opt_to_token(init_method: &Option<syn::Ident>) -> proc_macro2::TokenStream {
if let Some(method_name) = init_method {
quote! { Some(stringify!(#method_name)) }
} else {
quote! { None::<&str> }
}
}
fn init_method_opt_to_call(
init_method: &Option<syn::Ident>,
self_ty: &Box<syn::Type>,
) -> proc_macro2::TokenStream {
if let Some(method_name) = init_method {
quote! {
let state_ptr: *mut #self_ty = &mut state;
hyperware_process_lib::hyperapp::spawn(async move {
unsafe { (*state_ptr).#method_name().await };
});
}
} else {
quote! {}
}
}
fn ws_method_opt_to_token(ws_method: &Option<WsMethodInfo>) -> proc_macro2::TokenStream {
if let Some(method_info) = ws_method {
let method_name = &method_info.name;
quote! { Some(stringify!(#method_name)) }
} else {
quote! { None::<&str> }
}
}
fn ws_method_opt_to_call(
ws_method: &Option<WsMethodInfo>,
self_ty: &Box<syn::Type>,
) -> proc_macro2::TokenStream {
if let Some(method_info) = ws_method {
let method_name = &method_info.name;
if method_info.is_async {
quote! {
let state_ptr: *mut #self_ty = state;
hyperware_process_lib::hyperapp::spawn(async move {
unsafe { (*state_ptr).#method_name(channel_id, message_type, blob).await };
});
}
} else {
quote! { unsafe { (*state).#method_name(channel_id, message_type, blob) }; }
}
} else {
quote! {}
}
}
fn ws_client_method_opt_to_token(
ws_client_method: &Option<WsClientMethodInfo>,
) -> proc_macro2::TokenStream {
if let Some(method_info) = ws_client_method {
let method_name = &method_info.name;
quote! { Some(stringify!(#method_name)) }
} else {
quote! { None::<&str> }
}
}
fn ws_client_method_opt_to_call(
ws_client_method: &Option<WsClientMethodInfo>,
self_ty: &Box<syn::Type>,
) -> proc_macro2::TokenStream {
if let Some(method_info) = ws_client_method {
let method_name = &method_info.name;
if method_info.is_async {
quote! {
let state_ptr: *mut #self_ty = state;
hyperware_process_lib::hyperapp::spawn(async move {
unsafe { (*state_ptr).#method_name(channel_id, message_type, blob).await };
});
}
} else {
quote! { unsafe { (*state).#method_name(channel_id, message_type, blob) }; }
}
} else {
quote! {}
}
}
fn eth_method_opt_to_token(eth_method: &Option<EthMethodInfo>) -> proc_macro2::TokenStream {
if let Some(method_info) = eth_method {
let method_name = &method_info.name;
quote! { Some(stringify!(#method_name)) }
} else {
quote! { None::<&str> }
}
}
fn eth_method_opt_to_call(
eth_method: &Option<EthMethodInfo>,
self_ty: &Box<syn::Type>,
) -> proc_macro2::TokenStream {
if let Some(method_info) = eth_method {
let method_name = &method_info.name;
if method_info.is_async {
quote! {
let state_ptr: *mut #self_ty = state;
hyperware_process_lib::hyperapp::spawn(async move {
unsafe { (*state_ptr).#method_name(eth_sub_result).await };
});
}
} else {
quote! { unsafe { (*state).#method_name(eth_sub_result) }; }
}
} else {
quote! {}
}
}
fn generate_http_context_setup() -> proc_macro2::TokenStream {
quote! {
hyperware_process_lib::hyperapp::APP_HELPERS.with(|helpers| {
helpers.borrow_mut().current_http_context = Some(hyperware_process_lib::hyperapp::HttpRequestContext {
request: http_request,
response_headers: std::collections::HashMap::new(),
response_status: hyperware_process_lib::http::StatusCode::OK,
});
});
hyperware_process_lib::logging::debug!("HTTP context established");
}
}
fn generate_http_context_cleanup() -> proc_macro2::TokenStream {
quote! {
hyperware_process_lib::hyperapp::clear_http_request_context();
}
}
fn generate_http_error_response(
status: &str,
message: proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
let status_ident = format_ident!("{}", status);
let cleanup = generate_http_context_cleanup();
quote! {
hyperware_process_lib::http::server::send_response(
hyperware_process_lib::http::StatusCode::#status_ident,
None,
#message.into_bytes()
);
#cleanup
}
}
fn generate_http_request_parsing() -> proc_macro2::TokenStream {
quote! {
let http_method = hyperware_process_lib::hyperapp::get_http_method()
.unwrap_or_else(|| {
hyperware_process_lib::logging::warn!("Failed to get HTTP method from request context");
"UNKNOWN".to_string()
});
let current_path = match hyperware_process_lib::hyperapp::get_path() {
Some(path) => {
hyperware_process_lib::logging::debug!("Successfully parsed HTTP path: '{}'", path);
path
},
None => {
hyperware_process_lib::logging::error!("Failed to get HTTP path: no HTTP context available");
hyperware_process_lib::http::server::send_response(
hyperware_process_lib::http::StatusCode::BAD_REQUEST,
None,
b"Invalid path: no HTTP context available".to_vec(),
);
hyperware_process_lib::hyperapp::clear_http_request_context();
return;
}
};
}
}
fn generate_parameterized_handler_dispatch(
parameterized_handlers: &[&&FunctionMetadata],
self_ty: &Box<syn::Type>,
http_request_match_arms: &proc_macro2::TokenStream,
specific_paths: &[&String],
) -> proc_macro2::TokenStream {
let mut sorted_handlers = parameterized_handlers.to_vec();
sorted_handlers.sort_by_key(|handler| handler.http_path.is_none());
let dispatch_arms: Vec<_> = sorted_handlers.iter().map(|handler| {
let fn_name = &handler.name;
let variant_name = format_ident!("{}", &handler.variant_name);
let path_check = if let Some(path) = &handler.http_path {
quote! { ¤t_path == #path }
} else {
quote! { ![#(#specific_paths),*].contains(¤t_path.as_str()) }
};
let methods = &handler.http_methods;
let method_check = quote! { [#(#methods),*].contains(&http_method.as_str()) };
quote! {
hyperware_process_lib::logging::debug!("Checking parameterized handler {} for {} {} - path_check: {}, method_check: {}",
stringify!(#fn_name), http_method, current_path, (#path_check), (#method_check));
if #path_check && #method_check {
hyperware_process_lib::logging::debug!("Matched parameterized handler {} for {} {}", stringify!(#fn_name), http_method, current_path);
if let Some(ref blob) = blob_opt {
hyperware_process_lib::logging::debug!("Got blob with {} bytes: {}", blob.bytes.len(), String::from_utf8_lossy(&blob.bytes));
match serde_json::from_slice::<HPMRequest>(&blob.bytes) {
Ok(request) => {
match request {
HPMRequest::#variant_name(..) => {
unsafe {
#http_request_match_arms
hyperware_process_lib::hyperapp::maybe_save_state(&mut *state);
}
},
_ => {
hyperware_process_lib::logging::error!("Request body contains wrong handler name for {} {}", http_method, current_path);
hyperware_process_lib::http::server::send_response(
hyperware_process_lib::http::StatusCode::BAD_REQUEST,
None,
format!("Expected handler name '{}' in request body", stringify!(#variant_name)).into_bytes()
);
}
}
},
Err(e) => {
let error_details = if blob.bytes.is_empty() {
"Request body is empty but was expected to contain handler parameters.".to_string()
} else if let Ok(json_value) = serde_json::from_slice::<serde_json::Value>(&blob.bytes) {
format!(
"Invalid request format. Expected one of the parameterized handler formats, but got: {}",
serde_json::to_string(&json_value).unwrap_or_else(|_| "invalid JSON".to_string())
)
} else {
format!(
"Invalid JSON in request body. Parse error: {}",
e
)
};
hyperware_process_lib::logging::error!("Failed to parse request body for {} {}: {}", http_method, current_path, error_details);
hyperware_process_lib::http::server::send_response(
hyperware_process_lib::http::StatusCode::BAD_REQUEST,
None,
error_details.into_bytes()
);
hyperware_process_lib::hyperapp::clear_http_request_context();
return;
}
}
} else {
hyperware_process_lib::logging::error!("Handler {} requires a request body", stringify!(#fn_name));
hyperware_process_lib::http::server::send_response(
hyperware_process_lib::http::StatusCode::BAD_REQUEST,
None,
format!("Handler {} requires a request body", stringify!(#fn_name)).into_bytes()
);
}
return;
}
}
}).collect();
quote! { #(#dispatch_arms)* }
}
fn generate_parameterless_handler_dispatch(
parameterless_handlers: &[&&FunctionMetadata],
self_ty: &Box<syn::Type>,
specific_paths: &[&String],
) -> proc_macro2::TokenStream {
let mut sorted_handlers = parameterless_handlers.to_vec();
sorted_handlers.sort_by_key(|handler| handler.http_path.is_none());
let dispatch_arms: Vec<_> = sorted_handlers.iter().map(|handler| {
let fn_name = &handler.name;
let path_check = if let Some(path) = &handler.http_path {
quote! { ¤t_path == #path }
} else {
quote! { ![#(#specific_paths),*].contains(¤t_path.as_str()) }
};
let methods = &handler.http_methods;
let method_check = quote! { [#(#methods),*].contains(&http_method.as_str()) };
let response_handling = quote! {
let response_bytes = match serde_json::to_vec(&result) {
Ok(bytes) => bytes,
Err(e) => {
hyperware_process_lib::logging::error!("Failed to serialize response: {}", e);
hyperware_process_lib::http::server::send_response(
hyperware_process_lib::http::StatusCode::INTERNAL_SERVER_ERROR,
None,
"Failed to serialize response".as_bytes().to_vec(),
);
return;
}
};
let headers_opt = hyperware_process_lib::hyperapp::APP_HELPERS.with(|helpers| {
helpers.borrow().current_http_context.as_ref().and_then(|ctx| {
if ctx.response_headers.is_empty() {
None
} else {
Some(ctx.response_headers.clone())
}
})
});
let response_status = hyperware_process_lib::hyperapp::APP_HELPERS.with(|helpers| {
helpers
.borrow()
.current_http_context
.as_ref()
.map(|ctx| ctx.response_status)
.unwrap_or(hyperware_process_lib::http::StatusCode::OK)
});
hyperware_process_lib::http::server::send_response(
response_status,
headers_opt,
response_bytes
);
hyperware_process_lib::hyperapp::clear_http_request_context();
};
let handler_body = if handler.is_async {
quote! {
let state_ptr: *mut #self_ty = state;
hyperware_process_lib::hyperapp::spawn(async move {
let result = unsafe { (*state_ptr).#fn_name().await };
#response_handling
});
unsafe { hyperware_process_lib::hyperapp::maybe_save_state(&mut *state); }
}
} else {
quote! {
let result = unsafe { (*state).#fn_name() };
#response_handling
unsafe { hyperware_process_lib::hyperapp::maybe_save_state(&mut *state); }
}
};
quote! {
hyperware_process_lib::logging::debug!("Checking parameter-less handler {} for {} {} - path_check: {}, method_check: {}",
stringify!(#fn_name), http_method, current_path, (#path_check), (#method_check));
if #path_check && #method_check {
hyperware_process_lib::logging::debug!("Matched parameter-less handler {} for {} {}", stringify!(#fn_name), http_method, current_path);
#handler_body
return;
}
}
}).collect();
quote! { #(#dispatch_arms)* }
}
fn generate_http_handler_dispatcher(
http_handlers: &[&FunctionMetadata],
self_ty: &Box<syn::Type>,
http_request_match_arms: &proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
let specific_paths: Vec<_> = http_handlers
.iter()
.filter_map(|h| h.http_path.as_ref())
.collect();
let parameterized_handlers: Vec<_> = http_handlers
.iter()
.filter(|h| !h.params.is_empty())
.collect();
let parameterless_handlers: Vec<_> = http_handlers
.iter()
.filter(|h| h.params.is_empty())
.collect();
let parameterized_dispatch = generate_parameterized_handler_dispatch(
¶meterized_handlers,
self_ty,
http_request_match_arms,
&specific_paths,
);
let parameterless_dispatch =
generate_parameterless_handler_dispatch(¶meterless_handlers, self_ty, &specific_paths);
quote! {
hyperware_process_lib::logging::debug!("Starting handler matching for {} {}", http_method, current_path);
if blob_opt.is_some() && !blob_opt.as_ref().unwrap().bytes.is_empty() {
hyperware_process_lib::logging::debug!("Request has body, using two-phase matching");
if let Some(ref blob) = blob_opt {
match serde_json::from_slice::<HPMRequest>(&blob.bytes) {
Ok(request) => {
hyperware_process_lib::logging::debug!("Successfully parsed request body, dispatching to specific handler");
unsafe {
#http_request_match_arms
hyperware_process_lib::hyperapp::maybe_save_state(&mut *state);
}
return;
},
Err(e) => {
let error_details = if blob.bytes.is_empty() {
"Request body is empty but was expected to contain handler parameters.".to_string()
} else if let Ok(json_value) = serde_json::from_slice::<serde_json::Value>(&blob.bytes) {
format!(
"Invalid request format. Expected one of the parameterized handler formats, but got: {}",
serde_json::to_string(&json_value).unwrap_or_else(|_| "invalid JSON".to_string())
)
} else {
format!(
"Invalid JSON in request body. Parse error: {}",
e
)
};
hyperware_process_lib::logging::error!("Failed to parse request body for {} {}: {}", http_method, current_path, error_details);
hyperware_process_lib::http::server::send_response(
hyperware_process_lib::http::StatusCode::BAD_REQUEST,
None,
error_details.into_bytes()
);
hyperware_process_lib::hyperapp::clear_http_request_context();
return;
}
}
}
} else {
hyperware_process_lib::logging::debug!("Request has no body, trying parameter-less handlers first");
#parameterless_dispatch
}
hyperware_process_lib::logging::error!("No handler found for {} {} - all handlers checked", http_method, current_path);
hyperware_process_lib::http::server::send_response(
hyperware_process_lib::http::StatusCode::NOT_FOUND,
None,
format!("No handler found for {} {}", http_method, current_path).into_bytes(),
);
hyperware_process_lib::hyperapp::clear_http_request_context();
}
}
fn generate_websocket_client_handler(
ws_client_method_call: &proc_macro2::TokenStream,
self_ty: &Box<syn::Type>,
) -> proc_macro2::TokenStream {
quote! {
let blob_opt = message.blob();
match serde_json::from_slice::<hyperware_process_lib::http::client::HttpClientRequest>(message.body()) {
Ok(request) => {
match request {
hyperware_process_lib::http::client::HttpClientRequest::WebSocketPush { channel_id, message_type } => {
hyperware_process_lib::logging::debug!("Received WebSocket client push on channel {}, type: {:?}", channel_id, message_type);
if message_type == hyperware_process_lib::http::server::WsMessageType::Pong {
return;
}
if message_type == hyperware_process_lib::http::server::WsMessageType::Ping {
hyperware_process_lib::http::client::send_ws_client_push(
channel_id,
hyperware_process_lib::http::server::WsMessageType::Pong,
hyperware_process_lib::LazyLoadBlob::default(),
);
return;
}
let Some(blob) = blob_opt else {
hyperware_process_lib::logging::error!(
"Failed to get blob for WebSocket client push on channel {}. This indicates a malformed WebSocket message.",
channel_id
);
return;
};
hyperware_process_lib::logging::debug!("Processing WebSocket client message with {} bytes", blob.bytes.len());
#ws_client_method_call
unsafe {
hyperware_process_lib::hyperapp::maybe_save_state(&mut *state);
}
},
hyperware_process_lib::http::client::HttpClientRequest::WebSocketClose { channel_id } => {
hyperware_process_lib::logging::debug!("WebSocket client connection closed on channel {}", channel_id);
let message_type = hyperware_process_lib::http::server::WsMessageType::Close;
let blob = hyperware_process_lib::LazyLoadBlob::default();
#ws_client_method_call
unsafe {
hyperware_process_lib::hyperapp::maybe_save_state(&mut *state);
}
}
}
},
Err(e) => {
hyperware_process_lib::logging::error!(
"Failed to parse WebSocket client request: {}\n\
Source: {:?}\n\
This usually indicates a malformed message from the http-client service.",
e, message.source()
);
}
}
}
}
fn generate_websocket_handler(
ws_method_call: &proc_macro2::TokenStream,
self_ty: &Box<syn::Type>,
) -> proc_macro2::TokenStream {
quote! {
hyperware_process_lib::http::server::HttpServerRequest::WebSocketPush { channel_id, message_type } => {
hyperware_process_lib::logging::debug!("Received WebSocket message on channel {}, type: {:?}", channel_id, message_type);
if message_type == hyperware_process_lib::http::server::WsMessageType::Pong {
return;
}
if message_type == hyperware_process_lib::http::server::WsMessageType::Ping {
hyperware_process_lib::http::server::send_ws_push(
channel_id,
hyperware_process_lib::http::server::WsMessageType::Pong,
hyperware_process_lib::LazyLoadBlob::default(),
);
return;
}
let Some(blob) = blob_opt else {
hyperware_process_lib::logging::error!(
"Failed to get blob for WebSocketPush on channel {}. This indicates a malformed WebSocket message.",
channel_id
);
return;
};
hyperware_process_lib::logging::debug!("Processing WebSocket message with {} bytes", blob.bytes.len());
#ws_method_call
unsafe {
hyperware_process_lib::hyperapp::maybe_save_state(&mut *state);
}
},
hyperware_process_lib::http::server::HttpServerRequest::WebSocketOpen { path, channel_id, source_socket_addr, forwarded_for } => {
hyperware_process_lib::logging::debug!("WebSocket connection opened on path '{}' with channel {}", path, channel_id);
match hyperware_process_lib::hyperapp::get_server() {
Some(server) => server.handle_websocket_open(&path, channel_id, source_socket_addr, forwarded_for),
None => hyperware_process_lib::logging::error!("Failed to get server instance for WebSocket open event")
}
},
hyperware_process_lib::http::server::HttpServerRequest::WebSocketClose(channel_id) => {
hyperware_process_lib::logging::debug!("WebSocket connection closed on channel {}", channel_id);
match hyperware_process_lib::hyperapp::get_server() {
Some(server) => server.handle_websocket_close(channel_id),
None => hyperware_process_lib::logging::error!("Failed to get server instance for WebSocket close event")
}
}
}
}
fn generate_local_message_handler(
self_ty: &Box<syn::Type>,
match_arms: &proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
quote! {
fn handle_local_message(state: *mut #self_ty, message: hyperware_process_lib::Message) {
hyperware_process_lib::logging::debug!("Processing local message from: {:?}", message.source());
match serde_json::from_slice::<HPMRequest>(message.body()) {
Ok(request) => {
unsafe {
#match_arms
hyperware_process_lib::hyperapp::maybe_save_state(&mut *state);
}
},
Err(e) => {
let raw_body = String::from_utf8_lossy(message.body());
hyperware_process_lib::logging::error!(
"Failed to deserialize local request into HPMRequest enum.\n\
Error: {}\n\
Source: {:?}\n\
Body: {}\n\
\n\
💡 This usually means the message format doesn't match any of your #[local] or #[remote] handlers.\n\
💡 If you are sending an HTTP message, if it is malformed, it might have ended up in the local message handler.",
e, message.source(), raw_body
);
}
}
}
}
}
fn generate_remote_message_handler(
self_ty: &Box<syn::Type>,
match_arms: &proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
quote! {
fn handle_remote_message(state: *mut #self_ty, message: hyperware_process_lib::Message) {
hyperware_process_lib::logging::debug!("Processing remote message from: {:?}", message.source());
match serde_json::from_slice::<HPMRequest>(message.body()) {
Ok(request) => {
unsafe {
#match_arms
hyperware_process_lib::hyperapp::maybe_save_state(&mut *state);
}
},
Err(e) => {
let raw_body = String::from_utf8_lossy(message.body());
hyperware_process_lib::logging::error!(
"Failed to deserialize remote request into HPMRequest enum.\n\
Error: {}\n\
Source: {:?}\n\
Body: {}\n\
\n\
💡 This usually means the message format doesn't match any of your #[remote] handlers.",
e, message.source(), raw_body
);
}
}
}
}
}
fn generate_message_handlers(
self_ty: &Box<syn::Type>,
handler_arms: &HandlerDispatch,
ws_method_call: &proc_macro2::TokenStream,
ws_client_method_call: &proc_macro2::TokenStream,
eth_method_call: &proc_macro2::TokenStream,
http_handlers: &[&FunctionMetadata],
) -> proc_macro2::TokenStream {
let http_request_match_arms = &handler_arms.http;
let local_and_remote_request_match_arms = &handler_arms.local_and_remote;
let remote_request_match_arms = &handler_arms.remote;
let http_context_setup = generate_http_context_setup();
let http_request_parsing = generate_http_request_parsing();
let http_dispatcher =
generate_http_handler_dispatcher(http_handlers, self_ty, http_request_match_arms);
let websocket_handlers = generate_websocket_handler(ws_method_call, self_ty);
let websocket_client_handler =
generate_websocket_client_handler(ws_client_method_call, self_ty);
let local_message_handler =
generate_local_message_handler(self_ty, local_and_remote_request_match_arms);
let remote_message_handler =
generate_remote_message_handler(self_ty, remote_request_match_arms);
let eth_message_handler = generate_eth_message_handler(self_ty, eth_method_call);
quote! {
fn handle_websocket_client_message(state: *mut #self_ty, message: hyperware_process_lib::Message) {
#websocket_client_handler
}
fn handle_http_server_message(state: *mut #self_ty, http_server_request: hyperware_process_lib::http::server::HttpServerRequest, blob_opt: Option<hyperware_process_lib::LazyLoadBlob>) {
match http_server_request {
hyperware_process_lib::http::server::HttpServerRequest::Http(http_request) => {
hyperware_process_lib::logging::debug!("Processing HTTP request, message has blob: {}", blob_opt.is_some());
if let Some(ref blob) = blob_opt {
hyperware_process_lib::logging::debug!("Blob size: {} bytes, content: {}", blob.bytes.len(), String::from_utf8_lossy(&blob.bytes[..std::cmp::min(200, blob.bytes.len())]));
}
#http_context_setup
#http_request_parsing
#http_dispatcher
},
#websocket_handlers
}
}
#local_message_handler
#remote_message_handler
#eth_message_handler
}
}
fn generate_eth_message_handler(
self_ty: &Box<syn::Type>,
eth_method_call: &proc_macro2::TokenStream,
) -> proc_macro2::TokenStream {
quote! {
fn handle_eth_message(state: *mut #self_ty, message: hyperware_process_lib::Message) {
hyperware_process_lib::logging::debug!("Processing ETH message from: {:?}", message.source());
match serde_json::from_slice::<hyperware_process_lib::eth::EthSubResult>(message.body()) {
Ok(eth_sub_result) => {
hyperware_process_lib::logging::debug!("Successfully parsed EthSubResult, calling ETH handler");
#eth_method_call
unsafe {
hyperware_process_lib::hyperapp::maybe_save_state(&mut *state);
}
},
Err(e) => {
let raw_body = String::from_utf8_lossy(message.body());
hyperware_process_lib::logging::error!(
"Failed to deserialize ETH message into EthSubResult.\n\
Error: {}\n\
Source: {:?}\n\
Body: {}\n\
\n\
💡 This usually means the message format from eth:distro:sys doesn't match EthSubResult.",
e, message.source(), raw_body
);
}
}
}
}
}
fn is_none_literal(expr: &Expr) -> bool {
if let Expr::Path(expr_path) = expr {
if let Some(ident) = expr_path.path.get_ident() {
return ident == "None";
}
}
false
}
fn generate_component_impl(
args: &HyperProcessArgs,
self_ty: &Box<syn::Type>,
cleaned_impl_block: &ItemImpl,
request_enum: &proc_macro2::TokenStream,
response_enum: &proc_macro2::TokenStream,
init_method_details: &InitMethodDetails,
ws_method_details: &WsMethodDetails,
ws_client_method_details: &WsClientMethodDetails,
eth_method_details: &EthMethodDetails,
handler_arms: &HandlerDispatch,
has_init_logging: bool,
http_handlers: &[&FunctionMetadata],
) -> proc_macro2::TokenStream {
let name = &args.name;
let endpoints = &args.endpoints;
let save_config = &args.save_config;
let wit_world = &args.wit_world;
let icon = match &args.icon {
Some(icon_str) => quote! { Some(#icon_str.to_string()) },
None => quote! { None },
};
let widget = match &args.widget {
Some(widget_str) => quote! { Some(#widget_str.to_string()) },
None => quote! { None },
};
let ui = match &args.ui {
Some(ui_expr) => {
if is_none_literal(ui_expr) {
quote! { None }
} else {
quote! { Some(#ui_expr) }
}
}
None => quote! { None },
};
let ui_path = match &args.ui_path {
Some(path_str) => quote! { Some(#path_str.to_string()) },
None => quote! { None },
};
let init_method_ident = &init_method_details.identifier;
let init_method_call = &init_method_details.call;
let ws_method_call = &ws_method_details.call;
let ws_client_method_call = &ws_client_method_details.call;
let eth_method_call = ð_method_details.call;
let message_handlers = generate_message_handlers(
self_ty,
handler_arms,
ws_method_call,
ws_client_method_call,
eth_method_call,
http_handlers,
);
let logging_init = if !has_init_logging {
quote! {
hyperware_process_lib::logging::init_logging(
hyperware_process_lib::logging::Level::DEBUG,
hyperware_process_lib::logging::Level::INFO,
None, Some((0, 0, 1, 1)), None
).unwrap();
}
} else {
quote! {}
};
quote! {
wit_bindgen::generate!({
path: "../target/wit",
world: #wit_world,
generate_unused_types: true,
additional_derives: [serde::Deserialize, serde::Serialize, process_macros::SerdeJsonInto],
});
use hyperware_process_lib::http::server::HttpBindingConfig;
use hyperware_process_lib::http::server::WsBindingConfig;
use hyperware_process_lib::hyperapp::Binding;
#cleaned_impl_block
#request_enum
#response_enum
#message_handlers
struct Component;
impl Guest for Component {
fn init(_our: String) {
let mut state = hyperware_process_lib::hyperapp::initialize_state::<#self_ty>();
hyperware_process_lib::hyperapp::APP_CONTEXT.with(|ctx| {
ctx.borrow_mut().hidden_state = Some(
hyperware_process_lib::hyperapp::HiddenState::new(#save_config)
);
});
let app_name = #name;
let app_icon = #icon;
let app_widget = #widget;
let ui_config = #ui;
let ui_path = #ui_path;
let endpoints = #endpoints;
if app_icon.is_some() && app_widget.is_some() {
hyperware_process_lib::homepage::add_to_homepage(app_name, app_icon, Some("/"), app_widget);
}
#logging_init
let mut server = hyperware_process_lib::hyperapp::setup_server(ui_config.as_ref(), ui_path, &endpoints);
hyperware_process_lib::hyperapp::APP_HELPERS.with(|ctx| {
ctx.borrow_mut().current_server = Some(&mut server);
});
if #init_method_ident.is_some() {
#init_method_call
}
loop {
hyperware_process_lib::hyperapp::APP_CONTEXT.with(|ctx| {
ctx.borrow_mut().executor.poll_all_tasks();
});
match hyperware_process_lib::await_message() {
Ok(message) => {
hyperware_process_lib::hyperapp::APP_HELPERS.with(|ctx| {
ctx.borrow_mut().current_message = Some(message.clone());
});
hyperware_process_lib::hyperapp::store_old_state(&state);
match message {
hyperware_process_lib::Message::Response { body, context, .. } => {
let correlation_id = context
.as_deref()
.map(|bytes| String::from_utf8_lossy(bytes).to_string())
.unwrap_or_else(|| "no context".to_string());
let was_cancelled =
hyperware_process_lib::hyperapp::CANCELLED_RESPONSES.with(|set| {
set.borrow_mut().remove(&correlation_id)
});
if !was_cancelled {
hyperware_process_lib::hyperapp::RESPONSE_REGISTRY.with(|registry| {
registry.borrow_mut().insert(correlation_id, body);
});
}
}
hyperware_process_lib::Message::Request { .. } => {
if message.is_local() && message.source().process == "http-server:distro:sys" {
if let Ok(http_server_request) = serde_json::from_slice::<hyperware_process_lib::http::server::HttpServerRequest>(message.body()) {
handle_http_server_message(&mut state, http_server_request, message.blob());
} else {
handle_local_message(&mut state, message);
}
} else if message.is_local() && message.source().process == "http-client:distro:sys" {
handle_websocket_client_message(&mut state, message);
} else if message.is_local() && message.source().process == "eth:distro:sys" {
handle_eth_message(&mut state, message);
} else if message.is_local() {
handle_local_message(&mut state, message);
} else {
handle_remote_message(&mut state, message);
}
}
}
},
Err(ref error) => {
if let hyperware_process_lib::SendError {
context: Some(context),
..
} = error
{
let correlation_id = String::from_utf8_lossy(context)
.to_string();
let was_cancelled =
hyperware_process_lib::hyperapp::CANCELLED_RESPONSES.with(|set| {
set.borrow_mut().remove(&correlation_id)
});
if !was_cancelled {
hyperware_process_lib::hyperapp::RESPONSE_REGISTRY.with(|registry| {
registry.borrow_mut().insert(
correlation_id,
serde_json::to_vec(error).unwrap(),
);
});
}
}
}
}
}
}
}
export!(Component);
}
}
#[proc_macro_attribute]
pub fn hyperapp(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr_args = parse_macro_input!(attr as MetaList);
let impl_block = parse_macro_input!(item as ItemImpl);
let args = match parse_args(attr_args) {
Ok(args) => args,
Err(e) => return e.to_compile_error().into(),
};
let self_ty = &impl_block.self_ty;
let (init_method, ws_method, ws_client_method, eth_method, function_metadata, has_init_logging) =
match analyze_methods(&impl_block) {
Ok(methods) => methods,
Err(e) => return e.to_compile_error().into(),
};
let handlers = HandlerGroups::from_function_metadata(&function_metadata);
let http_handlers_with_params: Vec<_> = handlers.http.iter().cloned().collect();
let metadata_for_enum: Vec<_> = function_metadata.iter().cloned().collect();
let (request_enum, response_enum) = generate_request_response_enums(&metadata_for_enum);
let handler_arms = HandlerDispatch {
local: generate_handler_dispatch(&handlers.local, self_ty, HandlerType::Local),
remote: generate_handler_dispatch(&handlers.remote, self_ty, HandlerType::Remote),
http: generate_handler_dispatch(&http_handlers_with_params, self_ty, HandlerType::Http),
local_and_remote: generate_handler_dispatch(
&handlers.local_and_remote,
self_ty,
HandlerType::Local,
),
};
let cleaned_impl_block = clean_impl_block(&impl_block);
let init_method_details = InitMethodDetails {
identifier: init_method_opt_to_token(&init_method),
call: init_method_opt_to_call(&init_method, self_ty),
};
let ws_method_details = WsMethodDetails {
identifier: ws_method_opt_to_token(&ws_method),
call: ws_method_opt_to_call(&ws_method, self_ty),
};
let ws_client_method_details = WsClientMethodDetails {
identifier: ws_client_method_opt_to_token(&ws_client_method),
call: ws_client_method_opt_to_call(&ws_client_method, self_ty),
};
let eth_method_details = EthMethodDetails {
identifier: eth_method_opt_to_token(ð_method),
call: eth_method_opt_to_call(ð_method, self_ty),
};
generate_component_impl(
&args,
self_ty,
&cleaned_impl_block,
&request_enum,
&response_enum,
&init_method_details,
&ws_method_details,
&ws_client_method_details,
ð_method_details,
&handler_arms,
has_init_logging,
&handlers.http,
)
.into()
}