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#[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}