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
292 changes: 165 additions & 127 deletions TODO.md

Large diffs are not rendered by default.

14 changes: 9 additions & 5 deletions crates/anyedge-adapter-fastly/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,7 @@ fn build_fastly_request(
uri: &Uri,
headers: HeaderMap,
) -> Result<FastlyRequest, EdgeError> {
let mut fastly_request = FastlyRequest::new(
method.clone(),
uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("/"),
);
let mut fastly_request = FastlyRequest::new(method.clone(), uri.to_string());
fastly_request.set_method(method);

for (name, value) in headers.iter() {
Expand Down Expand Up @@ -103,7 +100,14 @@ fn ensure_backend(uri: &Uri) -> Result<Backend, EdgeError> {

let name = backend_name(&target, uri.scheme_str());

match Backend::from_name(&name) {
let host_with_port = match port {
Some(p) => format!("{}:{}", host, p),
None => host.to_string(),
};

let builder = Backend::builder(&name, &host_with_port).override_host(host);

match builder.finish() {
Ok(backend) => Ok(backend),
Err(_) => {
let mut builder = Backend::builder(&name, &target);
Expand Down
3 changes: 2 additions & 1 deletion crates/anyedge-cli/src/templates/core/src/handlers.rs.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ pub(crate) async fn echo_json(Json(body): Json<EchoBody>) -> Text<String> {
Text::new(format!("Hello, {}!", body.name))
}

pub(crate) async fn proxy_demo(ctx: RequestContext) -> Result<Response, EdgeError> {
#[action]
pub(crate) async fn proxy_demo(RequestContext(ctx): RequestContext) -> Result<Response, EdgeError> {
let params: ProxyPath = ctx.path()?;
let proxy_handle = ctx.proxy_handle();
let request = ctx.into_request();
Expand Down
208 changes: 206 additions & 2 deletions crates/anyedge-macros/src/action.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{spanned::Spanned, FnArg, ItemFn};
use syn::{spanned::Spanned, Error, FnArg, ItemFn, Pat, PathArguments, Type};

pub fn expand_action(attr: TokenStream, item: TokenStream) -> TokenStream {
expand_action_impl(attr.into(), item.into()).into()
Expand Down Expand Up @@ -42,8 +42,13 @@ pub(crate) fn expand_action_impl(
inner_fn.vis = syn::Visibility::Inherited;
inner_fn.attrs.clear();

if let Err(err) = normalize_request_context_patterns(&mut inner_fn) {
return err.to_compile_error();
}

let mut extract_stmts = Vec::new();
let mut arg_idents = Vec::new();
let mut has_request_context = false;

for (index, arg) in func.sig.inputs.iter().enumerate() {
let pat_type = match arg {
Expand All @@ -52,11 +57,24 @@ pub(crate) fn expand_action_impl(
};

let ty = &pat_type.ty;
if is_request_context_type(ty) {
if has_request_context {
return syn::Error::new(
ty.span(),
"#[action] functions support at most one RequestContext argument",
)
.to_compile_error();
}
has_request_context = true;
arg_idents.push(quote! { __ctx });
continue;
}

let var_ident = format_ident!("__arg{}", index);
extract_stmts.push(quote! {
let #var_ident = <#ty as ::anyedge_core::extractor::FromRequest>::from_request(&__ctx).await?;
});
arg_idents.push(var_ident);
arg_idents.push(quote! { #var_ident });
}

let output = quote! {
Expand All @@ -75,6 +93,71 @@ pub(crate) fn expand_action_impl(
output
}

fn is_request_context_type(ty: &Type) -> bool {
let Type::Path(type_path) = ty else {
return false;
};
if type_path.qself.is_some() {
return false;
}
path_is_request_context(&type_path.path)
}

fn normalize_request_context_pat(pat: &mut Box<Pat>) -> syn::Result<()> {
let Some(replacement) = extract_request_context_binding(pat.as_ref())? else {
return Ok(());
};
*pat = Box::new(replacement);
Ok(())
}

fn extract_request_context_binding(pat: &Pat) -> syn::Result<Option<Pat>> {
let Pat::TupleStruct(tuple_pat) = pat else {
return Ok(None);
};
if !path_is_request_context(&tuple_pat.path) {
return Ok(None);
}
if tuple_pat.elems.len() != 1 {
return Err(syn::Error::new(
tuple_pat.span(),
"RequestContext destructuring expects exactly one binding",
));
}
Ok(tuple_pat.elems.first().cloned())
}

fn path_is_request_context(path: &syn::Path) -> bool {
path.segments
.last()
.map(|segment| {
segment.ident == "RequestContext" && matches!(segment.arguments, PathArguments::None)
})
.unwrap_or(false)
}

fn normalize_request_context_patterns(func: &mut ItemFn) -> Result<(), Error> {
let mut error: Option<Error> = None;
for arg in func.sig.inputs.iter_mut() {
if let FnArg::Typed(pat_type) = arg {
if is_request_context_type(&pat_type.ty) {
if let Err(err) = normalize_request_context_pat(&mut pat_type.pat) {
if let Some(existing) = error.as_mut() {
existing.combine(err);
} else {
error = Some(err);
}
}
}
}
}

if let Some(err) = error {
return Err(err);
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::expand_action_impl;
Expand All @@ -85,6 +168,10 @@ mod tests {
tokens.to_string()
}

fn collapse_whitespace(input: &str) -> String {
input.split_whitespace().collect()
}

#[test]
fn wraps_async_function() {
let input = quote! {
Expand All @@ -111,4 +198,121 @@ mod tests {
let rendered = render(output);
assert!(rendered.contains("must be async"));
}

#[test]
fn rejects_attribute_arguments() {
let input = quote! {
async fn demo(ctx: ::anyedge_core::context::RequestContext) -> ::anyedge_core::http::Response {
unimplemented!()
}
};
let output = expand_action_impl(quote!(path = "/demo"), input);
let rendered = render(output);
assert!(rendered.contains("does not accept arguments"));
}

#[test]
fn rejects_self_receivers() {
let input = quote! {
async fn invalid(&self) -> ::anyedge_core::http::Response {
unimplemented!()
}
};
let output = expand_action_impl(TokenStream::new(), input);
let rendered = render(output);
assert!(rendered.contains("does not support self receivers"));
}

#[test]
fn allows_request_context_argument() {
let input = quote! {
async fn with_ctx(
ctx: ::anyedge_core::context::RequestContext
) -> ::std::result::Result<
::anyedge_core::http::Response,
::anyedge_core::error::EdgeError
> {
let _ = ctx;
Ok(::anyedge_core::http::response_builder()
.status(::anyedge_core::http::StatusCode::OK)
.body(::anyedge_core::body::Body::empty())
.unwrap())
}
};
let output = expand_action_impl(TokenStream::new(), input);
let rendered = render(output);
let collapsed = collapse_whitespace(&rendered);
assert!(collapsed.contains("__with_ctx_inner(__ctx)"));
}

#[test]
fn allows_request_context_tuple_pattern_argument() {
let input = quote! {
async fn tuple_ctx(
RequestContext(ctx): ::anyedge_core::context::RequestContext
) -> ::std::result::Result<
::anyedge_core::http::Response,
::anyedge_core::error::EdgeError
> {
let _ = ctx;
Ok(::anyedge_core::http::response_builder()
.status(::anyedge_core::http::StatusCode::OK)
.body(::anyedge_core::body::Body::empty())
.unwrap())
}
};
let output = expand_action_impl(TokenStream::new(), input);
let rendered = render(output);
let collapsed = collapse_whitespace(&rendered);
assert!(collapsed.contains("__tuple_ctx_inner(__ctx)"));
}

#[test]
fn rejects_multiple_request_context_arguments() {
let input = quote! {
async fn invalid(
first: ::anyedge_core::context::RequestContext,
second: ::anyedge_core::context::RequestContext,
) {}
};
let output = expand_action_impl(TokenStream::new(), input);
let rendered = render(output);
assert!(rendered.contains("support at most one RequestContext argument"));
}

#[test]
fn rejects_request_context_tuple_with_multiple_bindings() {
let input = quote! {
async fn invalid(
RequestContext(a, b): ::anyedge_core::context::RequestContext
) -> ::anyedge_core::http::Response {
unimplemented!()
}
};
let output = expand_action_impl(TokenStream::new(), input);
let rendered = render(output);
assert!(rendered.contains("expects exactly one binding"));
}

#[test]
fn generates_extractor_calls_for_arguments() {
let input = quote! {
async fn demo(
value: demo::ExtractorType
) -> ::anyedge_core::http::Response {
let _ = value;
::anyedge_core::http::response_builder()
.status(::anyedge_core::http::StatusCode::OK)
.body(::anyedge_core::body::Body::empty())
.unwrap()
}
};
let output = expand_action_impl(TokenStream::new(), input);
let rendered = render(output);
let collapsed = collapse_whitespace(&rendered);
assert!(
collapsed.contains("FromRequest>::from_request"),
"expected extractor call in generated output: {rendered}"
);
}
}
Loading