1use std::{
2 collections::BTreeMap,
3 sync::{
4 Mutex,
5 atomic::{AtomicUsize, Ordering},
6 },
7};
8
9use sim_kernel::{Cx, Error, Expr, Result, Symbol, Value};
10
11use crate::{SkillCard, SkillEventSink, SkillTransport};
12
13#[derive(Clone, Debug)]
15pub enum FixtureBehavior {
16 EchoArgs,
18 SumNumbers,
20 ConstantString(String),
22}
23
24pub struct FixtureTransport {
29 id: String,
30 handlers: Mutex<BTreeMap<String, FixtureBehavior>>,
31 calls: AtomicUsize,
32}
33
34impl FixtureTransport {
35 pub fn new(id: impl Into<String>) -> Self {
37 Self {
38 id: id.into(),
39 handlers: Mutex::new(BTreeMap::new()),
40 calls: AtomicUsize::new(0),
41 }
42 }
43
44 pub fn insert(&self, operation: impl Into<String>, behavior: FixtureBehavior) -> Result<()> {
46 self.handlers
47 .lock()
48 .map_err(|_| Error::PoisonedLock("fixture skill transport"))?
49 .insert(operation.into(), behavior);
50 Ok(())
51 }
52
53 pub fn call_count(&self) -> usize {
55 self.calls.load(Ordering::SeqCst)
56 }
57}
58
59impl SkillTransport for FixtureTransport {
60 fn id(&self) -> &str {
61 &self.id
62 }
63
64 fn kind(&self) -> &str {
65 "fixture"
66 }
67
68 fn discover(&self, _cx: &mut Cx) -> Result<Vec<SkillCard>> {
69 Ok(Vec::new())
70 }
71
72 fn call(
73 &self,
74 cx: &mut Cx,
75 card: &SkillCard,
76 args: Value,
77 _events: Option<&mut dyn SkillEventSink>,
78 ) -> Result<Value> {
79 let behavior = self
80 .handlers
81 .lock()
82 .map_err(|_| Error::PoisonedLock("fixture skill transport"))?
83 .get(&card.operation)
84 .cloned()
85 .ok_or_else(|| {
86 Error::Eval(format!(
87 "fixture transport {} has no operation {}",
88 self.id, card.operation
89 ))
90 })?;
91 self.calls.fetch_add(1, Ordering::SeqCst);
92 match behavior {
93 FixtureBehavior::EchoArgs => Ok(args),
94 FixtureBehavior::SumNumbers => sum_number_args(cx, args),
95 FixtureBehavior::ConstantString(text) => cx.factory().string(text),
96 }
97 }
98
99 fn health(&self, cx: &mut Cx) -> Result<Value> {
100 cx.factory().table(vec![
101 (
102 Symbol::new("kind"),
103 cx.factory().symbol(Symbol::new("skill/health"))?,
104 ),
105 (Symbol::new("id"), cx.factory().string(self.id.clone())?),
106 (
107 Symbol::new("calls"),
108 cx.factory().string(self.call_count().to_string())?,
109 ),
110 ])
111 }
112}
113
114fn sum_number_args(cx: &mut Cx, args: Value) -> Result<Value> {
115 let Expr::List(items) = args.object().as_expr(cx)? else {
116 return Err(Error::TypeMismatch {
117 expected: "argument list",
118 found: "non-list",
119 });
120 };
121 let mut sum = 0.0;
122 for item in items {
123 let Expr::Number(number) = item else {
124 return Err(Error::TypeMismatch {
125 expected: "number",
126 found: "non-number",
127 });
128 };
129 sum += number
130 .canonical
131 .parse::<f64>()
132 .map_err(|err| Error::Eval(format!("invalid number literal: {err}")))?;
133 }
134 let canonical = if sum.fract() == 0.0 {
135 format!("{}", sum as i64)
136 } else {
137 sum.to_string()
138 };
139 cx.factory()
140 .number_literal(Symbol::qualified("numbers", "f64"), canonical)
141}