Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,4 @@ docs/.astro

# Examples - ignore lock files since they use local file: links
examples/*/package-lock.json
examples/pumpfun-server/
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions hyperstack-macros/src/ast/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,27 @@ pub struct ComputedFieldSpec {
#[serde(rename_all = "lowercase")]
pub enum ResolverType {
Token,
Url(UrlResolverConfig),
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
#[serde(rename_all = "lowercase")]
pub enum HttpMethod {
#[default]
Get,
Post,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct UrlResolverConfig {
/// Field path to get the URL from (e.g., "info.uri")
pub url_path: String,
/// HTTP method to use (default: GET)
#[serde(default)]
pub method: HttpMethod,
/// JSON path to extract from response (None = full payload)
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extract_path: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down
48 changes: 46 additions & 2 deletions hyperstack-macros/src/codegen/vixen_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ pub fn generate_vm_handler(
health_monitor: Option<hyperstack::runtime::hyperstack_server::HealthMonitor>,
slot_tracker: hyperstack::runtime::hyperstack_server::SlotTracker,
resolver_client: Option<std::sync::Arc<ResolverClient>>,
url_resolver_client: std::sync::Arc<hyperstack::runtime::hyperstack_interpreter::resolvers::UrlResolverClient>,
}

impl std::fmt::Debug for VmHandler {
Expand All @@ -278,6 +279,7 @@ pub fn generate_vm_handler(
health_monitor: Option<hyperstack::runtime::hyperstack_server::HealthMonitor>,
slot_tracker: hyperstack::runtime::hyperstack_server::SlotTracker,
resolver_client: Option<std::sync::Arc<ResolverClient>>,
url_resolver_client: std::sync::Arc<hyperstack::runtime::hyperstack_interpreter::resolvers::UrlResolverClient>,
) -> Self {
Self {
vm,
Expand All @@ -286,6 +288,7 @@ pub fn generate_vm_handler(
health_monitor,
slot_tracker,
resolver_client,
url_resolver_client,
}
}

Expand Down Expand Up @@ -347,13 +350,18 @@ pub fn generate_vm_handler(
};

let mut token_requests = Vec::new();
let mut url_requests = Vec::new();
let mut other_requests = Vec::new();

for request in requests {
match request.resolver {
match &request.resolver {
hyperstack::runtime::hyperstack_interpreter::ast::ResolverType::Token => {
token_requests.push(request)
}
hyperstack::runtime::hyperstack_interpreter::ast::ResolverType::Url(_) => {
url_requests.push(request)
}
#[allow(unreachable_patterns)]
_ => other_requests.push(request),
}
}
Expand Down Expand Up @@ -429,6 +437,17 @@ pub fn generate_vm_handler(
}
}

// Process URL resolver requests in parallel with deduplication
if !url_requests.is_empty() {
let mut url_mutations = hyperstack::runtime::hyperstack_interpreter::resolvers::resolve_url_batch(
&self.vm,
self.bytecode.as_ref(),
&self.url_resolver_client,
url_requests,
).await;
mutations.append(&mut url_mutations);
}

if !other_requests.is_empty() {
let other_count = other_requests.len();
let mut vm = self.vm.lock().unwrap_or_else(|e| e.into_inner());
Expand Down Expand Up @@ -858,6 +877,8 @@ pub fn generate_spec_function(
}
};

let url_resolver_client = Arc::new(hyperstack::runtime::hyperstack_interpreter::resolvers::UrlResolverClient::new());

let slot_tracker = hyperstack::runtime::hyperstack_server::SlotTracker::new();
let mut attempt = 0u32;
let mut backoff = reconnection_config.initial_delay;
Expand Down Expand Up @@ -899,6 +920,7 @@ pub fn generate_spec_function(
health_monitor.clone(),
slot_tracker.clone(),
resolver_client.clone(),
url_resolver_client.clone(),
);

let account_parser = parsers::AccountParser;
Expand Down Expand Up @@ -1167,6 +1189,7 @@ pub fn generate_vm_handler_struct() -> TokenStream {
health_monitor: Option<hyperstack::runtime::hyperstack_server::HealthMonitor>,
slot_tracker: hyperstack::runtime::hyperstack_server::SlotTracker,
resolver_client: Option<std::sync::Arc<ResolverClient>>,
url_resolver_client: std::sync::Arc<hyperstack::runtime::hyperstack_interpreter::resolvers::UrlResolverClient>,
}

impl std::fmt::Debug for VmHandler {
Expand All @@ -1186,6 +1209,7 @@ pub fn generate_vm_handler_struct() -> TokenStream {
health_monitor: Option<hyperstack::runtime::hyperstack_server::HealthMonitor>,
slot_tracker: hyperstack::runtime::hyperstack_server::SlotTracker,
resolver_client: Option<std::sync::Arc<ResolverClient>>,
url_resolver_client: std::sync::Arc<hyperstack::runtime::hyperstack_interpreter::resolvers::UrlResolverClient>,
) -> Self {
Self {
vm,
Expand All @@ -1194,6 +1218,7 @@ pub fn generate_vm_handler_struct() -> TokenStream {
health_monitor,
slot_tracker,
resolver_client,
url_resolver_client,
}
}

Expand Down Expand Up @@ -1255,13 +1280,18 @@ pub fn generate_vm_handler_struct() -> TokenStream {
};

let mut token_requests = Vec::new();
let mut url_requests = Vec::new();
let mut other_requests = Vec::new();

for request in requests {
match request.resolver {
match &request.resolver {
hyperstack::runtime::hyperstack_interpreter::ast::ResolverType::Token => {
token_requests.push(request)
}
hyperstack::runtime::hyperstack_interpreter::ast::ResolverType::Url(_) => {
url_requests.push(request)
}
#[allow(unreachable_patterns)]
_ => other_requests.push(request),
}
}
Expand Down Expand Up @@ -1337,6 +1367,17 @@ pub fn generate_vm_handler_struct() -> TokenStream {
}
}

// Process URL resolver requests in parallel with deduplication
if !url_requests.is_empty() {
let mut url_mutations = hyperstack::runtime::hyperstack_interpreter::resolvers::resolve_url_batch(
&self.vm,
self.bytecode.as_ref(),
&self.url_resolver_client,
url_requests,
).await;
mutations.append(&mut url_mutations);
}

if !other_requests.is_empty() {
let other_count = other_requests.len();
let mut vm = self.vm.lock().unwrap_or_else(|e| e.into_inner());
Expand Down Expand Up @@ -1834,6 +1875,8 @@ pub fn generate_multi_pipeline_spec_function(
}
};

let url_resolver_client = Arc::new(hyperstack::runtime::hyperstack_interpreter::resolvers::UrlResolverClient::new());

let slot_tracker = hyperstack::runtime::hyperstack_server::SlotTracker::new();
let mut attempt = 0u32;
let mut backoff = reconnection_config.initial_delay;
Expand Down Expand Up @@ -1875,6 +1918,7 @@ pub fn generate_multi_pipeline_spec_function(
health_monitor.clone(),
slot_tracker.clone(),
resolver_client.clone(),
url_resolver_client.clone(),
);

if attempt == 0 {
Expand Down
1 change: 1 addition & 0 deletions hyperstack-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ pub fn hyperstack(attr: TokenStream, item: TokenStream) -> TokenStream {
/// - `#[aggregate(...)]` - Aggregate field values
/// - `#[computed(...)]` - Computed fields from other fields
/// - `#[derive_from(...)]` - Derive values from instructions
/// - `#[resolve(...)]` - Resolve external data (token metadata via DAS API or data from URLs)
#[proc_macro_derive(
Stream,
attributes(
Expand Down
58 changes: 55 additions & 3 deletions hyperstack-macros/src/parse/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,8 @@ pub fn parse_aggregate_attribute(
pub struct ResolveAttribute {
pub from: Option<String>,
pub address: Option<String>,
pub url: Option<String>,
pub method: Option<String>,
pub extract: Option<String>,
pub target_field_name: String,
pub resolver: Option<String>,
Expand All @@ -1023,6 +1025,8 @@ pub struct ResolveSpec {
struct ResolveAttributeArgs {
from: Option<String>,
address: Option<String>,
url: Option<String>,
method: Option<syn::Ident>,
extract: Option<String>,
resolver: Option<String>,
strategy: Option<String>,
Expand All @@ -1032,6 +1036,8 @@ impl Parse for ResolveAttributeArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut from = None;
let mut address = None;
let mut url = None;
let mut method = None;
let mut extract = None;
let mut resolver = None;
let mut strategy = None;
Expand All @@ -1048,6 +1054,29 @@ impl Parse for ResolveAttributeArgs {
} else if ident_str == "address" {
let lit: syn::LitStr = input.parse()?;
address = Some(lit.value());
} else if ident_str == "url" {
// Parse as dotted path (e.g., info.uri) - handle both dot-separated and single identifiers
let mut parts = Vec::new();
let first: syn::Ident = input.parse()?;
parts.push(first.to_string());

// Parse any additional .identifier segments
while input.peek(Token![.]) {
input.parse::<Token![.]>()?;
let next: syn::Ident = input.parse()?;
parts.push(next.to_string());
}

url = Some(parts.join("."));
} else if ident_str == "method" {
let method_ident: syn::Ident = input.parse()?;
match method_ident.to_string().to_lowercase().as_str() {
"get" | "post" => method = Some(method_ident),
_ => return Err(syn::Error::new(
method_ident.span(),
"Invalid HTTP method. Only 'GET' or 'POST' are supported.",
)),
}
} else if ident_str == "extract" {
let lit: syn::LitStr = input.parse()?;
extract = Some(lit.value());
Expand Down Expand Up @@ -1077,6 +1106,8 @@ impl Parse for ResolveAttributeArgs {
Ok(ResolveAttributeArgs {
from,
address,
url,
method,
extract,
resolver,
strategy,
Expand All @@ -1094,17 +1125,37 @@ pub fn parse_resolve_attribute(

let args: ResolveAttributeArgs = attr.parse_args()?;

// Check for mutually exclusive parameters: url vs (from/address)
let has_url = args.url.is_some();
let has_token_source = args.from.is_some() || args.address.is_some();

if has_url && has_token_source {
return Err(syn::Error::new_spanned(
attr,
"#[resolve] cannot specify 'url' together with 'from' or 'address'",
));
}

if !has_url && !has_token_source {
return Err(syn::Error::new_spanned(
attr,
"#[resolve] requires either 'url' or 'from'/'address' parameter",
));
}

// Token resolvers: cannot have both from and address
if args.from.is_some() && args.address.is_some() {
return Err(syn::Error::new_spanned(
attr,
"#[resolve] cannot specify both 'from' and 'address'",
));
}

if args.from.is_none() && args.address.is_none() {
// URL resolvers require extract parameter
if has_url && args.extract.is_none() {
return Err(syn::Error::new_spanned(
attr,
"#[resolve] requires either 'from' or 'address' parameter",
"#[resolve] with 'url' requires 'extract' parameter",
));
}

Expand All @@ -1113,6 +1164,8 @@ pub fn parse_resolve_attribute(
Ok(Some(ResolveAttribute {
from: args.from,
address: args.address,
url: args.url,
method: args.method.map(|m| m.to_string()),
extract: args.extract,
target_field_name: target_field_name.to_string(),
resolver: args.resolver,
Expand Down Expand Up @@ -1146,7 +1199,6 @@ pub fn parse_computed_attribute(
target_field_name: target_field_name.to_string(),
}))
}

pub fn has_entity_attribute(attrs: &[Attribute]) -> bool {
attrs.iter().any(|attr| attr.path().is_ident("entity"))
}
Expand Down
9 changes: 6 additions & 3 deletions hyperstack-macros/src/stream_spec/ast_writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,11 @@ fn build_resolver_specs(resolve_specs: &[parse::ResolveSpec]) -> Vec<ResolverSpe
extracts: Vec::new(),
});

let source_path = spec.extract.clone();

let extract = ResolverExtractSpec {
target_path: spec.target_field_name.clone(),
source_path: spec.extract.clone(),
source_path,
transform: None,
};

Expand All @@ -232,9 +234,10 @@ fn parse_resolve_strategy(strategy: &str) -> ResolveStrategy {
}
}

fn resolver_type_key(resolver: &ResolverType) -> &'static str {
fn resolver_type_key(resolver: &ResolverType) -> String {
match resolver {
ResolverType::Token => "token",
ResolverType::Token => "token".to_string(),
ResolverType::Url(config) => format!("url:{}", config.url_path),
}
}

Expand Down
Loading