azure-functions-rs/azure-functions-shared-codegen/src/binding.rs

509 lines
17 KiB
Rust

use crate::{iter_attribute_args, last_segment_in_path, macro_panic, parse_attribute_args};
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use syn::{parse, spanned::Spanned, AttributeArgs, Fields, Ident, ItemStruct, Lit, Type, TypePath};
fn get_string_value(name: &str, value: &Lit) -> String {
if let Lit::Str(s) = value {
return s.value();
}
macro_panic(
value.span(),
format!(
"expected a literal string value for the '{}' argument",
name
),
)
}
fn get_boolean_value(name: &str, value: &Lit) -> bool {
if let Lit::Bool(b) = value {
return b.value;
}
macro_panic(
value.span(),
format!(
"expected a literal boolean value for the '{}' argument",
name
),
)
}
struct BindingArguments {
name: String,
direction: Option<String>,
validate: Option<String>,
}
impl BindingArguments {
fn get_validation_call(&self) -> TokenStream {
if let Some(validate) = self.validate.as_ref() {
let ident = Ident::new(validate, Span::call_site());
quote!(
if let Err(message) = __binding.#ident() {
crate::codegen::macro_panic(__args_and_span.1, message);
}
)
} else {
quote!()
}
}
}
impl From<AttributeArgs> for BindingArguments {
fn from(args: AttributeArgs) -> Self {
let mut name = None;
let mut direction = None;
let mut validate = None;
iter_attribute_args(&args, |key, value| {
let key_name = key.to_string();
match key_name.as_ref() {
"name" => name = Some(get_string_value("name", value)),
"direction" => direction = Some(get_string_value("direction", value)),
"validate" => validate = Some(get_string_value("validate", value)),
_ => macro_panic(
key.span(),
format!("unsupported binding attribute argument '{}'", key_name),
),
};
true
});
if name.is_none() {
macro_panic(
Span::call_site(),
"the 'name' argument is required for a binding.",
);
}
BindingArguments {
name: name.unwrap(),
direction,
validate,
}
}
}
#[derive(Debug)]
struct FieldArguments {
name: Option<String>,
camel_case_value: Option<bool>,
values: Option<String>,
}
impl From<AttributeArgs> for FieldArguments {
fn from(args: AttributeArgs) -> Self {
let mut name = None;
let mut camel_case_value = None;
let mut values = None;
iter_attribute_args(&args, |key, value| {
let key_name = key.to_string();
match key_name.as_ref() {
"name" => name = Some(get_string_value("name", value)),
"camel_case_value" => {
camel_case_value = Some(get_boolean_value("camel_case", value))
}
"values" => values = Some(get_string_value("values", value)),
_ => macro_panic(
key.span(),
format!("unsupported binding attribute argument '{}'", key_name),
),
};
true
});
FieldArguments {
name,
camel_case_value,
values,
}
}
}
fn drain_field_attributes(fields: &mut Fields) {
if let Fields::Named(fields) = fields {
for field in fields.named.iter_mut() {
field
.attrs
.retain(|a| last_segment_in_path(&a.path).ident != "field");
}
} else {
macro_panic(fields.span(), "binding structure fields must be named");
}
}
enum FieldType {
String, // Cow<'static, str>,
OptionalString, // Option<Cow<'static, str>>,
Boolean, // bool,
OptionalBoolean, // Option<bool>
Direction, // Direction,
StringArray, // Cow<'static, [Cow<'static, str>]>,
Integer, // i64,
OptionalInteger, // Option<i64>,
}
impl From<&TypePath> for FieldType {
fn from(tp: &TypePath) -> Self {
let mut stream = TokenStream::new();
tp.path.to_tokens(&mut stream);
let mut type_name = stream.to_string();
type_name.retain(|c| c != ' ');
match type_name.as_ref() {
"Cow<'static,str>" => FieldType::String,
"Option<Cow<'static,str>>" => FieldType::OptionalString,
"bool" => FieldType::Boolean,
"Option<bool>" => FieldType::OptionalBoolean,
"Direction" => FieldType::Direction,
"Cow<'static,[Cow<'static,str>]>" => FieldType::StringArray,
"i64" => FieldType::Integer,
"Option<i64>" => FieldType::OptionalInteger,
_ => macro_panic(
tp.span(),
format!("field type '{}' is not supported for a binding", type_name),
),
}
}
}
struct Field {
ident: Ident,
args: Option<FieldArguments>,
ty: FieldType,
}
impl Field {
pub fn get_serialization(&self) -> TokenStream {
let ident = &self.ident;
let mut name = &ident.to_string();
if let Some(args) = &self.args {
if let Some(n) = &args.name {
name = n;
}
}
match &self.ty {
FieldType::OptionalString | FieldType::OptionalBoolean | FieldType::OptionalInteger => {
quote!(
if let Some(#ident) = self.#ident.as_ref() {
map.serialize_entry(#name, #ident)?;
}
)
}
FieldType::StringArray => quote!(
if !self.#ident.is_empty() {
map.serialize_entry(#name, &self.#ident)?;
}
),
_ => quote!(map.serialize_entry(#name, &self.#ident)?;),
}
}
pub fn get_field_decl(&self) -> Option<TokenStream> {
let ident = &self.ident;
match &self.ty {
FieldType::Direction => None,
_ => Some(quote!(let mut #ident = None;)),
}
}
pub fn get_field_match(&self) -> Option<TokenStream> {
match &self.ty {
FieldType::Direction => None,
_ => {
let ident = &self.ident;
let camel_case_value = match &self.args {
Some(args) => args.camel_case_value.unwrap_or(false),
_ => false,
};
match self.ty {
FieldType::String | FieldType::OptionalString => {
let validation = self.get_field_validation();
if camel_case_value {
Some(quote!(
stringify!(#ident) => {
let __v = crate::util::to_camel_case(&crate::codegen::get_string_value(stringify!(#ident), &__value));
#validation
#ident = Some(Cow::from(__v));
}
))
} else {
Some(quote!(
stringify!(#ident) => {
let __v = crate::codegen::get_string_value(stringify!(#ident), &__value);
#validation
#ident = Some(Cow::from(__v));
}
))
}
}
FieldType::Boolean | FieldType::OptionalBoolean => Some(
quote!(stringify!(#ident) => #ident = Some(crate::codegen::get_boolean_value(stringify!(#ident), &__value))),
),
FieldType::Direction => panic!("cannot get a match type for a direction field"),
FieldType::StringArray => {
let validation = self.get_field_validation();
Some(quote!(stringify!(#ident) => {
let __v: Vec<_> = crate::codegen::get_string_value(stringify!(#ident), &__value).split('|').map(|v| Cow::from(v.to_string())).collect();
#validation
#ident = Some(Cow::from(__v));
}
))
}
FieldType::Integer | FieldType::OptionalInteger => Some(
quote!(stringify!(#ident) => #ident = Some(crate::codegen::get_integer_value(stringify!(#ident), &__value))),
),
}
}
}
}
pub fn get_required_check(&self) -> Option<TokenStream> {
let ident = &self.ident;
match &self.ty {
FieldType::String | FieldType::Boolean | FieldType::Integer => Some(quote!(
if #ident.is_none() {
crate::codegen::macro_panic(__args_and_span.1, concat!("the '", stringify!(#ident), "' argument is required for this binding"));
}
)),
_ => None,
}
}
pub fn get_field_validation(&self) -> TokenStream {
if let Some(args) = &self.args {
if let Some(values) = &args.values {
let ident = &self.ident;
match self.ty {
FieldType::String | FieldType::OptionalString => {
return quote!(
if !#values.split('|').map(str::trim).any(|v| v == __v.to_lowercase()) {
crate::codegen::macro_panic(__key.span(), format!(concat!("'{}' is not a valid value for the '", stringify!(#ident), "' attribute"), __v));
}
);
},
FieldType::StringArray => {
return quote!(
let __acceptable: Vec<&str> = #values.split('|').map(str::trim).collect();
for v in __v.iter() {
if !__acceptable.contains(&v.as_ref().to_lowercase().as_ref()) {
crate::codegen::macro_panic(__key.span(), format!(concat!("'{}' is not a valid value for the '", stringify!(#ident), "' attribute"), v));
}
}
);
},
_ => macro_panic(self.ident.span(), "only fields of type string or arrays of string can have a 'values' attribute")
}
}
}
quote!()
}
pub fn get_field_assignment(&self) -> TokenStream {
let ident = &self.ident;
match &self.ty {
FieldType::Direction => quote!(#ident: Default::default()),
FieldType::OptionalString | FieldType::OptionalBoolean | FieldType::OptionalInteger => {
quote!(#ident)
}
FieldType::StringArray => quote!(#ident: #ident.unwrap_or(Cow::Borrowed(&[]))),
_ => quote!(#ident: #ident.unwrap()),
}
}
pub fn get_quotable_decl(&self) -> TokenStream {
let ident = &self.ident;
match self.ty {
FieldType::String => {
quote!(let #ident = crate::codegen::quotable::QuotableBorrowedStr(&self.#ident);)
}
FieldType::OptionalString => {
quote!(let #ident = crate::codegen::quotable::QuotableOption(self.#ident.as_ref().map(|x| crate::codegen::quotable::QuotableBorrowedStr(x)));)
}
FieldType::Boolean => quote!(let #ident = self.#ident;),
FieldType::Direction => {
quote!(let #ident = crate::codegen::quotable::QuotableDirection(self.#ident);)
}
FieldType::StringArray => {
quote!(let #ident = crate::codegen::quotable::QuotableStrArray(self.#ident.as_ref());)
}
FieldType::Integer => quote!(let #ident = self.#ident),
FieldType::OptionalBoolean | FieldType::OptionalInteger => {
quote!(let #ident = crate::codegen::quotable::QuotableOption(self.#ident);)
}
}
}
pub fn get_quotable_assignment(&self) -> TokenStream {
let ident = &self.ident;
quote!(#ident: ##ident)
}
}
impl From<&syn::Field> for Field {
fn from(field: &syn::Field) -> Self {
let mut args = None;
for attr in field
.attrs
.iter()
.filter(|a| last_segment_in_path(&a.path).ident == "field")
{
if args.is_some() {
macro_panic(
attr.span(),
"a field can only have at most one attribute applied",
);
}
args = Some(parse_attribute_args(&attr));
}
Field {
ident: field.ident.as_ref().unwrap().clone(),
args: args.map(Into::into),
ty: match &field.ty {
Type::Path(tp) => tp.into(),
_ => panic!("expected a type path for field type"),
},
}
}
}
fn get_default_direction_serialization(
binding_args: &BindingArguments,
fields: &[Field],
) -> TokenStream {
if fields.iter().any(|f| match f.ty {
FieldType::Direction => true,
_ => false,
}) {
return quote!();
}
if let Some(direction) = binding_args.direction.as_ref() {
quote!(map.serialize_entry("direction", #direction)?;)
} else {
macro_panic(
Span::call_site(),
"binding requires a 'direction' argument be specified",
);
}
}
pub fn binding_impl(
args: proc_macro::TokenStream,
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let mut definition: ItemStruct = parse(input)
.map_err(|_| {
macro_panic(
Span::call_site(),
"the 'binding' attribute can only be used on a struct",
)
})
.unwrap();
let binding_args =
BindingArguments::from(match syn::parse_macro_input::parse::<AttributeArgs>(args) {
Ok(args) => args,
Err(e) => macro_panic(
Span::call_site(),
format!("failed to parse attribute arguments: {}", e),
),
});
let fields: Vec<Field> = definition.fields.iter().map(|f| f.into()).collect();
drain_field_attributes(&mut definition.fields);
let binding_name = &binding_args.name;
let validate = binding_args.get_validation_call();
let ident = &definition.ident;
let default_direction = get_default_direction_serialization(&binding_args, &fields);
let serializations = fields.iter().map(Field::get_serialization);
let field_decls = fields.iter().filter_map(Field::get_field_decl);
let field_matches = fields.iter().filter_map(Field::get_field_match);
let required_checks = fields.iter().filter_map(Field::get_required_check);
let field_assignments = fields.iter().map(Field::get_field_assignment);
let quotable_decls = fields.iter().map(Field::get_quotable_decl);
let quoteable_assignments = fields.iter().map(Field::get_quotable_assignment);
quote!(
#[derive(Debug, Clone)]
#definition
impl #ident {
pub fn binding_type() -> &'static str {
#binding_name
}
}
impl serde::Serialize for #ident {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("type", #ident::binding_type())?;
#default_direction
#(#serializations)*
map.end()
}
}
impl From<(syn::AttributeArgs, proc_macro2::Span)> for #ident {
fn from(__args_and_span: (syn::AttributeArgs, proc_macro2::Span)) -> Self {
#(#field_decls)*
crate::codegen::iter_attribute_args(&__args_and_span.0, |__key, __value| {
let __key_name = __key.to_string();
match __key_name.as_str() {
#(#field_matches,)*
_ => crate::codegen::macro_panic(__key.span(), format!("unsupported binding attribute argument '{}'", __key_name)),
};
true
});
#(#required_checks)*
let __binding = #ident {
#(#field_assignments,)*
};
#validate
__binding
}
}
impl quote::ToTokens for #ident {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
#(#quotable_decls)*
quote::quote!(
::azure_functions::codegen::bindings::#ident {
#(#quoteable_assignments,)*
}
).to_tokens(tokens)
}
}
)
.into()
}