Skip to main content

odra_cli/cmd/
args.rs

1use std::any::Any;
2
3use crate::{
4    parser::{CLTypedParser, CsprTokenAmountParser, EnumCLParser, GasParser},
5    types
6};
7use clap::{builder::PathBufValueParser, ArgAction, ArgMatches};
8use odra::schema::casper_contract_schema::NamedCLType;
9
10pub const ARG_ATTACHED_VALUE: &str = "attached_value";
11pub const ARG_GAS: &str = "gas";
12pub const ARG_CONTRACTS: &str = "contracts-toml";
13pub const ARG_PRINT_EVENTS: &str = "print-events";
14pub const ARG_NUMBER: &str = "number";
15pub const ARG_JSON: &str = "json";
16pub const ARG_DEPLOY_MODE: &str = "deploy_mode";
17const ARG_DEPLOY_MODE_LONG: &str = "deploy-mode";
18
19pub const DEPLOY_MODE_OVERRIDE: &str = "override";
20pub const DEPLOY_MODE_ARCHIVE: &str = "archive";
21
22#[derive(Debug, thiserror::Error)]
23pub enum ArgsError {
24    #[error("Invalid arg value: {0}")]
25    TypesError(#[from] crate::types::Error),
26    #[error("Decoding error: {0}")]
27    DecodingError(String),
28    #[error("Arg not found: {0}")]
29    ArgNotFound(String),
30    #[error("Arg type not found: {0}")]
31    ArgTypeNotFound(String)
32}
33
34/// A typed command argument.
35#[derive(Debug, PartialEq)]
36pub struct CommandArg {
37    pub name: String,
38    pub required: bool,
39    pub description: String,
40    pub ty: NamedCLType,
41    pub is_list_element: bool,
42    pub enum_variants: Option<Vec<(String, u16)>>
43}
44
45impl CommandArg {
46    pub fn new(name: &str, description: &str, ty: NamedCLType) -> Self {
47        Self {
48            name: name.to_string(),
49            description: description.to_string(),
50            ty,
51            required: false,
52            is_list_element: false,
53            enum_variants: None
54        }
55    }
56
57    pub fn required(self) -> Self {
58        Self {
59            required: true,
60            ..self
61        }
62    }
63
64    pub fn list(self) -> Self {
65        Self {
66            is_list_element: true,
67            ..self
68        }
69    }
70
71    pub fn with_enum_variants(self, variants: Vec<(String, u16)>) -> Self {
72        Self {
73            enum_variants: Some(variants),
74            ..self
75        }
76    }
77
78    pub(crate) fn split_name(&self) -> Vec<String> {
79        self.name
80            .split('.')
81            .map(|s| s.to_string())
82            .collect::<Vec<_>>()
83    }
84}
85
86impl From<CommandArg> for clap::Arg {
87    fn from(arg: CommandArg) -> Self {
88        let CommandArg {
89            name,
90            required,
91            description,
92            ty,
93            is_list_element,
94            enum_variants
95        } = arg;
96
97        let value_hint = if let Some(ref variants) = enum_variants {
98            types::format_variant_list(variants)
99        } else {
100            types::format_type_hint(&ty)
101        };
102
103        let base = clap::Arg::new(&name)
104            .long(name)
105            .value_name(value_hint)
106            .required(required)
107            .help(description);
108
109        let base = if let Some(variants) = enum_variants {
110            base.value_parser(EnumCLParser::new(variants))
111        } else {
112            base.value_parser(CLTypedParser::new(ty))
113        };
114
115        match is_list_element {
116            true => base.action(ArgAction::Append),
117            false => base.action(ArgAction::Set)
118        }
119    }
120}
121
122pub enum Arg {
123    AttachedValue,
124    Gas,
125    Contracts,
126    EventsNumber,
127    PrintEvents,
128    DeployMode,
129    Json
130}
131
132impl Arg {
133    pub fn name(&self) -> &str {
134        match self {
135            Arg::AttachedValue => ARG_ATTACHED_VALUE,
136            Arg::Gas => ARG_GAS,
137            Arg::Contracts => ARG_CONTRACTS,
138            Arg::EventsNumber => ARG_NUMBER,
139            Arg::PrintEvents => ARG_PRINT_EVENTS,
140            Arg::DeployMode => ARG_DEPLOY_MODE,
141            Arg::Json => ARG_JSON
142        }
143    }
144}
145
146impl From<Arg> for clap::Arg {
147    fn from(arg: Arg) -> Self {
148        match arg {
149            Arg::AttachedValue => arg_attached_value(),
150            Arg::Gas => arg_gas(),
151            Arg::Contracts => arg_contracts(),
152            Arg::EventsNumber => arg_number("Number of events to print"),
153            Arg::PrintEvents => arg_print_events(),
154            Arg::DeployMode => arg_deploy_mode(),
155            Arg::Json => arg_json()
156        }
157    }
158}
159
160fn arg_attached_value() -> clap::Arg {
161    clap::Arg::new(ARG_ATTACHED_VALUE)
162        .help("The amount of CSPRs attached to the call")
163        .long(ARG_ATTACHED_VALUE)
164        .required(false)
165        .value_name("AMOUNT (motes, or 'X.Y cspr')")
166        .value_parser(CsprTokenAmountParser)
167        .action(ArgAction::Set)
168}
169
170fn arg_gas() -> clap::Arg {
171    clap::Arg::new(ARG_GAS)
172        .help("The amount of gas to attach to the call")
173        .long(ARG_GAS)
174        .required(true)
175        .value_name("AMOUNT (motes, or 'X.Y cspr')")
176        .value_parser(GasParser)
177        .action(ArgAction::Set)
178}
179
180fn arg_contracts() -> clap::Arg {
181    clap::Arg::new(ARG_CONTRACTS)
182        .help("The path to the file with the deployed contracts. Relative to the project root.")
183        .long(ARG_CONTRACTS)
184        .short('c')
185        .required(false)
186        .value_name("PathBuf")
187        .value_parser(PathBufValueParser::new())
188        .action(ArgAction::Set)
189}
190
191fn arg_number(description: &'static str) -> clap::Arg {
192    clap::Arg::new(ARG_NUMBER)
193        .short('n')
194        .long(ARG_NUMBER)
195        .value_name("N")
196        .default_value("10")
197        .value_parser(clap::value_parser!(u32).range(1..50))
198        .help(description)
199}
200
201fn arg_print_events() -> clap::Arg {
202    clap::Arg::new(ARG_PRINT_EVENTS)
203        .long(ARG_PRINT_EVENTS)
204        .short('p')
205        .help("Print events emitted by the contract")
206        .action(ArgAction::SetTrue)
207}
208
209fn arg_deploy_mode() -> clap::Arg {
210    clap::Arg::new(ARG_DEPLOY_MODE)
211        .long(ARG_DEPLOY_MODE_LONG)
212        .help("Deployment mode strategy.")
213        .long_help(
214            "Deployment mode strategy:\n\
215             - default: Use existing contract if available, otherwise deploy a new one\n\
216             - override: Force redeploy, overwrite the existing contract configuration\n\
217             - archive: Redeploy contracts, archive the existing contract configuration and create a new one."
218        )
219        .value_name("MODE")
220        .required(false)
221        .default_value("default")
222        .value_parser(["default", DEPLOY_MODE_OVERRIDE, DEPLOY_MODE_ARCHIVE])
223        .action(ArgAction::Set)
224}
225
226/// The global `--json` flag. Declared once on the root command and marked `global`, so it is
227/// accepted in any position (`odra-cli --json status` or `odra-cli status --json`) and is readable
228/// from every subcommand's matches. Commands that don't support JSON simply ignore it.
229fn arg_json() -> clap::Arg {
230    clap::Arg::new(ARG_JSON)
231        .long(ARG_JSON)
232        .help("Emit machine-readable JSON instead of human-readable text (read commands only)")
233        .global(true)
234        .action(ArgAction::SetTrue)
235}
236
237pub fn read_arg<T: ToOwned<Owned = T> + Any + Clone + Send + Sync + 'static>(
238    matches: &ArgMatches,
239    arg: Arg
240) -> Option<T> {
241    matches.get_one::<T>(arg.name()).map(ToOwned::to_owned)
242}