reactive_graph_command_model/
builder.rs

1use std::ops::Deref;
2
3use serde_json::Value;
4use serde_json::json;
5use typed_builder::TypedBuilder;
6use uuid::Uuid;
7
8use crate::component::COMPONENT_COMMAND;
9use crate::component::CommandProperties::COMMAND_ARGS;
10use crate::component::CommandProperties::COMMAND_HELP;
11use crate::component::CommandProperties::COMMAND_NAME;
12use crate::component::CommandProperties::COMMAND_NAMESPACE;
13use crate::component::CommandProperties::COMMAND_RESULT;
14use crate::entity::Command;
15use crate::entity::CommandArgs;
16use reactive_graph_graph::ComponentTypeIds;
17use reactive_graph_graph::EntityTypeId;
18use reactive_graph_graph::NamespacedTypeGetter;
19use reactive_graph_graph::PropertyInstanceSetter;
20use reactive_graph_graph::PropertyInstances;
21use reactive_graph_graph::PropertyTypeDefinition;
22use reactive_graph_reactive_model_impl::ReactiveEntity;
23use reactive_graph_reactive_model_impl::ReactiveProperties;
24use reactive_graph_runtime_model::ActionProperties::TRIGGER;
25use reactive_graph_runtime_model::COMPONENT_ACTION;
26use reactive_graph_runtime_model::COMPONENT_LABELED;
27use reactive_graph_runtime_model::LabeledProperties::LABEL;
28
29pub type CommandExecutor = dyn FnMut(&ReactiveEntity) -> Value + 'static + Send;
30
31#[derive(TypedBuilder)]
32#[builder(
33    build_method(vis="pub", into=Command),
34    builder_method(vis="pub"),
35    builder_type(vis="pub", name=CommandDefinitionBuilder),
36)]
37pub struct CommandDefinition {
38    #[builder(setter(into))]
39    ty: EntityTypeId,
40    #[builder(default=Uuid::new_v4(), setter(into))]
41    id: Uuid,
42    #[builder(default, setter(strip_option, into))]
43    namespace: Option<String>,
44    #[builder(default, setter(strip_option, into))]
45    name: Option<String>,
46    #[builder(default, setter(into))]
47    description: String,
48    #[builder(default, setter(into))]
49    help: String,
50    #[builder(default, setter(into))]
51    arguments: CommandArgs,
52    // #[builder(setter(into))]
53    executor: Box<CommandExecutor>,
54    // #[builder(setter(into))]
55    // scope: String,
56    // #[builder(setter(into))]
57    // scope: String,
58    // .scope("testing")
59    // .name("concat")
60    // .label("/io/reactive-graph/test/concat")
61    // .help("Concatenates two strings")
62}
63
64impl From<CommandDefinition> for Command {
65    fn from(definition: CommandDefinition) -> Self {
66        let handle_id = Uuid::new_v4().as_u128();
67
68        let namespace = definition.namespace.unwrap_or_else(|| definition.ty.namespace());
69        let name = definition.name.unwrap_or_else(|| definition.ty.type_name());
70
71        let label = format!("/io/reactive-graph/commands/{namespace}/{name}");
72
73        let components = ComponentTypeIds::new()
74            .component(COMPONENT_LABELED.deref())
75            .component(COMPONENT_ACTION.deref())
76            .component(COMPONENT_COMMAND.deref());
77        // components.insert(COMPONENT_ACTION.clone());
78        // components.insert(COMPONENT_COMMAND.clone());
79
80        // let properties = PropertyTypes::new()
81        //     .property(PropertyType::string("help"));
82
83        //         let label = format!("/io/reactive-graph/commands/{scope}/{name}");
84        //         builder.property(COMMAND_NAMESPACE, json!(scope));
85        //         builder.property(COMMAND_NAME, json!(name));
86        //         builder.component(&COMPONENT_LABELED.clone());
87
88        let properties = PropertyInstances::new()
89            .property(LABEL.property_name(), json!(label))
90            .property(TRIGGER.property_name(), json!(false))
91            .property(COMMAND_NAMESPACE.property_name(), json!(namespace))
92            .property(COMMAND_NAME.property_name(), json!(name))
93            .property(COMMAND_ARGS.property_name(), definition.arguments.to_value())
94            .property(COMMAND_HELP.property_name(), json!(definition.help))
95            .property(COMMAND_RESULT, json!(0));
96
97        for arg in definition.arguments.to_vec() {
98            if !properties.contains_key(&arg.name) {
99                properties.insert(arg.name.clone(), json!(0));
100            }
101        }
102
103        let reactive_entity = ReactiveEntity::builder()
104            .ty(definition.ty)
105            .id(definition.id)
106            .description(definition.description)
107            .components(components)
108            .properties(ReactiveProperties::new_with_id_from_properties(definition.id, properties))
109            .build();
110        let reactive_entity_inner = reactive_entity.clone();
111        let mut executor = definition.executor;
112        if let Some(property_instance) = reactive_entity.properties.get(&TRIGGER.property_name()) {
113            property_instance.stream.read().unwrap().observe_with_handle(
114                move |trigger| {
115                    if trigger.as_bool().unwrap_or_default() {
116                        // let x = executor(&reactive_entity_inner);
117                        reactive_entity_inner.set(COMMAND_RESULT, executor(&reactive_entity_inner));
118                    }
119                },
120                handle_id,
121            );
122        };
123        Command::new_unchecked(reactive_entity)
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use crate::Command;
130    use crate::CommandArgs;
131    use reactive_graph_graph::EntityTypeId;
132    use reactive_graph_reactive_model_impl::ReactiveEntity;
133    use serde_json::json;
134
135    #[test]
136    fn command_builder_test() {
137        let args = CommandArgs::new();
138        // TODO: fill args
139        let executor = Box::new(move |_: &ReactiveEntity| json!("abc"));
140        let command = Command::builder()
141            .ty(("core", "num_commands"))
142            .description("The number of commands")
143            .help("Number of commands")
144            .arguments(args)
145            .executor(executor)
146            .build();
147
148        assert_eq!(command.ty(), EntityTypeId::new_from_type("core", "num_commands"));
149        // assert!(command.get
150    }
151}
152
153// pub struct CommandBuilder<S> {
154//     ty: Option<EntityTypeId>,
155//     builder: Option<ReactiveEntityBuilder>,
156//     arguments: CommandArgs,
157//     subscriber: Option<Box<CommandExecutor>>,
158//     handle_id: Option<u128>,
159//     state: PhantomData<S>,
160// }
161//
162// pub mod command_builder_state {
163//     pub enum EntityType {}
164//     pub enum Scope {}
165//     pub enum Name {}
166//     pub enum Label {}
167//     pub enum Help {}
168//     pub enum Components {}
169//     pub enum Arguments {}
170//     pub enum Properties {}
171//     pub enum Executor {}
172//     pub enum Finish {}
173// }
174//
175// impl Default for CommandBuilder<command_builder_state::EntityType> {
176//     fn default() -> Self {
177//         Self::new()
178//     }
179// }
180//
181// impl CommandBuilder<command_builder_state::EntityType> {
182//     pub fn new() -> CommandBuilder<command_builder_state::EntityType> {
183//         Self {
184//             ty: None,
185//             builder: None,
186//             arguments: CommandArgs::new(),
187//             subscriber: None,
188//             handle_id: None,
189//             state: PhantomData,
190//         }
191//     }
192//
193//     pub fn ty(self, ty: &EntityTypeId) -> CommandBuilder<command_builder_state::Scope> {
194//         let mut builder = ReactiveEntityBuilder::new(ty.clone());
195//         builder.component(&COMPONENT_ACTION.clone());
196//         builder.component(&COMPONENT_COMMAND.clone());
197//         builder.property(TRIGGER.property_name(), json!(false));
198//         builder.property(COMMAND_RESULT, json!(0));
199//         CommandBuilder {
200//             ty: Some(ty.clone()),
201//             builder: Some(builder),
202//             arguments: CommandArgs::new(),
203//             subscriber: None,
204//             handle_id: None,
205//             state: PhantomData,
206//         }
207//     }
208//
209//     /// Uses the type information to build a command.
210//     /// Useful for entity types with exactly one instance.
211//     pub fn singleton(self, ty: &EntityTypeId) -> CommandBuilder<command_builder_state::Help> {
212//         let command = ReactiveEntity::builder()
213//             .ty(ty)
214//             .components(vec![
215//                 &COMPONENT_ACTION,
216//                 &COMPONENT_COMMAND
217//             ])
218//             // .properties(vec![
219//             //     TRIGGER.property_name(), json!(false)
220//             // ])
221//             .build();
222//
223//         let entity_instance = ReactiveEntity::builder()
224//             .ty(ty)
225//             .components(
226//                 Components::new()
227//
228//             )
229//             .build();
230//         let mut builder = ReactiveEntityBuilder::new(ty.clone());
231//         builder.component(&COMPONENT_ACTION.clone());
232//         builder.component(&COMPONENT_COMMAND.clone());
233//         builder.property(TRIGGER.property_name(), json!(false));
234//         builder.property(COMMAND_RESULT, json!(0));
235//         let scope = ty.namespace();
236//         let name = ty.type_name();
237//         let label = format!("/io/reactive-graph/commands/{scope}/{name}");
238//         builder.property(COMMAND_NAMESPACE, json!(scope));
239//         builder.property(COMMAND_NAME, json!(name));
240//         builder.component(&COMPONENT_LABELED.clone());
241//         builder.property("label", json!(label));
242//         CommandBuilder {
243//             ty: Some(ty.clone()),
244//             builder: Some(builder),
245//             arguments: CommandArgs::new(),
246//             subscriber: None,
247//             handle_id: None,
248//             state: PhantomData,
249//         }
250//     }
251//
252//     pub fn singleton_from_type<S1: Into<String>, S2: Into<String>>(self, namespace: S1, type_name: S2) -> CommandBuilder<command_builder_state::Help> {
253//         let ty = EntityTypeId::new_from_type(namespace.into(), type_name.into());
254//         self.singleton(&ty)
255//     }
256// }
257//
258// impl CommandBuilder<command_builder_state::Scope> {
259//     pub fn scope<S: Into<String>>(mut self, scope: S) -> CommandBuilder<command_builder_state::Name> {
260//         if let Some(builder) = self.builder.as_mut() {
261//             builder.property(COMMAND_NAMESPACE, json!(scope.into()));
262//         }
263//         CommandBuilder {
264//             ty: self.ty,
265//             builder: self.builder,
266//             arguments: self.arguments,
267//             subscriber: None,
268//             handle_id: None,
269//             state: PhantomData,
270//         }
271//     }
272//
273//     pub fn scope_and_name<S1: Into<String>, S2: Into<String>>(mut self, scope: S1, name: S2) -> CommandBuilder<command_builder_state::Help> {
274//         if let Some(builder) = self.builder.as_mut() {
275//             let scope = scope.into();
276//             let name = name.into();
277//             let label = format!("/io/reactive-graph/commands/{scope}/{name}");
278//             builder.property(COMMAND_NAMESPACE, json!(scope));
279//             builder.property(COMMAND_NAME, json!(name));
280//             builder.component(&COMPONENT_LABELED.clone());
281//             builder.property("label", json!(label));
282//         }
283//         CommandBuilder {
284//             ty: self.ty,
285//             builder: self.builder,
286//             arguments: self.arguments,
287//             subscriber: None,
288//             handle_id: None,
289//             state: PhantomData,
290//         }
291//     }
292// }
293//
294// impl CommandBuilder<command_builder_state::Name> {
295//     pub fn name<S: Into<String>>(mut self, name: S) -> CommandBuilder<command_builder_state::Label> {
296//         if let Some(builder) = self.builder.as_mut() {
297//             builder.property(COMMAND_NAME, json!(name.into()));
298//         }
299//         CommandBuilder {
300//             ty: self.ty,
301//             builder: self.builder,
302//             arguments: self.arguments,
303//             subscriber: None,
304//             handle_id: None,
305//             state: PhantomData,
306//         }
307//     }
308// }
309//
310// impl CommandBuilder<command_builder_state::Label> {
311//     pub fn label<S: Into<String>>(mut self, label: S) -> CommandBuilder<command_builder_state::Help> {
312//         if let Some(builder) = self.builder.as_mut() {
313//             builder.component(&COMPONENT_LABELED.clone());
314//             builder.property("label", json!(label.into()));
315//         }
316//         CommandBuilder {
317//             ty: self.ty,
318//             builder: self.builder,
319//             arguments: self.arguments,
320//             subscriber: None,
321//             handle_id: None,
322//             state: PhantomData,
323//         }
324//     }
325//
326//     pub fn no_label(self) -> CommandBuilder<command_builder_state::Help> {
327//         CommandBuilder {
328//             ty: self.ty,
329//             builder: self.builder,
330//             arguments: self.arguments,
331//             subscriber: None,
332//             handle_id: None,
333//             state: PhantomData,
334//         }
335//     }
336// }
337//
338// impl CommandBuilder<command_builder_state::Help> {
339//     pub fn help<S: Into<String>>(mut self, help: S) -> CommandBuilder<command_builder_state::Components> {
340//         if let Some(builder) = self.builder.as_mut() {
341//             builder.property(COMMAND_HELP, json!(help.into()));
342//         }
343//         CommandBuilder {
344//             ty: self.ty,
345//             builder: self.builder,
346//             arguments: self.arguments,
347//             subscriber: None,
348//             handle_id: None,
349//             state: PhantomData,
350//         }
351//     }
352//
353//     pub fn no_help(self) -> CommandBuilder<command_builder_state::Components> {
354//         CommandBuilder {
355//             ty: self.ty,
356//             builder: self.builder,
357//             arguments: self.arguments,
358//             subscriber: None,
359//             handle_id: None,
360//             state: PhantomData,
361//         }
362//     }
363// }
364//
365// impl CommandBuilder<command_builder_state::Components> {
366//     pub fn component(mut self, ty: &ComponentTypeId) -> CommandBuilder<command_builder_state::Components> {
367//         if let Some(builder) = self.builder.as_mut() {
368//             builder.component(ty.clone());
369//         }
370//         self
371//     }
372//
373//     pub fn component_from_type<S1: Into<String>, S2: Into<String>>(
374//         mut self,
375//         namespace: S1,
376//         type_name: S2,
377//     ) -> CommandBuilder<command_builder_state::Components> {
378//         if let Some(builder) = self.builder.as_mut() {
379//             let ty = ComponentTypeId::new_from_type(namespace.into(), type_name.into());
380//             builder.component(&ty);
381//         }
382//         self
383//     }
384//
385//     pub fn arguments(self) -> CommandBuilder<command_builder_state::Arguments> {
386//         CommandBuilder {
387//             ty: self.ty,
388//             builder: self.builder,
389//             arguments: self.arguments,
390//             subscriber: None,
391//             handle_id: None,
392//             state: PhantomData,
393//         }
394//     }
395//
396//     pub fn no_arguments(self) -> CommandBuilder<command_builder_state::Executor> {
397//         CommandBuilder {
398//             ty: self.ty,
399//             builder: self.builder,
400//             arguments: self.arguments,
401//             subscriber: None,
402//             handle_id: None,
403//             state: PhantomData,
404//         }
405//     }
406// }
407//
408// impl CommandBuilder<command_builder_state::Arguments> {
409//     pub fn argument<A: Into<CommandArg>>(mut self, arg: A, value: Value) -> CommandBuilder<command_builder_state::Arguments> {
410//         let arg = arg.into();
411//         if let Some(builder) = self.builder.as_mut() {
412//             builder.property(arg.name.clone(), value);
413//             self.arguments.push(arg);
414//         }
415//         self
416//     }
417//
418//     fn create_arguments_property(&mut self) {
419//         if let Some(builder) = self.builder.as_mut() {
420//             builder.property(COMMAND_ARGS, self.arguments.to_value());
421//         }
422//     }
423//
424//     pub fn properties(mut self) -> CommandBuilder<command_builder_state::Properties> {
425//         self.create_arguments_property();
426//         CommandBuilder {
427//             ty: self.ty,
428//             builder: self.builder,
429//             arguments: self.arguments,
430//             subscriber: None,
431//             handle_id: None,
432//             state: PhantomData,
433//         }
434//     }
435//
436//     pub fn no_properties(mut self) -> CommandBuilder<command_builder_state::Executor> {
437//         self.create_arguments_property();
438//         CommandBuilder {
439//             ty: self.ty,
440//             builder: self.builder,
441//             arguments: self.arguments,
442//             subscriber: None,
443//             handle_id: None,
444//             state: PhantomData,
445//         }
446//     }
447// }
448//
449// impl CommandBuilder<command_builder_state::Properties> {
450//     pub fn property<S: Into<String>>(mut self, property_name: S, value: Value) -> CommandBuilder<command_builder_state::Properties> {
451//         if let Some(builder) = self.builder.as_mut() {
452//             builder.property(property_name.into(), value);
453//         }
454//         self
455//     }
456//
457//     pub fn no_more_properties(self) -> CommandBuilder<command_builder_state::Executor> {
458//         CommandBuilder {
459//             ty: self.ty,
460//             builder: self.builder,
461//             arguments: self.arguments,
462//             subscriber: None,
463//             handle_id: None,
464//             state: PhantomData,
465//         }
466//     }
467// }
468//
469// impl CommandBuilder<command_builder_state::Executor> {
470//     pub fn executor<F>(self, subscriber: F) -> CommandBuilder<command_builder_state::Finish>
471//     where
472//         F: FnMut(&ReactiveEntity) -> Value + 'static + Send,
473//     {
474//         CommandBuilder {
475//             ty: self.ty,
476//             builder: self.builder,
477//             arguments: self.arguments,
478//             subscriber: Some(Box::new(subscriber)),
479//             handle_id: self.handle_id,
480//             state: PhantomData,
481//         }
482//     }
483//
484//     pub fn executor_with_handle<F>(self, subscriber: F, handle_id: Option<u128>) -> CommandBuilder<command_builder_state::Finish>
485//     where
486//         F: FnMut(&ReactiveEntity) -> Value + 'static + Send,
487//     {
488//         CommandBuilder {
489//             ty: self.ty,
490//             builder: self.builder,
491//             arguments: self.arguments,
492//             subscriber: Some(Box::new(subscriber)),
493//             handle_id,
494//             state: PhantomData,
495//         }
496//     }
497// }
498//
499// impl CommandBuilder<command_builder_state::Finish> {
500//     pub fn id(mut self, id: Uuid) -> CommandBuilder<command_builder_state::Finish> {
501//         if let Some(builder) = self.builder.as_mut() {
502//             builder.id(id);
503//         };
504//         self
505//     }
506//
507//     pub fn build(self) -> Result<Command, CommandBuilderError> {
508//         let Some(builder) = self.builder else {
509//             return Err(CommandBuilderError::NotACommand);
510//         };
511//         let Some(mut subscriber) = self.subscriber else {
512//             return Err(CommandBuilderError::MissingExecutor);
513//         };
514//
515//         let entity_instance = builder.build();
516//         let e = entity_instance.clone();
517//         let Some(property_instance) = e.properties.get(&TRIGGER.property_name()) else {
518//             return Err(CommandBuilderError::MissingTrigger);
519//         };
520//
521//         let entity_instance_inner = entity_instance.clone();
522//         let handle_id = self.handle_id.unwrap_or(Uuid::new_v4().as_u128());
523//         property_instance.stream.read().unwrap().observe_with_handle(
524//             move |trigger| {
525//                 if trigger.as_bool().unwrap_or_default() {
526//                     entity_instance_inner.set(COMMAND_RESULT, subscriber(&entity_instance_inner));
527//                 }
528//             },
529//             handle_id,
530//         );
531//         Command::try_from(entity_instance).map_err(|_| CommandBuilderError::NotACommand)
532//     }
533//
534//     pub fn build_with_type(self) -> Result<(Command, EntityType), CommandBuilderError> {
535//         let Some(builder) = self.builder else {
536//             return Err(CommandBuilderError::NotACommand);
537//         };
538//         let Some(mut subscriber) = self.subscriber else {
539//             return Err(CommandBuilderError::MissingExecutor);
540//         };
541//
542//         let entity_instance = builder.build();
543//         let e = entity_instance.clone();
544//         let Some(property_instance) = e.properties.get(&TRIGGER.property_name()) else {
545//             return Err(CommandBuilderError::MissingTrigger);
546//         };
547//
548//         let entity_instance_inner = entity_instance.clone();
549//         let handle_id = self.handle_id.unwrap_or(Uuid::new_v4().as_u128());
550//         property_instance.stream.read().unwrap().observe_with_handle(
551//             move |trigger| {
552//                 if trigger.as_bool().unwrap_or_default() {
553//                     entity_instance_inner.set(COMMAND_RESULT, subscriber(&entity_instance_inner));
554//                 }
555//             },
556//             handle_id,
557//         );
558//         let entity_type = EntityType::builder()
559//             .ty(self.ty.unwrap())
560//             .description(entity_instance.as_string(COMMAND_HELP).unwrap_or_default())
561//             .components(ComponentTypeIds::from(&entity_instance.components))
562//             .properties(self.arguments.to_property_types())
563//             .build();
564//         Command::try_from(entity_instance)
565//             .map_err(|_| CommandBuilderError::NotACommand)
566//             .map(|command| (command, entity_type))
567//     }
568// }
569//
570// #[cfg(test)]
571// mod tests {
572//     use std::collections::HashMap;
573//
574//     use serde_json::json;
575//
576//     use crate::builder::CommandBuilder;
577//     use crate::entity::CommandArg;
578//     use reactive_graph_graph::ComponentTypeId;
579//     use reactive_graph_graph::EntityTypeId;
580//     use reactive_graph_graph::PropertyInstanceGetter;
581//     use reactive_graph_reactive_model_api::ReactivePropertyContainer;
582//
583//     #[test]
584//     fn test_builder() {
585//         let command = CommandBuilder::new()
586//             .ty(&EntityTypeId::new_from_type("testing", "concat"))
587//             .scope("testing")
588//             .name("concat")
589//             .label("/io/reactive-graph/test/concat")
590//             .help("Concatenates two strings")
591//             // Additional components
592//             .component(&ComponentTypeId::new_from_type("test", "test"))
593//             // Arguments
594//             .arguments()
595//             .argument(
596//                 CommandArg::new("argument1")
597//                     .short('a')
598//                     .long("argument1")
599//                     .help("The first argument")
600//                     .required(true),
601//                 json!(""),
602//             )
603//             .argument(CommandArg::new("argument2").short('b').long("argument2").help("The second argument"), json!(""))
604//             // Additional properties
605//             .properties()
606//             .property("something", json!(""))
607//             .no_more_properties()
608//             .executor(|e| {
609//                 let mut result = String::new();
610//                 if let Some(argument1) = e.as_string("argument1") {
611//                     result.push_str(&argument1);
612//                 }
613//                 if let Some(argument2) = e.as_string("argument2") {
614//                     result.push_str(&argument2);
615//                 }
616//                 json!(result)
617//             })
618//             .build()
619//             .expect("Failed to create command");
620//         assert_eq!("testing", command.namespace().expect("No command namespace"));
621//         assert_eq!("concat", command.name().expect("No command name"));
622//         assert_eq!("Concatenates two strings", command.help().expect("No help text"));
623//
624//         assert!(command.get_instance().has_property("argument1"));
625//         assert!(command.get_instance().has_property("argument2"));
626//         assert!(command.get_instance().has_property("something"));
627//
628//         let args = command.args().expect("No command args");
629//         assert_eq!(2, args.len());
630//         assert!(args.contains("argument1"));
631//         assert!(args.contains("argument2"));
632//         assert!(!args.contains("something"));
633//
634//         let mut exec_args = HashMap::new();
635//         exec_args.insert(String::from("argument1"), json!("Hello, "));
636//         exec_args.insert(String::from("argument2"), json!("World"));
637//         let command_result = command
638//             .execute_with_args(exec_args)
639//             .expect("Command execution failed")
640//             .expect("No return value")
641//             .as_str()
642//             .expect("Failed to extract command result string")
643//             .to_string();
644//         assert_eq!("Hello, World", command_result);
645//     }
646//
647//     #[test]
648//     fn test_builder_scope_and_name() {
649//         let command = CommandBuilder::new()
650//             .ty(&EntityTypeId::new_from_type("testing", "test"))
651//             .scope_and_name("testing", "test")
652//             .help("A test command")
653//             .no_arguments()
654//             .executor(|_| json!(""))
655//             .build()
656//             .expect("Failed to create command");
657//         assert_eq!("testing", command.namespace().expect("No command namespace"));
658//         assert_eq!("test", command.name().expect("No command name"));
659//         // Automatically generated label
660//         assert_eq!("/io/reactive-graph/commands/testing/test", command.label().expect("No label"));
661//         assert_eq!("A test command", command.help().expect("No help text"));
662//     }
663//
664//     #[test]
665//     fn test_builder_singleton() {
666//         // Singleton Command
667//         // command scope = entity type namespace
668//         // command name = entity type name
669//         let command = CommandBuilder::new()
670//             .singleton_from_type("testing", "add")
671//             .help("Adds two numbers")
672//             .arguments()
673//             .argument(CommandArg::new("lhs").short('l').long("lhs").help("The left hand side argument").required(true), json!(0))
674//             .argument(
675//                 CommandArg::new("rhs")
676//                     .short('r')
677//                     .long("rhs")
678//                     .help("The right hand side argument")
679//                     .required(true),
680//                 json!(0),
681//             )
682//             .no_properties()
683//             .executor(|e| {
684//                 let mut result = 0;
685//                 if let (Some(lhs), Some(rhs)) = (e.as_i64("lhs"), e.as_i64("rhs")) {
686//                     result = lhs + rhs;
687//                 }
688//                 json!(result)
689//             })
690//             .build()
691//             .expect("Failed to create command");
692//         assert_eq!("testing", command.namespace().expect("No command namespace"));
693//         assert_eq!("add", command.name().expect("No command name"));
694//         // Automatically generated label
695//         assert_eq!("/io/reactive-graph/commands/testing/add", command.label().expect("No label"));
696//         assert_eq!("Adds two numbers", command.help().expect("No help text"));
697//         let mut exec_args = HashMap::new();
698//         exec_args.insert(String::from("lhs"), json!(1));
699//         exec_args.insert(String::from("rhs"), json!(2));
700//         let command_result = command
701//             .execute_with_args(exec_args)
702//             .expect("Command execution failed")
703//             .expect("No return value");
704//         assert_eq!(3, command_result.as_i64().unwrap());
705//     }
706// }