microrm 0.6.3

Lightweight ORM using sqlite as a backend
Documentation
//! This module provides autogeneration of clap commands to manipulate entities that are stored in
//! a microrm database.
//!
//! The main motivations of this module are:
//! - Providing an introspection interface for debugging.
//! - Providing an admin tool for services built on microrm.
//!
//! Broadly speaking, this module aims to play nicely with whatever other clap interface has been
//! set up. The main entry point of interest is [`Autogenerate`], which is a clap `Subcommand`
//! that can be included in an existing derived clap-parsed structure or enum; it can also be
//! included into a clap parse tree if you are using clap's builder interfaces.
//!
//! Once a [`Autogenerate`] object has been constructed, the [`Autogenerate::perform`] method
//! will execute the constructed action, possibly deferring to a user-defined set of custom
//! commands.

use crate::{
    prelude::{Insertable, Queryable},
    schema::entity::Entity,
    Error, Transaction,
};

mod eval;
mod parse;

/// Trait used to expose errors from the autogenerated clap interface code. Implemented for
/// [`Error`] but can be implemented for other error types if extra information needs to be passed
/// back.
pub trait CLIError: From<Error> {
    /// Complain about an entity by a given list of members not existing.
    fn no_such_entity(ename: &'static str, query: String) -> Self;
}

impl CLIError for Error {
    fn no_such_entity(_ename: &'static str, _query: String) -> Self {
        Error::EmptyResult
    }
}

/// Enumeration that describes the role of a value, used by overrides in [`EntityInterface`].
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
pub enum ValueRole {
    /// This value is for the 'base' object, i.e. the first object type specified in a command.
    BaseTarget,
    /// This value is for the 'attachment' object, i.e. the second object type specified in a command.
    AttachmentTarget,
}

/// Trait for providing a CLI interface for manipulating a single Entity type.
pub trait EntityInterface {
    /// What entity type this helper is for.
    type Entity: Entity;
    /// A precise error type to generate.
    type Error: CLIError;
    /// Arbitrary context for custom commands.
    type Context;

    /// Specific per-helper commands. See [`EmptyCommand`] if you need to specify "none".
    type CustomCommand: clap::Subcommand + std::fmt::Debug;

    /// Invoked when a custom command is parsed.
    fn run_custom(
        ctx: &Self::Context,
        cmd: Self::CustomCommand,
        txn: &mut Transaction,
        query_ctx: impl Queryable<EntityOutput = Self::Entity> + Insertable<Self::Entity>,
    ) -> Result<(), Self::Error>;

    /// Provide a summary of the entity, ideally a string with no newlines that can identify the
    /// entity at a glance.
    fn summarize(_: &Self::Entity) -> Option<String> {
        None
    }

    /// Provided to allow overriding the value passed to autogenerated commands, so that it is not
    /// requested on the command-line.
    fn should_override(_entity: &'static str, _field: &'static str, _role: ValueRole) -> bool {
        false
    }

    /// Invoked to request the concrete value for a value. Will only be invoked if
    /// should_override() for the same parameters previously returned true.
    fn override_for(
        _ctx: &Self::Context,
        _entity: &'static str,
        _field: &'static str,
        _role: ValueRole,
    ) -> String {
        unreachable!()
    }
}

/// Empty subcommand, used as a default value for CLIObjects that have no implemented additional
/// commands (i.e. the `ExtraCommands` associated type).
#[derive(Debug)]
pub struct EmptyCommand;

impl clap::FromArgMatches for EmptyCommand {
    fn from_arg_matches(_matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
        Err(clap::Error::new(clap::error::ErrorKind::UnknownArgument))
    }

    fn update_from_arg_matches(&mut self, _matches: &clap::ArgMatches) -> Result<(), clap::Error> {
        Ok(())
    }
}

impl clap::Subcommand for EmptyCommand {
    fn augment_subcommands(cmd: clap::Command) -> clap::Command {
        cmd
    }

    fn augment_subcommands_for_update(cmd: clap::Command) -> clap::Command {
        cmd
    }

    fn has_subcommand(_name: &str) -> bool {
        false
    }
}

/// Type that implements clap::Subcommand and represents a (manually-augmented) autogenerated CLI for an entity.
#[derive(Debug)]
pub struct Autogenerate<EI: EntityInterface> {
    verb: parse::Verb<EI>,
    _ghost: std::marker::PhantomData<(EI,)>,
}

// Subcommand is implemented in the parse module.
// ::perform is implemented in the eval module.

#[cfg(test)]
mod tests {
    use crate::ConnectionPool;

    use super::{Autogenerate, EntityInterface};
    use clap::Parser;
    use microrm::prelude::*;
    use test_log::test;

    struct CTRelation;
    impl Relation for CTRelation {
        type Range = Customer;
        type Domain = Transaction;
        const NAME: &'static str = "CTRelation";
    }

    struct ETRelation;
    impl Relation for ETRelation {
        type Range = Employee;
        type Domain = Transaction;
        const NAME: &'static str = "ETRelation";
    }

    #[derive(Entity)]
    struct Customer {
        #[key]
        name: String,

        txs: microrm::RelationRange<CTRelation>,
    }

    #[derive(Entity)]
    struct Employee {
        #[key]
        name: String,

        txs: microrm::RelationRange<ETRelation>,
    }

    #[derive(Entity)]
    struct Transaction {
        #[key]
        title: String,
        amount: isize,

        customer: microrm::RelationDomain<CTRelation>,
        employee: microrm::RelationDomain<ETRelation>,
    }

    #[derive(Schema)]
    struct TransactionTestDB {
        customers: microrm::IDMap<Customer>,
        employees: microrm::IDMap<Employee>,
        transactions: microrm::IDMap<Transaction>,
    }

    #[derive(Debug, clap::Subcommand)]
    enum CCustom {
        Create { name: String },
    }

    #[derive(Debug)]
    struct CustomerInterface;
    impl EntityInterface for CustomerInterface {
        type Entity = Customer;
        type Error = microrm::Error;
        type Context = ();

        type CustomCommand = CCustom;
        fn run_custom(
            _ctx: &Self::Context,
            cmd: Self::CustomCommand,
            txn: &mut microrm::Transaction,
            query_ctx: impl Queryable<EntityOutput = Self::Entity> + Insertable<Self::Entity>,
        ) -> Result<(), Self::Error> {
            match cmd {
                CCustom::Create { name } => {
                    query_ctx.insert(
                        txn,
                        Customer {
                            name,
                            txs: Default::default(),
                        },
                    )?;
                },
            }
            Ok(())
        }
    }

    #[derive(Debug, clap::Subcommand)]
    enum ECustom {
        Create { name: String },
    }

    #[derive(Debug)]
    struct EmployeeInterface;
    impl EntityInterface for EmployeeInterface {
        type Entity = Employee;
        type Error = microrm::Error;
        type Context = ();

        type CustomCommand = ECustom;
        fn run_custom(
            _ctx: &Self::Context,
            cmd: Self::CustomCommand,
            txn: &mut microrm::Transaction,
            query_ctx: impl Queryable<EntityOutput = Self::Entity> + Insertable<Self::Entity>,
        ) -> Result<(), Self::Error> {
            match cmd {
                ECustom::Create { name } => {
                    query_ctx.insert(
                        txn,
                        Employee {
                            name,
                            txs: Default::default(),
                        },
                    )?;
                },
            }
            Ok(())
        }
    }

    #[derive(Debug, clap::Subcommand)]
    enum TCustom {
        Create { title: String, amount: isize },
    }

    #[derive(Debug)]
    struct TransactionInterface;
    impl EntityInterface for TransactionInterface {
        type Entity = Transaction;
        type Error = microrm::Error;
        type Context = ();

        type CustomCommand = TCustom;
        fn run_custom(
            _ctx: &Self::Context,
            cmd: Self::CustomCommand,
            txn: &mut microrm::Transaction,
            query_ctx: impl Queryable<EntityOutput = Self::Entity> + Insertable<Self::Entity>,
        ) -> Result<(), Self::Error> {
            match cmd {
                TCustom::Create { title, amount } => {
                    query_ctx.insert(
                        txn,
                        Transaction {
                            title,
                            amount,
                            customer: Default::default(),
                            employee: Default::default(),
                        },
                    )?;
                },
            }
            Ok(())
        }
    }

    #[derive(Debug, clap::Parser)]
    enum Params {
        Customer {
            #[clap(subcommand)]
            cmd: Autogenerate<CustomerInterface>,
        },
        Employee {
            #[clap(subcommand)]
            cmd: Autogenerate<EmployeeInterface>,
        },
        Tx {
            #[clap(subcommand)]
            cmd: Autogenerate<TransactionInterface>,
        },
    }

    fn run_cmd(txn: &mut microrm::Transaction, db: &TransactionTestDB, args: &[&str]) {
        match <Params as Parser>::try_parse_from(args) {
            Ok(Params::Customer { cmd }) => {
                cmd.perform(&(), txn, &db.customers)
                    .expect("couldn't perform command");
            },
            Ok(Params::Employee { cmd }) => {
                cmd.perform(&(), txn, &db.employees)
                    .expect("couldn't perform command");
            },
            Ok(Params::Tx { cmd }) => {
                cmd.perform(&(), txn, &db.transactions)
                    .expect("couldn't perform command");
            },
            Err(e) => {
                println!("{}", e.render());
                panic!("error parsing arguments")
            },
        }
    }

    #[test]
    fn simple_entity_create_delete() {
        let (pool, db) = ConnectionPool::open::<TransactionTestDB>(":memory:").unwrap();
        let mut txn = pool.start().unwrap();

        assert_eq!(
            db.customers
                .keyed("a_key")
                .count(&mut txn)
                .expect("couldn't count entries"),
            0
        );
        run_cmd(&mut txn, &db, &["execname", "customer", "create", "a_key"]);
        assert_eq!(
            db.customers
                .keyed("a_key")
                .count(&mut txn)
                .expect("couldn't count entries"),
            1
        );
        run_cmd(&mut txn, &db, &["execname", "customer", "delete", "a_key"]);
        assert_eq!(
            db.customers
                .keyed("a_key")
                .count(&mut txn)
                .expect("couldn't count entries"),
            0
        );
    }

    #[test]
    fn create_and_attach() {
        let (pool, db) = ConnectionPool::open::<TransactionTestDB>(":memory:").unwrap();
        let mut txn = pool.start().unwrap();

        run_cmd(&mut txn, &db, &["execname", "customer", "create", "cname"]);
        run_cmd(&mut txn, &db, &["execname", "employee", "create", "ename"]);
        run_cmd(&mut txn, &db, &["execname", "tx", "create", "tname", "100"]);
        run_cmd(
            &mut txn,
            &db,
            &["execname", "customer", "attach", "cname", "txs", "tname"],
        );
        run_cmd(
            &mut txn,
            &db,
            &["execname", "employee", "attach", "ename", "txs", "tname"],
        );

        assert_eq!(
            db.customers
                .keyed("cname")
                .join(Customer::Txs)
                .join(Transaction::Employee)
                .first()
                .get(&mut txn)
                .expect("couldn't run query")
                .expect("no such employee")
                .name,
            "ename"
        );
    }
}