reactive_graph_command_model/entity/
command.rs1use 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 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 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 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 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#[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 .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}