From: Ian Jackson Date: Wed, 2 Jun 2021 22:56:05 +0000 (+0100) Subject: Break out clisupport.rs X-Git-Tag: otter-0.7.0~86 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?a=commitdiff_plain;h=4796321d2d8a9b036d3a6e3a0effe5c77efb1af5;p=otter.git Break out clisupport.rs Signed-off-by: Ian Jackson --- diff --git a/cli/clisupport.rs b/cli/clisupport.rs new file mode 100644 index 00000000..ffb7151c --- /dev/null +++ b/cli/clisupport.rs @@ -0,0 +1,429 @@ +// Copyright 2020-2021 Ian Jackson and contributors to Otter +// SPDX-License-Identifier: AGPL-3.0-or-later +// There is NO WARRANTY. + +use super::*; + +#[derive(Clone)] +pub struct MapStore Result>(pub F); + +pub struct BoundMapStore<'r, T, F: FnMut(&str) -> Result> { + f: Rc>, + r: Rc>, +} + +impl<'f,T,F> TypedAction for MapStore +where F: 'f + Clone + FnMut(&str) -> Result, + 'f: 'static // ideally TypedAction wuld have a lifetime parameter +{ + fn bind<'x>(&self, r: Rc>) -> Action<'x> { + Action::Single(Box::new(BoundMapStore { + f: Rc::new(RefCell::new(self.0.clone())), + r, + })) + } +} + +impl<'x, T, F: FnMut(&str) -> Result> + IArgAction for BoundMapStore<'x, T, F> +{ + fn parse_arg(&self, arg: &str) -> ParseResult { + let v: T = match self.f.borrow_mut()(arg) { + Ok(r) => r, + Err(e) => return ParseResult::Error(e), + }; + **self.r.borrow_mut() = v; + ParseResult::Parsed + } +} + +#[derive(Error,Debug,Clone,Display)] +pub struct ArgumentParseError(pub String); + +impl From<&anyhow::Error> for ArgumentParseError { + fn from(ae: &anyhow::Error) -> ArgumentParseError { + eprintln!("error during argument parsing/startup: {}\n", ae); + exit(EXIT_USAGE); + } +} + +pub type ApMaker<'apm, T> = + &'apm dyn for <'a> Fn(&'a mut T) -> ArgumentParser<'a>; + +pub fn parse_args( + args: Vec, + apmaker: ApMaker, + completer: &dyn Fn(T) -> Result, + extra_help: Option<&dyn Fn(&mut dyn Write) -> Result<(), io::Error>>, +) -> U { + let mut parsed = default(); + let ap = apmaker(&mut parsed); + let us = args.get(0).expect("argv[0] must be provided!").clone(); + + let mut stdout = CookedStdout::new(); + let mut stderr = io::stderr(); + + let r = ap.parse(args, &mut stdout, &mut stderr); + if let Err(rc) = r { + exit(match rc { + 0 => { + if let Some(eh) = extra_help { + eh(&mut stdout).unwrap(); + } + 0 + }, + 2 => EXIT_USAGE, + _ => panic!("unexpected error rc {} from ArgumentParser::parse", rc), + }); + } + mem::drop(stdout); + mem::drop(ap); + let completed = completer(parsed) + .unwrap_or_else(|e:ArgumentParseError| { + let mut def = default(); + let ap = apmaker(&mut def); + ap.error(&us, &e.0, &mut stderr); + exit(EXIT_USAGE); + }); + completed +} + +pub fn ok_id(t: T) -> Result { Ok(t) } + +pub fn clone_via_serde(t: &T) -> T { + (|| { + let s = serde_json::to_string(t).context("ser")?; + let c = serde_json::from_str(&s).context("de")?; + Ok::<_,AE>(c) + })() + .with_context(|| format!("clone {:?} via serde failed", t)) + .unwrap() +} + +#[derive(Debug)] +pub struct AccessOpt(Box); +impl Clone for AccessOpt { + fn clone(&self) -> Self { Self(clone_via_serde(&self.0)) } +} +impl From for AccessOpt { + fn from(t: T) -> Self { AccessOpt(Box::new(t)) } +} +impl From for Box { + fn from(a: AccessOpt) -> Self { a.0 } +} + +pub type ExecutableRelatedError = AE; +fn ere(s: String) -> ExecutableRelatedError { anyhow!(s) } + +#[throws(ExecutableRelatedError)] +pub fn find_executable() -> String { + let e = env::current_exe() + .map_err(|e| ere( + format!("could not find current executable ({})", &e) + ))?; + let s = e.to_str() + .ok_or_else(|| ere( + format!("current executable has non-UTF8 filename!") + ))?; + s.into() +} + +pub fn in_basedir(verbose: bool, + from: Result, + from_what: &str, + from_exp_in: &str, from_must_be_in_exp: bool, + now_what: &str, + then_in: &str, + leaf: &str, + local_subdir: &str) + -> String +{ + match (||{ + let from = from?; + if from_must_be_in_exp { + let mut comps = from.rsplitn(3,'/').skip(1); + if_chain! { + if Some(from_exp_in) == comps.next(); + if let Some(path) = comps.next(); + then { Ok(path.to_string()) } + else { Err(ere( + format!("{} is not in a directory called {}", from_what, from_exp_in) + )) } + } + } else { + let mut comps = from.rsplitn(2,'/'); + if_chain! { + if let Some(dirname) = comps.nth(1); + let mut dir_comps = dirname.rsplitn(2,'/'); + then { + if_chain! { + if Some(from_exp_in) == dir_comps.next(); + if let Some(above) = dir_comps.next(); + then { Ok(above.to_string()) } + else { Ok(dirname.to_string()) } + } + } + else { + Ok(from.to_string()) + } + } + } + })() { + Err(whynot) => { + let r = format!("{}/{}", local_subdir, leaf); + if verbose { + eprintln!("{}: looking for {} in {}", &whynot, now_what, &r); + } + r + } + Ok(basedir) => { + format!("{}/{}/{}", basedir, then_in, leaf) + } + } +} + +// argparse is pretty insistent about references and they are awkward +#[ext(pub)] +impl String { + fn leak(self) -> &'static str { Box::::leak(self.into()) } +} + +pub struct Conn { + pub chan: ClientMgmtChannel, +} + +deref_to_field_mut!{Conn, MgmtChannel, chan} + +impl Conn { + #[throws(AE)] + pub fn prep_access_account(&mut self, ma: &MainOpts, + maybe_update_account: bool) { + #[derive(Debug)] + struct Wantup(bool); + impl Wantup { + fn u(&mut self, rhs: &Option) -> Option { + if rhs.is_some() { self.0 = true } + rhs.clone() + } + } + let mut wantup = Wantup(false); + + let mut ad = if maybe_update_account { AccountDetails { + account: ma.account.clone(), + nick: wantup.u(&ma.nick), + timezone: wantup.u(&ma.timezone), + layout: wantup.u(&ma.layout), + access: wantup.u(&ma.access).map(Into::into), + } } else { + AccountDetails::default(ma.account.clone()) + }; + + fn is_no_account(r: &Result) -> bool { + if_chain! { + if let Err(e) = r; + if let Some(&ME::AccountNotFound(_)) = e.downcast_ref(); + then { return true } + else { return false } + } + } + + { + let mut desc; + let mut resp; + if wantup.0 { + desc = "UpdateAccount"; + resp = self.cmd(&MC::UpdateAccount(clone_via_serde(&ad))); + } else { + desc = "CheckAccount"; + resp = self.cmd(&MC::CheckAccount); + }; + if is_no_account(&resp) { + ad.access.get_or_insert(Box::new(UrlOnStdout)); + desc = "CreateAccount"; + resp = self.cmd(&MC::CreateAccount(clone_via_serde(&ad))); + } + resp.with_context(||format!("response to {}", &desc))?; + } + } +} + +#[throws(E)] +pub fn connect_chan(ma: &MainOpts) -> MgmtChannel { + match &ma.server { + + SL::Socket(socket) => { + MgmtChannel::connect(socket)? + }, + + SL::Ssh(user_host) => { + + let user_host = { + let (user,host) = + user_host.split_once('@') + .unwrap_or_else(|| ("Otter", user_host)); + format!("{}@{}", user, host) + }; + + let mut cmd = Command::new("sh"); + cmd.arg(if ma.verbose > 2 { "-xec" } else { "-ec" }); + cmd.arg(format!(r#"exec {} "$@""#, &ma.ssh_command)); + cmd.arg("x"); + let args = [ + &user_host, + &ma.ssh_proxy_command, + ]; + cmd.args(args); + + let desc = format!("ssh: {:?} {:?}", &ma.ssh_command, &args); + + let (w,r) = childio::run_pair(cmd, desc.clone()) + .with_context(|| desc.clone()) + .context("run remote command")?; + MgmtChannel::new_boxed(r,w) + }, + + } +} + +#[throws(E)] +pub fn connect(ma: &MainOpts) -> Conn { + let chan = connect_chan(ma)?; + let mut chan = Conn { chan }; + if ma.superuser { + chan.cmd(&MC::SetSuperuser(true))?; + } + if ! ma.sc.props.suppress_selectaccount { + chan.cmd(&MC::SelectAccount(ma.account.clone()))?; + } + chan +} + +pub const PLAYER_ALWAYS_PERMS: &[TablePermission] = &[ + TP::TestExistence, + TP::ShowInList, + TP::ViewNotSecret, + TP::Play, +]; + +pub const PLAYER_DEFAULT_PERMS: &[TablePermission] = &[ + TP::ChangePieces, + TP::UploadBundles, +]; + +#[throws(AE)] +pub fn setup_table(_ma: &MainOpts, instance_name: &InstanceName, spec: &TableSpec) + -> Vec { + let TableSpec { players, player_perms, acl, links } = spec; + let mut player_perms = player_perms.clone() + .unwrap_or(PLAYER_DEFAULT_PERMS.iter().cloned().collect()); + player_perms.extend(PLAYER_ALWAYS_PERMS.iter()); + + let acl: RawAcl<_> = + players.iter().map(|tps| AclEntry { + account_glob: tps.account_glob(instance_name), + allow: player_perms.clone(), + deny: default(), + }) + .chain( + acl.ents.iter().cloned() + ) + .collect(); + + let acl = acl.try_into()?; + + let mut insns = vec![]; + insns.push(MGI::SetACL { acl }); + insns.push(MGI::SetLinks(links.clone())); + insns +} + +pub trait SomeSpec { + const WHAT : &'static str; + const FNCOMP : &'static str; +} + +impl SomeSpec for GameSpec { + const WHAT : &'static str = "game spec"; + const FNCOMP : &'static str = "game"; +} + +impl SomeSpec for TableSpec { + const WHAT : &'static str = "table spec"; + const FNCOMP : &'static str = "table"; +} + +pub trait SpecParse { + type T; + type S: SomeSpec; + fn parse(s: String) -> Result; +} +#[derive(Debug,Copy,Clone)] +pub struct SpecParseToml(pub PhantomData); +impl SpecParse for SpecParseToml { + type T = T; + type S = T; + #[throws(AE)] + fn parse(buf: String) -> T { + let tv: toml::Value = buf.parse().context("parse TOML")?; + let spec: T = toml_de::from_value(&tv).context("parse value")?; + spec + } +} +impl SpecParseToml { pub fn new() -> Self { Self(default()) } } +pub struct SpecRaw(pub PhantomData); +impl SpecParse for SpecRaw { + type T = String; + type S = T; + #[throws(AE)] + fn parse(buf: String) -> String { buf } +} +impl SpecRaw { pub fn new() -> Self { Self(default()) } } + +pub fn spec_arg_is_path(specname: &str) -> Option { + if specname.contains('/') { + Some(specname.to_string()) + } else { + None + } +} + +#[throws(AE)] +pub fn read_spec(ma: &MainOpts, specname: &str, p: P) -> P::T +{ + let filename = spec_arg_is_path(specname).unwrap_or_else( + || format!("{}/{}.{}.toml", &ma.spec_dir, specname, P::S::FNCOMP) + ); + read_spec_from_path(filename, p)? +} + +#[throws(AE)] +pub fn read_spec_from_path(filename: String, _: P) -> P::T +{ + (||{ + let mut f = File::open(&filename).context("open")?; + let mut buf = String::new(); + f.read_to_string(&mut buf).context("read")?; + let spec = P::parse(buf)?; + Ok::<_,AE>(spec) + })().with_context(|| format!("read {} {:?}", P::S::WHAT, &filename))? +} + +#[macro_export] +macro_rules! inventory_subcmd { + {$verb:expr, $help:expr $(,)?} => { + inventory::submit!{Subcommand { + verb: $verb, + help: $help, + call, + props: default(), + }} + }; + {$verb:expr, $help:expr, $($prop:tt)+} => { + inventory::submit!{Subcommand { + verb: $verb, + help: $help, + call, + props: SubcommandProperties { $($prop)* ..default() }, + }} + }; +} diff --git a/cli/otter.rs b/cli/otter.rs index b0e2acc5..9c1b4193 100644 --- a/cli/otter.rs +++ b/cli/otter.rs @@ -6,58 +6,28 @@ pub type MgmtChannel = ClientMgmtChannel; -use otter::imports::*; +pub use otter::imports::*; -use std::cell::Cell; -use std::cell::RefCell; -use std::rc::Rc; +pub use std::cell::Cell; +pub use std::cell::RefCell; +pub use std::rc::Rc; -use argparse::{self,ArgumentParser,action::{TypedAction,ParseResult}}; -use argparse::action::{Action,IFlagAction,IArgAction}; -use derive_more::Display; +pub use argparse::{self,ArgumentParser,action::{TypedAction,ParseResult}}; +pub use argparse::action::{Action,IFlagAction,IArgAction}; +pub use derive_more::Display; -use otter::prelude::*; -use otter::commands::*; +pub use otter::prelude::*; +pub use otter::commands::*; -type APE = ArgumentParseError; -type E = anyhow::Error; -type PL = PresentationLayout; -type TP = TablePermission; +pub type APE = ArgumentParseError; +pub type E = anyhow::Error; +pub type PL = PresentationLayout; +pub type TP = TablePermission; -use argparse::action::ParseResult::Parsed; +pub use argparse::action::ParseResult::Parsed; -#[derive(Clone)] -struct MapStore Result>(F); - -struct BoundMapStore<'r, T, F: FnMut(&str) -> Result> { - f: Rc>, - r: Rc>, -} - -impl<'f,T,F> TypedAction for MapStore -where F: 'f + Clone + FnMut(&str) -> Result, - 'f: 'static // ideally TypedAction wuld have a lifetime parameter -{ - fn bind<'x>(&self, r: Rc>) -> Action<'x> { - Action::Single(Box::new(BoundMapStore { - f: Rc::new(RefCell::new(self.0.clone())), - r, - })) - } -} - -impl<'x, T, F: FnMut(&str) -> Result> - IArgAction for BoundMapStore<'x, T, F> -{ - fn parse_arg(&self, arg: &str) -> ParseResult { - let v: T = match self.f.borrow_mut()(arg) { - Ok(r) => r, - Err(e) => return ParseResult::Error(e), - }; - **self.r.borrow_mut() = v; - ParseResult::Parsed - } -} +pub mod clisupport; +use clisupport::*; #[derive(Debug)] enum ServerLocation { @@ -67,7 +37,7 @@ enum ServerLocation { use ServerLocation as SL; #[derive(Debug)] -struct MainOpts { +pub struct MainOpts { account: AccountName, nick: Option, timezone: Option, @@ -165,157 +135,6 @@ pub struct SubCommandCallArgs { } pub type SCCA = SubCommandCallArgs; -#[derive(Error,Debug,Clone,Display)] -struct ArgumentParseError(String); - -impl From<&anyhow::Error> for ArgumentParseError { - fn from(ae: &anyhow::Error) -> ArgumentParseError { - eprintln!("error during argument parsing/startup: {}\n", ae); - exit(EXIT_USAGE); - } -} - -pub type ApMaker<'apm, T> = - &'apm dyn for <'a> Fn(&'a mut T) -> ArgumentParser<'a>; - -fn parse_args( - args: Vec, - apmaker: ApMaker, - completer: &dyn Fn(T) -> Result, - extra_help: Option<&dyn Fn(&mut dyn Write) -> Result<(), io::Error>>, -) -> U { - let mut parsed = default(); - let ap = apmaker(&mut parsed); - let us = args.get(0).expect("argv[0] must be provided!").clone(); - - let mut stdout = CookedStdout::new(); - let mut stderr = io::stderr(); - - let r = ap.parse(args, &mut stdout, &mut stderr); - if let Err(rc) = r { - exit(match rc { - 0 => { - if let Some(eh) = extra_help { - eh(&mut stdout).unwrap(); - } - 0 - }, - 2 => EXIT_USAGE, - _ => panic!("unexpected error rc {} from ArgumentParser::parse", rc), - }); - } - mem::drop(stdout); - mem::drop(ap); - let completed = completer(parsed) - .unwrap_or_else(|e:ArgumentParseError| { - let mut def = default(); - let ap = apmaker(&mut def); - ap.error(&us, &e.0, &mut stderr); - exit(EXIT_USAGE); - }); - completed -} - -pub fn ok_id(t: T) -> Result { Ok(t) } - -pub fn clone_via_serde(t: &T) -> T { - (|| { - let s = serde_json::to_string(t).context("ser")?; - let c = serde_json::from_str(&s).context("de")?; - Ok::<_,AE>(c) - })() - .with_context(|| format!("clone {:?} via serde failed", t)) - .unwrap() -} - -#[derive(Debug)] -struct AccessOpt(Box); -impl Clone for AccessOpt { - fn clone(&self) -> Self { Self(clone_via_serde(&self.0)) } -} -impl From for AccessOpt { - fn from(t: T) -> Self { AccessOpt(Box::new(t)) } -} -impl From for Box { - fn from(a: AccessOpt) -> Self { a.0 } -} - -type ExecutableRelatedError = AE; -fn ere(s: String) -> ExecutableRelatedError { anyhow!(s) } - -#[throws(ExecutableRelatedError)] -pub fn find_executable() -> String { - let e = env::current_exe() - .map_err(|e| ere( - format!("could not find current executable ({})", &e) - ))?; - let s = e.to_str() - .ok_or_else(|| ere( - format!("current executable has non-UTF8 filename!") - ))?; - s.into() -} - -pub fn in_basedir(verbose: bool, - from: Result, - from_what: &str, - from_exp_in: &str, from_must_be_in_exp: bool, - now_what: &str, - then_in: &str, - leaf: &str, - local_subdir: &str) - -> String -{ - match (||{ - let from = from?; - if from_must_be_in_exp { - let mut comps = from.rsplitn(3,'/').skip(1); - if_chain! { - if Some(from_exp_in) == comps.next(); - if let Some(path) = comps.next(); - then { Ok(path.to_string()) } - else { Err(ere( - format!("{} is not in a directory called {}", from_what, from_exp_in) - )) } - } - } else { - let mut comps = from.rsplitn(2,'/'); - if_chain! { - if let Some(dirname) = comps.nth(1); - let mut dir_comps = dirname.rsplitn(2,'/'); - then { - if_chain! { - if Some(from_exp_in) == dir_comps.next(); - if let Some(above) = dir_comps.next(); - then { Ok(above.to_string()) } - else { Ok(dirname.to_string()) } - } - } - else { - Ok(from.to_string()) - } - } - } - })() { - Err(whynot) => { - let r = format!("{}/{}", local_subdir, leaf); - if verbose { - eprintln!("{}: looking for {} in {}", &whynot, now_what, &r); - } - r - } - Ok(basedir) => { - format!("{}/{}/{}", basedir, then_in, leaf) - } - } -} - -// argparse is pretty insistent about references and they are awkward -#[ext] -impl String { - fn leak(self) -> &'static str { Box::::leak(self.into()) } -} - fn main() { #[derive(Default,Debug)] struct RawMainArgs { @@ -560,245 +379,6 @@ fn main() { .unwrap_or_else(|e| e.end_process(12)); } -struct Conn { - chan: ClientMgmtChannel, -} - -deref_to_field_mut!{Conn, MgmtChannel, chan} - -impl Conn { - #[throws(AE)] - fn prep_access_account(&mut self, ma: &MainOpts, - maybe_update_account: bool) { - #[derive(Debug)] - struct Wantup(bool); - impl Wantup { - fn u(&mut self, rhs: &Option) -> Option { - if rhs.is_some() { self.0 = true } - rhs.clone() - } - } - let mut wantup = Wantup(false); - - let mut ad = if maybe_update_account { AccountDetails { - account: ma.account.clone(), - nick: wantup.u(&ma.nick), - timezone: wantup.u(&ma.timezone), - layout: wantup.u(&ma.layout), - access: wantup.u(&ma.access).map(Into::into), - } } else { - AccountDetails::default(ma.account.clone()) - }; - - fn is_no_account(r: &Result) -> bool { - if_chain! { - if let Err(e) = r; - if let Some(&ME::AccountNotFound(_)) = e.downcast_ref(); - then { return true } - else { return false } - } - } - - { - let mut desc; - let mut resp; - if wantup.0 { - desc = "UpdateAccount"; - resp = self.cmd(&MC::UpdateAccount(clone_via_serde(&ad))); - } else { - desc = "CheckAccount"; - resp = self.cmd(&MC::CheckAccount); - }; - if is_no_account(&resp) { - ad.access.get_or_insert(Box::new(UrlOnStdout)); - desc = "CreateAccount"; - resp = self.cmd(&MC::CreateAccount(clone_via_serde(&ad))); - } - resp.with_context(||format!("response to {}", &desc))?; - } - } -} - -#[throws(E)] -fn connect_chan(ma: &MainOpts) -> MgmtChannel { - match &ma.server { - - SL::Socket(socket) => { - MgmtChannel::connect(socket)? - }, - - SL::Ssh(user_host) => { - - let user_host = { - let (user,host) = - user_host.split_once('@') - .unwrap_or_else(|| ("Otter", user_host)); - format!("{}@{}", user, host) - }; - - let mut cmd = Command::new("sh"); - cmd.arg(if ma.verbose > 2 { "-xec" } else { "-ec" }); - cmd.arg(format!(r#"exec {} "$@""#, &ma.ssh_command)); - cmd.arg("x"); - let args = [ - &user_host, - &ma.ssh_proxy_command, - ]; - cmd.args(args); - - let desc = format!("ssh: {:?} {:?}", &ma.ssh_command, &args); - - let (w,r) = childio::run_pair(cmd, desc.clone()) - .with_context(|| desc.clone()) - .context("run remote command")?; - MgmtChannel::new_boxed(r,w) - }, - - } -} - -#[throws(E)] -fn connect(ma: &MainOpts) -> Conn { - let chan = connect_chan(ma)?; - let mut chan = Conn { chan }; - if ma.superuser { - chan.cmd(&MC::SetSuperuser(true))?; - } - if ! ma.sc.props.suppress_selectaccount { - chan.cmd(&MC::SelectAccount(ma.account.clone()))?; - } - chan -} - -const PLAYER_ALWAYS_PERMS: &[TablePermission] = &[ - TP::TestExistence, - TP::ShowInList, - TP::ViewNotSecret, - TP::Play, -]; - -const PLAYER_DEFAULT_PERMS: &[TablePermission] = &[ - TP::ChangePieces, - TP::UploadBundles, -]; - -#[throws(AE)] -fn setup_table(_ma: &MainOpts, instance_name: &InstanceName, spec: &TableSpec) - -> Vec { - let TableSpec { players, player_perms, acl, links } = spec; - let mut player_perms = player_perms.clone() - .unwrap_or(PLAYER_DEFAULT_PERMS.iter().cloned().collect()); - player_perms.extend(PLAYER_ALWAYS_PERMS.iter()); - - let acl: RawAcl<_> = - players.iter().map(|tps| AclEntry { - account_glob: tps.account_glob(instance_name), - allow: player_perms.clone(), - deny: default(), - }) - .chain( - acl.ents.iter().cloned() - ) - .collect(); - - let acl = acl.try_into()?; - - let mut insns = vec![]; - insns.push(MGI::SetACL { acl }); - insns.push(MGI::SetLinks(links.clone())); - insns -} - -trait SomeSpec { - const WHAT : &'static str; - const FNCOMP : &'static str; -} - -impl SomeSpec for GameSpec { - const WHAT : &'static str = "game spec"; - const FNCOMP : &'static str = "game"; -} - -impl SomeSpec for TableSpec { - const WHAT : &'static str = "table spec"; - const FNCOMP : &'static str = "table"; -} - -trait SpecParse { - type T; - type S: SomeSpec; - fn parse(s: String) -> Result; -} -#[derive(Debug,Copy,Clone)] -struct SpecParseToml(pub PhantomData); -impl SpecParse for SpecParseToml { - type T = T; - type S = T; - #[throws(AE)] - fn parse(buf: String) -> T { - let tv: toml::Value = buf.parse().context("parse TOML")?; - let spec: T = toml_de::from_value(&tv).context("parse value")?; - spec - } -} -impl SpecParseToml { pub fn new() -> Self { Self(default()) } } -struct SpecRaw(pub PhantomData); -impl SpecParse for SpecRaw { - type T = String; - type S = T; - #[throws(AE)] - fn parse(buf: String) -> String { buf } -} -impl SpecRaw { pub fn new() -> Self { Self(default()) } } - -fn spec_arg_is_path(specname: &str) -> Option { - if specname.contains('/') { - Some(specname.to_string()) - } else { - None - } -} - -#[throws(AE)] -fn read_spec(ma: &MainOpts, specname: &str, p: P) -> P::T -{ - let filename = spec_arg_is_path(specname).unwrap_or_else( - || format!("{}/{}.{}.toml", &ma.spec_dir, specname, P::S::FNCOMP) - ); - read_spec_from_path(filename, p)? -} - -#[throws(AE)] -fn read_spec_from_path(filename: String, _: P) -> P::T -{ - (||{ - let mut f = File::open(&filename).context("open")?; - let mut buf = String::new(); - f.read_to_string(&mut buf).context("read")?; - let spec = P::parse(buf)?; - Ok::<_,AE>(spec) - })().with_context(|| format!("read {} {:?}", P::S::WHAT, &filename))? -} - -macro_rules! inventory_subcmd { - {$verb:expr, $help:expr $(,)?} => { - inventory::submit!{Subcommand { - verb: $verb, - help: $help, - call, - props: default(), - }} - }; - {$verb:expr, $help:expr, $($prop:tt)+} => { - inventory::submit!{Subcommand { - verb: $verb, - help: $help, - call, - props: SubcommandProperties { $($prop)* ..default() }, - }} - }; -} - //---------- list-games ---------- mod list_games {