include-sql 0.3.2

A Yesql inspired macro for using SQL in Rust
Documentation
use proc_macro2::{TokenStream, Span, Group, Delimiter, Literal, Ident, Punct, Spacing};
use quote::{ToTokens, TokenStreamExt};
use crate::sql::{IncludedSql, Stmt, StmtItem, TakeStmtItem};
use crate::conv::StringExt;

impl ToTokens for IncludedSql {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        tokens.append(Ident::new("impl_sql", Span::call_site()));
        tokens.append(Punct::new('!', Spacing::Alone));

        let mut macro_args = TokenStream::new();
        let mut name = self.file_name.to_camel_case(); // crate::conv::to_camel_case(&self.file_name);
        name.push_str("Sql");
        macro_args.append(Ident::new(&name, Span::call_site()));
        macro_args.append(Punct::new('=', Spacing::Alone));

        macro_args.append_separated(&self.stmt_list, Punct::new(',', Spacing::Alone));

        tokens.append(Group::new(Delimiter::Brace, macro_args));
    }
}

impl ToTokens for Stmt {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let mut stmt_tokens = TokenStream::new();

        let last = self.into.len() - 1;
        for (i, ch) in self.into.chars().enumerate() {
            let spacing = if i < last { Spacing::Joint } else { Spacing::Alone };
            stmt_tokens.append(Punct::new(ch, spacing));
        }

        stmt_tokens.append(Ident::new(&self.name, Span::call_site()));

        let mut stmt_params = TokenStream::new();
        let mut binds = self.unique_binds();
        for param in &self.params {
            if let Some(bind) = binds.take_by_name(&param.name) {
                bind.to_params_tokens(Some(param.rust_type.as_str()), &mut stmt_params);
            }
        }
        // append remaining untyped parameter placeholders
        for bind in binds {
            bind.to_params_tokens(None, &mut stmt_params);
        }

        stmt_tokens.append(Group::new(Delimiter::Parenthesis, stmt_params));

        if let Some(doc_comment) = self.docs.as_ref() {
            stmt_tokens.append(Literal::string(doc_comment));
        } else {
            stmt_tokens.append(Literal::string(""));
        }
        stmt_tokens.append(Punct::new('$', Spacing::Alone));
        stmt_tokens.append_all(&self.items);

        tokens.append(Group::new(Delimiter::Brace, stmt_tokens));
    }
}

impl ToTokens for StmtItem {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        match self {
            Self::Text( text ) => {
                tokens.append(Literal::string(text));
            },
            Self::Bind( name ) => {
                tokens.append(Punct::new(':', Spacing::Alone));
                tokens.append(Ident::new(name, Span::call_site()));
            },
            Self::List( name ) => {
                tokens.append(Punct::new('#', Spacing::Alone));
                tokens.append(Ident::new(name, Span::call_site()));
            }
        }
    }
}

impl StmtItem {
    fn to_params_tokens(&self, opt_type_name: Option<&str>, stmt_params: &mut TokenStream) {
        match self {
            StmtItem::Bind(name) => {
                stmt_params.append(Punct::new(':', Spacing::Alone));
                stmt_params.append(Ident::new(name, Span::call_site()));
                let type_tree = opt_type_name
                    .and_then(|type_name| syn::parse_str::<syn::Type>(type_name).ok())
                    .map(|param_type| {
                        let mut type_tokens = TokenStream::new();
                        #[cfg(feature = "async")]
                        if let syn::Type::Reference(_) = &param_type {
                            lifetime(name).to_tokens(&mut type_tokens);
                        }
                        param_type.to_tokens(&mut type_tokens);
                        Group::new(Delimiter::Parenthesis, type_tokens)
                    })
                ;
                if let Some(tt) = type_tree {
                    stmt_params.append(tt);
                } else {
                    stmt_params.append(Ident::new("_", Span::call_site()));
                }
            },
            StmtItem::List(name) => {
                stmt_params.append(Punct::new('#', Spacing::Alone));
                stmt_params.append(Ident::new(name, Span::call_site()));
                let type_tree = opt_type_name
                    .and_then(|type_name| syn::parse_str::<syn::Type>(type_name).ok())
                    .map(|param_type| {
                        let mut type_tokens = TokenStream::new();
                        #[cfg(feature = "async")] {
                            lifetime(name).to_tokens(&mut type_tokens);
                            if let syn::Type::Reference(_) = &param_type {
                                let mut item = String::with_capacity(name.len() + 5);
                                item.push_str(name);
                                item.push_str("_item");
                                lifetime(&item).to_tokens(&mut type_tokens);
                            }
                        }
                        param_type.to_tokens(&mut type_tokens);
                        Group::new(Delimiter::Parenthesis, type_tokens)
                    })
                ;
                if let Some(tt) = type_tree {
                    stmt_params.append(tt);
                } else {
                    let type_name = name.to_camel_case();
                    let mut type_tokens = TokenStream::new();
                    #[cfg(feature = "async")]
                    lifetime(name).to_tokens(&mut type_tokens);
                    type_tokens.append(Ident::new(&type_name, Span::call_site()));
                    stmt_params.append(Group::new(Delimiter::Bracket, type_tokens));
                }
            },
            _ => {}
        }
    }
}

#[cfg(feature = "async")]
fn lifetime(name: &str) -> syn::Lifetime {
    syn::Lifetime { apostrophe: Span::call_site(), ident: Ident::new(name, Span::call_site()) }
}