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::handle_web_api_with_plugin;
113    use crate::{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                #[allow(unused_variables)]
184                ArgsEnum::Set { time, console } => {
185                    if *time < context.get_current_time() {
186                        return Err(IxaError::from(format!(
187                            "Breakpoint time {time} is in the past"
188                        )));
189                    }
190
191                    #[cfg(feature = "web_api")]
192                    if *console {
193                        context.schedule_debugger(*time, None, Box::new(enter_debugger));
194                    } else {
195                        context.schedule_debugger(
196                            *time,
197                            None,
198                            Box::new(handle_web_api_with_plugin),
199                        );
200                    }
201                    #[cfg(not(feature = "web_api"))]
202                    context.schedule_debugger(*time, None, Box::new(enter_debugger));
203
204                    trace!("Breakpoint set at t={time}");
205                    Ok(Retval::Ok)
206                }
207
208                ArgsEnum::Delete { id, all } => {
209                    if let Some(id) = id {
210                        assert!(!all);
211                        trace!("Deleting breakpoint {id}");
212                        let cancelled = context.delete_breakpoint(u64::from(*id));
213                        if cancelled.is_none() {
214                            Err(IxaError::from(format!(
215                                "Attempted to delete a nonexistent breakpoint {id}",
216                            )))
217                        } else {
218                            Ok(Retval::Ok)
219                        }
220                    } else {
221                        assert!(all);
222                        trace!("Deleting all breakpoints");
223                        context.clear_breakpoints();
224                        Ok(Retval::Ok)
225                    }
226                }
227
228                ArgsEnum::Disable => {
229                    trace!("Disabling all breakpoints");
230                    context.disable_breakpoints();
231                    Ok(Retval::Ok)
232                }
233
234                ArgsEnum::Enable => {
235                    trace!("Enabling all breakpoints");
236                    context.enable_breakpoints();
237                    Ok(Retval::Ok)
238                }
239            }
240        }
241    }
242}
243
244pub(crate) mod next {
245    use crate::context::Context;
246    use crate::external_api::EmptyArgs;
247    use crate::IxaError;
248    use clap::Parser;
249    use serde::Serialize;
250    use serde_derive::Deserialize;
251
252    #[derive(Parser, Debug, Serialize, Deserialize)]
253    pub enum Args {
254        /// Execute the next item in the event loop
255        Next,
256    }
257
258    #[derive(Serialize)]
259    pub(crate) enum Retval {
260        Ok,
261    }
262    #[allow(unused)]
263    pub(crate) struct Api {}
264    impl super::ExtApi for Api {
265        type Args = EmptyArgs;
266        type Retval = Retval;
267
268        fn run(_context: &mut Context, _args: &EmptyArgs) -> Result<Retval, IxaError> {
269            // This is a no-op which allows for arg checking.
270            Ok(Retval::Ok)
271        }
272    }
273}
274
275pub(crate) mod halt {
276    use crate::context::Context;
277    use crate::external_api::EmptyArgs;
278    use crate::IxaError;
279    use clap::Parser;
280    use serde::Serialize;
281    use serde_derive::Deserialize;
282
283    #[derive(Parser, Debug, Serialize, Deserialize)]
284    pub enum Args {
285        /// End the simulation
286        Halt,
287    }
288
289    #[derive(Serialize)]
290    pub(crate) enum Retval {
291        Ok,
292    }
293    #[allow(unused)]
294    pub(crate) struct Api {}
295    impl super::ExtApi for Api {
296        type Args = EmptyArgs;
297        type Retval = Retval;
298
299        fn run(_context: &mut Context, _args: &EmptyArgs) -> Result<Retval, IxaError> {
300            // This is a no-op which allows for arg checking.
301            Ok(Retval::Ok)
302        }
303    }
304}
305
306pub(crate) mod r#continue {
307    use crate::context::Context;
308    use crate::external_api::EmptyArgs;
309    use crate::IxaError;
310    use clap::Parser;
311    use serde_derive::{Deserialize, Serialize};
312
313    #[derive(Parser, Debug, Serialize, Deserialize)]
314    pub enum Args {
315        /// Continue running the simulation
316        Continue,
317    }
318
319    #[derive(Serialize)]
320    pub(crate) enum Retval {
321        Ok,
322    }
323    #[allow(unused)]
324    pub(crate) struct Api {}
325    impl super::ExtApi for Api {
326        type Args = EmptyArgs;
327        type Retval = Retval;
328
329        fn run(_context: &mut Context, _args: &EmptyArgs) -> Result<Retval, IxaError> {
330            // This is a no-op which allows for arg checking.
331            Ok(Retval::Ok)
332        }
333    }
334}
335
336pub(crate) mod people {
337    use crate::{HashMap, HashMapExt};
338    use std::cell::RefCell;
339
340    use crate::people::{external_api::ContextPeopleExtCrate, ContextPeopleExt, PersonId};
341    use crate::Context;
342    use crate::IxaError;
343    use clap::{Parser, Subcommand};
344    use serde::{Deserialize, Serialize};
345
346    fn person_id_from_str(s: &str) -> Result<PersonId, String> {
347        match s.parse::<usize>() {
348            Ok(id) => Ok(PersonId(id)),
349            Err(_) => Err("Person id must be an integer".to_string()),
350        }
351    }
352
353    #[derive(Subcommand, Clone, Debug, Serialize, Deserialize)]
354    pub(crate) enum ArgsEnum {
355        /// Get the value of a property for a person
356        Get {
357            #[arg(value_parser = person_id_from_str)]
358            person_id: PersonId,
359            property: String,
360        },
361        Query {
362            #[clap(skip)]
363            properties: Vec<(String, String)>,
364        },
365        /// Tabulate the values of a set of properties
366        Tabulate {
367            properties: Vec<String>,
368        },
369        Properties,
370    }
371
372    #[derive(Parser, Debug, Serialize, Deserialize)]
373    pub(crate) enum Args {
374        /// Access people properties
375        #[command(subcommand)]
376        People(ArgsEnum),
377    }
378
379    #[derive(Serialize, Debug, Eq, PartialEq)]
380    pub(crate) enum Retval {
381        Properties(Vec<(String, String)>),
382        Tabulated(Vec<(HashMap<String, String>, usize)>),
383        PropertyNames(Vec<String>),
384    }
385    pub(crate) struct Api {}
386
387    impl super::ExtApi for Api {
388        type Args = Args;
389        type Retval = Retval;
390
391        fn run(context: &mut Context, args: &Args) -> Result<Retval, IxaError> {
392            match args {
393                Args::People(args_enum) => match args_enum {
394                    ArgsEnum::Get {
395                        person_id,
396                        property,
397                    } => {
398                        if person_id.0 >= context.get_current_population() {
399                            return Err(IxaError::IxaError(format!(
400                                "No person with id {person_id:?}"
401                            )));
402                        }
403                        let value = context.get_person_property_by_name(property, *person_id)?;
404                        Ok(Retval::Properties(vec![(property.clone(), value)]))
405                    }
406                    ArgsEnum::Query { properties: _ } => Err(IxaError::IxaError(String::from(
407                        "People querying not implemented",
408                    ))),
409                    ArgsEnum::Tabulate { properties } => {
410                        let results = RefCell::new(Vec::new());
411
412                        context.tabulate_person_properties_by_name(
413                            properties.clone(),
414                            |_, values, count| {
415                                let mut hm = HashMap::new();
416                                for (key, value) in properties.iter().zip(values.iter()) {
417                                    hm.insert(key.clone(), value.clone());
418                                }
419                                results.borrow_mut().push((hm, count));
420                            },
421                        )?;
422                        Ok(Retval::Tabulated(results.take()))
423                    }
424                    ArgsEnum::Properties => {
425                        Ok(Retval::PropertyNames(context.get_person_property_names()))
426                    }
427                },
428            }
429        }
430    }
431
432    #[cfg(test)]
433    mod test {
434        use super::*;
435        use crate::external_api::run_ext_api;
436        use crate::{define_person_property, Context};
437        use crate::{HashSet, HashSetExt};
438        #[test]
439        fn query_nonexistent_user() {
440            let mut context = Context::new();
441
442            let res = run_ext_api::<super::Api>(
443                &mut context,
444                &Args::People(ArgsEnum::Get {
445                    person_id: PersonId(0),
446                    property: String::from("abc"),
447                }),
448            );
449
450            println!("{res:?}");
451            assert!(matches!(res, Err(IxaError::IxaError(_))));
452        }
453
454        #[test]
455        fn query_nonexistent_property() {
456            let mut context = Context::new();
457            let _ = context.add_person(());
458            let res = run_ext_api::<super::Api>(
459                &mut context,
460                &Args::People(ArgsEnum::Get {
461                    person_id: PersonId(0),
462                    property: String::from("abc"),
463                }),
464            );
465
466            println!("{res:?}");
467            assert!(matches!(res, Err(IxaError::IxaError(_))));
468        }
469
470        define_person_property!(Age, u8);
471
472        #[test]
473        fn query_valid_property() {
474            let mut context = Context::new();
475            let _ = context.add_person((Age, 10));
476            let res = run_ext_api::<super::Api>(
477                &mut context,
478                &Args::People(ArgsEnum::Get {
479                    person_id: PersonId(0),
480                    property: String::from("Age"),
481                }),
482            );
483
484            println!("{res:?}");
485            let res = res.unwrap();
486            #[allow(clippy::match_wildcard_for_single_variants)]
487            match res {
488                Retval::Properties(val) => {
489                    assert_eq!(val, vec![(String::from("Age"), String::from("10"))]);
490                }
491                _ => panic!("Unexpected result"),
492            }
493        }
494
495        #[test]
496        fn tabulate() {
497            let mut context = Context::new();
498            let _ = context.add_person((Age, 10));
499            let _ = context.add_person((Age, 20));
500
501            let res = run_ext_api::<super::Api>(
502                &mut context,
503                &Args::People(ArgsEnum::Tabulate {
504                    properties: vec![String::from("Age")],
505                }),
506            );
507            println!("{res:?}");
508            let res = res.unwrap();
509            let mut expected = HashSet::new();
510            expected.insert(String::from("10"));
511            expected.insert(String::from("20"));
512
513            #[allow(clippy::match_wildcard_for_single_variants)]
514            match res {
515                Retval::Tabulated(val) => {
516                    for (columns, ct) in val {
517                        assert_eq!(ct, 1);
518                        let age = columns.get("Age").unwrap();
519                        assert!(expected.remove(age));
520                    }
521                    assert_eq!(expected.len(), 0);
522                }
523                _ => panic!("Unexpected result"),
524            }
525        }
526
527        #[test]
528        fn get_person_property_names() {
529            let mut context = Context::new();
530            let _ = context.add_person((Age, 10));
531            let _ = context.add_person((Age, 20));
532
533            let res = run_ext_api::<super::Api>(&mut context, &Args::People(ArgsEnum::Properties));
534            println!("{res:?}");
535            let res = res.unwrap();
536
537            #[allow(clippy::match_wildcard_for_single_variants)]
538            match res {
539                Retval::PropertyNames(names) => assert_eq!(names, vec!["Age"]),
540                _ => panic!("Unexpected result"),
541            }
542        }
543    }
544}
545
546pub(crate) mod time {
547    #![allow(clippy::float_cmp)]
548    use crate::context::Context;
549    use crate::external_api::EmptyArgs;
550    use crate::IxaError;
551    use clap::Parser;
552    use serde::{Deserialize, Serialize};
553
554    pub(crate) struct Api {}
555    #[derive(Parser, Debug, Deserialize)]
556    pub(crate) enum Args {
557        /// Get the time of the simulation.
558        Time,
559    }
560
561    #[derive(Serialize)]
562    pub(crate) struct Retval {
563        pub time: f64,
564    }
565    impl super::ExtApi for Api {
566        type Args = super::EmptyArgs;
567        type Retval = Retval;
568
569        fn run(context: &mut Context, _args: &EmptyArgs) -> Result<Retval, IxaError> {
570            Ok(Retval {
571                time: context.get_current_time(),
572            })
573        }
574    }
575
576    #[cfg(test)]
577    mod test {
578        use crate::Context;
579
580        #[test]
581        fn test() {
582            let mut context = Context::new();
583
584            let result = crate::external_api::run_ext_api::<super::Api>(
585                &mut context,
586                &crate::external_api::EmptyArgs {},
587            );
588
589            assert_eq!(result.unwrap().time, 0.0);
590        }
591    }
592}