This commit is contained in:
2025-09-08 14:53:18 -04:00
parent fc96d91c77
commit 3043170396
2 changed files with 98 additions and 163 deletions

97
Cargo.lock generated
View File

@ -21,9 +21,9 @@ dependencies = [
[[package]]
name = "actix-http"
version = "3.11.0"
version = "3.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44dfe5c9e0004c623edc65391dfd51daa201e7e30ebd9c9bedf873048ec32bc2"
checksum = "44cceded2fb55f3c4b67068fa64962e2ca59614edc5b03167de9ff82ae803da0"
dependencies = [
"actix-codec",
"actix-rt",
@ -60,9 +60,9 @@ dependencies = [
[[package]]
name = "actix-rt"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208"
checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63"
dependencies = [
"futures-core",
"tokio",
@ -210,9 +210,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "2.9.2"
version = "2.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]]
name = "block-buffer"
@ -261,10 +261,11 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.33"
version = "1.2.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f"
checksum = "5252b3d2648e5eedbc1a6f501e3c795e07025c1e93bbf8bbdd6eef7f447a6d54"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
@ -317,9 +318,9 @@ dependencies = [
[[package]]
name = "deranged"
version = "0.4.0"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc"
dependencies = [
"powerfmt",
]
@ -370,6 +371,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "find-msvc-tools"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
[[package]]
name = "flate2"
version = "1.1.2"
@ -394,9 +401,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
@ -451,7 +458,7 @@ dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
"wasi 0.14.4+wasi-0.2.4",
]
[[package]]
@ -549,9 +556,9 @@ checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
[[package]]
name = "indexmap"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9"
dependencies = [
"equivalent",
"hashbrown",
@ -559,9 +566,9 @@ dependencies = [
[[package]]
name = "io-uring"
version = "0.7.9"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
dependencies = [
"bitflags",
"cfg-if",
@ -576,9 +583,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jobserver"
version = "0.1.33"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom",
"libc",
@ -625,9 +632,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.27"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "memchr"
@ -707,9 +714,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
version = "2.3.1"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project-lite"
@ -938,12 +945,11 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.41"
version = "0.3.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
@ -953,15 +959,15 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.4"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
[[package]]
name = "time-macros"
version = "0.2.22"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
dependencies = [
"num-conv",
"time-core",
@ -1096,11 +1102,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
version = "0.14.4+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a"
dependencies = [
"wit-bindgen-rt",
"wit-bindgen",
]
[[package]]
@ -1178,33 +1184,30 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.12"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
name = "wit-bindgen"
version = "0.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36"
[[package]]
name = "zerocopy"
version = "0.8.26"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.26"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
@ -1231,9 +1234,9 @@ dependencies = [
[[package]]
name = "zstd-sys"
version = "2.0.15+zstd.1.5.7"
version = "2.0.16+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
dependencies = [
"cc",
"pkg-config",

View File

@ -5,12 +5,12 @@ use quote::format_ident;
use http_core::{Queryable, ApiDispatch, HasHttp, Keys};
#[proc_macro_derive(HttpRequest, attributes(http_response, http_error_type))]
pub fn derive_http_get_request(input: TokenStream) -> TokenStream {
pub fn derive_http_request(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let query_name = &input.ident;
let query_name_str = query_name.to_string();
// Parse optional #[http_response = "..."] attribute
// Parse optional #[http_response = "..."]
let mut response_name_opt: Option<String> = None;
for attr in &input.attrs {
if attr.path().is_ident("http_response") {
@ -22,15 +22,14 @@ pub fn derive_http_get_request(input: TokenStream) -> TokenStream {
}
}
Ok(())
}).unwrap_or_else(|e| panic!("Error parsing http_response attribute: {}", e));
}).unwrap();
}
}
let response_name_str = response_name_opt.unwrap_or_else(|| format!("{}Resp", query_name_str));
let response_name = format_ident!("{}", response_name_str);
// Parse optional #[http_error_type = "..."] attribute (default to `E`)
let mut error_type = syn::Path::from(syn::Ident::new("E", proc_macro2::Span::call_site()));
// Parse optional #[http_error_type = "..."] (default to Box<dyn Error>)
let mut error_type = syn::parse_str::<syn::Path>("Box<dyn std::error::Error>").unwrap();
for attr in &input.attrs {
if attr.path().is_ident("http_error_type") {
attr.parse_nested_meta(|meta| {
@ -41,11 +40,11 @@ pub fn derive_http_get_request(input: TokenStream) -> TokenStream {
}
}
Ok(())
}).unwrap_or_else(|e| panic!("Error parsing http_error_type attribute: {}", e));
}).unwrap();
}
}
// Collect query parameters from lnk_p_* fields
// Collect query parameters from fields prefixed with lnk_p_
let query_param_code = if let Data::Struct(data_struct) = &input.data {
if let Fields::Named(fields_named) = &data_struct.fields {
fields_named.named.iter().filter_map(|field| {
@ -58,51 +57,62 @@ pub fn derive_http_get_request(input: TokenStream) -> TokenStream {
query_params.push((#key.to_string(), val.to_string()));
}
})
} else {
None
}
} else { None }
}).collect::<Vec<_>>()
} else {
Vec::new()
}
} else {
Vec::new()
};
} else { Vec::new() }
} else { Vec::new() };
let expanded = quote! {
#[async_trait::async_trait]
impl Queryable for #query_name {
impl http_core::Queryable for #query_name {
type R = #response_name;
type E = #error_type;
async fn send(
&self,
base_url: &str,
override_url: Option<&str>,
sandbox: bool,
method_override: Option<&str>,
headers: Option<Vec<(&str, &str)>>,
) -> Result<Self::R, #error_type> {
) -> Result<Self::R, Self::E> {
use awc::Client;
use urlencoding::encode;
use http_core::HasHttp;
// collect lnk_p_* query params
let mut query_params: Vec<(String, String)> = Vec::new();
#(#query_param_code)*
let mut url = base_url.to_string();
// pick URL
let mut url = if let Some(u) = override_url {
u.to_string()
} else if sandbox {
<Self as HasHttp>::sandbox_url().to_string()
} else {
<Self as HasHttp>::live_url().to_string()
};
if !query_params.is_empty() {
let mut query_string = String::new();
let mut first = true;
for (k, v) in &query_params {
if !first {
query_string.push('&');
}
first = false;
query_string.push_str(&format!("{}={}", k, encode(v)));
}
let query_string = query_params.into_iter()
.map(|(k,v)| format!("{}={}", k, encode(&v)))
.collect::<Vec<_>>()
.join("&");
url.push('?');
url.push_str(&query_string);
}
// choose method
let client = Client::default();
let mut request = client.get(url);
let mut request = match method_override.unwrap_or("GET") {
"GET" => client.get(url),
"POST" => client.post(url),
"PUT" => client.put(url),
"DELETE" => client.delete(url),
"PATCH" => client.patch(url),
m => panic!("Unsupported method override: {}", m),
};
// add headers
if let Some(hdrs) = headers {
for (k, v) in hdrs {
request = request.append_header((k, v));
@ -120,7 +130,6 @@ pub fn derive_http_get_request(input: TokenStream) -> TokenStream {
TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn alpaca_cli(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input_enum = parse_macro_input!(item as ItemEnum);
@ -159,31 +168,19 @@ pub fn alpaca_cli(_attr: TokenStream, item: TokenStream) -> TokenStream {
let client = Arc::new(awc::Client::default());
let keys = Arc::new(crate::load_api_keys()?);
const THREADS: usize = 4;
let total = queries.len();
let per_thread = std::cmp::max(1, total / THREADS);
let shared_queries = Arc::new(queries);
// Spawn all queries as async tasks
let mut handles = Vec::new();
for i in 0..THREADS {
let queries = Arc::clone(&shared_queries);
for q in queries {
let client = Arc::clone(&client);
let keys = Arc::clone(&keys);
let start = i * per_thread;
let end = if i == THREADS - 1 { total } else { start + per_thread };
let handle = std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().unwrap();
for q in &queries[start..end] {
rt.block_on(q.send_all(&client, &keys)).unwrap();
}
});
handles.push(handle);
handles.push(tokio::spawn(async move {
q.send_all(&client, &keys).await
}));
}
// Await all results and propagate first error (if any)
for h in handles {
h.join().expect("Thread panicked");
h.await??;
}
}
other => {
@ -212,71 +209,6 @@ pub fn alpaca_cli(_attr: TokenStream, item: TokenStream) -> TokenStream {
}
Ok(())
}
// Trait for dispatching API calls
pub trait ApiDispatch {
fn send_all(
&self,
client: &awc::Client,
keys: &std::collections::HashMap<String, crate::Keys>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), Box<dyn std::error::Error>>> + Send>>;
}
};
TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn api_dispatch(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as syn::ItemEnum);
let enum_ident = &input.ident;
// Parse attribute input: input = "MyQuery"
let meta_args = attr.to_string();
let input_type: syn::Ident = {
let cleaned = meta_args.trim().replace("input", "").replace('=', "").replace('"', "").trim().to_string();
syn::Ident::new(&cleaned, proc_macro2::Span::call_site())
};
let expanded = quote! {
#input
impl ApiDispatch for #enum_ident {
fn send_all(
&self,
client: &awc::Client,
keys: &std::collections::HashMap<String, crate::Keys>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), Box<dyn std::error::Error>>> + Send>> {
Box::pin(async move {
match self {
#enum_ident::Single { query } => {
let parsed: #input_type = serde_json::from_str(query)?;
parsed.send(client, keys).await?;
}
#enum_ident::Bulk { input } => {
let json = if let Some(raw) = input {
if std::path::Path::new(&raw).exists() {
std::fs::read_to_string(raw)?
} else {
raw.clone()
}
} else {
use std::io::Read;
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
buf
};
let items: Vec<#input_type> = serde_json::from_str(&json)?;
for item in items {
item.send(client, keys).await?;
}
}
}
Ok(())
})
}
}
};
TokenStream::from(expanded)