Przeglądaj źródła

Add documentation and initial autogenerated CLI customization interface.

Kestrel 7 miesięcy temu
rodzic
commit
3f7805947e
5 zmienionych plików z 423 dodań i 160 usunięć
  1. 4 0
      microrm/Cargo.toml
  2. 206 92
      microrm/src/cli.rs
  3. 175 62
      microrm/src/cli/clap_interface.rs
  4. 23 0
      microrm/src/lib.rs
  5. 15 6
      microrm/src/schema/datum.rs

+ 4 - 0
microrm/Cargo.toml

@@ -9,6 +9,10 @@ description = "Lightweight ORM using sqlite as a backend."
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
+[package.metadata.docs.rs]
+features = ["clap"]
+all-features = true
+
 [features]
 clap = ["dep:clap"]
 

+ 206 - 92
microrm/src/cli.rs

@@ -1,3 +1,19 @@
+//! 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 [`ClapInterface`], 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 [`ClapInterface`] object has been constructed, the [`ClapInterface::perform`] method
+//! will execute the constructed action, possibly deferring to a user-defined set of custom
+//! per-`CLIObject` commands.
+
 use crate::{
     prelude::{Insertable, Queryable},
     schema::entity::Entity,
@@ -6,27 +22,36 @@ use crate::{
 
 mod clap_interface;
 
-pub use clap_interface::ClapInterface;
+pub use clap_interface::{ClapInterface, InterfaceCustomization, ValueRole};
 
+/// 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> {
     fn no_such_entity(ename: &'static str, query: String) -> Self;
 }
 
 impl CLIError for Error {
-    fn no_such_entity(ename: &'static str, query: String) -> Self {
+    fn no_such_entity(_ename: &'static str, _query: String) -> Self {
         Error::EmptyResult
     }
 }
 
+/// 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 {
     type Error: CLIError;
+    type CommandData;
+
     type CreateParameters: clap::Parser + std::fmt::Debug;
-    fn create_from_params(_: &Self::CreateParameters) -> Result<Self, Self::Error>;
+    fn create_from_params(
+        _: &Self::CommandData,
+        _: &Self::CreateParameters,
+    ) -> Result<Self, Self::Error>;
 
     type ExtraCommands: clap::Subcommand + std::fmt::Debug;
-    type ExtraCommandData;
     fn run_extra_command(
-        _data: &Self::ExtraCommandData,
+        _data: &Self::CommandData,
         _extra: &Self::ExtraCommands,
         _query_ctx: impl Queryable<EntityOutput = Self>,
         _insert_ctx: &impl Insertable<Self>,
@@ -34,9 +59,13 @@ pub trait CLIObject: Sized + Entity + std::fmt::Debug {
         unreachable!()
     }
 
-    fn shortname(&self) -> &str;
+    /// A short string used for display purposes to describe this object. Can be the output of
+    /// Debug.
+    fn shortname(&self) -> String;
 }
 
+/// 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;
 
@@ -66,101 +95,190 @@ impl clap::Subcommand for EmptyCommand {
 
 #[cfg(test)]
 mod tests {
-    use super::{CLIObject, ClapInterface};
+    use crate::schema::{AssocDomain, AssocRange, Relation};
+
+    use super::{CLIObject, ClapInterface, InterfaceCustomization, ValueRole};
     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 KVEntry {
+    struct Customer {
         #[key]
         name: String,
-        value: String,
+
+        txs: AssocRange<CTRelation>,
     }
 
     #[derive(Entity)]
-    struct Node {
-        parent_id: Option<NodeID>,
+    struct Employee {
         #[key]
-        node_name: String,
-        node_value: Option<String>,
+        name: String,
 
-        children: AssocMap<Node>,
+        txs: AssocRange<ETRelation>,
+    }
+
+    #[derive(Entity)]
+    struct Transaction {
+        #[key]
+        title: String,
+        amount: isize,
+
+        customer: AssocDomain<CTRelation>,
+        employee: AssocDomain<ETRelation>,
     }
 
     #[derive(Database)]
-    struct KVStore {
-        entries: IDMap<KVEntry>,
-        nodes: IDMap<Node>,
+    struct TransactionTestDB {
+        customers: IDMap<Customer>,
+        employees: IDMap<Employee>,
+        transactions: IDMap<Transaction>,
     }
 
     #[derive(clap::Parser, Debug)]
-    struct KVCreate {
-        key: String,
-        value: String,
+    struct PCreate {
+        name: String,
     }
 
-    impl CLIObject for KVEntry {
+    impl CLIObject for Customer {
         type Error = microrm::Error;
-        type CreateParameters = KVCreate;
-        fn create_from_params(create: &Self::CreateParameters) -> Result<Self, Self::Error> {
-            Ok(KVEntry {
-                name: create.key.clone(),
-                value: create.value.clone(),
+        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 ExtraCommandData = ();
+        type CommandData = ();
 
-        fn shortname(&self) -> &str {
-            "kventry"
+        fn shortname(&self) -> String {
+            self.name.clone()
+        }
+    }
+
+    impl CLIObject for Employee {
+        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()
         }
     }
 
     #[derive(clap::Parser, Debug)]
-    struct NodeCreate {
-        name: String,
+    struct TCreate {
+        title: String,
+        amount: isize,
     }
 
-    impl CLIObject for Node {
+    impl CLIObject for Transaction {
         type Error = microrm::Error;
-        type CreateParameters = NodeCreate;
-        fn create_from_params(create: &Self::CreateParameters) -> Result<Self, Self::Error> {
-            Ok(Node {
-                parent_id: None,
-                node_name: create.name.clone(),
-                node_value: None,
-                children: Default::default(),
+        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 ExtraCommandData = ();
+        type CommandData = ();
 
-        fn shortname(&self) -> &str {
-            "node"
+        fn shortname(&self) -> String {
+            String::new()
+        }
+    }
+
+    #[derive(Debug)]
+    struct TestCustomization {}
+
+    impl TestCustomization {
+        fn new() -> Self {
+            Self {}
+        }
+    }
+
+    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
+            }
+        }
+
+        fn value_for(
+            &self,
+            _entity: &'static str,
+            _field: &'static str,
+            _role: super::ValueRole,
+        ) -> String {
+            String::from("")
         }
     }
 
     #[derive(Debug, clap::Parser)]
     enum Params {
-        KV {
+        Customer {
+            #[clap(subcommand)]
+            cmd: ClapInterface<Customer, TestCustomization>,
+        },
+        Employee {
             #[clap(subcommand)]
-            cmd: ClapInterface<KVEntry>,
+            cmd: ClapInterface<Employee, TestCustomization>,
         },
-        Node {
+        Tx {
             #[clap(subcommand)]
-            cmd: ClapInterface<Node>,
+            cmd: ClapInterface<Transaction, ()>,
         },
     }
 
-    fn run_cmd(db: &KVStore, args: &[&str]) {
+    fn run_cmd(db: &TransactionTestDB, c: &TestCustomization, args: &[&str]) {
         match <Params as Parser>::try_parse_from(args) {
-            Ok(Params::KV { cmd }) => {
-                cmd.perform(&(), &db.entries, &db.entries)
+            Ok(Params::Customer { cmd }) => {
+                cmd.perform(&(), c, &db.customers, &db.customers)
                     .expect("couldn't perform command");
             }
-            Ok(Params::Node { cmd }) => {
-                cmd.perform(&(), &db.nodes, &db.nodes)
+            Ok(Params::Employee { cmd }) => {
+                cmd.perform(&(), c, &db.employees, &db.employees)
+                    .expect("couldn't perform command");
+            }
+            Ok(Params::Tx { cmd }) => {
+                cmd.perform(&(), &(), &db.transactions, &db.transactions)
                     .expect("couldn't perform command");
             }
             Err(e) => {
@@ -171,48 +289,35 @@ mod tests {
     }
 
     #[test]
-    fn simple_entity_create() {
-        let db = KVStore::open_path(":memory:").unwrap();
+    fn simple_entity_create_delete() {
+        let db = TransactionTestDB::open_path(":memory:").unwrap();
 
         assert_eq!(
-            db.entries
+            db.customers
                 .keyed("a_key")
                 .count()
                 .expect("couldn't count entries"),
             0
         );
-        run_cmd(&db, &["execname", "kv", "create", "a_key", "a_value"]);
-        assert_eq!(
-            db.entries
-                .keyed("a_key")
-                .count()
-                .expect("couldn't count entries"),
-            1
-        );
-    }
-
-    #[test]
-    fn simple_entity_delete() {
-        let db = KVStore::open_path(":memory:").unwrap();
-
-        assert_eq!(
-            db.entries
-                .keyed("a_key")
-                .count()
-                .expect("couldn't count entries"),
-            0
+        run_cmd(
+            &db,
+            &TestCustomization::new(),
+            &["execname", "customer", "create", "a_key"],
         );
-        run_cmd(&db, &["execname", "kv", "create", "a_key", "a_value"]);
         assert_eq!(
-            db.entries
+            db.customers
                 .keyed("a_key")
                 .count()
                 .expect("couldn't count entries"),
             1
         );
-        run_cmd(&db, &["execname", "kv", "delete", "a_key"]);
+        run_cmd(
+            &db,
+            &TestCustomization::new(),
+            &["execname", "customer", "delete", "a_key"],
+        );
         assert_eq!(
-            db.entries
+            db.customers
                 .keyed("a_key")
                 .count()
                 .expect("couldn't count entries"),
@@ -221,27 +326,36 @@ mod tests {
     }
 
     #[test]
-    fn create_and_attach_node() {
-        let db = KVStore::open_path(":memory:").unwrap();
+    fn create_and_attach() {
+        let db = TransactionTestDB::open_path(":memory:").unwrap();
 
-        run_cmd(&db, &["execname", "node", "create", "aname"]);
-        run_cmd(&db, &["execname", "node", "create", "bname"]);
+        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,
+            &tc,
+            &["execname", "customer", "attach", "cname", "txs", "tname"],
+        );
         run_cmd(
             &db,
-            &["execname", "node", "attach", "aname", "children", "bname"],
+            &tc,
+            &["execname", "employee", "attach", "ename", "txs", "tname"],
         );
 
         assert_eq!(
-            db.nodes
-                .keyed("aname")
+            db.customers
+                .keyed("cname")
+                .join(Customer::Txs)
+                .join(Transaction::Employee)
+                .first()
                 .get()
-                .ok()
-                .flatten()
-                .expect("couldn't get aname")
-                .children
-                .count()
-                .expect("couldn't get children of aname"),
-            1
+                .expect("couldn't run query")
+                .expect("no such employee")
+                .name,
+            "ename"
         );
     }
 }

+ 175 - 62
microrm/src/cli/clap_interface.rs

@@ -10,56 +10,107 @@ use super::{CLIError, CLIObject};
 use clap::{FromArgMatches, Subcommand};
 
 /// iterate across the list of key parts (E::Keys) and add args for each
-fn add_keys<E: Entity>(mut cmd: clap::Command) -> clap::Command {
-    struct UVisitor<'a>(&'a mut clap::Command);
-    impl<'a> EntityPartVisitor for UVisitor<'a> {
+fn add_keys<E: Entity, IC: InterfaceCustomization>(
+    mut cmd: clap::Command,
+    role: ValueRole,
+) -> clap::Command {
+    struct UVisitor<'a, IC: InterfaceCustomization>(
+        &'a mut clap::Command,
+        ValueRole,
+        std::marker::PhantomData<IC>,
+    );
+    impl<'a, IC: InterfaceCustomization> EntityPartVisitor for UVisitor<'a, IC> {
         fn visit<EP: microrm::schema::entity::EntityPart>(&mut self) {
-            let arg = clap::Arg::new(EP::part_name())
-                .required(true)
-                .help(EP::desc());
-            *self.0 = self.0.clone().arg(arg);
+            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(&mut cmd));
+    <E::Keys as EntityPartList>::accept_part_visitor(&mut UVisitor::<IC>(
+        &mut cmd,
+        role,
+        Default::default(),
+    ));
 
     cmd
 }
 
-fn collect_keys<E: Entity>(matches: &clap::ArgMatches) -> Vec<String> {
-    struct UVisitor<'a>(&'a clap::ArgMatches, &'a mut Vec<String>);
-    impl<'a> EntityPartVisitor for UVisitor<'a> {
+fn collect_keys<E: Entity, IC: InterfaceCustomization>(
+    matches: &clap::ArgMatches,
+    role: ValueRole,
+) -> Vec<EntityKey> {
+    struct UVisitor<'a, IC: InterfaceCustomization>(
+        &'a clap::ArgMatches,
+        &'a mut Vec<EntityKey>,
+        ValueRole,
+        std::marker::PhantomData<IC>,
+    );
+    impl<'a, IC: InterfaceCustomization> EntityPartVisitor for UVisitor<'a, IC> {
         fn visit<EP: microrm::schema::entity::EntityPart>(&mut self) {
-            self.1.push(
-                self.0
-                    .get_one::<std::string::String>(EP::part_name())
-                    .unwrap()
-                    .clone(),
-            );
+            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(matches, &mut key_values));
+    <E::Keys as EntityPartList>::accept_part_visitor(&mut UVisitor::<IC>(
+        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: &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<String>,
+        local_keys: Vec<EntityKey>,
         relation: String,
-        remote_keys: Vec<String>,
+        remote_keys: Vec<EntityKey>,
     },
     Create(O::CreateParameters),
-    Delete(Vec<String>),
+    Delete(Vec<EntityKey>),
     Detach {
-        local_keys: Vec<String>,
+        local_keys: Vec<EntityKey>,
         relation: String,
-        remote_keys: Vec<String>,
+        remote_keys: Vec<EntityKey>,
     },
     ListAll,
-    Inspect(Vec<String>),
+    Inspect(Vec<EntityKey>),
     Extra(O::ExtraCommands),
 }
 
@@ -67,22 +118,23 @@ pub enum InterfaceVerb<O: CLIObject> {
 type UniqueList<E> = <<E as Entity>::Keys as EntityPartList>::DatumList;
 
 impl<O: CLIObject> InterfaceVerb<O> {
-    fn parse_attachment(
+    fn parse_attachment<IC: InterfaceCustomization>(
         matches: &clap::ArgMatches,
-    ) -> Result<(Vec<String>, String, Vec<String>), clap::Error> {
-        let local_keys = collect_keys::<O>(matches);
+    ) -> 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> {
+        struct RelationFinder<'l, IC: InterfaceCustomization> {
             subcommand: &'l str,
             submatches: &'l clap::ArgMatches,
-            keys: &'l mut Vec<String>,
+            keys: &'l mut Vec<EntityKey>,
+            _ghost: std::marker::PhantomData<IC>,
         }
-        impl<'l> EntityPartVisitor for RelationFinder<'l> {
+        impl<'l, IC: InterfaceCustomization> EntityPartVisitor for RelationFinder<'l, IC> {
             fn visit<EP: microrm::schema::entity::EntityPart>(&mut self) {
                 if EP::part_name() != self.subcommand {
                     return;
@@ -94,31 +146,34 @@ impl<O: CLIObject> InterfaceVerb<O> {
             }
         }
 
-        impl<'l> EntityVisitor for RelationFinder<'l> {
+        impl<'l, IC: InterfaceCustomization> EntityVisitor for RelationFinder<'l, IC> {
             fn visit<E: Entity>(&mut self) {
                 println!("\trelationfinder visiting entity {}", E::entity_name());
-                *self.keys = collect_keys::<E>(self.submatches);
+                *self.keys = collect_keys::<E, IC>(self.submatches, ValueRole::AttachmentTarget);
             }
         }
 
         let mut remote_keys = vec![];
-        O::accept_part_visitor(&mut RelationFinder {
+        O::accept_part_visitor(&mut RelationFinder::<IC> {
             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> {
+    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(matches)?;
+                let (local_keys, relation, remote_keys) = Self::parse_attachment::<IC>(matches)?;
                 InterfaceVerb::Attach {
                     local_keys,
                     relation,
@@ -128,9 +183,11 @@ impl<O: CLIObject> InterfaceVerb<O> {
             "create" => InterfaceVerb::Create(
                 <O::CreateParameters as clap::FromArgMatches>::from_arg_matches(matches)?,
             ),
-            "delete" => InterfaceVerb::Delete(collect_keys::<O>(matches)),
+            "delete" => {
+                InterfaceVerb::Delete(collect_keys::<O, IC>(matches, ValueRole::BaseTarget))
+            }
             "detach" => {
-                let (local_keys, relation, remote_keys) = Self::parse_attachment(matches)?;
+                let (local_keys, relation, remote_keys) = Self::parse_attachment::<IC>(matches)?;
                 InterfaceVerb::Detach {
                     local_keys,
                     relation,
@@ -138,7 +195,9 @@ impl<O: CLIObject> InterfaceVerb<O> {
                 }
             }
             "list" => InterfaceVerb::ListAll,
-            "inspect" => InterfaceVerb::Inspect(collect_keys::<O>(matches)),
+            "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)?)
@@ -154,7 +213,7 @@ impl<O: CLIObject> InterfaceVerb<O> {
 struct Attacher<'l, Error: CLIError> {
     do_attach: bool,
     relation: &'l str,
-    remote_keys: &'l Vec<String>,
+    remote_keys: Vec<String>,
     err: Option<Error>,
 }
 
@@ -233,16 +292,43 @@ impl<'l, Error: CLIError> DatumDiscriminatorRef for Attacher<'l, Error> {
     }
 }
 
+#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
+pub enum ValueRole {
+    BaseTarget,
+    AttachmentTarget,
+}
+
+pub trait InterfaceCustomization {
+    fn has_value_for(entity: &'static str, field: &'static str, role: ValueRole) -> bool;
+    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)]
-pub struct ClapInterface<O: CLIObject> {
+/// Type implementing `Subcommand` for a given CLIObject and specialization.
+///
+///
+pub struct ClapInterface<O: CLIObject, IC: InterfaceCustomization> {
     verb: InterfaceVerb<O>,
-    _ghost: std::marker::PhantomData<O>,
+    _ghost: std::marker::PhantomData<IC>,
 }
 
-impl<O: CLIObject> ClapInterface<O> {
+impl<O: CLIObject, IC: InterfaceCustomization> ClapInterface<O, IC> {
+    /// Execute the action that this ClapInterface instance describes. Note that if `Spec` is
+    /// chosen as [`EmptyList`](../schema/entity/struct.EmptyList.html), the value passed for the
+    /// `spec` parameter should be the unit type, `()`.
     pub fn perform(
         &self,
-        data: &O::ExtraCommandData,
+        data: &O::CommandData,
+        ic: &IC,
         query_ctx: impl microrm::prelude::Queryable<EntityOutput = O>,
         insert_ctx: &impl microrm::prelude::Insertable<O>,
     ) -> Result<(), O::Error> {
@@ -252,6 +338,8 @@ impl<O: CLIObject> ClapInterface<O> {
                 relation,
                 remote_keys,
             } => {
+                let local_keys = EntityKey::to_string_vec(local_keys, ic);
+                let remote_keys = EntityKey::to_string_vec(remote_keys, ic);
                 let outer_obj = query_ctx
                     .keyed(
                         UniqueList::<O>::build_equivalent(local_keys.iter().map(String::as_str))
@@ -283,9 +371,10 @@ impl<O: CLIObject> ClapInterface<O> {
                 }
             }
             InterfaceVerb::Create(params) => {
-                insert_ctx.insert(O::create_from_params(params)?)?;
+                insert_ctx.insert(O::create_from_params(data, params)?)?;
             }
             InterfaceVerb::Delete(keys) => {
+                let keys = EntityKey::to_string_vec(keys, ic);
                 query_ctx
                     .keyed(
                         UniqueList::<O>::build_equivalent(keys.iter().map(String::as_str)).unwrap(),
@@ -297,6 +386,8 @@ impl<O: CLIObject> ClapInterface<O> {
                 relation,
                 remote_keys,
             } => {
+                let local_keys = EntityKey::to_string_vec(local_keys, ic);
+                let remote_keys = EntityKey::to_string_vec(remote_keys, ic);
                 let outer_obj = query_ctx
                     .keyed(
                         UniqueList::<O>::build_equivalent(local_keys.iter().map(String::as_str))
@@ -338,6 +429,7 @@ impl<O: CLIObject> ClapInterface<O> {
                 }
             }
             InterfaceVerb::Inspect(keys) => {
+                let keys = EntityKey::to_string_vec(keys, ic);
                 let obj = query_ctx
                     .keyed(
                         UniqueList::<O>::build_equivalent(keys.iter().map(String::as_str)).unwrap(),
@@ -413,12 +505,19 @@ impl<O: CLIObject> ClapInterface<O> {
     fn make_relation_subcommands() -> impl Iterator<Item = clap::Command> {
         let mut out = vec![];
 
-        struct PartVisitor<'l>(&'l mut Vec<clap::Command>);
-        impl<'l> EntityPartVisitor for PartVisitor<'l> {
+        struct PartVisitor<'l, IC: InterfaceCustomization>(
+            &'l mut Vec<clap::Command>,
+            std::marker::PhantomData<IC>,
+        );
+        impl<'l, IC: InterfaceCustomization> EntityPartVisitor for PartVisitor<'l, IC> {
             fn visit<EP: microrm::schema::entity::EntityPart>(&mut self) {
-                struct Discriminator<'l>(&'l mut Vec<clap::Command>, &'static str);
+                struct Discriminator<'l, IC: InterfaceCustomization>(
+                    &'l mut Vec<clap::Command>,
+                    &'static str,
+                    std::marker::PhantomData<IC>,
+                );
 
-                impl<'l> DatumDiscriminator for Discriminator<'l> {
+                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,
@@ -426,34 +525,42 @@ impl<O: CLIObject> ClapInterface<O> {
                     }
                     fn visit_bare_field<T: Datum>(&mut self) {}
                     fn visit_assoc_map<E: Entity>(&mut self) {
-                        self.0.push(add_keys::<E>(clap::Command::new(self.1)));
+                        self.0.push(add_keys::<E, IC>(
+                            clap::Command::new(self.1),
+                            ValueRole::AttachmentTarget,
+                        ));
                     }
                     fn visit_assoc_domain<R: microrm::schema::Relation>(&mut self) {
-                        self.0
-                            .push(add_keys::<R::Range>(clap::Command::new(self.1)));
+                        self.0.push(add_keys::<R::Range, IC>(
+                            clap::Command::new(self.1),
+                            ValueRole::AttachmentTarget,
+                        ));
                     }
                     fn visit_assoc_range<R: microrm::schema::Relation>(&mut self) {
-                        self.0
-                            .push(add_keys::<R::Domain>(clap::Command::new(self.1)));
+                        self.0.push(add_keys::<R::Domain, IC>(
+                            clap::Command::new(self.1),
+                            ValueRole::AttachmentTarget,
+                        ));
                     }
                 }
 
-                <EP::Datum as Datum>::accept_discriminator(&mut Discriminator(
+                <EP::Datum as Datum>::accept_discriminator(&mut Discriminator::<IC>(
                     self.0,
                     EP::part_name(),
+                    Default::default(),
                 ));
             }
         }
 
-        O::accept_part_visitor(&mut PartVisitor(&mut out));
+        O::accept_part_visitor(&mut PartVisitor::<IC>(&mut out, Default::default()));
 
         out.into_iter()
     }
 }
 
-impl<O: CLIObject> FromArgMatches for ClapInterface<O> {
+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(matches);
+        let verb = InterfaceVerb::from_matches::<IC>(matches);
 
         Ok(Self {
             verb: verb?,
@@ -466,7 +573,7 @@ impl<O: CLIObject> FromArgMatches for ClapInterface<O> {
     }
 }
 
-impl<O: CLIObject> Subcommand for ClapInterface<O> {
+impl<O: CLIObject, IC: InterfaceCustomization> Subcommand for ClapInterface<O, IC> {
     fn has_subcommand(_name: &str) -> bool {
         todo!()
     }
@@ -474,18 +581,24 @@ impl<O: CLIObject> Subcommand for ClapInterface<O> {
     fn augment_subcommands(cmd: clap::Command) -> clap::Command {
         let cmd = cmd
             .subcommand(
-                add_keys::<O>(clap::Command::new("attach"))
+                add_keys::<O, IC>(clap::Command::new("attach"), ValueRole::BaseTarget)
                     .subcommands(Self::make_relation_subcommands())
                     .subcommand_required(true),
             )
             .subcommand(
-                add_keys::<O>(clap::Command::new("detach"))
+                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>(clap::Command::new("delete")))
-            .subcommand(add_keys::<O>(clap::Command::new("inspect")))
+            .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)

+ 23 - 0
microrm/src/lib.rs

@@ -1,3 +1,26 @@
+//! microrm is a simple object relational manager (ORM) for sqlite.
+//!
+//! Unlike many fancier ORM systems, microrm is designed to be lightweight, both in terms of
+//! runtime overhead and developer LoC. By necessity, it sacrifices flexibility towards these
+//! goals, and so can be thought of as more opinionated than, say,
+//! [SeaORM](https://www.sea-ql.org/SeaORM/) or [Diesel](https://diesel.rs/). Major limitations of
+//! microrm are:
+//! - lack of database migration support
+//! - limited vocabulary for describing object-to-object relations
+//!
+//! There are three externally-facing components in microrm:
+//! - Object modelling (via the [`Datum`](schema/datum/trait.Datum.html) and
+//! [`Entity`](schema/entity/trait.Entity.html) traits)
+//! - Database querying (via [`Queryable`](prelude/trait.Queryable.html),
+//! [`AssocInterface`](prelude/trait.AssocInterface.html) and
+//! [`Insertable`](prelude/trait.Insertable.html) traits)
+//! - Command-line interface generation via the [`clap`](https://docs.rs/clap/latest/clap/) crate
+//! (see [`cli::CLIObject`] and [`cli::ClapInterface`]; requires the optional crate feature `clap`)
+//!
+//! microrm pushes the Rust type system somewhat to provide better ergonomics, so the MSRV is
+//! currently 1.75. Don't be scared off by the web of traits in the `schema` module --- you should
+//! never need to interact with any of them!
+
 // to make the proc_macros work inside the microrm crate; needed for tests and the metaschema.
 extern crate self as microrm;
 

+ 15 - 6
microrm/src/schema/datum.rs

@@ -49,7 +49,8 @@ pub trait Datum: Clone + std::fmt::Debug {
     }
 }
 
-/// marker trait for 'concrete' types, i.e. those with a static lifetime.
+/// Marker trait for 'concrete' datums, i.e. those with a static lifetime. This separation is used
+/// to ensure [`QueryEquivalent`] can be implemented in the current Rust type system.
 pub trait ConcreteDatum: 'static + Datum {}
 
 /// Visitor for allowing for type-specific behaviour
@@ -93,10 +94,18 @@ pub trait DatumVisitor {
 /// A forward-edge mapping between datums where one can stand in for the other during queries.
 ///
 /// This is purely a marker trait, since the equivalence is based on how sqlite handles things.
-/// Because this is a forward-edge mapping, if type X implements QueryEquivalent<Y>, it means X can
-/// stand in for Y. For example, the following should be true:
-/// - for datum T, &T should implement QueryEquivalent<T>
-/// - for datum T, StringQuery should implement QueryEquivalent<T>
+/// Because this is a forward-edge mapping, if type `X` implements `QueryEquivalent<Y>`, it means `X` can
+/// stand in for `Y` during a query. For example, the following should be true:
+/// 1. for datum `T`, `&T` should implement `QueryEquivalent<T>`
+///     - rationale: you should be able to query for something matching a value without passing an
+///     owned value
+/// 2. for datum `T`, `StringQuery` should implement `QueryEquivalent<T>`
+///     - rationale: `StringQuery` explicitly shoves off equality checking to sqlite, which means
+///     you should be able to use it in any query place.
+///
+/// Note that this is abstracted across [`ConcreteDatum`] and not [`Datum`]; this is to avoid
+/// issues with [`StringQuery`] -- which is not concrete -- having two implementations of
+/// `QueryEquivalent<StringQuery>`: one from the reflexive implementation and one from (2) above.
 pub trait QueryEquivalent<T: ConcreteDatum>: Datum {}
 
 // Every type is query-equivalent to itself.
@@ -108,5 +117,5 @@ impl<'l, T: ConcreteDatum> QueryEquivalent<T> for &'l T {}
 #[derive(Clone, Debug)]
 pub struct StringQuery<'l>(pub &'l str);
 
-/// A list version of `QueryEquivalent`.
+/// A list version of [`QueryEquivalent`].
 pub trait QueryEquivalentList<T: DatumList>: DatumList {}