Skip to main content

sim_lib_skill/
registry.rs

1use std::{
2    collections::BTreeMap,
3    sync::{Arc, Mutex},
4};
5
6use sim_citizen_derive::non_citizen;
7use sim_kernel::{Cx, Error, Object, ObjectCompat, Result, Symbol, Value};
8
9#[cfg(any(feature = "cache", feature = "cassette"))]
10use crate::record::SkillAuditEntry;
11use crate::{SkillCallable, SkillCard, SkillTransport, SkillTransportValue};
12
13/// Live registry of skill transports and bound cards.
14///
15/// The registry owns the installed [`SkillTransport`]s and the [`SkillCard`]s
16/// bound to them, and (with the `cache`/`cassette` features) the audit log of
17/// skill calls. It is a cheaply clonable handle over shared state, so clones
18/// observe the same registry. It is a live handle rather than a serializable
19/// value; cards are projected through the `skill/Card` descriptor.
20#[derive(Clone, Default)]
21#[non_citizen(
22    reason = "live skill registry handle; cards use skill/Card descriptor",
23    kind = "handle"
24)]
25pub struct SkillRegistry {
26    state: Arc<Mutex<SkillRegistryState>>,
27}
28
29#[derive(Default)]
30struct SkillRegistryState {
31    transports: BTreeMap<String, Arc<dyn SkillTransport>>,
32    cards: BTreeMap<String, SkillCard>,
33    #[cfg(any(feature = "cache", feature = "cassette"))]
34    audit: Vec<SkillAuditEntry>,
35}
36
37impl SkillRegistry {
38    /// Installs `transport`, keyed by its id, replacing any prior transport
39    /// with the same id.
40    pub fn install_transport(&self, transport: Arc<dyn SkillTransport>) -> Result<()> {
41        self.state
42            .lock()
43            .map_err(|_| Error::PoisonedLock("skill registry"))?
44            .transports
45            .insert(transport.id().to_owned(), transport);
46        Ok(())
47    }
48
49    /// Binds `card` to its installed transport and registers it as a callable.
50    ///
51    /// Resolves the card's transport, builds a [`SkillCallable`], registers it
52    /// under the card's symbol, publishes its browse claims, and stores the
53    /// card. Errors if the card's transport is not installed. Returns the
54    /// registered callable value.
55    pub fn bind_card(&self, cx: &mut Cx, card: SkillCard) -> Result<Value> {
56        let transport = {
57            let state = self
58                .state
59                .lock()
60                .map_err(|_| Error::PoisonedLock("skill registry"))?;
61            state
62                .transports
63                .get(&card.transport_id)
64                .cloned()
65                .ok_or_else(|| {
66                    Error::Eval(format!("missing skill transport {}", card.transport_id))
67                })?
68        };
69        #[cfg(any(feature = "cache", feature = "cassette"))]
70        let skill_callable = SkillCallable::new_bound(card.clone(), transport, self.clone());
71        #[cfg(not(any(feature = "cache", feature = "cassette")))]
72        let skill_callable = SkillCallable::new(card.clone(), transport);
73        let callable = cx.factory().opaque(Arc::new(skill_callable))?;
74        cx.registry_mut()
75            .register_function_value(card.symbol.clone(), callable.clone())?;
76        crate::browse::publish_card_claims(cx, &card)?;
77        self.state
78            .lock()
79            .map_err(|_| Error::PoisonedLock("skill registry"))?
80            .cards
81            .insert(card.id.clone(), card);
82        Ok(callable)
83    }
84
85    /// Returns all bound cards.
86    pub fn cards(&self) -> Result<Vec<SkillCard>> {
87        Ok(self
88            .state
89            .lock()
90            .map_err(|_| Error::PoisonedLock("skill registry"))?
91            .cards
92            .values()
93            .cloned()
94            .collect())
95    }
96
97    /// Looks up a bound card by its `id`.
98    pub fn card_by_id(&self, id: &str) -> Result<Option<SkillCard>> {
99        Ok(self
100            .state
101            .lock()
102            .map_err(|_| Error::PoisonedLock("skill registry"))?
103            .cards
104            .get(id)
105            .cloned())
106    }
107
108    /// Looks up a bound card by its registered `symbol`.
109    pub fn card_by_symbol(&self, symbol: &Symbol) -> Result<Option<SkillCard>> {
110        Ok(self
111            .state
112            .lock()
113            .map_err(|_| Error::PoisonedLock("skill registry"))?
114            .cards
115            .values()
116            .find(|card| &card.symbol == symbol)
117            .cloned())
118    }
119
120    #[cfg(any(feature = "cache", feature = "cassette"))]
121    pub(crate) fn record_audit(&self, entry: SkillAuditEntry) -> Result<()> {
122        self.state
123            .lock()
124            .map_err(|_| Error::PoisonedLock("skill registry"))?
125            .audit
126            .push(entry);
127        Ok(())
128    }
129
130    #[cfg(any(feature = "cache", feature = "cassette"))]
131    pub(crate) fn audit_values(&self, cx: &mut Cx) -> Result<Value> {
132        let entries = self
133            .state
134            .lock()
135            .map_err(|_| Error::PoisonedLock("skill registry"))?
136            .audit
137            .clone();
138        let values = entries
139            .iter()
140            .map(|entry| entry.value(cx))
141            .collect::<Result<Vec<_>>>()?;
142        cx.factory().list(values)
143    }
144}
145
146impl Object for SkillRegistry {
147    fn display(&self, _cx: &mut Cx) -> Result<String> {
148        Ok("#<skill-registry>".to_owned())
149    }
150
151    fn as_any(&self) -> &dyn std::any::Any {
152        self
153    }
154}
155
156impl ObjectCompat for SkillRegistry {
157    fn as_table(&self, cx: &mut Cx) -> Result<Value> {
158        cx.factory().table(vec![
159            (
160                Symbol::new("kind"),
161                cx.factory().symbol(Symbol::new("skill/registry"))?,
162            ),
163            (
164                Symbol::new("cards"),
165                cx.factory().string(self.cards()?.len().to_string())?,
166            ),
167        ])
168    }
169}
170
171/// Returns the symbol the [`SkillRegistry`] value is bound under.
172pub fn skill_registry_symbol() -> Symbol {
173    Symbol::qualified("skill", "registry")
174}
175
176/// Resolves the [`SkillRegistry`] installed in `cx`.
177///
178/// Errors if the registry value is missing or is not a registry.
179pub fn skill_registry(cx: &mut Cx) -> Result<SkillRegistry> {
180    let value = cx.resolve_value(&skill_registry_symbol())?;
181    value
182        .object()
183        .downcast_ref::<SkillRegistry>()
184        .cloned()
185        .ok_or(Error::TypeMismatch {
186            expected: "skill registry",
187            found: "non-registry",
188        })
189}
190
191pub(crate) fn transport_from_value(value: &Value) -> Result<Arc<dyn SkillTransport>> {
192    value
193        .object()
194        .downcast_ref::<SkillTransportValue>()
195        .map(SkillTransportValue::transport)
196        .ok_or(Error::TypeMismatch {
197            expected: "skill transport",
198            found: "non-transport",
199        })
200}
201
202pub(crate) fn card_from_value(cx: &mut Cx, value: &Value) -> Result<SkillCard> {
203    if let Some(card) = value.object().downcast_ref::<SkillCard>() {
204        return Ok(card.clone());
205    }
206    let expr = value.object().as_expr(cx)?;
207    SkillCard::from_expr(&expr)
208}