reactive_graph_command_model/entity/
command.rs

1use std::collections::HashMap;
2use std::ops::Deref;
3use std::ops::DerefMut;
4
5use serde_json::Value;
6
7use crate::builder::CommandDefinition;
8use crate::builder::CommandDefinitionBuilder;
9use crate::component::COMMAND_PROPERTIES;
10use crate::component::CommandProperties::COMMAND_ARGS;
11use crate::component::CommandProperties::COMMAND_HELP;
12use crate::component::CommandProperties::COMMAND_NAME;
13use crate::component::CommandProperties::COMMAND_NAMESPACE;
14use crate::component::CommandProperties::COMMAND_RESULT;
15use crate::component::command::COMPONENT_COMMAND;
16use crate::entity::arg::CommandArgs;
17use crate::error::CommandArgsError;
18use crate::error::CommandExecutionFailed;
19use crate::error::NotACommand;
20use reactive_graph_graph::ComponentContainer;
21use reactive_graph_graph::ComponentTypeIds;
22use reactive_graph_graph::DataType;
23use reactive_graph_graph::EntityType;
24use reactive_graph_graph::EntityTypeId;
25use reactive_graph_graph::Mutability;
26use reactive_graph_graph::PropertyInstanceGetter;
27use reactive_graph_graph::PropertyInstanceSetter;
28use reactive_graph_graph::PropertyType;
29use reactive_graph_graph::PropertyTypeDefinition;
30use reactive_graph_graph::SocketType;
31use reactive_graph_reactive_model_api::ReactivePropertyContainer;
32use reactive_graph_reactive_model_impl::ReactiveEntity;
33use reactive_graph_runtime_model::ActionProperties::TRIGGER;
34use reactive_graph_runtime_model::COMPONENT_ACTION;
35use reactive_graph_runtime_model::COMPONENT_LABELED;
36use reactive_graph_runtime_model::LabeledProperties::LABEL;
37
38pub struct Command(ReactiveEntity);
39
40impl Command {}
41
42impl Command {
43    pub fn new(entity: ReactiveEntity) -> Result<Self, NotACommand> {
44        if !entity.is_a(&COMPONENT_ACTION) || !entity.is_a(&COMPONENT_COMMAND) {
45            return Err(NotACommand);
46        }
47        Ok(Command(entity))
48    }
49
50    pub fn new_unchecked(entity: ReactiveEntity) -> Self {
51        Self(entity)
52    }
53
54    pub fn builder() -> CommandDefinitionBuilder {
55        CommandDefinition::builder()
56    }
57
58    pub fn get_entity_type(&self) -> EntityType {
59        let components = ComponentTypeIds::new()
60            .component(COMPONENT_LABELED.deref())
61            .component(COMPONENT_ACTION.deref())
62            .component(COMPONENT_COMMAND.deref());
63        let properties = COMMAND_PROPERTIES.clone();
64        if let Some(args) = self.get(COMMAND_ARGS).and_then(|args| CommandArgs::try_from(args).ok()) {
65            for arg in args.to_vec() {
66                if !properties.contains_key(&arg.name) {
67                    properties.insert(
68                        arg.name.clone(),
69                        PropertyType::builder()
70                            .name(arg.name.clone())
71                            .description(arg.help.unwrap_or_default())
72                            .data_type(DataType::Any)
73                            .socket_type(SocketType::Input)
74                            .mutability(Mutability::Mutable)
75                            .build(),
76                    );
77                }
78            }
79        }
80        EntityType::builder()
81            .ty(self.0.ty.clone())
82            .description(self.0.description.clone())
83            .components(components)
84            .properties(COMMAND_PROPERTIES.clone())
85            .build()
86    }
87
88    /// Executes a command
89    pub fn execute(&self) -> Result<Option<Value>, CommandExecutionFailed> {
90        if !self.0.is_a(&COMPONENT_ACTION) || !self.0.is_a(&COMPONENT_COMMAND) {
91            return Err(CommandExecutionFailed::NotACommand);
92        }
93        self.0.set(TRIGGER.property_name(), Value::Bool(true));
94        Ok(self.0.get(COMMAND_RESULT))
95    }
96
97    /// Executes a command with the given arguments
98    /// Stores the command result in the command result property
99    pub fn execute_with_args(&self, args: HashMap<String, Value>) -> Result<Option<Value>, CommandExecutionFailed> {
100        if !self.0.is_a(&COMPONENT_ACTION) || !self.0.is_a(&COMPONENT_COMMAND) {
101            return Err(CommandExecutionFailed::NotACommand);
102        }
103        // Check that all given arguments are valid arguments and the properties exists
104        match self.args() {
105            Ok(command_args) => {
106                for property_name in args.keys() {
107                    if !command_args.contains(property_name.clone()) {
108                        return Err(CommandExecutionFailed::InvalidArgument(property_name.clone()));
109                    } else if !self.0.has_property(property_name) {
110                        return Err(CommandExecutionFailed::MissingArgumentProperty(property_name.clone()));
111                    }
112                }
113                for command_arg in command_args.to_vec() {
114                    if command_arg.required && !args.contains_key(&command_arg.name) {
115                        return Err(CommandExecutionFailed::MissingMandatoryArgument(command_arg.name));
116                    }
117                }
118            }
119            Err(e) => {
120                return Err(CommandExecutionFailed::CommandArgsError(e));
121            }
122        }
123        for (property_name, value) in args {
124            if self.0.has_property(&property_name) {
125                self.0.set_checked(property_name, value)
126            }
127        }
128        match self.execute() {
129            Ok(Some(result)) => {
130                self.0.set(COMMAND_RESULT, result.clone());
131                Ok(Some(result))
132            }
133            Ok(None) => Ok(None),
134            Err(e) => Err(e),
135        }
136    }
137
138    pub fn namespace(&self) -> Option<String> {
139        self.0.as_string(COMMAND_NAMESPACE)
140    }
141
142    pub fn name(&self) -> Option<String> {
143        self.0.as_string(COMMAND_NAME)
144    }
145
146    pub fn label(&self) -> Option<String> {
147        self.0.as_string(LABEL)
148    }
149
150    pub fn args(&self) -> Result<CommandArgs, CommandArgsError> {
151        match self.0.get(COMMAND_ARGS) {
152            Some(v) => CommandArgs::try_from(v),
153            None => Err(CommandArgsError::CommandArgDefinitionMissing),
154        }
155    }
156
157    pub fn command(&self) -> Option<clap::Command> {
158        let name = self.name()?;
159        let Ok(args) = self.args() else {
160            return None;
161        };
162        Some(clap::Command::new(name).args(args.as_args()))
163    }
164
165    pub fn help(&self) -> Option<String> {
166        self.0.as_string(COMMAND_HELP)
167    }
168
169    pub fn ty(&self) -> EntityTypeId {
170        self.0.ty.clone()
171    }
172
173    // TODO: impl Deref instead
174    pub fn get_instance(&self) -> ReactiveEntity {
175        self.0.clone()
176    }
177}
178
179impl Deref for Command {
180    type Target = ReactiveEntity;
181
182    fn deref(&self) -> &Self::Target {
183        &self.0
184    }
185}
186
187impl DerefMut for Command {
188    fn deref_mut(&mut self) -> &mut Self::Target {
189        &mut self.0
190    }
191}
192
193impl TryFrom<ReactiveEntity> for Command {
194    type Error = NotACommand;
195
196    fn try_from(entity: ReactiveEntity) -> Result<Self, Self::Error> {
197        Command::new(entity)
198    }
199}
200
201// pub fn command_property_types() -> PropertyTypes {
202//     PropertyTypes::new()
203//         .property(
204//             PropertyType::builder()
205//                 .name(LABEL.property_name())
206//                 .data_type(DataType::String)
207//                 .mutability(Immutable)
208//                 .build()
209//         )
210//         .property(PropertyType::bool(TRIGGER.property_name()))
211//         .property(
212//             PropertyType::builder()
213//                 .name(COMMAND_NAMESPACE.property_name())
214//                 .data_type(DataType::String)
215//                 .mutability(Immutable)
216//                 .build()
217//         )
218//         .property(
219//             PropertyType::builder()
220//                 .name(COMMAND_NAME.property_name())
221//                 .data_type(DataType::String)
222//                 .mutability(Immutable)
223//                 .build()
224//         )
225//         .property(PropertyType::object(COMMAND_ARGS.property_name()))
226//         .property(
227//             PropertyType::builder()
228//                 .name(COMMAND_HELP.property_name())
229//                 .data_type(DataType::String)
230//                 .mutability(Immutable)
231//                 .build()
232//         )
233//         .property(
234//             PropertyType::builder()
235//                 .name(COMMAND_RESULT.property_name())
236//                 .data_type(DataType::Any)
237//                 .build()
238//         )
239// }
240
241#[cfg(test)]
242mod tests {
243    use std::collections::HashMap;
244    use std::ops::Deref;
245
246    use serde_json::json;
247    use uuid::Uuid;
248
249    use reactive_graph_graph::ComponentTypeIds;
250    use reactive_graph_graph::PropertyInstances;
251    use reactive_graph_reactive_model_impl::ReactiveEntity;
252    use reactive_graph_reactive_model_impl::ReactiveProperties;
253
254    use crate::CommandProperties::COMMAND_RESULT;
255    use crate::component::COMPONENT_COMMAND;
256    use crate::component::CommandProperties::COMMAND_ARGS;
257    use crate::component::CommandProperties::COMMAND_HELP;
258    use crate::component::CommandProperties::COMMAND_NAME;
259    use crate::entity::Command;
260    use crate::error::CommandExecutionFailed;
261    use reactive_graph_graph::EntityTypeId;
262    use reactive_graph_graph::PropertyInstanceGetter;
263    use reactive_graph_graph::PropertyInstanceSetter;
264    use reactive_graph_graph::PropertyTypeDefinition;
265    use reactive_graph_reactive_model_api::ReactivePropertyContainer;
266    use reactive_graph_runtime_model::ActionProperties::TRIGGER;
267    use reactive_graph_runtime_model::COMPONENT_ACTION;
268
269    #[test]
270    fn test_command() {
271        let ty = EntityTypeId::new_from_type("test", "test");
272        let reactive_entity = ReactiveEntity::builder().ty(&ty).build();
273        assert!(Command::try_from(reactive_entity).is_err());
274
275        let components = ComponentTypeIds::new().component(COMPONENT_ACTION.deref()).component(COMPONENT_COMMAND.deref());
276        let properties = PropertyInstances::new()
277            .property(&TRIGGER.property_name(), json!(false))
278            .property("arg1", json!(0))
279            .property("arg2", json!(1))
280            .property(COMMAND_RESULT, json!(0))
281            .property(COMMAND_NAME, json!("hello_command"))
282            .property(
283                COMMAND_ARGS,
284                json!([
285                    {
286                        "name": "arg1"
287                    },
288                    {
289                        "name": "arg2",
290                        "required": true
291                    }
292                ]),
293            )
294            .property(COMMAND_HELP, json!("Help text"));
295
296        let id = Uuid::new_v4();
297        let reactive_properties = ReactiveProperties::new_with_id_from_properties(id, properties);
298        let reactive_entity = ReactiveEntity::builder()
299            .ty(&ty)
300            .id(id)
301            .components(components)
302            .properties(reactive_properties)
303            // .component(COMPONENT_ACTION.clone())
304            // .component(COMPONENT_COMMAND.clone())
305            // .property(&TRIGGER.property_name(), json!(false))
306            // .property("arg1", json!(0))
307            // .property("arg2", json!(1))
308            // .property(COMMAND_RESULT, json!(0))
309            // .property(COMMAND_NAME, json!("hello_command"))
310            // .property(
311            //     COMMAND_ARGS,
312            //     json!([
313            //         {
314            //             "name": "arg1"
315            //         },
316            //         {
317            //             "name": "arg2",
318            //             "required": true
319            //         }
320            //     ]),
321            // )
322            // .property(COMMAND_HELP, json!("Help text"))
323            .build();
324        let e1 = reactive_entity.clone();
325        let e2 = reactive_entity.clone();
326        e1.observe_with_handle(
327            &TRIGGER.property_name(),
328            move |_| {
329                let arg1 = e2.as_u64("arg1").unwrap_or_default();
330                let arg2 = e2.as_u64("arg2").unwrap_or_default();
331                e2.set(COMMAND_RESULT, json!(arg1 + arg2));
332            },
333            0,
334        );
335        let command = Command::new(reactive_entity).expect("Failed to create a command");
336        assert_eq!("hello_command", command.name().expect("Failed to get command name"));
337        assert_eq!("Help text", command.help().expect("Failed to get help text"));
338        assert_eq!(0, e1.as_u64(COMMAND_RESULT).expect("Failed to get initial result"));
339        command.execute().expect("Command execution failed");
340        assert_eq!(1, e1.as_u64(COMMAND_RESULT).expect("Failed to get changed result"));
341        let mut args = HashMap::new();
342        args.insert(String::from("arg1"), json!(1));
343        args.insert(String::from("arg2"), json!(2));
344        let _ = command.execute_with_args(args).expect("Failed to execute command with args");
345        assert_eq!(3, e1.as_u64(COMMAND_RESULT).expect("Failed to get changed result"));
346        let mut args = HashMap::new();
347        args.insert(String::from("arg1"), json!(1));
348        args.insert(String::from("arg2"), json!(2));
349        args.insert(String::from("arg3"), json!(3));
350        assert_eq!(CommandExecutionFailed::InvalidArgument(String::from("arg3")), command.execute_with_args(args).unwrap_err());
351        let mut args = HashMap::new();
352        args.insert(String::from("arg1"), json!(1));
353        assert_eq!(
354            CommandExecutionFailed::MissingMandatoryArgument(String::from("arg2")),
355            command.execute_with_args(args).unwrap_err()
356        );
357    }
358}