ixa/
external_api.rs

1// Now all features of the external API are used internally, so we expect dead code.
2#![allow(dead_code)]
3
4use crate::context::Context;
5use crate::error::IxaError;
6use serde::{de::DeserializeOwned, Deserialize, Serialize};
7
8pub(crate) trait ExtApi {
9    type Args: DeserializeOwned;
10    type Retval: Serialize;
11
12    fn run(context: &mut Context, args: &Self::Args) -> Result<Self::Retval, IxaError>;
13}
14
15#[derive(Serialize, Deserialize, Debug)]
16pub(crate) struct EmptyArgs {}
17
18pub(crate) fn run_ext_api<T: ExtApi>(
19    context: &mut Context,
20    args: &T::Args,
21) -> Result<T::Retval, IxaError> {
22    T::run(context, args)
23}
24
25pub(crate) mod population {
26    use crate::context::Context;
27    use crate::external_api::EmptyArgs;
28    use crate::people::ContextPeopleExt;
29    use crate::IxaError;
30    use clap::Parser;
31    use serde::{Deserialize, Serialize};
32
33    pub(crate) struct Api {}
34    #[derive(Parser, Debug, Deserialize)]
35    pub(crate) enum Args {
36        /// Get the total number of people
37        Population,
38    }
39
40    #[derive(Serialize)]
41    pub(crate) struct Retval {
42        pub population: usize,
43    }
44    impl super::ExtApi for Api {
45        type Args = EmptyArgs;
46        type Retval = Retval;
47
48        fn run(context: &mut Context, _args: &EmptyArgs) -> Result<Retval, IxaError> {
49            Ok(Retval {
50                population: context.get_current_population(),
51            })
52        }
53    }
54}
55
56pub(crate) mod global_properties {
57    use crate::context::Context;
58    use crate::global_properties::ContextGlobalPropertiesExt;
59    use crate::IxaError;
60    use clap::{Parser, Subcommand};
61    use serde::{Deserialize, Serialize};
62
63    pub(crate) struct Api {}
64    #[derive(Serialize, Deserialize, Debug)]
65    pub(crate) enum Retval {
66        List(Vec<String>),
67        Value(String),
68    }
69    #[derive(Subcommand, Clone, Debug, Serialize, Deserialize)]
70    /// Access global properties
71    pub(crate) enum ArgsEnum {
72        /// List all global properties
73        List,
74
75        /// Get the value of a global property
76        Get {
77            /// The property name
78            property: String,
79        },
80    }
81
82    #[derive(Parser, Debug, Serialize, Deserialize)]
83    pub(crate) enum Args {
84        #[command(subcommand)]
85        Global(ArgsEnum),
86    }
87    impl super::ExtApi for Api {
88        type Args = Args;
89        type Retval = Retval;
90
91        fn run(context: &mut Context, args: &Args) -> Result<Retval, IxaError> {
92            let Args::Global(global_args) = args;
93
94            match global_args {
95                ArgsEnum::List => Ok(Retval::List(context.list_registered_global_properties())),
96                ArgsEnum::Get { property: name } => {
97                    let output = context.get_serialized_value_by_string(name)?;
98                    match output {
99                        Some(value) => Ok(Retval::Value(value)),
100                        None => Err(IxaError::IxaError(format!("Property {name} is not set"))),
101                    }
102                }
103            }
104        }
105    }
106}
107
108pub(crate) mod breakpoint {
109    use crate::context::Context;
110    use crate::debugger::enter_debugger;
111    #[cfg(feature = "web_api")]
112    use crate::web_api::enter_web_debugger;
113    use crate::{info, trace, IxaError};
114    use clap::{Parser, Subcommand};
115    use serde::{Deserialize, Serialize};
116
117    #[derive(Subcommand, Clone, Debug, Serialize, Deserialize)]
118    /// Manipulate Debugger Breakpoints
119    pub(crate) enum ArgsEnum {
120        /// List all scheduled breakpoints
121        List,
122        /// Set a breakpoint at a given time
123        Set {
124            #[arg(required = true)]
125            time: f64,
126            #[arg(long, hide = true, default_value_t = true)]
127            console: bool,
128        },
129        /// Delete the breakpoint with the specified id.
130        /// Providing the `--all` option removes all breakpoints.
131        #[group(multiple = false, required = true)]
132        Delete {
133            /// The ID of the breakpoint to delete
134            #[arg(value_name = "ID")]
135            id: Option<u32>,
136
137            /// Remove all breakpoints
138            #[arg(long, action)]
139            all: bool,
140        },
141        /// Disables but does not delete breakpoints globally
142        Disable,
143        /// Enables breakpoints globally
144        Enable,
145    }
146
147    #[derive(Parser, Debug, Serialize, Deserialize)]
148    pub(crate) enum Args {
149        #[command(subcommand)]
150        Breakpoint(ArgsEnum),
151    }
152
153    #[derive(Serialize, Deserialize, Debug)]
154    pub(crate) enum Retval {
155        List(Vec<String>),
156        Ok,
157    }
158
159    pub(crate) struct Api {}
160    impl super::ExtApi for Api {
161        type Args = Args;
162        type Retval = Retval;
163
164        fn run(context: &mut Context, args: &Args) -> Result<Retval, IxaError> {
165            let Args::Breakpoint(breakpoint_args) = args;
166
167            match breakpoint_args {
168                ArgsEnum::List => {
169                    trace!("Listing breakpoints");
170                    let list = context.list_breakpoints(0);
171                    let list = list
172                        .iter()
173                        .map(|schedule| {
174                            format!(
175                                "{}: t={} ({})",
176                                schedule.plan_id, schedule.time, schedule.priority
177                            )
178                        })
179                        .collect::<Vec<String>>();
180                    Ok(Retval::List(list))
181                }
182
183                ArgsEnum::Set { time, console: _ } => {
184                    if *time < context.get_current_time() {
185                        return Err(IxaError::from(format!(
186                            "Breakpoint time {time} is in the past"
187                        )));
188                    }
189
190                    #[cfg(feature = "web_api")]
191                    if *console {
192                        context.schedule_debugger(*time, None, Box::new(enter_debugger));
193                    } else {
194                        context.schedule_debugger(*time, None, Box::new(enter_web_debugger));
195                    }
196                    #[cfg(not(feature = "web_api"))]
197                    context.schedule_debugger(*time, None, Box::new(enter_debugger));
198
199                    info!("Breakpoint set at t={time}");
200                    Ok(Retval::Ok)
201                }
202
203                ArgsEnum::Delete { id, all } => {
204                    if let Some(id) = id {
205                        assert!(!all);
206                        trace!("Deleting breakpoint {id}");
207                        let cancelled = context.delete_breakpoint(u64::from(*id));
208                        if cancelled.is_none() {
209                            Err(IxaError::from(format!(
210                                "Attempted to delete a nonexistent breakpoint {id}",
211                            )))
212                        } else {
213                            Ok(Retval::Ok)
214                        }
215                    } else {
216                        assert!(all);
217                        trace!("Deleting all breakpoints");
218                        context.clear_breakpoints();
219                        Ok(Retval::Ok)
220                    }
221                }
222
223                ArgsEnum::Disable => {
224                    trace!("Disabling all breakpoints");
225                    context.disable_breakpoints();
226                    Ok(Retval::Ok)
227                }
228
229                ArgsEnum::Enable => {
230                    trace!("Enabling all breakpoints");
231                    context.enable_breakpoints();
232                    Ok(Retval::Ok)
233                }
234            }
235        }
236    }
237}
238
239pub(crate) mod next {
240    use crate::context::Context;
241    use crate::external_api::EmptyArgs;
242    use crate::IxaError;
243    use clap::Parser;
244    use serde::Serialize;
245    use serde_derive::Deserialize;
246
247    #[derive(Parser, Debug, Serialize, Deserialize)]
248    pub enum Args {
249        /// Execute the next item in the event loop
250        Next,
251    }
252
253    #[derive(Serialize)]
254    pub(crate) enum Retval {
255        Ok,
256    }
257    #[allow(unused)]
258    pub(crate) struct Api {}
259    impl super::ExtApi for Api {
260        type Args = EmptyArgs;
261        type Retval = Retval;
262
263        fn run(_context: &mut Context, _args: &EmptyArgs) -> Result<Retval, IxaError> {
264            // This is a no-op which allows for arg checking.
265            Ok(Retval::Ok)
266        }
267    }
268}
269
270pub(crate) mod halt {
271    use crate::context::Context;
272    use crate::external_api::EmptyArgs;
273    use crate::IxaError;
274    use clap::Parser;
275    use serde::Serialize;
276    use serde_derive::Deserialize;
277
278    #[derive(Parser, Debug, Serialize, Deserialize)]
279    pub enum Args {
280        /// End the simulation
281        Halt,
282    }
283
284    #[derive(Serialize)]
285    pub(crate) enum Retval {
286        Ok,
287    }
288    #[allow(unused)]
289    pub(crate) struct Api {}
290    impl super::ExtApi for Api {
291        type Args = EmptyArgs;
292        type Retval = Retval;
293
294        fn run(_context: &mut Context, _args: &EmptyArgs) -> Result<Retval, IxaError> {
295            // This is a no-op which allows for arg checking.
296            Ok(Retval::Ok)
297        }
298    }
299}
300
301pub(crate) mod r#continue {
302    use crate::context::Context;
303    use crate::external_api::EmptyArgs;
304    use crate::IxaError;
305    use clap::Parser;
306    use serde_derive::{Deserialize, Serialize};
307
308    #[derive(Parser, Debug, Serialize, Deserialize)]
309    pub enum Args {
310        /// Continue running the simulation
311        Continue,
312    }
313
314    #[derive(Serialize)]
315    pub(crate) enum Retval {
316        Ok,
317    }
318    #[allow(unused)]
319    pub(crate) struct Api {}
320    impl super::ExtApi for Api {
321        type Args = EmptyArgs;
322        type Retval = Retval;
323
324        fn run(_context: &mut Context, _args: &EmptyArgs) -> Result<Retval, IxaError> {
325            // This is a no-op which allows for arg checking.
326            Ok(Retval::Ok)
327        }
328    }
329}
330
331pub(crate) mod people {
332    use crate::{HashMap, HashMapExt};
333    use std::cell::RefCell;
334
335    use crate::people::{external_api::ContextPeopleExtCrate, ContextPeopleExt, PersonId};
336    use crate::Context;
337    use crate::IxaError;
338    use clap::{Parser, Subcommand};
339    use serde::{Deserialize, Serialize};
340
341    fn person_id_from_str(s: &str) -> Result<PersonId, String> {
342        match s.parse::<usize>() {
343            Ok(id) => Ok(PersonId(id)),
344            Err(_) => Err("Person id must be an integer".to_string()),
345        }
346    }
347
348    #[derive(Subcommand, Clone, Debug, Serialize, Deserialize)]
349    pub(crate) enum ArgsEnum {
350        /// Get the value of a property for a person
351        Get {
352            #[arg(value_parser = person_id_from_str)]
353            person_id: PersonId,
354            property: String,
355        },
356        Query {
357            #[clap(skip)]
358            properties: Vec<(String, String)>,
359        },
360        /// Tabulate the values of a set of properties
361        Tabulate {
362            properties: Vec<String>,
363        },
364        Properties,
365    }
366
367    #[derive(Parser, Debug, Serialize, Deserialize)]
368    pub(crate) enum Args {
369        /// Access people properties
370        #[command(subcommand)]
371        People(ArgsEnum),
372    }
373
374    #[derive(Serialize, Debug, Eq, PartialEq)]
375    pub(crate) enum Retval {
376        Properties(Vec<(String, String)>),
377        Tabulated(Vec<(HashMap<String, String>, usize)>),
378        PropertyNames(Vec<String>),
379    }
380    pub(crate) struct Api {}
381
382    impl super::ExtApi for Api {
383        type Args = Args;
384        type Retval = Retval;
385
386        fn run(context: &mut Context, args: &Args) -> Result<Retval, IxaError> {
387            match args {
388                Args::People(args_enum) => match args_enum {
389                    ArgsEnum::Get {
390                        person_id,
391                        property,
392                    } => {
393                        if person_id.0 >= context.get_current_population() {
394                            return Err(IxaError::IxaError(format!(
395                                "No person with id {person_id:?}"
396                            )));
397                        }
398                        let value = context.get_person_property_by_name(property, *person_id)?;
399                        Ok(Retval::Properties(vec![(property.clone(), value)]))
400                    }
401                    ArgsEnum::Query { properties: _ } => Err(IxaError::IxaError(String::from(
402                        "People querying not implemented",
403                    ))),
404                    ArgsEnum::Tabulate { properties } => {
405                        let results = RefCell::new(Vec::new());
406
407                        context.tabulate_person_properties_by_name(
408                            properties.clone(),
409                            |_, values, count| {
410                                let mut hm = HashMap::new();
411                                for (key, value) in properties.iter().zip(values.iter()) {
412                                    hm.insert(key.clone(), value.clone());
413                                }
414                                results.borrow_mut().push((hm, count));
415                            },
416                        )?;
417                        Ok(Retval::Tabulated(results.take()))
418                    }
419                    ArgsEnum::Properties => {
420                        Ok(Retval::PropertyNames(context.get_person_property_names()))
421                    }
422                },
423            }
424        }
425    }
426
427    #[cfg(test)]
428    mod test {
429        use super::*;
430        use crate::external_api::run_ext_api;
431        use crate::{define_person_property, Context};
432        use crate::{HashSet, HashSetExt};
433        #[test]
434        fn query_nonexistent_user() {
435            let mut context = Context::new();
436
437            let res = run_ext_api::<super::Api>(
438                &mut context,
439                &Args::People(ArgsEnum::Get {
440                    person_id: PersonId(0),
441                    property: String::from("abc"),
442                }),
443            );
444
445            println!("{res:?}");
446            assert!(matches!(res, Err(IxaError::IxaError(_))));
447        }
448
449        #[test]
450        fn query_nonexistent_property() {
451            let mut context = Context::new();
452            let _ = context.add_person(());
453            let res = run_ext_api::<super::Api>(
454                &mut context,
455                &Args::People(ArgsEnum::Get {
456                    person_id: PersonId(0),
457                    property: String::from("abc"),
458                }),
459            );
460
461            println!("{res:?}");
462            assert!(matches!(res, Err(IxaError::IxaError(_))));
463        }
464
465        define_person_property!(Age, u8);
466
467        #[test]
468        fn query_valid_property() {
469            let mut context = Context::new();
470            let _ = context.add_person((Age, 10));
471            let res = run_ext_api::<super::Api>(
472                &mut context,
473                &Args::People(ArgsEnum::Get {
474                    person_id: PersonId(0),
475                    property: String::from("Age"),
476                }),
477            );
478
479            println!("{res:?}");
480            let res = res.unwrap();
481            #[allow(clippy::match_wildcard_for_single_variants)]
482            match res {
483                Retval::Properties(val) => {
484                    assert_eq!(val, vec![(String::from("Age"), String::from("10"))]);
485                }
486                _ => panic!("Unexpected result"),
487            }
488        }
489
490        #[test]
491        fn tabulate() {
492            let mut context = Context::new();
493            let _ = context.add_person((Age, 10));
494            let _ = context.add_person((Age, 20));
495
496            let res = run_ext_api::<super::Api>(
497                &mut context,
498                &Args::People(ArgsEnum::Tabulate {
499                    properties: vec![String::from("Age")],
500                }),
501            );
502            println!("{res:?}");
503            let res = res.unwrap();
504            let mut expected = HashSet::new();
505            expected.insert(String::from("10"));
506            expected.insert(String::from("20"));
507
508            #[allow(clippy::match_wildcard_for_single_variants)]
509            match res {
510                Retval::Tabulated(val) => {
511                    for (columns, ct) in val {
512                        assert_eq!(ct, 1);
513                        let age = columns.get("Age").unwrap();
514                        assert!(expected.remove(age));
515                    }
516                    assert_eq!(expected.len(), 0);
517                }
518                _ => panic!("Unexpected result"),
519            }
520        }
521
522        #[test]
523        fn get_person_property_names() {
524            let mut context = Context::new();
525            let _ = context.add_person((Age, 10));
526            let _ = context.add_person((Age, 20));
527
528            let res = run_ext_api::<super::Api>(&mut context, &Args::People(ArgsEnum::Properties));
529            println!("{res:?}");
530            let res = res.unwrap();
531
532            #[allow(clippy::match_wildcard_for_single_variants)]
533            match res {
534                Retval::PropertyNames(names) => assert_eq!(names, vec!["Age"]),
535                _ => panic!("Unexpected result"),
536            }
537        }
538    }
539}
540
541pub(crate) mod time {
542    #![allow(clippy::float_cmp)]
543    use crate::context::Context;
544    use crate::external_api::EmptyArgs;
545    use crate::IxaError;
546    use clap::Parser;
547    use serde::{Deserialize, Serialize};
548
549    pub(crate) struct Api {}
550    #[derive(Parser, Debug, Deserialize)]
551    pub(crate) enum Args {
552        /// Get the time of the simulation.
553        Time,
554    }
555
556    #[derive(Serialize)]
557    pub(crate) struct Retval {
558        pub time: f64,
559    }
560    impl super::ExtApi for Api {
561        type Args = super::EmptyArgs;
562        type Retval = Retval;
563
564        fn run(context: &mut Context, _args: &EmptyArgs) -> Result<Retval, IxaError> {
565            Ok(Retval {
566                time: context.get_current_time(),
567            })
568        }
569    }
570
571    #[cfg(test)]
572    mod test {
573        use crate::Context;
574
575        #[test]
576        fn test() {
577            let mut context = Context::new();
578
579            let result = crate::external_api::run_ext_api::<super::Api>(
580                &mut context,
581                &crate::external_api::EmptyArgs {},
582            );
583
584            assert_eq!(result.unwrap().time, 0.0);
585        }
586    }
587}