Skip to main content

genmeta_access/
cli.rs

1use std::str::FromStr;
2
3use clap::{Args, Parser, Subcommand, error::ErrorKind};
4use dhttp::access::{db::identity::Name, expr::exprs::LocationRuleExprs, pattern::LocationPattern};
5use snafu::{IntoError, ResultExt, Snafu};
6
7/// Wrapper for clap that uses [`snafu::Report`] for richer error display.
8///
9/// Use as a clap argument type to get multi-line error chain output when
10/// parsing complex types like [`LocationPattern`] or [`Name`]:
11///
12/// ```ignore
13/// #[derive(clap::Parser)]
14/// struct Cli {
15///     pattern: ReportFromStr<LocationPattern>,
16/// }
17/// ```
18#[derive(Debug, Clone, Copy)]
19pub struct ReportFromStr<T>(pub T);
20
21#[derive(Debug, Snafu)]
22#[snafu(display("{}", snafu::Report::from_error(source)))]
23pub struct ReportError<E: std::error::Error + 'static> {
24    #[snafu(source(false))]
25    source: E,
26}
27
28impl<T> FromStr for ReportFromStr<T>
29where
30    T: FromStr<Err: std::error::Error + 'static>,
31{
32    type Err = ReportError<T::Err>;
33
34    fn from_str(s: &str) -> Result<Self, Self::Err> {
35        match T::from_str(s) {
36            Ok(value) => Ok(Self(value)),
37            Err(source) => Err(ReportError { source }),
38        }
39    }
40}
41
42#[derive(Parser, Debug, Clone)]
43#[command(
44    version,
45    about,
46    override_usage = "genmeta access [OPTIONS] <path> <operation> ...\n       genmeta access [OPTIONS] list [--wide]\n       genmeta access [OPTIONS] remove <path>...",
47    after_help = "Examples:\n  genmeta access \"/\" allow luffy.pilot\n  genmeta access \"/\" list\n  genmeta access list --wide\n  genmeta access --identity reimu.pilot \"/\" deny \"*?\""
48)]
49pub struct Options {
50    #[arg(
51        long,
52        value_name = "NAME",
53        help = "identity to manage; defaults to `genmeta identity default`"
54    )]
55    identity: Option<ReportFromStr<Name<'static>>>,
56
57    #[command(subcommand)]
58    command: CliCommand,
59}
60
61impl Options {
62    pub(crate) fn into_parts(self) -> Result<(Option<Name<'static>>, Command), ParseCommandError> {
63        let identity = self.identity.map(|ReportFromStr(identity)| identity);
64        let command = self.command.try_into()?;
65        Ok((identity, command))
66    }
67}
68
69#[derive(Subcommand, Debug, Clone)]
70enum CliCommand {
71    #[command(visible_alias = "ls")]
72    List(GlobalList),
73    #[command(visible_alias = "rm")]
74    Remove(GlobalRemove),
75    #[command(external_subcommand)]
76    Path(Vec<String>),
77}
78
79#[derive(Args, Debug, Clone)]
80struct GlobalList {
81    #[arg(short, long)]
82    wide: bool,
83}
84
85#[derive(Args, Debug, Clone)]
86struct GlobalRemove {
87    #[arg(required = true)]
88    patterns: Vec<ReportFromStr<LocationPattern>>,
89}
90
91impl TryFrom<CliCommand> for Command {
92    type Error = ParseCommandError;
93
94    fn try_from(value: CliCommand) -> Result<Self, Self::Error> {
95        match value {
96            CliCommand::List(GlobalList { wide }) => Ok(Self::List { wide }),
97            CliCommand::Remove(GlobalRemove { patterns }) => Ok(Self::RemovePaths {
98                patterns: patterns
99                    .into_iter()
100                    .map(|ReportFromStr(pattern)| pattern)
101                    .collect(),
102            }),
103            CliCommand::Path(arguments) => parse_path_command(arguments),
104        }
105    }
106}
107
108#[derive(Debug, Clone)]
109pub(crate) enum Command {
110    Print {
111        output: String,
112    },
113    List {
114        wide: bool,
115    },
116    RemovePaths {
117        patterns: Vec<LocationPattern>,
118    },
119    Path {
120        pattern: LocationPattern,
121        operation: PathOperation,
122    },
123}
124
125#[derive(Debug, Clone)]
126pub(crate) enum PathOperation {
127    List,
128    Remove { all: bool, sequence: Vec<usize> },
129    Clear,
130    Allow { expr: LocationRuleExprs },
131    Deny { expr: LocationRuleExprs },
132}
133
134#[derive(Debug, Snafu)]
135#[snafu(module)]
136pub enum ParseCommandError {
137    #[snafu(display("failed to parse access path command"))]
138    ParsePathCommand { source: clap::Error },
139
140    #[snafu(display("failed to parse rule expression `{input}`"))]
141    InvalidRuleExpr {
142        input: String,
143        source: <LocationRuleExprs as FromStr>::Err,
144    },
145}
146
147#[derive(Parser, Debug, Clone)]
148#[command(name = "genmeta access")]
149struct PathCommand {
150    pattern: ReportFromStr<LocationPattern>,
151
152    #[command(subcommand)]
153    operation: PathOperationCommand,
154}
155
156#[derive(Subcommand, Debug, Clone)]
157enum PathOperationCommand {
158    #[command(visible_alias = "ls")]
159    List,
160    #[command(visible_alias = "rm")]
161    Remove(PathRemove),
162    Clear,
163    Allow(RuleExprArgs),
164    Deny(RuleExprArgs),
165}
166
167#[derive(Args, Debug, Clone)]
168struct PathRemove {
169    #[arg(long, conflicts_with = "sequence")]
170    all: bool,
171
172    #[arg(value_name = "SEQUENCE", required_unless_present = "all")]
173    sequence: Vec<usize>,
174}
175
176#[derive(Args, Debug, Clone)]
177struct RuleExprArgs {
178    #[arg(
179        value_name = "EXPR",
180        required = true,
181        num_args = 1..,
182        allow_hyphen_values = true,
183        trailing_var_arg = true
184    )]
185    expr: Vec<String>,
186}
187
188impl TryFrom<PathCommand> for Command {
189    type Error = ParseCommandError;
190
191    fn try_from(value: PathCommand) -> Result<Self, Self::Error> {
192        let ReportFromStr(pattern) = value.pattern;
193        let operation = value.operation.try_into()?;
194        Ok(Command::Path { pattern, operation })
195    }
196}
197
198impl TryFrom<PathOperationCommand> for PathOperation {
199    type Error = ParseCommandError;
200
201    fn try_from(value: PathOperationCommand) -> Result<Self, Self::Error> {
202        match value {
203            PathOperationCommand::List => Ok(Self::List),
204            PathOperationCommand::Remove(PathRemove { all, sequence }) => {
205                Ok(Self::Remove { all, sequence })
206            }
207            PathOperationCommand::Clear => Ok(Self::Clear),
208            PathOperationCommand::Allow(args) => args.into_expr().map(|expr| Self::Allow { expr }),
209            PathOperationCommand::Deny(args) => args.into_expr().map(|expr| Self::Deny { expr }),
210        }
211    }
212}
213
214impl RuleExprArgs {
215    fn into_expr(self) -> Result<LocationRuleExprs, ParseCommandError> {
216        let input = self.expr.join(" ");
217        input
218            .parse()
219            .context(parse_command_error::InvalidRuleExprSnafu { input })
220    }
221}
222
223fn parse_path_command(arguments: Vec<String>) -> Result<Command, ParseCommandError> {
224    let command = match PathCommand::try_parse_from(
225        std::iter::once("genmeta access").chain(arguments.iter().map(String::as_str)),
226    ) {
227        Ok(command) => command,
228        Err(error)
229            if matches!(
230                error.kind(),
231                ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
232            ) =>
233        {
234            return Ok(Command::Print {
235                output: error.to_string(),
236            });
237        }
238        Err(source) => return Err(parse_command_error::ParsePathCommandSnafu.into_error(source)),
239    };
240    command.try_into()
241}