feat(v1): harden typed runtime boundaries
Elect one canonical EffectBatch codec, remove the parallel postcard API, and strengthen fail-closed host, form, sync, WASM, macro, generated-contract, and test-harness proofs with mutation-driven coverage. req: wire/008 req: wire/009 req: wire/010 req: push/008 req: client_local/015 req: client_local/016 req: client_local/017 req: client_local/018 req: client_local/019 req: sync/024 req: sync/025 req: sync/026 req: sync/027 req: sync/028 req: sync/029 req: test/020 req: test/021
This commit is contained in:
+110
-25
@@ -1,55 +1,52 @@
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, Error, FnArg, ItemFn, Pat, ReturnType};
|
||||
use syn::{Error, FnArg, ItemFn, Pat, ReturnType};
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn presence(attributes: TokenStream, item: TokenStream) -> TokenStream {
|
||||
expand_presence(attributes.into(), item.into())
|
||||
.unwrap_or_else(Error::into_compile_error)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn expand_presence(attributes: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
|
||||
if !attributes.is_empty() {
|
||||
return Error::new(
|
||||
return Err(Error::new(
|
||||
proc_macro2::Span::call_site(),
|
||||
"#[hemx_sync::presence] does not accept arguments",
|
||||
)
|
||||
.to_compile_error()
|
||||
.into();
|
||||
));
|
||||
}
|
||||
|
||||
let mut function = parse_macro_input!(item as ItemFn);
|
||||
if function.sig.asyncness.is_some() {
|
||||
return Error::new_spanned(
|
||||
function.sig.asyncness,
|
||||
let mut function: ItemFn = syn::parse2(item)?;
|
||||
if let Some(asyncness) = &function.sig.asyncness {
|
||||
return Err(Error::new_spanned(
|
||||
asyncness,
|
||||
"presence projections must be synchronous",
|
||||
)
|
||||
.to_compile_error()
|
||||
.into();
|
||||
));
|
||||
}
|
||||
if matches!(function.sig.output, ReturnType::Default) {
|
||||
return Error::new_spanned(
|
||||
return Err(Error::new_spanned(
|
||||
&function.sig,
|
||||
"presence projections must return impl IntoEffect",
|
||||
)
|
||||
.to_compile_error()
|
||||
.into();
|
||||
));
|
||||
}
|
||||
let argument = match function.sig.inputs.first() {
|
||||
Some(FnArg::Typed(argument)) if function.sig.inputs.len() == 1 => argument,
|
||||
_ => {
|
||||
return Error::new_spanned(
|
||||
return Err(Error::new_spanned(
|
||||
&function.sig.inputs,
|
||||
"presence projections require exactly one typed presence argument",
|
||||
)
|
||||
.to_compile_error()
|
||||
.into();
|
||||
));
|
||||
}
|
||||
};
|
||||
let argument_name = match argument.pat.as_ref() {
|
||||
Pat::Ident(argument) => argument.ident.clone(),
|
||||
pattern => {
|
||||
return Error::new_spanned(
|
||||
return Err(Error::new_spanned(
|
||||
pattern,
|
||||
"presence projection argument must be a simple identifier",
|
||||
)
|
||||
.to_compile_error()
|
||||
.into();
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -61,5 +58,93 @@ pub fn presence(attributes: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let __hemx_sync_effect = (|| #body)();
|
||||
::hemx_sync::PresenceProjection::new(__hemx_sync_channel, __hemx_sync_effect)
|
||||
}));
|
||||
quote!(#function).into()
|
||||
Ok(quote!(#function))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::expand_presence;
|
||||
use quote::quote;
|
||||
|
||||
#[test]
|
||||
fn presence_expansion_enforces_the_typed_projection_contract() {
|
||||
assert!(expand_presence(quote!(), quote!(not a function)).is_err());
|
||||
|
||||
for (attributes, item, expected) in [
|
||||
(
|
||||
quote!(unexpected),
|
||||
quote!(
|
||||
fn project(scope: Scope) -> Effect {
|
||||
effect()
|
||||
}
|
||||
),
|
||||
"#[hemx_sync::presence] does not accept arguments",
|
||||
),
|
||||
(
|
||||
quote!(),
|
||||
quote!(
|
||||
async fn project(scope: Scope) -> Effect {
|
||||
effect()
|
||||
}
|
||||
),
|
||||
"presence projections must be synchronous",
|
||||
),
|
||||
(
|
||||
quote!(),
|
||||
quote!(
|
||||
fn project(scope: Scope) {}
|
||||
),
|
||||
"presence projections must return impl IntoEffect",
|
||||
),
|
||||
(
|
||||
quote!(),
|
||||
quote!(
|
||||
fn project() -> Effect {
|
||||
effect()
|
||||
}
|
||||
),
|
||||
"presence projections require exactly one typed presence argument",
|
||||
),
|
||||
(
|
||||
quote!(),
|
||||
quote!(
|
||||
fn project(a: Scope, b: Scope) -> Effect {
|
||||
effect()
|
||||
}
|
||||
),
|
||||
"presence projections require exactly one typed presence argument",
|
||||
),
|
||||
(
|
||||
quote!(),
|
||||
quote!(
|
||||
fn project((scope,): (Scope,)) -> Effect {
|
||||
effect()
|
||||
}
|
||||
),
|
||||
"presence projection argument must be a simple identifier",
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
expand_presence(attributes, item).unwrap_err().to_string(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
let expanded = expand_presence(
|
||||
quote!(),
|
||||
quote!(
|
||||
pub fn project(scope: Scope) -> Effect {
|
||||
effect(scope)
|
||||
}
|
||||
),
|
||||
)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
assert!(expanded.contains("pub fn project"));
|
||||
assert!(expanded.contains("impl :: hemx_sync :: PresenceUpdate"));
|
||||
assert!(expanded.contains("PresenceScope :: presence_channel (& scope)"));
|
||||
assert!(expanded.contains("PresenceProjection :: new"));
|
||||
assert!(expanded.contains("effect (scope)"));
|
||||
// test req: sync/003 req: sync/005
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user