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