Browse Source

Import CLI autogeneration work from uidc.

Kestrel 7 tháng trước cách đây
mục cha
commit
82bebba997
4 tập tin đã thay đổi với 716 bổ sung0 xóa
  1. 7 0
      microrm/Cargo.toml
  2. 247 0
      microrm/src/cli.rs
  3. 458 0
      microrm/src/cli/clap_interface.rs
  4. 4 0
      microrm/src/lib.rs

+ 7 - 0
microrm/Cargo.toml

@@ -9,6 +9,9 @@ description = "Lightweight ORM using sqlite as a backend."
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
+[features]
+clap = ["dep:clap"]
+
 [dependencies]
 base64 = "0.13"
 sha2 = "0.10"
@@ -23,8 +26,12 @@ log = "0.4.17"
 
 topological-sort = { version = "0.2" }
 
+clap = { version = "4", optional = true }
+
 [dev-dependencies]
 test-log = "0.2.15"
+
+clap = { version = "4", features = ["derive"] }
 # criterion = "0.5"
 # rand = "0.8.5"
 # stats_alloc = "0.1.10"

+ 247 - 0
microrm/src/cli.rs

@@ -0,0 +1,247 @@
+use crate::{
+    prelude::{Insertable, Queryable},
+    schema::entity::Entity,
+    Error,
+};
+
+mod clap_interface;
+
+pub use clap_interface::ClapInterface;
+
+pub trait CLIError: From<Error> {
+    fn no_such_entity() -> Self;
+}
+
+impl CLIError for Error {
+    fn no_such_entity() -> Self {
+        Error::EmptyResult
+    }
+}
+
+pub trait CLIObject: Sized + Entity + std::fmt::Debug {
+    type Error: CLIError;
+    type CreateParameters: clap::Parser + std::fmt::Debug;
+    fn create_from_params(_: &Self::CreateParameters) -> Result<Self, Self::Error>;
+
+    type ExtraCommands: clap::Subcommand + std::fmt::Debug;
+    type ExtraCommandData;
+    fn run_extra_command(
+        _data: &Self::ExtraCommandData,
+        _extra: &Self::ExtraCommands,
+        _query_ctx: impl Queryable<EntityOutput = Self>,
+        _insert_ctx: &impl Insertable<Self>,
+    ) -> Result<(), Self::Error> {
+        unreachable!()
+    }
+
+    fn shortname(&self) -> &str;
+}
+
+#[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
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{CLIObject, ClapInterface};
+    use clap::Parser;
+    use microrm::prelude::*;
+    use test_log::test;
+
+    #[derive(Entity)]
+    struct KVEntry {
+        #[key]
+        name: String,
+        value: String,
+    }
+
+    #[derive(Entity)]
+    struct Node {
+        parent_id: Option<NodeID>,
+        #[key]
+        node_name: String,
+        node_value: Option<String>,
+
+        children: AssocMap<Node>,
+    }
+
+    #[derive(Database)]
+    struct KVStore {
+        entries: IDMap<KVEntry>,
+        nodes: IDMap<Node>,
+    }
+
+    #[derive(clap::Parser, Debug)]
+    struct KVCreate {
+        key: String,
+        value: String,
+    }
+
+    impl CLIObject for KVEntry {
+        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 ExtraCommands = super::EmptyCommand;
+        type ExtraCommandData = ();
+
+        fn shortname(&self) -> &str {
+            "kventry"
+        }
+    }
+
+    #[derive(clap::Parser, Debug)]
+    struct NodeCreate {
+        name: String,
+    }
+
+    impl CLIObject for Node {
+        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 ExtraCommands = super::EmptyCommand;
+        type ExtraCommandData = ();
+
+        fn shortname(&self) -> &str {
+            "node"
+        }
+    }
+
+    #[derive(Debug, clap::Parser)]
+    enum Params {
+        KV {
+            #[clap(subcommand)]
+            cmd: ClapInterface<KVEntry>,
+        },
+        Node {
+            #[clap(subcommand)]
+            cmd: ClapInterface<Node>,
+        },
+    }
+
+    fn run_cmd(db: &KVStore, args: &[&str]) {
+        match <Params as Parser>::try_parse_from(args) {
+            Ok(Params::KV { cmd }) => {
+                cmd.perform(&(), &db.entries, &db.entries)
+                    .expect("couldn't perform command");
+            }
+            Ok(Params::Node { cmd }) => {
+                cmd.perform(&(), &db.nodes, &db.nodes)
+                    .expect("couldn't perform command");
+            }
+            Err(e) => {
+                println!("{}", e.render());
+                panic!("error parsing arguments")
+            }
+        }
+    }
+
+    #[test]
+    fn simple_entity_create() {
+        let db = KVStore::open_path(":memory:").unwrap();
+
+        assert_eq!(
+            db.entries
+                .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, &["execname", "kv", "create", "a_key", "a_value"]);
+        assert_eq!(
+            db.entries
+                .keyed("a_key")
+                .count()
+                .expect("couldn't count entries"),
+            1
+        );
+        run_cmd(&db, &["execname", "kv", "delete", "a_key"]);
+        assert_eq!(
+            db.entries
+                .keyed("a_key")
+                .count()
+                .expect("couldn't count entries"),
+            0
+        );
+    }
+
+    #[test]
+    fn create_and_attach_node() {
+        let db = KVStore::open_path(":memory:").unwrap();
+
+        run_cmd(&db, &["execname", "node", "create", "aname"]);
+        run_cmd(&db, &["execname", "node", "create", "bname"]);
+        run_cmd(
+            &db,
+            &["execname", "node", "attach", "aname", "children", "bname"],
+        );
+
+        assert_eq!(
+            db.nodes
+                .keyed("aname")
+                .get()
+                .ok()
+                .flatten()
+                .expect("couldn't get aname")
+                .children
+                .count()
+                .expect("couldn't get children of aname"),
+            1
+        );
+    }
+}

+ 458 - 0
microrm/src/cli/clap_interface.rs

@@ -0,0 +1,458 @@
+use microrm::{
+    prelude::*,
+    schema::{
+        datum::{ConcreteDatumList, Datum, DatumDiscriminator, DatumDiscriminatorRef},
+        entity::{Entity, EntityID, EntityPartList, EntityPartVisitor, EntityVisitor},
+    },
+};
+
+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 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);
+        }
+    }
+
+    <E::Keys as EntityPartList>::accept_part_visitor(&mut UVisitor(&mut cmd));
+
+    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 visit<EP: microrm::schema::entity::EntityPart>(&mut self) {
+            self.1.push(
+                self.0
+                    .get_one::<std::string::String>(EP::part_name())
+                    .unwrap()
+                    .clone(),
+            );
+        }
+    }
+
+    let mut key_values = vec![];
+    <E::Keys as EntityPartList>::accept_part_visitor(&mut UVisitor(matches, &mut key_values));
+    key_values
+}
+
+#[derive(Clone, Debug)]
+pub enum InterfaceVerb<O: CLIObject> {
+    Attach {
+        local_keys: Vec<String>,
+        relation: String,
+        remote_keys: Vec<String>,
+    },
+    Create(O::CreateParameters),
+    Delete(Vec<String>),
+    Detach {
+        local_keys: Vec<String>,
+        relation: String,
+        remote_keys: Vec<String>,
+    },
+    ListAll,
+    Inspect(Vec<String>),
+    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(
+        matches: &clap::ArgMatches,
+    ) -> Result<(Vec<String>, String, Vec<String>), clap::Error> {
+        let local_keys = collect_keys::<O>(matches);
+
+        let (subcommand, submatches) = matches
+            .subcommand()
+            .ok_or(clap::Error::new(clap::error::ErrorKind::MissingSubcommand))?;
+
+        // find the relevant relation
+        struct RelationFinder<'l> {
+            subcommand: &'l str,
+            submatches: &'l clap::ArgMatches,
+            keys: &'l mut Vec<String>,
+        }
+        impl<'l> EntityPartVisitor for RelationFinder<'l> {
+            fn visit<EP: microrm::schema::entity::EntityPart>(&mut self) {
+                if EP::part_name() != self.subcommand {
+                    return;
+                }
+
+                println!("EP: {}", std::any::type_name::<EP>());
+                // println!("EP: {}", std::any::
+                EP::Datum::accept_entity_visitor(self);
+            }
+        }
+
+        impl<'l> EntityVisitor for RelationFinder<'l> {
+            fn visit<E: Entity>(&mut self) {
+                println!("\trelationfinder visiting entity {}", E::entity_name());
+                *self.keys = collect_keys::<E>(self.submatches);
+            }
+        }
+
+        let mut remote_keys = vec![];
+        O::accept_part_visitor(&mut RelationFinder {
+            subcommand,
+            submatches,
+            keys: &mut remote_keys,
+        });
+
+        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)?;
+                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>(matches)),
+            "detach" => {
+                let (local_keys, relation, remote_keys) = Self::parse_attachment(matches)?;
+                InterfaceVerb::Detach {
+                    local_keys,
+                    relation,
+                    remote_keys,
+                }
+            }
+            "list" => InterfaceVerb::ListAll,
+            "inspect" => InterfaceVerb::Inspect(collect_keys::<O>(matches)),
+            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> {
+    do_attach: bool,
+    relation: &'l str,
+    remote_keys: &'l Vec<String>,
+    err: Option<Error>,
+}
+
+impl<'l, Error: CLIError> Attacher<'l, Error> {
+    fn do_operation<E: Entity>(&mut self, map: &impl AssocInterface<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());
+            }
+            Err(e) => {
+                self.err = Some(e.into());
+            }
+        }
+    }
+}
+
+impl<'l, Error: CLIError> EntityPartVisitor for Attacher<'l, Error> {
+    fn visit_datum<EP: microrm::schema::entity::EntityPart>(&mut self, datum: &EP::Datum) {
+        if EP::part_name() != self.relation {
+            return;
+        }
+
+        datum.accept_discriminator_ref(self);
+    }
+}
+
+impl<'l, Error: CLIError> DatumDiscriminatorRef for Attacher<'l, Error> {
+    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_assoc_map<E: Entity>(&mut self, map: &AssocMap<E>) {
+        self.do_operation(map);
+    }
+    fn visit_assoc_range<R: microrm::schema::Relation>(
+        &mut self,
+        map: &microrm::schema::AssocRange<R>,
+    ) {
+        self.do_operation(map);
+    }
+    fn visit_assoc_domain<R: microrm::schema::Relation>(
+        &mut self,
+        map: &microrm::schema::AssocDomain<R>,
+    ) {
+        self.do_operation(map);
+    }
+}
+
+#[derive(Debug)]
+pub struct ClapInterface<O: CLIObject> {
+    verb: InterfaceVerb<O>,
+    _ghost: std::marker::PhantomData<O>,
+}
+
+impl<O: CLIObject> ClapInterface<O> {
+    pub fn perform(
+        &self,
+        data: &O::ExtraCommandData,
+        query_ctx: impl microrm::prelude::Queryable<EntityOutput = O>,
+        insert_ctx: &impl microrm::prelude::Insertable<O>,
+    ) -> Result<(), O::Error> {
+        match &self.verb {
+            InterfaceVerb::Attach {
+                local_keys,
+                relation,
+                remote_keys,
+            } => {
+                let outer_obj = query_ctx
+                    .keyed(
+                        UniqueList::<O>::build_equivalent(local_keys.iter().map(String::as_str))
+                            .unwrap(),
+                    )
+                    .get()?
+                    .ok_or(<O::Error>::no_such_entity())?;
+
+                let mut attacher = Attacher {
+                    do_attach: true,
+                    relation,
+                    remote_keys,
+                    err: None,
+                };
+                outer_obj.accept_part_visitor_ref(&mut attacher);
+
+                if let Some(err) = attacher.err {
+                    return Err(err);
+                }
+            }
+            InterfaceVerb::Create(params) => {
+                insert_ctx.insert(O::create_from_params(params)?)?;
+            }
+            InterfaceVerb::Delete(keys) => {
+                query_ctx
+                    .keyed(
+                        UniqueList::<O>::build_equivalent(keys.iter().map(String::as_str)).unwrap(),
+                    )
+                    .delete()?;
+            }
+            InterfaceVerb::Detach {
+                local_keys,
+                relation,
+                remote_keys,
+            } => {
+                let outer_obj = query_ctx
+                    .keyed(
+                        UniqueList::<O>::build_equivalent(local_keys.iter().map(String::as_str))
+                            .unwrap(),
+                    )
+                    .get()?
+                    .ok_or(<O::Error>::no_such_entity())?;
+
+                let mut attacher = Attacher {
+                    do_attach: false,
+                    relation,
+                    remote_keys,
+                    err: None,
+                };
+                outer_obj.accept_part_visitor_ref(&mut attacher);
+
+                if let Some(err) = attacher.err {
+                    return Err(err);
+                }
+            }
+            InterfaceVerb::ListAll => {
+                println!(
+                    "Listing all {}(s): ({})",
+                    O::entity_name(),
+                    query_ctx.clone().count()?
+                );
+                for obj in query_ctx.get()?.into_iter() {
+                    println!(" - {}", obj.shortname());
+                }
+            }
+            InterfaceVerb::Inspect(keys) => {
+                let obj = query_ctx
+                    .keyed(
+                        UniqueList::<O>::build_equivalent(keys.iter().map(String::as_str)).unwrap(),
+                    )
+                    .get()?
+                    .ok_or(<O::Error>::no_such_entity())?;
+                println!("{:#?}", obj.as_ref());
+
+                fn inspect_ai<AI: AssocInterface>(name: &'static str, ai: &AI) {
+                    println!("{}: ({})", name, ai.count().unwrap());
+                    for a in ai.get().expect("couldn't get object associations") {
+                        println!("[#{:3}]: {:?}", a.id().into_raw(), a.wrapped());
+                    }
+                }
+
+                struct AssocFieldWalker;
+                impl EntityPartVisitor for AssocFieldWalker {
+                    fn visit_datum<EP: microrm::schema::entity::EntityPart>(
+                        &mut self,
+                        datum: &EP::Datum,
+                    ) {
+                        struct Discriminator<'l, D: Datum>(&'l D, &'static str);
+
+                        impl<'l, D: Datum> DatumDiscriminatorRef for Discriminator<'l, D> {
+                            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_assoc_map<E: Entity>(&mut self, amap: &AssocMap<E>) {
+                                inspect_ai(self.1, amap);
+                            }
+                            fn visit_assoc_domain<R: microrm::schema::Relation>(
+                                &mut self,
+                                adomain: &microrm::schema::AssocDomain<R>,
+                            ) {
+                                inspect_ai(self.1, adomain);
+                            }
+                            fn visit_assoc_range<R: microrm::schema::Relation>(
+                                &mut self,
+                                arange: &microrm::schema::AssocRange<R>,
+                            ) {
+                                inspect_ai(self.1, arange);
+                            }
+                        }
+
+                        datum.accept_discriminator_ref(&mut Discriminator(datum, EP::part_name()));
+                    }
+                }
+
+                obj.accept_part_visitor_ref(&mut AssocFieldWalker);
+            }
+            InterfaceVerb::Extra(extra) => {
+                O::run_extra_command(data, extra, query_ctx, insert_ctx)?;
+            }
+        }
+        Ok(())
+    }
+
+    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> {
+            fn visit<EP: microrm::schema::entity::EntityPart>(&mut self) {
+                struct Discriminator<'l>(&'l mut Vec<clap::Command>, &'static str);
+
+                impl<'l> DatumDiscriminator for Discriminator<'l> {
+                    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_assoc_map<E: Entity>(&mut self) {
+                        self.0.push(add_keys::<E>(clap::Command::new(self.1)));
+                    }
+                    fn visit_assoc_domain<R: microrm::schema::Relation>(&mut self) {
+                        self.0
+                            .push(add_keys::<R::Range>(clap::Command::new(self.1)));
+                    }
+                    fn visit_assoc_range<R: microrm::schema::Relation>(&mut self) {
+                        self.0
+                            .push(add_keys::<R::Domain>(clap::Command::new(self.1)));
+                    }
+                }
+
+                <EP::Datum as Datum>::accept_discriminator(&mut Discriminator(
+                    self.0,
+                    EP::part_name(),
+                ));
+            }
+        }
+
+        O::accept_part_visitor(&mut PartVisitor(&mut out));
+
+        out.into_iter()
+    }
+}
+
+impl<O: CLIObject> FromArgMatches for ClapInterface<O> {
+    fn from_arg_matches(matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
+        let verb = InterfaceVerb::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<O: CLIObject> Subcommand for ClapInterface<O> {
+    fn has_subcommand(_name: &str) -> bool {
+        todo!()
+    }
+
+    fn augment_subcommands(cmd: clap::Command) -> clap::Command {
+        let cmd = cmd
+            .subcommand(
+                add_keys::<O>(clap::Command::new("attach"))
+                    .subcommands(Self::make_relation_subcommands())
+                    .subcommand_required(true),
+            )
+            .subcommand(
+                add_keys::<O>(clap::Command::new("detach"))
+                    .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(clap::Command::new("list"));
+
+        <O::ExtraCommands>::augment_subcommands(cmd)
+    }
+
+    fn augment_subcommands_for_update(_cmd: clap::Command) -> clap::Command {
+        todo!()
+    }
+}

+ 4 - 0
microrm/src/lib.rs

@@ -5,6 +5,9 @@ pub mod db;
 mod query;
 pub mod schema;
 
+#[cfg(feature = "clap")]
+pub mod cli;
+
 pub mod prelude {
     pub use crate::query::{AssocInterface, Insertable, Queryable};
     // pub use crate::schema::entity::Entity;
@@ -24,6 +27,7 @@ pub enum Error {
     InternalError(&'static str),
     EncodingError(std::str::Utf8Error),
     LogicError(&'static str),
+    ConstraintViolation(&'static str),
     Sqlite {
         code: i32,
         msg: String,