Переглянути джерело

Refactored and changed the clap autogeneration interface.

Kestrel 7 місяців тому
батько
коміт
8aceb08183
4 змінених файлів з 686 додано та 539 видалено
  1. 113 131
      microrm/src/cli.rs
  2. 2 408
      microrm/src/cli/clap_interface.rs
  3. 260 0
      microrm/src/cli/eval.rs
  4. 311 0
      microrm/src/cli/parse.rs

+ 113 - 131
microrm/src/cli.rs

@@ -20,9 +20,8 @@ use crate::{
     Error,
 };
 
-mod clap_interface;
-
-pub use clap_interface::{ClapInterface, InterfaceCustomization, ValueRole};
+mod parse;
+mod eval;
 
 /// 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
@@ -38,37 +37,41 @@ impl CLIError for Error {
     }
 }
 
-/// Extension trait on top of [`Entity`] that provides necessary functionality to interact with
-/// instances from the command-line, such as the data required to create an object.
-pub trait CLIObject: Sized + Entity + std::fmt::Debug {
-    /// What kind of error type should be generated when issues are encountered.
-    type Error: CLIError;
-    /// Extra data to pass into methods.
-    type CommandData;
-
-    /// Required data to create an instance of `Self`.
-    type CreateParameters: clap::Parser + std::fmt::Debug;
-    /// Create an instance of `Self` from a `CommandData` instance and a `CreateParameters` instance.
-    fn create_from_params(
-        _: &Self::CommandData,
-        _: &Self::CreateParameters,
-    ) -> Result<Self, Self::Error>;
-
-    /// Subcommand type representing all custom operations on this CLIObject implementation.
-    type ExtraCommands: clap::Subcommand + std::fmt::Debug;
-    /// Function that handles a constructed `Self::ExtraCommands` instance from clap.
-    fn run_extra_command(
-        _data: &Self::CommandData,
-        _extra: &Self::ExtraCommands,
-        _query_ctx: impl Queryable<EntityOutput = Self>,
-        _insert_ctx: &impl Insertable<Self>,
-    ) -> Result<(), Self::Error> {
-        unreachable!()
-    }
+/// 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,
+}
 
-    /// A short string used for display purposes to describe this object. Can be the output of
-    /// Debug or empty.
-    fn shortname(&self) -> String;
+/// 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, query_ctx: impl Queryable<EntityOutput = Self::Entity>, insert_ctx: &impl 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
@@ -100,9 +103,19 @@ impl clap::Subcommand for EmptyCommand {
     }
 }
 
+/// 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 super::{CLIObject, ClapInterface, InterfaceCustomization, ValueRole};
+    use super::{Autogenerate, EntityInterface};
     use clap::Parser;
     use microrm::prelude::*;
     use test_log::test;
@@ -154,105 +167,80 @@ mod tests {
         transactions: microrm::IDMap<Transaction>,
     }
 
-    #[derive(clap::Parser, Debug)]
-    struct PCreate {
-        name: String,
-    }
-
-    impl CLIObject for Customer {
-        type Error = microrm::Error;
-        type CreateParameters = PCreate;
-        fn create_from_params(
-            _extra: &(),
-            create: &Self::CreateParameters,
-        ) -> Result<Self, Self::Error> {
-            Ok(Customer {
-                name: create.name.clone(),
-                txs: Default::default(),
-            })
-        }
-        type ExtraCommands = super::EmptyCommand;
-        type CommandData = ();
 
-        fn shortname(&self) -> String {
-            self.name.clone()
+    #[derive(Debug, clap::Subcommand)]
+    enum CCustom {
+        Create {
+            name: String,
         }
     }
 
-    impl CLIObject for Employee {
+    #[derive(Debug)]
+    struct CustomerInterface;
+    impl EntityInterface for CustomerInterface {
+        type Entity = Customer;
         type Error = microrm::Error;
-        type CreateParameters = PCreate;
-        fn create_from_params(
-            _extra: &(),
-            create: &Self::CreateParameters,
-        ) -> Result<Self, Self::Error> {
-            Ok(Employee {
-                name: create.name.clone(),
-                txs: Default::default(),
-            })
-        }
-        type ExtraCommands = super::EmptyCommand;
-        type CommandData = ();
-
-        fn shortname(&self) -> String {
-            self.name.clone()
+        type Context = ();
+
+        type CustomCommand = CCustom;
+        fn run_custom(_ctx: &Self::Context, cmd: Self::CustomCommand, _query_ctx: impl Queryable<EntityOutput = Self::Entity>, insert_ctx: &impl Insertable<Self::Entity>) -> Result<(), Self::Error> {
+            match cmd {
+                CCustom::Create { name } => {
+                    insert_ctx.insert(Customer { name, txs: Default::default() })?;
+                },
+            }
+            Ok(())
         }
     }
 
-    #[derive(clap::Parser, Debug)]
-    struct TCreate {
-        title: String,
-        amount: isize,
+    #[derive(Debug, clap::Subcommand)]
+    enum ECustom {
+        Create {
+            name: String,
+        }
     }
 
-    impl CLIObject for Transaction {
+    #[derive(Debug)]
+    struct EmployeeInterface;
+    impl EntityInterface for EmployeeInterface {
+        type Entity = Employee;
         type Error = microrm::Error;
-        type CreateParameters = TCreate;
-        fn create_from_params(
-            _: &Self::CommandData,
-            cp: &Self::CreateParameters,
-        ) -> Result<Self, Self::Error> {
-            Ok(Transaction {
-                title: cp.title.clone(),
-                amount: cp.amount,
-                customer: Default::default(),
-                employee: Default::default(),
-            })
-        }
-
-        type ExtraCommands = super::EmptyCommand;
-        type CommandData = ();
-
-        fn shortname(&self) -> String {
-            String::new()
+        type Context = ();
+
+        type CustomCommand = ECustom;
+        fn run_custom(_ctx: &Self::Context, cmd: Self::CustomCommand, _query_ctx: impl Queryable<EntityOutput = Self::Entity>, insert_ctx: &impl Insertable<Self::Entity>) -> Result<(), Self::Error> {
+            match cmd {
+                ECustom::Create { name } => {
+                    insert_ctx.insert(Employee { name, txs: Default::default() })?;
+                },
+            }
+            Ok(())
         }
     }
 
-    #[derive(Debug)]
-    struct TestCustomization {}
-
-    impl TestCustomization {
-        fn new() -> Self {
-            Self {}
+    #[derive(Debug, clap::Subcommand)]
+    enum TCustom {
+        Create {
+            title: String,
+            amount: isize,
         }
     }
 
-    impl InterfaceCustomization for TestCustomization {
-        fn has_value_for(entity: &'static str, field: &'static str, role: ValueRole) -> bool {
-            if entity == "node" && field == "parent" && role == ValueRole::BaseTarget {
-                true
-            } else {
-                false
+    #[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, _query_ctx: impl Queryable<EntityOutput = Self::Entity>, insert_ctx: &impl Insertable<Self::Entity>) -> Result<(), Self::Error> {
+            match cmd {
+                TCustom::Create { title, amount } => {
+                    insert_ctx.insert(Transaction { title, amount, customer: Default::default(), employee: Default::default() })?;
+                },
             }
-        }
-
-        fn value_for(
-            &self,
-            _entity: &'static str,
-            _field: &'static str,
-            _role: super::ValueRole,
-        ) -> String {
-            String::from("")
+            Ok(())
         }
     }
 
@@ -260,30 +248,30 @@ mod tests {
     enum Params {
         Customer {
             #[clap(subcommand)]
-            cmd: ClapInterface<Customer, TestCustomization>,
+            cmd: Autogenerate<CustomerInterface>,
         },
         Employee {
             #[clap(subcommand)]
-            cmd: ClapInterface<Employee, TestCustomization>,
+            cmd: Autogenerate<EmployeeInterface>,
         },
         Tx {
             #[clap(subcommand)]
-            cmd: ClapInterface<Transaction, ()>,
+            cmd: Autogenerate<TransactionInterface>,
         },
     }
 
-    fn run_cmd(db: &TransactionTestDB, c: &TestCustomization, args: &[&str]) {
+    fn run_cmd(db: &TransactionTestDB, args: &[&str]) {
         match <Params as Parser>::try_parse_from(args) {
             Ok(Params::Customer { cmd }) => {
-                cmd.perform(&(), c, &db.customers, &db.customers)
+                cmd.perform(&(), &db.customers, &db.customers)
                     .expect("couldn't perform command");
             },
             Ok(Params::Employee { cmd }) => {
-                cmd.perform(&(), c, &db.employees, &db.employees)
+                cmd.perform(&(), &db.employees, &db.employees)
                     .expect("couldn't perform command");
             },
             Ok(Params::Tx { cmd }) => {
-                cmd.perform(&(), &(), &db.transactions, &db.transactions)
+                cmd.perform(&(), &db.transactions, &db.transactions)
                     .expect("couldn't perform command");
             },
             Err(e) => {
@@ -306,7 +294,6 @@ mod tests {
         );
         run_cmd(
             &db,
-            &TestCustomization::new(),
             &["execname", "customer", "create", "a_key"],
         );
         assert_eq!(
@@ -318,7 +305,6 @@ mod tests {
         );
         run_cmd(
             &db,
-            &TestCustomization::new(),
             &["execname", "customer", "delete", "a_key"],
         );
         assert_eq!(
@@ -334,19 +320,15 @@ mod tests {
     fn create_and_attach() {
         let db = TransactionTestDB::open_path(":memory:").unwrap();
 
-        let tc = TestCustomization::new();
-
-        run_cmd(&db, &tc, &["execname", "customer", "create", "cname"]);
-        run_cmd(&db, &tc, &["execname", "employee", "create", "ename"]);
-        run_cmd(&db, &tc, &["execname", "tx", "create", "tname", "100"]);
+        run_cmd(&db, &["execname", "customer", "create", "cname"]);
+        run_cmd(&db, &["execname", "employee", "create", "ename"]);
+        run_cmd(&db, &["execname", "tx", "create", "tname", "100"]);
         run_cmd(
             &db,
-            &tc,
             &["execname", "customer", "attach", "cname", "txs", "tname"],
         );
         run_cmd(
             &db,
-            &tc,
             &["execname", "employee", "attach", "ename", "txs", "tname"],
         );
 

+ 2 - 408
microrm/src/cli/clap_interface.rs

@@ -7,318 +7,17 @@ use crate::{
     },
 };
 
-use super::{CLIError, CLIObject};
+use super::{CLIError, EntityInterface, ValueRole};
 use clap::{FromArgMatches, Subcommand};
 
-/// iterate across the list of key parts (E::Keys) and add args for each
-fn add_keys<E: Entity, IC: InterfaceCustomization>(
-    mut cmd: clap::Command,
-    role: ValueRole,
-) -> clap::Command {
-    struct UVisitor<'a, IC: InterfaceCustomization, E: Entity>(
-        &'a mut clap::Command,
-        ValueRole,
-        std::marker::PhantomData<(IC, E)>,
-    );
-    impl<'a, IC: InterfaceCustomization, E: Entity> EntityPartVisitor for UVisitor<'a, IC, E> {
-        type Entity = E;
-        fn visit<EP: EntityPart>(&mut self) {
-            if !IC::has_value_for(EP::Entity::entity_name(), EP::part_name(), self.1) {
-                let arg = clap::Arg::new(EP::part_name())
-                    .required(true)
-                    .help(EP::desc());
-                *self.0 = self.0.clone().arg(arg);
-            }
-        }
-    }
-
-    <E::Keys as EntityPartList>::accept_part_visitor(&mut UVisitor::<IC, E>(
-        &mut cmd,
-        role,
-        Default::default(),
-    ));
-
-    cmd
-}
-
-fn collect_keys<E: Entity, IC: InterfaceCustomization>(
-    matches: &clap::ArgMatches,
-    role: ValueRole,
-) -> Vec<EntityKey> {
-    struct UVisitor<'a, IC: InterfaceCustomization, E: Entity>(
-        &'a clap::ArgMatches,
-        &'a mut Vec<EntityKey>,
-        ValueRole,
-        std::marker::PhantomData<(IC, E)>,
-    );
-    impl<'a, IC: InterfaceCustomization, E: Entity> EntityPartVisitor for UVisitor<'a, IC, E> {
-        type Entity = E;
-        fn visit<EP: EntityPart>(&mut self) {
-            if !IC::has_value_for(EP::Entity::entity_name(), EP::part_name(), self.2) {
-                self.1.push(EntityKey::UserInput(
-                    self.0
-                        .get_one::<std::string::String>(EP::part_name())
-                        .unwrap()
-                        .clone(),
-                ));
-            } else {
-                self.1.push(EntityKey::Placeholder(
-                    EP::Entity::entity_name(),
-                    EP::part_name(),
-                    self.2,
-                ));
-            }
-        }
-    }
-
-    let mut key_values = vec![];
-    <E::Keys as EntityPartList>::accept_part_visitor(&mut UVisitor::<IC, E>(
-        matches,
-        &mut key_values,
-        role,
-        Default::default(),
-    ));
-    key_values
-}
-
-#[derive(Debug, Clone)]
-pub enum EntityKey {
-    Placeholder(&'static str, &'static str, ValueRole),
-    UserInput(String),
-}
-
-impl EntityKey {
-    fn to_string_vec(vec: &[Self], ic: &impl InterfaceCustomization) -> Vec<String> {
-        vec.iter()
-            .map(|v| match v {
-                EntityKey::UserInput(s) => s.to_owned(),
-                EntityKey::Placeholder(entity, field, role) => ic.value_for(entity, field, *role),
-            })
-            .collect()
-    }
-}
-
-#[derive(Clone, Debug)]
-pub enum InterfaceVerb<O: CLIObject> {
-    Attach {
-        local_keys: Vec<EntityKey>,
-        relation: String,
-        remote_keys: Vec<EntityKey>,
-    },
-    Create(O::CreateParameters),
-    Delete(Vec<EntityKey>),
-    Detach {
-        local_keys: Vec<EntityKey>,
-        relation: String,
-        remote_keys: Vec<EntityKey>,
-    },
-    ListAll,
-    Inspect(Vec<EntityKey>),
-    Extra(O::ExtraCommands),
-}
 
 // helper alias for later
 type UniqueList<E> = <<E as Entity>::Keys as EntityPartList>::DatumList;
 
-impl<O: CLIObject> InterfaceVerb<O> {
-    fn parse_attachment<IC: InterfaceCustomization>(
-        matches: &clap::ArgMatches,
-    ) -> Result<(Vec<EntityKey>, String, Vec<EntityKey>), clap::Error> {
-        let local_keys = collect_keys::<O, IC>(matches, ValueRole::BaseTarget);
-
-        let (subcommand, submatches) = matches
-            .subcommand()
-            .ok_or(clap::Error::new(clap::error::ErrorKind::MissingSubcommand))?;
-
-        // find the relevant relation
-        struct RelationFinder<'l, IC: InterfaceCustomization, E: Entity> {
-            subcommand: &'l str,
-            submatches: &'l clap::ArgMatches,
-            keys: &'l mut Vec<EntityKey>,
-            _ghost: std::marker::PhantomData<(IC, E)>,
-        }
-        impl<'l, IC: InterfaceCustomization, E: Entity> EntityPartVisitor for RelationFinder<'l, IC, E> {
-            type Entity = E;
-            fn visit<EP: EntityPart>(&mut self) {
-                if EP::part_name() != self.subcommand {
-                    return;
-                }
-
-                EP::Datum::accept_entity_visitor(self);
-            }
-        }
-
-        impl<'l, IC: InterfaceCustomization, OE: Entity> EntityVisitor for RelationFinder<'l, IC, OE> {
-            fn visit<E: Entity>(&mut self) {
-                *self.keys = collect_keys::<E, IC>(self.submatches, ValueRole::AttachmentTarget);
-            }
-        }
-
-        let mut remote_keys = vec![];
-        O::accept_part_visitor(&mut RelationFinder::<IC, O> {
-            subcommand,
-            submatches,
-            keys: &mut remote_keys,
-            _ghost: Default::default(),
-        });
-
-        Ok((local_keys, subcommand.into(), remote_keys))
-    }
-
-    fn from_matches<IC: InterfaceCustomization>(
-        parent_matches: &clap::ArgMatches,
-    ) -> Result<Self, clap::Error> {
-        let (subcommand, matches) = parent_matches
-            .subcommand()
-            .ok_or(clap::Error::new(clap::error::ErrorKind::MissingSubcommand))?;
-
-        Ok(match subcommand {
-            "attach" => {
-                let (local_keys, relation, remote_keys) = Self::parse_attachment::<IC>(matches)?;
-                InterfaceVerb::Attach {
-                    local_keys,
-                    relation,
-                    remote_keys,
-                }
-            },
-            "create" => InterfaceVerb::Create(
-                <O::CreateParameters as clap::FromArgMatches>::from_arg_matches(matches)?,
-            ),
-            "delete" => {
-                InterfaceVerb::Delete(collect_keys::<O, IC>(matches, ValueRole::BaseTarget))
-            },
-            "detach" => {
-                let (local_keys, relation, remote_keys) = Self::parse_attachment::<IC>(matches)?;
-                InterfaceVerb::Detach {
-                    local_keys,
-                    relation,
-                    remote_keys,
-                }
-            },
-            "list" => InterfaceVerb::ListAll,
-            "inspect" => {
-                InterfaceVerb::Inspect(collect_keys::<O, IC>(matches, ValueRole::BaseTarget))
-            },
-            cmd => {
-                if <O::ExtraCommands>::has_subcommand(cmd) {
-                    InterfaceVerb::Extra(<O::ExtraCommands>::from_arg_matches(parent_matches)?)
-                } else {
-                    unreachable!()
-                }
-            },
-        })
-    }
-}
 
-/// helper type for attach and detach verbs
-struct Attacher<'l, Error: CLIError, E: Entity> {
-    do_attach: bool,
-    relation: &'l str,
-    remote_keys: Vec<String>,
-    err: Option<Error>,
-    _ghost: std::marker::PhantomData<E>,
-}
-
-impl<'l, Error: CLIError, OE: Entity> Attacher<'l, Error, OE> {
-    fn do_operation<E: Entity>(&mut self, map: &impl RelationInterface<RemoteEntity = E>) {
-        match map
-            .query_all()
-            .keyed(
-                UniqueList::<E>::build_equivalent(self.remote_keys.iter().map(String::as_str))
-                    .unwrap(),
-            )
-            .get()
-        {
-            Ok(Some(obj)) => {
-                if self.do_attach {
-                    self.err = map.connect_to(obj.id()).err().map(Into::into);
-                } else {
-                    self.err = map.disconnect_from(obj.id()).err().map(Into::into);
-                }
-            },
-            Ok(None) => {
-                self.err = Some(Error::no_such_entity(
-                    E::entity_name(),
-                    self.remote_keys
-                        .iter()
-                        .cloned()
-                        .reduce(|a, b| format!("{},{}", a, b))
-                        .unwrap()
-                        .to_string(),
-                ));
-            },
-            Err(e) => {
-                self.err = Some(e.into());
-            },
-        }
-    }
-}
-
-impl<'l, Error: CLIError, E: Entity> EntityPartVisitor for Attacher<'l, Error, E> {
-    type Entity = E;
-    fn visit_datum<EP: EntityPart>(&mut self, datum: &EP::Datum) {
-        if EP::part_name() != self.relation {
-            return;
-        }
-
-        datum.accept_discriminator_ref(self);
-    }
-}
-
-impl<'l, Error: CLIError, OE: Entity> DatumDiscriminatorRef for Attacher<'l, Error, OE> {
-    fn visit_entity_id<E: Entity>(&mut self, _: &E::ID) {
-        unreachable!()
-    }
-    fn visit_serialized<T: serde::Serialize + serde::de::DeserializeOwned>(&mut self, _: &T) {
-        unreachable!()
-    }
-    fn visit_bare_field<T: Datum>(&mut self, _: &T) {
-        unreachable!()
-    }
-
-    fn visit_relation_map<E: Entity>(&mut self, map: &RelationMap<E>) {
-        self.do_operation(map);
-    }
-    fn visit_relation_range<R: Relation>(&mut self, map: &RelationRange<R>) {
-        self.do_operation(map);
-    }
-    fn visit_relation_domain<R: Relation>(&mut self, map: &RelationDomain<R>) {
-        self.do_operation(map);
-    }
-}
-
-/// Enumeration that describes the role of a value in an [`InterfaceCustomization`] instance.
-#[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,
-}
-
-/// Allows customization of the autogenerated commands by programmatically providing values so they
-/// don't have to be specified on the command-line.
-pub trait InterfaceCustomization {
-    /// Returns true iff this customization will provide a value for this field.
-    fn has_value_for(entity: &'static str, field: &'static str, role: ValueRole) -> bool;
-    /// Returns a string representation of the value for this field, equivalent to a value entered
-    /// on the command-line.
-    fn value_for(&self, entity: &'static str, field: &'static str, role: ValueRole) -> String;
-}
-
-impl InterfaceCustomization for () {
-    fn has_value_for(_entity: &'static str, _field: &'static str, _role: ValueRole) -> bool {
-        false
-    }
-    fn value_for(&self, _: &'static str, _: &'static str, _: ValueRole) -> String {
-        unreachable!()
-    }
-}
 
 #[derive(Debug)]
-/// Type implementing `Subcommand` for a given CLIObject and specialization.
-///
-///
+/// Type implementing `Subcommand` for a given entity and interface.
 pub struct ClapInterface<O: CLIObject, IC: InterfaceCustomization> {
     verb: InterfaceVerb<O>,
     _ghost: std::marker::PhantomData<IC>,
@@ -499,110 +198,5 @@ impl<O: CLIObject, IC: InterfaceCustomization> ClapInterface<O, IC> {
         Ok(())
     }
 
-    fn make_relation_subcommands() -> impl Iterator<Item = clap::Command> {
-        let mut out = vec![];
-
-        struct PartVisitor<'l, IC: InterfaceCustomization, E: Entity>(
-            &'l mut Vec<clap::Command>,
-            std::marker::PhantomData<(IC, E)>,
-        );
-        impl<'l, IC: InterfaceCustomization, E: Entity> EntityPartVisitor for PartVisitor<'l, IC, E> {
-            type Entity = E;
-            fn visit<EP: EntityPart>(&mut self) {
-                struct Discriminator<'l, IC: InterfaceCustomization>(
-                    &'l mut Vec<clap::Command>,
-                    &'static str,
-                    std::marker::PhantomData<IC>,
-                );
-
-                impl<'l, IC: InterfaceCustomization> DatumDiscriminator for Discriminator<'l, IC> {
-                    fn visit_entity_id<E: Entity>(&mut self) {}
-                    fn visit_serialized<T: serde::Serialize + serde::de::DeserializeOwned>(
-                        &mut self,
-                    ) {
-                    }
-                    fn visit_bare_field<T: Datum>(&mut self) {}
-                    fn visit_relation_map<E: Entity>(&mut self) {
-                        self.0.push(add_keys::<E, IC>(
-                            clap::Command::new(self.1),
-                            ValueRole::AttachmentTarget,
-                        ));
-                    }
-                    fn visit_relation_domain<R: Relation>(&mut self) {
-                        self.0.push(add_keys::<R::Range, IC>(
-                            clap::Command::new(self.1),
-                            ValueRole::AttachmentTarget,
-                        ));
-                    }
-                    fn visit_relation_range<R: Relation>(&mut self) {
-                        self.0.push(add_keys::<R::Domain, IC>(
-                            clap::Command::new(self.1),
-                            ValueRole::AttachmentTarget,
-                        ));
-                    }
-                }
-
-                <EP::Datum as Datum>::accept_discriminator(&mut Discriminator::<IC>(
-                    self.0,
-                    EP::part_name(),
-                    Default::default(),
-                ));
-            }
-        }
-
-        O::accept_part_visitor(&mut PartVisitor::<IC, O>(&mut out, Default::default()));
-
-        out.into_iter()
-    }
 }
 
-impl<O: CLIObject, IC: InterfaceCustomization> FromArgMatches for ClapInterface<O, IC> {
-    fn from_arg_matches(matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
-        let verb = InterfaceVerb::from_matches::<IC>(matches);
-
-        Ok(Self {
-            verb: verb?,
-            _ghost: Default::default(),
-        })
-    }
-
-    fn update_from_arg_matches(&mut self, _matches: &clap::ArgMatches) -> Result<(), clap::Error> {
-        todo!()
-    }
-}
-
-impl<O: CLIObject, IC: InterfaceCustomization> Subcommand for ClapInterface<O, IC> {
-    fn has_subcommand(_name: &str) -> bool {
-        todo!()
-    }
-
-    fn augment_subcommands(cmd: clap::Command) -> clap::Command {
-        let cmd = cmd
-            .subcommand(
-                add_keys::<O, IC>(clap::Command::new("attach"), ValueRole::BaseTarget)
-                    .subcommands(Self::make_relation_subcommands())
-                    .subcommand_required(true),
-            )
-            .subcommand(
-                add_keys::<O, IC>(clap::Command::new("detach"), ValueRole::BaseTarget)
-                    .subcommands(Self::make_relation_subcommands())
-                    .subcommand_required(true),
-            )
-            .subcommand(<O::CreateParameters as clap::CommandFactory>::command().name("create"))
-            .subcommand(add_keys::<O, IC>(
-                clap::Command::new("delete"),
-                ValueRole::BaseTarget,
-            ))
-            .subcommand(add_keys::<O, IC>(
-                clap::Command::new("inspect"),
-                ValueRole::BaseTarget,
-            ))
-            .subcommand(clap::Command::new("list"));
-
-        <O::ExtraCommands>::augment_subcommands(cmd)
-    }
-
-    fn augment_subcommands_for_update(_cmd: clap::Command) -> clap::Command {
-        todo!()
-    }
-}

+ 260 - 0
microrm/src/cli/eval.rs

@@ -0,0 +1,260 @@
+use super::{parse::{EntityKey, Verb}, CLIError, EntityInterface, Autogenerate};
+use crate::{
+    schema::{
+        datum::{Datum,DatumDiscriminatorRef,ConcreteDatumList},
+        entity::{Entity, EntityID, EntityPartList, EntityPartVisitor, EntityPart},
+        relation::{Relation, RelationMap, RelationDomain, RelationRange}
+    },
+    query::{Insertable,Queryable,RelationInterface},
+};
+
+// helper alias for later
+type UniqueList<E> = <<E as Entity>::Keys as EntityPartList>::DatumList;
+
+impl<EI: EntityInterface> Autogenerate<EI> {
+    /// Execute the parsed command.
+    pub fn perform(
+        self,
+        ctx: &EI::Context, 
+        query_ctx: impl Queryable<EntityOutput = EI::Entity>,
+        insert_ctx: &impl Insertable<EI::Entity>,
+    ) -> Result<(), EI::Error> {
+        match self.verb {
+            Verb::Attach {
+                local_keys,
+                relation,
+                remote_keys,
+            } => {
+                let local_keys = EntityKey::to_string_vec::<EI>(&local_keys, ctx);
+                let remote_keys = EntityKey::to_string_vec::<EI>(&remote_keys, ctx);
+                let outer_obj = query_ctx
+                    .keyed(
+                        UniqueList::<EI::Entity>::build_equivalent(local_keys.iter().map(String::as_str))
+                            .unwrap(),
+                    )
+                    .get()?
+                    .ok_or(<EI::Error>::no_such_entity(
+                        EI::Entity::entity_name(),
+                        local_keys
+                            .iter()
+                            .cloned()
+                            .reduce(|a, b| format!("{},{}", a, b))
+                            .unwrap()
+                            .to_string(),
+                    ))?;
+
+                let mut attacher = Attacher {
+                    do_attach: true,
+                    relation: relation.as_str(),
+                    remote_keys,
+                    err: None,
+                    _ghost: Default::default(),
+                };
+                outer_obj.accept_part_visitor_ref(&mut attacher);
+
+                if let Some(err) = attacher.err {
+                    return Err(err);
+                }
+            },
+            Verb::Delete(keys) => {
+                let keys = EntityKey::to_string_vec::<EI>(&keys, ctx);
+                query_ctx
+                    .keyed(
+                        UniqueList::<EI::Entity>::build_equivalent(keys.iter().map(String::as_str)).unwrap(),
+                    )
+                    .delete()?;
+            },
+            Verb::Detach {
+                local_keys,
+                relation,
+                remote_keys,
+            } => {
+                let local_keys = EntityKey::to_string_vec::<EI>(&local_keys, ctx);
+                let remote_keys = EntityKey::to_string_vec::<EI>(&remote_keys, ctx);
+                let outer_obj = query_ctx
+                    .keyed(
+                        UniqueList::<EI::Entity>::build_equivalent(local_keys.iter().map(String::as_str))
+                            .unwrap(),
+                    )
+                    .get()?
+                    .ok_or(<EI::Error>::no_such_entity(
+                        EI::Entity::entity_name(),
+                        local_keys
+                            .iter()
+                            .cloned()
+                            .reduce(|a, b| format!("{},{}", a, b))
+                            .unwrap()
+                            .to_string(),
+                    ))?;
+
+                let mut attacher = Attacher {
+                    do_attach: false,
+                    relation: relation.as_str(),
+                    remote_keys,
+                    err: None,
+                    _ghost: Default::default(),
+                };
+                outer_obj.accept_part_visitor_ref(&mut attacher);
+
+                if let Some(err) = attacher.err {
+                    return Err(err);
+                }
+            },
+            Verb::ListAll => {
+                println!(
+                    "Listing all {}(s): ({})",
+                    EI::Entity::entity_name(),
+                    query_ctx.clone().count()?
+                );
+                for obj in query_ctx.get()?.into_iter() {
+                    println!(" - {}", EI::summarize(&obj).unwrap_or_else(|| format!("{:?}", obj)));
+                }
+            },
+            Verb::Inspect(keys) => {
+                let keys = EntityKey::to_string_vec::<EI>(&keys, ctx);
+                let obj = query_ctx
+                    .keyed(
+                        UniqueList::<EI::Entity>::build_equivalent(keys.iter().map(String::as_str)).unwrap(),
+                    )
+                    .get()?
+                    .ok_or(<EI::Error>::no_such_entity(
+                        EI::Entity::entity_name(),
+                        keys.iter()
+                            .cloned()
+                            .reduce(|a, b| format!("{},{}", a, b))
+                            .unwrap()
+                            .to_string(),
+                    ))?;
+                println!("{:#?}", obj.as_ref());
+
+                fn inspect_ai<AI: RelationInterface>(name: &'static str, ai: &AI) {
+                    println!("{}: ({})", name, ai.count().unwrap());
+                    for a in ai.get().expect("couldn't get object relations") {
+                        println!("[#{:3}]: {:?}", a.id().into_raw(), a.wrapped());
+                    }
+                }
+
+                struct RelationFieldWalker<E: Entity>(std::marker::PhantomData<E>);
+                impl<E: Entity> EntityPartVisitor for RelationFieldWalker<E> {
+                    type Entity = E;
+                    fn visit_datum<EP: EntityPart>(&mut self, datum: &EP::Datum) {
+                        struct Discriminator(&'static str);
+
+                        impl DatumDiscriminatorRef for Discriminator {
+                            fn visit_serialized<
+                                T: serde::Serialize + serde::de::DeserializeOwned,
+                            >(
+                                &mut self,
+                                _: &T,
+                            ) {
+                            }
+                            fn visit_bare_field<T: Datum>(&mut self, _: &T) {}
+                            fn visit_entity_id<E: Entity>(&mut self, _: &E::ID) {}
+                            fn visit_relation_map<E: Entity>(&mut self, amap: &RelationMap<E>) {
+                                inspect_ai(self.0, amap);
+                            }
+                            fn visit_relation_domain<R: Relation>(
+                                &mut self,
+                                adomain: &RelationDomain<R>,
+                            ) {
+                                inspect_ai(self.0, adomain);
+                            }
+                            fn visit_relation_range<R: Relation>(
+                                &mut self,
+                                arange: &RelationRange<R>,
+                            ) {
+                                inspect_ai(self.0, arange);
+                            }
+                        }
+
+                        datum.accept_discriminator_ref(&mut Discriminator(EP::part_name()));
+                    }
+                }
+
+                obj.accept_part_visitor_ref(&mut RelationFieldWalker(Default::default()));
+            },
+            Verb::Custom(custom) => {
+                EI::run_custom(ctx, custom, query_ctx, insert_ctx)?;
+            },
+        }
+
+        Ok(())
+    }
+}
+
+/// helper type for attach and detach verbs
+struct Attacher<'l, Error: CLIError, E: Entity> {
+    do_attach: bool,
+    relation: &'l str,
+    remote_keys: Vec<String>,
+    err: Option<Error>,
+    _ghost: std::marker::PhantomData<E>,
+}
+
+impl<'l, Error: CLIError, OE: Entity> Attacher<'l, Error, OE> {
+    fn do_operation<E: Entity>(&mut self, map: &impl RelationInterface<RemoteEntity = E>) {
+        match map
+            .query_all()
+            .keyed(
+                UniqueList::<E>::build_equivalent(self.remote_keys.iter().map(String::as_str))
+                    .unwrap(),
+            )
+            .get()
+        {
+            Ok(Some(obj)) => {
+                if self.do_attach {
+                    self.err = map.connect_to(obj.id()).err().map(Into::into);
+                } else {
+                    self.err = map.disconnect_from(obj.id()).err().map(Into::into);
+                }
+            },
+            Ok(None) => {
+                self.err = Some(Error::no_such_entity(
+                    E::entity_name(),
+                    self.remote_keys
+                        .iter()
+                        .cloned()
+                        .reduce(|a, b| format!("{},{}", a, b))
+                        .unwrap()
+                        .to_string(),
+                ));
+            },
+            Err(e) => {
+                self.err = Some(e.into());
+            },
+        }
+    }
+}
+
+impl<'l, Error: CLIError, E: Entity> EntityPartVisitor for Attacher<'l, Error, E> {
+    type Entity = E;
+    fn visit_datum<EP: EntityPart>(&mut self, datum: &EP::Datum) {
+        if EP::part_name() != self.relation {
+            return;
+        }
+
+        datum.accept_discriminator_ref(self);
+    }
+}
+
+impl<'l, Error: CLIError, OE: Entity> DatumDiscriminatorRef for Attacher<'l, Error, OE> {
+    fn visit_entity_id<E: Entity>(&mut self, _: &E::ID) {
+        unreachable!()
+    }
+    fn visit_serialized<T: serde::Serialize + serde::de::DeserializeOwned>(&mut self, _: &T) {
+        unreachable!()
+    }
+    fn visit_bare_field<T: Datum>(&mut self, _: &T) {
+        unreachable!()
+    }
+
+    fn visit_relation_map<E: Entity>(&mut self, map: &RelationMap<E>) {
+        self.do_operation(map);
+    }
+    fn visit_relation_range<R: Relation>(&mut self, map: &RelationRange<R>) {
+        self.do_operation(map);
+    }
+    fn visit_relation_domain<R: Relation>(&mut self, map: &RelationDomain<R>) {
+        self.do_operation(map);
+    }
+}

+ 311 - 0
microrm/src/cli/parse.rs

@@ -0,0 +1,311 @@
+use crate::schema::{
+    datum::{Datum, DatumDiscriminator},
+    entity::{Entity, EntityPart, EntityPartList, EntityPartVisitor, EntityVisitor},
+    relation::Relation,
+};
+use super::{Autogenerate, EntityInterface, ValueRole};
+use clap::{FromArgMatches,Subcommand};
+
+#[derive(Debug, Clone)]
+pub enum EntityKey {
+    Placeholder { entity: &'static str, field: &'static str, role: ValueRole },
+    UserInput(String),
+}
+
+impl EntityKey {
+    pub(crate) fn to_string_vec<EI: EntityInterface>(vec: &[Self], ctx: &EI::Context) -> Vec<String> {
+        vec.iter()
+            .map(|v| match v {
+                EntityKey::UserInput(s) => s.to_owned(),
+                EntityKey::Placeholder { entity, field, role } => EI::override_for(ctx, entity, field, *role),
+            })
+            .collect()
+    }
+}
+
+// --------------------------------------------------------------------------
+// helper functions
+// --------------------------------------------------------------------------
+/// iterate across the list of key parts (E::Keys) and add args for each
+fn add_keys<E: Entity, EI: EntityInterface>(
+    mut cmd: clap::Command,
+    role: ValueRole,
+) -> clap::Command {
+    struct UVisitor<'a, E: Entity, EI: EntityInterface>(
+        &'a mut clap::Command,
+        ValueRole,
+        std::marker::PhantomData<(E,EI)>,
+    );
+    impl<'a, E: Entity, EI: EntityInterface> EntityPartVisitor for UVisitor<'a, E, EI> {
+        type Entity = E;
+        fn visit<EP: EntityPart>(&mut self) {
+            if !EI::should_override(EP::Entity::entity_name(), EP::part_name(), self.1) {
+                let arg = clap::Arg::new(EP::part_name())
+                    .required(true)
+                    .help(EP::desc());
+                *self.0 = self.0.clone().arg(arg);
+            }
+        }
+    }
+
+    <E::Keys as EntityPartList>::accept_part_visitor(&mut UVisitor::<E, EI>(
+        &mut cmd,
+        role,
+        Default::default(),
+    ));
+
+    cmd
+}
+
+fn collect_keys<E: Entity, EI: EntityInterface>(
+    matches: &clap::ArgMatches,
+    role: ValueRole,
+) -> Vec<EntityKey> {
+    struct UVisitor<'a, E: Entity, EI: EntityInterface>(
+        &'a clap::ArgMatches,
+        &'a mut Vec<EntityKey>,
+        ValueRole,
+        std::marker::PhantomData<(E,EI)>,
+    );
+    impl<'a, E: Entity, EI: EntityInterface> EntityPartVisitor for UVisitor<'a, E, EI> {
+        type Entity = E;
+        fn visit<EP: EntityPart>(&mut self) {
+            if !EI::should_override(EP::Entity::entity_name(), EP::part_name(), self.2) {
+                self.1.push(EntityKey::UserInput(
+                    self.0
+                        .get_one::<std::string::String>(EP::part_name())
+                        .unwrap()
+                        .clone(),
+                ));
+            } else {
+                self.1.push(EntityKey::Placeholder {
+                    entity: EP::Entity::entity_name(),
+                    field: EP::part_name(),
+                    role: self.2,
+                });
+            }
+        }
+    }
+
+    let mut key_values = vec![];
+    <E::Keys as EntityPartList>::accept_part_visitor(&mut UVisitor::<E, EI>(
+        matches,
+        &mut key_values,
+        role,
+        Default::default(),
+    ));
+    key_values
+}
+
+#[derive(Clone, Debug)]
+pub(crate) enum Verb<EI: EntityInterface> {
+    Attach {
+        local_keys: Vec<EntityKey>,
+        relation: String,
+        remote_keys: Vec<EntityKey>,
+    },
+    Delete(Vec<EntityKey>),
+    Detach {
+        local_keys: Vec<EntityKey>,
+        relation: String,
+        remote_keys: Vec<EntityKey>,
+    },
+    ListAll,
+    Inspect(Vec<EntityKey>),
+    Custom(EI::CustomCommand),
+}
+
+impl<EI: EntityInterface> Verb<EI> {
+    fn parse_attachment(
+        matches: &clap::ArgMatches,
+    ) -> Result<(Vec<EntityKey>, String, Vec<EntityKey>), clap::Error> {
+        let local_keys = collect_keys::<EI::Entity, EI>(matches, ValueRole::BaseTarget);
+
+        let (subcommand, submatches) = matches
+            .subcommand()
+            .ok_or(clap::Error::new(clap::error::ErrorKind::MissingSubcommand))?;
+
+        // find the relevant relation
+        struct RelationFinder<'l, EI: EntityInterface> {
+            subcommand: &'l str,
+            submatches: &'l clap::ArgMatches,
+            keys: &'l mut Vec<EntityKey>,
+            _ghost: std::marker::PhantomData<(EI,)>,
+        }
+        impl<'l, EI: EntityInterface> EntityPartVisitor for RelationFinder<'l, EI> {
+            type Entity = EI::Entity;
+            fn visit<EP: EntityPart>(&mut self) {
+                if EP::part_name() != self.subcommand {
+                    return;
+                }
+
+                EP::Datum::accept_entity_visitor(self);
+            }
+        }
+
+        impl<'l, EI: EntityInterface> EntityVisitor for RelationFinder<'l, EI> {
+            fn visit<E: Entity>(&mut self) {
+                *self.keys = collect_keys::<E, EI>(self.submatches, ValueRole::AttachmentTarget);
+            }
+        }
+
+        let mut remote_keys = vec![];
+        EI::Entity::accept_part_visitor(&mut RelationFinder::<EI> {
+            subcommand,
+            submatches,
+            keys: &mut remote_keys,
+            _ghost: Default::default(),
+        });
+
+        Ok((local_keys, subcommand.into(), remote_keys))
+    }
+
+    fn from_matches(
+        parent_matches: &clap::ArgMatches,
+    ) -> Result<Self, clap::Error> {
+        let (subcommand, matches) = parent_matches
+            .subcommand()
+            .ok_or(clap::Error::new(clap::error::ErrorKind::MissingSubcommand))?;
+
+        Ok(match subcommand {
+            "attach" => {
+                let (local_keys, relation, remote_keys) = Self::parse_attachment(matches)?;
+                Self::Attach {
+                    local_keys,
+                    relation,
+                    remote_keys,
+                }
+            },
+            "delete" => {
+                Self::Delete(collect_keys::<EI::Entity, EI>(matches, ValueRole::BaseTarget))
+            },
+            "detach" => {
+                let (local_keys, relation, remote_keys) = Self::parse_attachment(matches)?;
+                Self::Detach {
+                    local_keys,
+                    relation,
+                    remote_keys,
+                }
+            },
+            "list" => Verb::ListAll,
+            "inspect" => {
+                Self::Inspect(collect_keys::<EI::Entity, EI>(matches, ValueRole::BaseTarget))
+            },
+            cmd => {
+                if EI::CustomCommand::has_subcommand(cmd) {
+                    Self::Custom(EI::CustomCommand::from_arg_matches(parent_matches)?)
+                } else {
+                    unreachable!()
+                }
+            },
+        })
+    }
+}
+
+impl<EI: EntityInterface> Autogenerate<EI> {
+    fn make_relation_subcommands() -> impl Iterator<Item = clap::Command> {
+        let mut out = vec![];
+
+        struct PartVisitor<'l, EI: EntityInterface>(
+            &'l mut Vec<clap::Command>,
+            std::marker::PhantomData<(EI,)>,
+        );
+        impl<'l, EI: EntityInterface> EntityPartVisitor for PartVisitor<'l, EI> {
+            type Entity = EI::Entity;
+            fn visit<EP: EntityPart>(&mut self) {
+                struct Discriminator<'l, EI: EntityInterface>(
+                    &'l mut Vec<clap::Command>,
+                    &'static str,
+                    std::marker::PhantomData<(EI,)>,
+                );
+
+                impl<'l, EI: EntityInterface> DatumDiscriminator for Discriminator<'l, EI> {
+                    fn visit_entity_id<E: Entity>(&mut self) {}
+                    fn visit_serialized<T: serde::Serialize + serde::de::DeserializeOwned>(
+                        &mut self,
+                    ) {
+                    }
+                    fn visit_bare_field<T: Datum>(&mut self) {}
+                    fn visit_relation_map<E: Entity>(&mut self) {
+                        self.0.push(add_keys::<E, EI>(
+                            clap::Command::new(self.1),
+                            ValueRole::AttachmentTarget,
+                        ));
+                    }
+                    fn visit_relation_domain<R: Relation>(&mut self) {
+                        self.0.push(add_keys::<R::Range, EI>(
+                            clap::Command::new(self.1),
+                            ValueRole::AttachmentTarget,
+                        ));
+                    }
+                    fn visit_relation_range<R: Relation>(&mut self) {
+                        self.0.push(add_keys::<R::Domain, EI>(
+                            clap::Command::new(self.1),
+                            ValueRole::AttachmentTarget,
+                        ));
+                    }
+                }
+
+                <EP::Datum as Datum>::accept_discriminator(&mut Discriminator::<EI>(
+                    self.0,
+                    EP::part_name(),
+                    Default::default(),
+                ));
+            }
+        }
+
+        EI::Entity::accept_part_visitor(&mut PartVisitor::<EI>(&mut out, Default::default()));
+
+        out.into_iter()
+    }
+}
+
+impl<EI: EntityInterface> clap::FromArgMatches for Autogenerate<EI> {
+    fn from_arg_matches(matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
+        let verb = Verb::from_matches(matches);
+
+        Ok(Self {
+            verb: verb?,
+            _ghost: Default::default(),
+        })
+    }
+
+    fn update_from_arg_matches(&mut self, _matches: &clap::ArgMatches) -> Result<(), clap::Error> {
+        todo!()
+    }
+}
+
+impl<EI: EntityInterface> clap::Subcommand for Autogenerate<EI> {
+    fn has_subcommand(_name: &str) -> bool {
+        todo!()
+    }
+
+    fn augment_subcommands(cmd: clap::Command) -> clap::Command {
+        let cmd = cmd
+            .subcommand(
+                add_keys::<EI::Entity, EI>(clap::Command::new("attach"), ValueRole::BaseTarget)
+                    .subcommands(Self::make_relation_subcommands())
+                    .subcommand_required(true),
+            )
+            .subcommand(
+                add_keys::<EI::Entity, EI>(clap::Command::new("detach"), ValueRole::BaseTarget)
+                    .subcommands(Self::make_relation_subcommands())
+                    .subcommand_required(true),
+            )
+            .subcommand(add_keys::<EI::Entity, EI>(
+                clap::Command::new("delete"),
+                ValueRole::BaseTarget,
+            ))
+            .subcommand(add_keys::<EI::Entity, EI>(
+                clap::Command::new("inspect"),
+                ValueRole::BaseTarget,
+            ))
+            .subcommand(clap::Command::new("list"));
+
+        EI::CustomCommand::augment_subcommands(cmd)
+    }
+
+    fn augment_subcommands_for_update(_cmd: clap::Command) -> clap::Command {
+        todo!()
+    }
+}