flow_lib/command/
mod.rs

1//! [`CommandTrait`] and command [`builder`].
2//!
3//! To make a new [`native`][crate::config::CommandType::Native] command:
4//! 1. Implement [`CommandTrait`], 2 ways;
5//!     - Manually implement it to your types.
6//!     - Use [`builder`] helper.
7//! 2. Use [`inventory::submit`] with a [`CommandDescription`] to register the command at compile-time.
8
9use crate::{
10    CommandType, ValueType,
11    config::{
12        CmdInputDescription, CmdOutputDescription, Name, ValueSet, client::NodeData,
13        node::Permissions,
14    },
15    context::CommandContext,
16};
17use futures::future::{Either, LocalBoxFuture, OptionFuture};
18use regex::Regex;
19use serde::{Deserialize, Serialize};
20use std::{borrow::Cow, collections::BTreeMap, future::ready};
21use value::Value;
22
23pub mod builder;
24
25/// Import common types for writing commands.
26pub mod prelude {
27    pub use crate::{
28        CmdInputDescription, CmdInputDescription as Input, CmdOutputDescription,
29        CmdOutputDescription as Output, FlowId, Name, ValueSet, ValueType,
30        command::{
31            CommandDescription, CommandError, CommandTrait, InstructionInfo,
32            builder::{BuildResult, BuilderCache, BuilderError, CmdBuilder},
33        },
34        config::{client::NodeData, node::Permissions},
35        context::CommandContext,
36        solana::Instructions,
37    };
38    pub use async_trait::async_trait;
39    pub use bytes::Bytes;
40    pub use futures::future::Either;
41    pub use serde::{Deserialize, Serialize};
42    pub use serde_json::Value as JsonValue;
43    pub use serde_with::serde_as;
44    pub use solana_keypair::Keypair;
45    pub use solana_pubkey::Pubkey;
46    pub use solana_signature::Signature;
47    pub use thiserror::Error as ThisError;
48    pub use value::{
49        self, Decimal, Value,
50        with::{AsDecimal, AsKeypair, AsPubkey, AsSignature},
51    };
52}
53
54/// Error type of commmands.
55pub type CommandError = anyhow::Error;
56
57/// Generic trait for implementing commands.
58#[async_trait::async_trait(?Send)]
59pub trait CommandTrait: 'static {
60    /// Unique name to identify the command.
61    fn name(&self) -> Name;
62
63    /// List of inputs that the command can receive.
64    fn inputs(&self) -> Vec<CmdInputDescription>;
65
66    /// List of outputs that the command will return.
67    fn outputs(&self) -> Vec<CmdOutputDescription>;
68
69    /// Run the command.
70    async fn run(&self, ctx: CommandContext, params: ValueSet) -> Result<ValueSet, CommandError>;
71
72    /// Specify if and how would this command output Solana instructions.
73    fn instruction_info(&self) -> Option<InstructionInfo> {
74        None
75    }
76
77    /// Specify requested permissions of this command.
78    fn permissions(&self) -> Permissions {
79        Permissions::default()
80    }
81
82    /// Async `Drop` method.
83    async fn destroy(&mut self) {}
84
85    /// Specify how [`form_data`][crate::config::NodeConfig::form_data] are read.
86    fn read_form_data(&self, data: serde_json::Value) -> ValueSet {
87        let mut result = ValueSet::new();
88        for i in self.inputs() {
89            if let Some(json) = data.get(&i.name) {
90                let value = Value::from(json.clone());
91                result.insert(i.name.clone(), value);
92            }
93        }
94        result
95    }
96
97    /// Specify how to convert inputs into passthrough outputs.
98    fn passthrough_outputs(&self, inputs: &ValueSet) -> ValueSet {
99        let mut res = ValueSet::new();
100        for i in self.inputs() {
101            if i.passthrough {
102                if let Some(value) = inputs.get(&i.name) {
103                    if !i.required && matches!(value, Value::Null) {
104                        continue;
105                    }
106
107                    let value = match i.type_bounds.first() {
108                        Some(ValueType::Pubkey) => {
109                            // keypair could be automatically converted into pubkey
110                            // we don't want to passthrough the keypair here, only pubkey
111                            value::pubkey::deserialize(value.clone()).map(Into::into)
112                        }
113                        _ => Ok(value.clone()),
114                    }
115                    .unwrap_or_else(|error| {
116                        tracing::warn!("error reading passthrough: {}", error);
117                        value.clone()
118                    });
119                    res.insert(i.name, value);
120                }
121            }
122        }
123        res
124    }
125
126    fn input_is_required(&self, name: &str) -> Option<bool> {
127        self.inputs()
128            .into_iter()
129            .find_map(|i| (i.name == name).then_some(i.required))
130    }
131
132    fn output_is_optional(&self, name: &str) -> Option<bool> {
133        self.outputs()
134            .into_iter()
135            .find_map(|o| (o.name == name).then_some(o.optional))
136            .or_else(|| {
137                self.inputs()
138                    .into_iter()
139                    .find_map(|i| (i.name == name && i.passthrough).then_some(!i.required))
140            })
141    }
142}
143
144/// Specify the order with which a command will return its output:
145/// - [`before`][InstructionInfo::before]: list of output names returned before instructions are sent.
146/// - [`signature`][InstructionInfo::signature]: name of the signature output port.
147/// - [`after`][InstructionInfo::after]: list of output names returned after instructions are sent.
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub struct InstructionInfo {
150    pub before: Vec<Name>,
151    pub signature: Name,
152    pub after: Vec<Name>,
153}
154
155impl InstructionInfo {
156    /// Simple `InstructionInfo` that can describe most commands:
157    /// - [`before`][InstructionInfo::before]: All passthroughs and outputs, except for `signature`.
158    /// - [`after`][InstructionInfo::after]: empty.
159    pub fn simple<C: CommandTrait>(cmd: &C, signature: &str) -> Self {
160        let before = cmd
161            .inputs()
162            .into_iter()
163            .filter(|i| i.passthrough)
164            .map(|i| i.name)
165            .chain(
166                cmd.outputs()
167                    .into_iter()
168                    .filter(|o| o.name != signature)
169                    .map(|o| o.name),
170            )
171            .collect();
172        Self {
173            before,
174            after: Vec::new(),
175            signature: signature.into(),
176        }
177    }
178}
179
180#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, bincode::Encode)]
181pub enum MatchName {
182    Exact(Cow<'static, str>),
183    Regex(Cow<'static, str>),
184}
185
186// generated by bincode macro
187impl<__Context> ::bincode::Decode<__Context> for MatchName {
188    fn decode<__D: ::bincode::de::Decoder<Context = __Context>>(
189        decoder: &mut __D,
190    ) -> core::result::Result<Self, ::bincode::error::DecodeError> {
191        let variant_index = <u32 as ::bincode::Decode<__D::Context>>::decode(decoder)?;
192        match variant_index {
193            0u32 => core::result::Result::Ok(Self::Exact(
194                ::bincode::Decode::<__D::Context>::decode(decoder)?,
195            )),
196            1u32 => core::result::Result::Ok(Self::Regex(
197                ::bincode::Decode::<__D::Context>::decode(decoder)?,
198            )),
199            variant => {
200                core::result::Result::Err(::bincode::error::DecodeError::UnexpectedVariant {
201                    found: variant,
202                    type_name: "MatchName",
203                    allowed: &::bincode::error::AllowedEnumVariants::Range { min: 0, max: 1 },
204                })
205            }
206        }
207    }
208}
209
210impl<'de, C> bincode::BorrowDecode<'de, C> for MatchName {
211    fn borrow_decode<D: bincode::de::BorrowDecoder<'de, Context = C>>(
212        decoder: &mut D,
213    ) -> Result<Self, bincode::error::DecodeError> {
214        bincode::Decode::decode(decoder)
215    }
216}
217
218impl std::fmt::Debug for MatchName {
219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        match self {
221            Self::Exact(arg0) => arg0.fmt(f),
222            Self::Regex(arg0) => {
223                f.write_str("/")?;
224                f.write_str(arg0)?;
225                f.write_str("/")
226            }
227        }
228    }
229}
230
231impl std::fmt::Display for MatchName {
232    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233        match self {
234            MatchName::Exact(cow) => cow.fmt(f),
235            MatchName::Regex(cow) => cow.fmt(f),
236        }
237    }
238}
239
240#[derive(Clone, bincode::Encode, bincode::Decode, PartialEq, Eq, PartialOrd, Ord)]
241pub struct MatchCommand {
242    pub r#type: CommandType,
243    pub name: MatchName,
244}
245
246impl std::fmt::Debug for MatchCommand {
247    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248        self.r#type.fmt(f)?;
249        f.write_str(":")?;
250        self.name.fmt(f)
251    }
252}
253
254impl MatchCommand {
255    pub fn is_match(&self, ty: CommandType, name: &str) -> bool {
256        self.r#type == ty
257            && match &self.name {
258                MatchName::Exact(cow) => cow == name,
259                MatchName::Regex(cow) => Regex::new(cow) // TODO: slow
260                    .map(|re| re.is_match(name))
261                    .ok()
262                    .unwrap_or(false),
263            }
264    }
265}
266
267pub type FnNewResult = Result<Box<dyn CommandTrait>, CommandError>;
268
269/// Use [`inventory::submit`] to register commands at compile-time.
270#[derive(Clone)]
271pub struct CommandDescription {
272    pub matcher: MatchCommand,
273    /// Function to initialize the command from a [`NodeData`].
274    pub fn_new:
275        Either<fn(&NodeData) -> FnNewResult, fn(&NodeData) -> LocalBoxFuture<'static, FnNewResult>>,
276}
277
278impl CommandDescription {
279    pub const fn new(name: &'static str, fn_new: fn(&NodeData) -> FnNewResult) -> Self {
280        Self {
281            matcher: MatchCommand {
282                r#type: CommandType::Native,
283                name: MatchName::Exact(Cow::Borrowed(name)),
284            },
285            fn_new: Either::Left(fn_new),
286        }
287    }
288}
289
290inventory::collect!(CommandDescription);
291
292pub fn collect_commands() -> BTreeMap<&'static MatchCommand, &'static CommandDescription> {
293    inventory::iter::<CommandDescription>()
294        .map(|c| (&c.matcher, c))
295        .collect()
296}
297
298#[derive(Debug, Clone)]
299pub struct CommandIndex<T> {
300    pub exact_match: BTreeMap<(CommandType, Cow<'static, str>), T>,
301    pub regex: Vec<(CommandType, regex::Regex, T)>,
302}
303
304impl<T> Default for CommandIndex<T> {
305    fn default() -> Self {
306        Self {
307            exact_match: <_>::default(),
308            regex: <_>::default(),
309        }
310    }
311}
312
313impl<T> FromIterator<(MatchCommand, T)> for CommandIndex<T> {
314    fn from_iter<I: IntoIterator<Item = (MatchCommand, T)>>(iter: I) -> Self {
315        let mut this = Self::default();
316        for (matcher, t) in iter {
317            match &matcher.name {
318                MatchName::Exact(cow) => {
319                    this.exact_match.insert((matcher.r#type, cow.clone()), t);
320                }
321                MatchName::Regex(cow) => {
322                    this.regex
323                        .push((matcher.r#type, Regex::new(cow).expect("invalid regex"), t));
324                }
325            }
326        }
327        this
328    }
329}
330
331impl<T> CommandIndex<T> {
332    pub fn get(&self, ty: CommandType, name: &str) -> Option<&T> {
333        if let Some(d) = self.exact_match.get(&(ty, name.to_owned().into())) {
334            Some(d)
335        } else {
336            let mut matched = None;
337            for r in &self.regex {
338                if r.0 == ty && r.1.is_match(name) {
339                    matched = Some(&r.2);
340                }
341            }
342            matched
343        }
344    }
345
346    pub fn availables(&self) -> impl Iterator<Item = MatchCommand> {
347        self.exact_match
348            .keys()
349            .cloned()
350            .map(|(r#type, name)| MatchCommand {
351                r#type,
352                name: MatchName::Exact(name),
353            })
354            .chain(self.regex.iter().map(|(ty, regex, _)| MatchCommand {
355                r#type: *ty,
356                name: MatchName::Regex(regex.to_string().into()),
357            }))
358    }
359}
360
361pub struct CommandFactory {
362    index: CommandIndex<&'static CommandDescription>,
363}
364
365impl CommandFactory {
366    pub fn collect() -> Self {
367        Self {
368            index: inventory::iter::<CommandDescription>()
369                .map(|c| (c.matcher.clone(), c))
370                .collect(),
371        }
372    }
373
374    pub fn init(
375        &self,
376        nd: &NodeData,
377    ) -> impl Future<Output = Result<Option<Box<dyn CommandTrait>>, CommandError>> + 'static {
378        let cmd = self.index.get(nd.r#type, &nd.node_id);
379
380        let either = cmd.map(|cmd| match cmd.fn_new {
381            Either::Left(fn_new) => Either::Left(ready(fn_new(nd))),
382            Either::Right(async_fn_new) => Either::Right(async_fn_new(nd)),
383        });
384        async move { OptionFuture::from(either).await.transpose() }
385    }
386
387    pub fn availables(&self) -> impl Iterator<Item = MatchCommand> {
388        self.index.availables()
389    }
390}