use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{
Data, DeriveInput, Field, Fields, Ident, LitStr, Type,
};
#[derive(Debug, Clone)]
enum FieldSource {
Cookie(String),
Header(String),
Query(String),
Peer,
AuthContext,
Default,
}
struct FieldInfo {
ident: Ident,
ty: Type,
source: FieldSource,
is_option: bool,
}
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input: DeriveInput = syn::parse_macro_input!(input);
match derive_impl(input) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}
fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream2> {
let struct_name = &input.ident;
let fields = match &input.data {
Data::Struct(s) => match &s.fields {
Fields::Named(named) => named.named.iter().cloned().collect::<Vec<_>>(),
_ => {
return Err(syn::Error::new_spanned(
struct_name,
"PlexusRequest can only be derived for structs with named fields",
))
}
},
_ => {
return Err(syn::Error::new_spanned(
struct_name,
"PlexusRequest can only be derived for structs",
))
}
};
let field_infos: Vec<FieldInfo> = fields
.iter()
.map(parse_field)
.collect::<syn::Result<Vec<_>>>()?;
let extract_impl = generate_extract_impl(struct_name, &field_infos, &input);
Ok(extract_impl)
}
fn parse_field(field: &Field) -> syn::Result<FieldInfo> {
let ident = field.ident.clone().ok_or_else(|| {
syn::Error::new_spanned(field, "unnamed fields are not supported")
})?;
let mut source = FieldSource::Default;
for attr in &field.attrs {
if attr.path().is_ident("from_cookie") {
let key: LitStr = attr.parse_args()?;
source = FieldSource::Cookie(key.value());
} else if attr.path().is_ident("from_header") {
let key: LitStr = attr.parse_args()?;
source = FieldSource::Header(key.value());
} else if attr.path().is_ident("from_query") {
let key: LitStr = attr.parse_args()?;
source = FieldSource::Query(key.value());
} else if attr.path().is_ident("from_peer") {
source = FieldSource::Peer;
} else if attr.path().is_ident("from_auth_context") {
source = FieldSource::AuthContext;
}
}
let is_option = is_option_type(&field.ty);
Ok(FieldInfo {
ident,
ty: field.ty.clone(),
source,
is_option,
})
}
fn is_option_type(ty: &Type) -> bool {
if let Type::Path(tp) = ty {
if let Some(seg) = tp.path.segments.last() {
return seg.ident == "Option";
}
}
false
}
fn generate_extract_impl(struct_name: &Ident, fields: &[FieldInfo], input: &DeriveInput) -> TokenStream2 {
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let field_extractions: Vec<TokenStream2> = fields
.iter()
.map(generate_field_extraction)
.collect();
let field_names: Vec<&Ident> = fields.iter().map(|f| &f.ident).collect();
let schema_value = generate_schema_value(fields);
let struct_name_str = struct_name.to_string();
quote! {
#[automatically_derived]
impl #impl_generics ::plexus_core::request::PlexusRequest for #struct_name #ty_generics #where_clause {
fn extract(
ctx: &::plexus_core::request::RawRequestContext
) -> ::core::result::Result<Self, ::plexus_core::plexus::PlexusError> {
#(#field_extractions)*
::core::result::Result::Ok(Self {
#(#field_names),*
})
}
fn request_schema() -> ::core::option::Option<::serde_json::Value> {
::core::option::Option::Some(#schema_value)
}
}
#[automatically_derived]
impl #impl_generics ::plexus_core::__schemars::JsonSchema for #struct_name #ty_generics #where_clause {
fn schema_name() -> ::std::borrow::Cow<'static, str> {
::std::borrow::Cow::Borrowed(#struct_name_str)
}
fn json_schema(
_gen: &mut ::plexus_core::__schemars::SchemaGenerator,
) -> ::plexus_core::__schemars::Schema {
let value: ::serde_json::Value = #schema_value;
match value {
::serde_json::Value::Object(map) => {
<::plexus_core::__schemars::Schema as ::core::convert::From<
::serde_json::Map<::std::string::String, ::serde_json::Value>,
>>::from(map)
}
_ => <::plexus_core::__schemars::Schema as ::core::convert::From<
::serde_json::Map<::std::string::String, ::serde_json::Value>,
>>::from(::serde_json::Map::new()),
}
}
}
}
}
fn generate_field_extraction(field: &FieldInfo) -> TokenStream2 {
let name = &field.ident;
match &field.source {
FieldSource::Cookie(key) => {
let key_lit = key.as_str();
if field.is_option {
quote! {
let #name = {
let _cookie_hdr = ctx.headers.get("cookie")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
::plexus_core::request::parse_cookie(_cookie_hdr, #key_lit)
.and_then(|v| v.parse().ok())
};
}
} else {
quote! {
let #name = {
let _cookie_hdr = ctx.headers.get("cookie")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let _raw = ::plexus_core::request::parse_cookie(_cookie_hdr, #key_lit)
.ok_or_else(|| ::plexus_core::plexus::PlexusError::Unauthenticated(
::std::string::String::from("Authentication required")
))?;
_raw.parse::<_>().map_err(|_| {
::plexus_core::plexus::PlexusError::Unauthenticated(
::std::format!("Cookie '{}' could not be parsed", #key_lit)
)
})?
};
}
}
}
FieldSource::Header(key) => {
let key_lit = key.as_str();
if field.is_option {
quote! {
let #name = ctx.headers.get(#key_lit)
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse().ok());
}
} else {
quote! {
let #name = ctx.headers.get(#key_lit)
.and_then(|v| v.to_str().ok())
.ok_or_else(|| ::plexus_core::plexus::PlexusError::Unauthenticated(
::std::format!(
"Required header '{}' not present",
#key_lit
)
))
.and_then(|s| s.parse::<_>().map_err(|_| {
::plexus_core::plexus::PlexusError::Unauthenticated(
::std::format!("Header '{}' could not be parsed", #key_lit)
)
}))?;
}
}
}
FieldSource::Query(key) => {
let key_lit = key.as_str();
if field.is_option {
quote! {
let #name = form_urlencoded::parse(ctx.uri.query().unwrap_or("").as_bytes())
.find(|(k, _)| k == #key_lit)
.and_then(|(_, v)| v.parse().ok());
}
} else {
quote! {
let #name = {
let _raw = form_urlencoded::parse(ctx.uri.query().unwrap_or("").as_bytes())
.find(|(k, _)| k == #key_lit)
.map(|(_, v)| v.into_owned())
.ok_or_else(|| ::plexus_core::plexus::PlexusError::Unauthenticated(
::std::format!(
"Required query parameter '{}' not present",
#key_lit
)
))?;
_raw.parse::<_>().map_err(|_| {
::plexus_core::plexus::PlexusError::Unauthenticated(
::std::format!("Query param '{}' could not be parsed", #key_lit)
)
})?
};
}
}
}
FieldSource::Peer => {
if field.is_option {
quote! { let #name = ctx.peer; }
} else {
quote! {
let #name = ctx.peer.ok_or_else(|| {
::plexus_core::plexus::PlexusError::Unauthenticated(
::std::string::String::from("Peer address not available")
)
})?;
}
}
}
FieldSource::AuthContext => {
if field.is_option {
quote! { let #name = ctx.auth.clone(); }
} else {
quote! {
let #name = ctx.auth.clone().ok_or_else(|| {
::plexus_core::plexus::PlexusError::Unauthenticated(
::std::string::String::from("Authentication required")
)
})?;
}
}
}
FieldSource::Default => {
let ty = &field.ty;
quote! {
let #name = <#ty as ::plexus_core::plexus::PlexusRequestField>::extract_from_raw(ctx)?;
}
}
}
}
fn generate_schema_value(fields: &[FieldInfo]) -> TokenStream2 {
let property_entries: Vec<TokenStream2> = fields.iter().map(|f| {
let fname_str = f.ident.to_string();
let source_json = match &f.source {
FieldSource::Cookie(key) => quote! {
::serde_json::json!({
"type": "string",
"x-plexus-source": { "from": "cookie", "key": #key }
})
},
FieldSource::Header(key) => {
if f.is_option {
quote! {
::serde_json::json!({
"anyOf": [{"type": "string"}, {"type": "null"}],
"x-plexus-source": { "from": "header", "key": #key }
})
}
} else {
quote! {
::serde_json::json!({
"type": "string",
"x-plexus-source": { "from": "header", "key": #key }
})
}
}
},
FieldSource::Query(key) => {
if f.is_option {
quote! {
::serde_json::json!({
"anyOf": [{"type": "string"}, {"type": "null"}],
"x-plexus-source": { "from": "query", "key": #key }
})
}
} else {
quote! {
::serde_json::json!({
"type": "string",
"x-plexus-source": { "from": "query", "key": #key }
})
}
}
},
FieldSource::Peer | FieldSource::AuthContext => quote! {
::serde_json::json!({
"x-plexus-source": { "from": "derived" }
})
},
FieldSource::Default => quote! {
::serde_json::json!({
"x-plexus-source": { "from": "derived" }
})
},
};
quote! {
__props.insert(
::std::string::String::from(#fname_str),
#source_json,
);
}
}).collect();
let required_entries: Vec<TokenStream2> = fields.iter()
.filter(|f| !f.is_option)
.map(|f| {
let fname_str = f.ident.to_string();
quote! {
__required.push(::serde_json::Value::String(::std::string::String::from(#fname_str)));
}
})
.collect();
quote! {
{
let mut __props = ::serde_json::Map::new();
let mut __required: ::std::vec::Vec<::serde_json::Value> = ::std::vec::Vec::new();
#(#property_entries)*
#(#required_entries)*
let mut __obj = ::serde_json::Map::new();
__obj.insert(
::std::string::String::from("type"),
::serde_json::Value::String(::std::string::String::from("object")),
);
__obj.insert(
::std::string::String::from("properties"),
::serde_json::Value::Object(__props),
);
if !__required.is_empty() {
__obj.insert(
::std::string::String::from("required"),
::serde_json::Value::Array(__required),
);
}
::serde_json::Value::Object(__obj)
}
}
}