odra_cli/cmd/
scenario.rs

1use std::{any::Any, collections::HashMap};
2
3use crate::{
4    args::CommandArg, container::ContractError, types, CustomTypeSet, DeployedContractsContainer
5};
6use anyhow::Result;
7use clap::ArgMatches;
8use odra::schema::NamedCLTyped;
9use odra::{casper_types::bytesrepr::FromBytes, host::HostEnv, prelude::OdraError};
10use thiserror::Error;
11
12use super::OdraCommand;
13
14/// Scenario is a trait that represents a custom scenario.
15///
16/// A scenario is a user-defined set of actions that can be run in the Odra CLI.
17/// If you want to run a custom scenario that calls multiple entry points,
18/// you need to implement this trait.
19pub trait Scenario: Any {
20    fn args(&self) -> Vec<CommandArg> {
21        vec![]
22    }
23    fn run(
24        &self,
25        env: &HostEnv,
26        container: DeployedContractsContainer,
27        args: ScenarioArgs
28    ) -> core::result::Result<(), ScenarioError>;
29}
30
31/// ScenarioCmd is a struct that represents a scenario command in the Odra CLI.
32///
33/// The scenario command runs a [Scenario]. A scenario is a user-defined set of actions that can be run in the Odra CLI.
34pub(crate) struct ScenarioCmd {
35    name: String,
36    scenario: Box<dyn Scenario>
37}
38
39impl ScenarioCmd {
40    pub fn new<S: ScenarioMetadata + Scenario>(scenario: S) -> Self {
41        ScenarioCmd {
42            name: S::NAME.to_string(),
43            scenario: Box::new(scenario)
44        }
45    }
46}
47
48impl OdraCommand for ScenarioCmd {
49    fn name(&self) -> &str {
50        &self.name
51    }
52
53    fn run(&self, env: &HostEnv, args: &ArgMatches, _types: &CustomTypeSet) -> Result<()> {
54        let container = DeployedContractsContainer::load()?;
55        let args = ScenarioArgs::new(self.scenario.args(), args);
56
57        self.scenario.run(env, container, args)?;
58        Ok(())
59    }
60}
61
62/// ScenarioError is an enum representing the different errors that can occur when running a scenario.
63#[derive(Debug, Error)]
64pub enum ScenarioError {
65    #[error("Odra error: {message}")]
66    OdraError { message: String },
67    #[error("Contract read error: {0}")]
68    ContractReadError(#[from] ContractError),
69    #[error("Arg error")]
70    ArgError(#[from] ArgError),
71    #[error("Types error")]
72    TypesError(#[from] types::Error)
73}
74
75impl From<OdraError> for ScenarioError {
76    fn from(err: OdraError) -> Self {
77        ScenarioError::OdraError {
78            message: format!("{:?}", err)
79        }
80    }
81}
82
83/// ScenarioArgs is a struct that represents the arguments passed to a scenario.
84pub struct ScenarioArgs(HashMap<String, ScenarioArg>);
85
86impl ScenarioArgs {
87    pub(crate) fn new(args: Vec<CommandArg>, matches: &ArgMatches) -> Self {
88        let map = args
89            .into_iter()
90            .filter_map(|arg| {
91                let arg_name = &arg.name;
92                let values = matches.get_many::<String>(arg_name);
93
94                if arg.required && values.is_none() {
95                    panic!("Missing argument: {}", arg.name);
96                }
97                values.as_ref()?;
98                let values = values
99                    .expect("Arg not found")
100                    .map(|v| v.to_string())
101                    .collect::<Vec<_>>();
102                let scenario_arg = match arg.is_list_element {
103                    true => ScenarioArg::Many(values),
104                    false => ScenarioArg::Single(values[0].clone())
105                };
106
107                Some((arg_name.clone(), scenario_arg))
108            })
109            .collect();
110        Self(map)
111    }
112
113    pub fn get_single<T: NamedCLTyped + FromBytes>(&self, name: &str) -> Result<T, ScenarioError> {
114        let arg = self
115            .0
116            .get(name)
117            .ok_or(ArgError::MissingArg(name.to_string()))?;
118
119        let result = match arg {
120            ScenarioArg::Single(value) => {
121                let bytes = types::into_bytes(&T::ty(), value)?;
122                T::from_bytes(&bytes)
123                    .map_err(|_| ArgError::Deserialization)
124                    .map(|t| t.0)
125            }
126            ScenarioArg::Many(_) => Err(ArgError::SingleExpected)
127        }?;
128        Ok(result)
129    }
130
131    pub fn get_many<T: NamedCLTyped + FromBytes>(
132        &self,
133        name: &str
134    ) -> Result<Vec<T>, ScenarioError> {
135        let arg = self
136            .0
137            .get(name)
138            .ok_or(ArgError::MissingArg(name.to_string()))?;
139        match arg {
140            ScenarioArg::Many(values) => values
141                .iter()
142                .map(|value| {
143                    let bytes = types::into_bytes(&T::ty(), value);
144                    bytes.map_err(ScenarioError::TypesError).and_then(|bytes| {
145                        T::from_bytes(&bytes)
146                            .map_err(|_| ScenarioError::ArgError(ArgError::Deserialization))
147                            .map(|t| t.0)
148                    })
149                })
150                .collect::<Result<Vec<T>, ScenarioError>>(),
151            ScenarioArg::Single(_) => Err(ScenarioError::ArgError(ArgError::ManyExpected))
152        }
153    }
154}
155
156/// ArgError is an enum representing the different errors that can occur when parsing scenario arguments.
157#[derive(Debug, Error)]
158pub enum ArgError {
159    #[error("Arg deserialization failed")]
160    Deserialization,
161    #[error("Multiple values expected")]
162    ManyExpected,
163    #[error("Single value expected")]
164    SingleExpected,
165    #[error("Missing arg: {0}")]
166    MissingArg(String)
167}
168
169enum ScenarioArg {
170    Single(String),
171    Many(Vec<String>)
172}
173
174/// ScenarioMetadata is a trait that represents the metadata of a scenario.
175pub trait ScenarioMetadata {
176    const NAME: &'static str;
177    const DESCRIPTION: &'static str;
178}