azure-functions-shared 0.11.0

Implementations shared between the azure-functions-codegen and azure-functions crates.
Documentation
use crate::codegen::{
    bindings::Direction,
    get_string_value, iter_attribute_args, macro_panic,
    quotable::{QuotableBorrowedStr, QuotableDirection},
    Value,
};
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use serde::{ser::SerializeMap, Serialize, Serializer};
use std::borrow::Cow;
use syn::{spanned::Spanned, AttributeArgs, Lit};

#[derive(Debug, Clone)]
pub struct Generic {
    pub ty: Cow<'static, str>,
    pub direction: Direction,
    pub name: Cow<'static, str>,
    pub values: Cow<'static, [(Cow<'static, str>, Value)]>,
}

impl Generic {
    pub fn binding_type(&self) -> &str {
        self.ty.as_ref()
    }
}

impl Serialize for Generic {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut map = serializer.serialize_map(None)?;
        map.serialize_entry("type", &self.ty)?;
        map.serialize_entry("direction", &self.direction)?;
        map.serialize_entry("name", &self.name)?;

        for value in self.values.iter() {
            map.serialize_entry(&value.0, &value.1)?;
        }

        map.end()
    }
}

impl From<(AttributeArgs, Span)> for Generic {
    fn from(args_and_span: (AttributeArgs, Span)) -> Self {
        let mut ty = None;
        let mut name = None;
        let mut values = Vec::new();

        iter_attribute_args(&args_and_span.0, |key, value| {
            let key_name = key.to_string();

            match key_name.as_str() {
                "type" => {
                    let binding_type = get_string_value("type", value);
                    if binding_type.to_lowercase() == "httptrigger" {
                        macro_panic(
                            value.span(),
                            "using a generic binding of type 'httpTrigger' is not supported",
                        );
                    }
                    ty = Some(binding_type);
                }
                "name" => name = Some(get_string_value("name", value)),
                _ => {
                    match value {
                        Lit::Str(s) => {
                            values.push((Cow::from(key_name), Value::String(Cow::from(s.value()))));
                        }
                        Lit::Int(i) => {
                            values.push((
                                Cow::from(key_name),
                                Value::Integer(i.base10_parse::<i64>().unwrap()),
                            ));
                        }
                        Lit::Bool(b) => values.push((Cow::from(key_name), Value::Boolean(b.value))),
                        _ => macro_panic(value.span(), "expected a string, integer, or boolean"),
                    };
                }
            };

            true
        });

        if ty.is_none() {
            macro_panic(
                args_and_span.1,
                "the 'type' argument is required for this binding",
            );
        }

        if name.is_none() {
            macro_panic(
                args_and_span.1,
                "the 'name' argument is required for this binding",
            );
        }

        Generic {
            ty: Cow::from(ty.unwrap()),
            direction: Direction::In,
            name: Cow::from(name.unwrap()),
            values: Cow::from(values),
        }
    }
}

impl ToTokens for Generic {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let ty = QuotableBorrowedStr(&self.ty);
        let direction = QuotableDirection(self.direction);
        let name = QuotableBorrowedStr(&self.name);
        let values = self.values.iter().map(|v| {
            let name = QuotableBorrowedStr(&v.0);
            let value = &v.1;
            quote!((#name, #value))
        });

        quote!(
            ::azure_functions::codegen::bindings::Generic {
                ty: #ty,
                direction: #direction,
                name: #name,
                values: ::std::borrow::Cow::Borrowed(&[#(#values,)*]),
            }
        )
        .to_tokens(tokens)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::codegen::tests::should_panic;
    use proc_macro2::{Span, TokenStream};
    use quote::ToTokens;
    use serde_json::to_string;
    use syn::{parse_str, NestedMeta};

    #[test]
    fn it_serializes_to_json() {
        let binding = Generic {
            ty: Cow::from("test"),
            direction: Direction::InOut,
            name: Cow::from("foo"),
            values: Cow::from(vec![
                (Cow::from("bar"), Value::String(Cow::from("hello"))),
                (Cow::from("baz"), Value::Integer(42)),
                (Cow::from("jam"), Value::Boolean(true)),
            ]),
        };

        assert_eq!(
            to_string(&binding).unwrap(),
            r#"{"type":"test","direction":"inout","name":"foo","bar":"hello","baz":42,"jam":true}"#
        );
    }

    #[test]
    fn it_parses_attribute_arguments() {
        let binding: Generic = (
            vec![
                parse_str::<NestedMeta>(r#"type = "test""#).unwrap(),
                parse_str::<NestedMeta>(r#"name = "foo""#).unwrap(),
                parse_str::<NestedMeta>(r#"bar = "hello""#).unwrap(),
                parse_str::<NestedMeta>(r#"baz = 42"#).unwrap(),
                parse_str::<NestedMeta>(r#"jam = true"#).unwrap(),
            ],
            Span::call_site(),
        )
            .into();

        assert_eq!(binding.ty.as_ref(), "test");
        assert_eq!(binding.direction, Direction::In);
        assert_eq!(binding.name.as_ref(), "foo");
        assert_eq!(
            binding.values.as_ref(),
            [
                (Cow::from("bar"), Value::String(Cow::from("hello"))),
                (Cow::from("baz"), Value::Integer(42)),
                (Cow::from("jam"), Value::Boolean(true)),
            ]
        );
    }

    #[test]
    fn it_requires_the_type_attribute_argument() {
        should_panic(
            || {
                let _: Generic = (vec![], Span::call_site()).into();
            },
            "the 'type' argument is required for this binding",
        );
    }

    #[test]
    fn it_requires_the_type_attribute_be_a_string() {
        should_panic(
            || {
                let _: Generic = (
                    vec![parse_str::<NestedMeta>(r#"type = false"#).unwrap()],
                    Span::call_site(),
                )
                    .into();
            },
            "expected a literal string value for the 'type' argument",
        );
    }

    #[test]
    fn it_requires_the_name_attribute_argument() {
        should_panic(
            || {
                let _: Generic = (
                    vec![parse_str::<NestedMeta>(r#"type = "foo""#).unwrap()],
                    Span::call_site(),
                )
                    .into();
            },
            "the 'name' argument is required for this binding",
        );
    }

    #[test]
    fn it_requires_the_name_attribute_be_a_string() {
        should_panic(
            || {
                let _: Generic = (
                    vec![parse_str::<NestedMeta>(r#"name = false"#).unwrap()],
                    Span::call_site(),
                )
                    .into();
            },
            "expected a literal string value for the 'name' argument",
        );
    }

    #[test]
    fn it_converts_to_tokens() {
        let binding = Generic {
            ty: Cow::from("test"),
            direction: Direction::InOut,
            name: Cow::from("foo"),
            values: Cow::from(vec![
                (Cow::from("bar"), Value::String(Cow::from("hello"))),
                (Cow::from("baz"), Value::Integer(42)),
                (Cow::from("jam"), Value::Boolean(true)),
            ]),
        };

        let mut stream = TokenStream::new();
        binding.to_tokens(&mut stream);
        let mut tokens = stream.to_string();
        tokens.retain(|c| c != ' ');

        assert_eq!(
            tokens,
            r#"::azure_functions::codegen::bindings::Generic{ty:::std::borrow::Cow::Borrowed("test"),direction:::azure_functions::codegen::bindings::Direction::InOut,name:::std::borrow::Cow::Borrowed("foo"),values:::std::borrow::Cow::Borrowed(&[(::std::borrow::Cow::Borrowed("bar"),::azure_functions::codegen::Value::String(::std::borrow::Cow::Borrowed("hello"))),(::std::borrow::Cow::Borrowed("baz"),::azure_functions::codegen::Value::Integer(42i64)),(::std::borrow::Cow::Borrowed("jam"),::azure_functions::codegen::Value::Boolean(true)),]),}"#
        );
    }
}