kosame_dsl 0.3.0

Macro-based Rust ORM focused on developer ergonomics
Documentation
mod delete;
mod insert;
mod select;
mod update;

use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::{
    Attribute,
    parse::{Parse, ParseStream},
};

pub use delete::*;
pub use insert::*;
pub use select::*;
pub use update::*;

// Re-export visit functions
pub use delete::{visit_delete, visit_using};
pub use insert::visit_insert;
pub use select::{
    visit_select_chain, visit_select_combinator, visit_select_command, visit_select_item,
};
pub use update::visit_update;

use crate::{
    clause::{Fields, FromChain, With},
    correlations::CorrelationId,
    keyword,
    parse_option::ParseOption,
    part::TargetTable,
    pretty::{PrettyPrint, Printer},
    quote_option::QuoteOption,
    scopes::{ScopeId, Scoped},
    visit::Visit,
};

pub struct Command {
    pub scope_id: ScopeId,
    pub attrs: Vec<Attribute>,
    pub with: Option<With>,
    pub command_type: CommandType,
    pub correlation_id: CorrelationId,
}

impl Command {
    #[must_use]
    pub fn fields(&self) -> Option<&Fields> {
        self.command_type.fields()
    }
}

pub fn visit_command<'a>(visit: &mut (impl Visit<'a> + ?Sized), command: &'a Command) {
    if let Some(inner) = &command.with {
        visit.visit_with(inner);
    }
    visit.visit_command_type(&command.command_type);
}

impl Scoped for Command {
    #[inline]
    fn scope_id(&self) -> ScopeId {
        self.scope_id
    }

    #[inline]
    fn with(&self) -> Option<&With> {
        self.with.as_ref()
    }

    #[inline]
    fn target_table(&self) -> Option<&TargetTable> {
        self.command_type.target_table()
    }

    #[inline]
    fn select_chain(&self) -> Option<&SelectChain> {
        match &self.command_type {
            CommandType::Select(select) => Some(&select.chain),
            _ => None,
        }
    }

    #[inline]
    #[allow(clippy::wrong_self_convention)]
    fn from_chain(&self) -> Option<&FromChain> {
        self.command_type.from_chain()
    }
}

impl Parse for Command {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        Ok(Self {
            scope_id: ScopeId::new(),
            attrs: input.call(Attribute::parse_outer)?,
            with: input.call(With::parse_option)?,
            command_type: input.parse()?,
            correlation_id: CorrelationId::new(),
        })
    }
}

impl ToTokens for Command {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let with = QuoteOption::from(&self.with);
        let command_type = &self.command_type;
        self.scope_id.scope(|| {
            quote! { ::kosame::repr::command::Command::new(#with, #command_type) }
                .to_tokens(tokens);
        });
    }
}

impl PrettyPrint for Command {
    fn pretty_print(&self, printer: &mut Printer<'_>) {
        self.attrs.pretty_print(printer);
        self.with.pretty_print(printer);
        self.command_type.pretty_print(printer);
    }
}

pub enum CommandType {
    Delete(Delete),
    Insert(Insert),
    Select(Box<Select>),
    Update(Update),
}

impl CommandType {
    pub fn peek(input: ParseStream) -> bool {
        Delete::peek(input) || Insert::peek(input) || Select::peek(input) || Update::peek(input)
    }

    #[must_use]
    pub fn fields(&self) -> Option<&Fields> {
        match self {
            Self::Delete(inner) => inner.returning.as_ref().map(|returning| &returning.fields),
            Self::Insert(inner) => inner.returning.as_ref().map(|returning| &returning.fields),
            Self::Select(inner) => Some(inner.fields()),
            Self::Update(inner) => inner.returning.as_ref().map(|returning| &returning.fields),
        }
    }

    #[must_use]
    pub fn target_table(&self) -> Option<&TargetTable> {
        match self {
            Self::Delete(delete) => Some(&delete.target_table),
            Self::Insert(insert) => Some(&insert.target_table),
            Self::Select(..) => None,
            Self::Update(update) => Some(&update.target_table),
        }
    }

    #[must_use]
    #[allow(clippy::wrong_self_convention)]
    pub fn from_chain(&self) -> Option<&FromChain> {
        match self {
            Self::Delete(delete) => delete.using.as_ref().map(|using| &using.chain),
            Self::Insert(..) => None,
            Self::Select(select) => {
                if select.chain.combinators.is_empty() {
                    match &select.chain.start {
                        SelectItem::Paren { .. } => None,
                        SelectItem::Core(core) => core.from_chain(),
                    }
                } else {
                    None
                }
            }
            Self::Update(update) => update.from.as_ref().map(|from| &from.chain),
        }
    }
}

pub fn visit_command_type<'a>(
    visit: &mut (impl Visit<'a> + ?Sized),
    command_type: &'a CommandType,
) {
    match command_type {
        CommandType::Delete(inner) => visit.visit_delete(inner),
        CommandType::Insert(inner) => visit.visit_insert(inner),
        CommandType::Select(inner) => visit.visit_select_command(inner),
        CommandType::Update(inner) => visit.visit_update(inner),
    }
}

impl Parse for CommandType {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        if Delete::peek(input) {
            Ok(Self::Delete(input.parse()?))
        } else if Insert::peek(input) {
            Ok(Self::Insert(input.parse()?))
        } else if Select::peek(input) {
            Ok(Self::Select(input.parse()?))
        } else if Update::peek(input) {
            Ok(Self::Update(input.parse()?))
        } else {
            keyword::group_command::error(input);
        }
    }
}

impl ToTokens for CommandType {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        match self {
            Self::Delete(inner) => quote! {
                ::kosame::repr::command::CommandType::Delete(#inner)
            },
            Self::Insert(inner) => quote! {
                ::kosame::repr::command::CommandType::Insert(#inner)
            },
            Self::Select(inner) => quote! {
                ::kosame::repr::command::CommandType::Select(#inner)
            },
            Self::Update(inner) => quote! {
                ::kosame::repr::command::CommandType::Update(#inner)
            },
        }
        .to_tokens(tokens);
    }
}

impl PrettyPrint for CommandType {
    fn pretty_print(&self, printer: &mut Printer<'_>) {
        match self {
            Self::Delete(inner) => inner.pretty_print(printer),
            Self::Insert(inner) => inner.pretty_print(printer),
            Self::Select(inner) => inner.pretty_print(printer),
            Self::Update(inner) => inner.pretty_print(printer),
        }
    }
}