|
@@ -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: µrm::schema::AssocRange<R>,
|
|
|
+ ) {
|
|
|
+ self.do_operation(map);
|
|
|
+ }
|
|
|
+ fn visit_assoc_domain<R: microrm::schema::Relation>(
|
|
|
+ &mut self,
|
|
|
+ map: µrm::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: µrm::schema::AssocDomain<R>,
|
|
|
+ ) {
|
|
|
+ inspect_ai(self.1, adomain);
|
|
|
+ }
|
|
|
+ fn visit_assoc_range<R: microrm::schema::Relation>(
|
|
|
+ &mut self,
|
|
|
+ arange: µrm::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!()
|
|
|
+ }
|
|
|
+}
|