Skip to main content

argumentation_schemes/
registry.rs

1//! `CatalogRegistry`: an in-memory collection of schemes with lookup by
2//! name, id, and category.
3
4use crate::scheme::SchemeSpec;
5use crate::types::{SchemeCategory, SchemeId};
6use std::collections::HashMap;
7
8/// A collection of argumentation schemes, indexed for lookup.
9#[derive(Debug, Clone)]
10pub struct CatalogRegistry {
11    schemes: Vec<SchemeSpec>,
12    by_id: HashMap<SchemeId, usize>,
13    by_key: HashMap<String, usize>,
14}
15
16impl CatalogRegistry {
17    /// Create an empty registry.
18    pub fn new() -> Self {
19        Self {
20            schemes: Vec::new(),
21            by_id: HashMap::new(),
22            by_key: HashMap::new(),
23        }
24    }
25
26    /// Register a scheme. The last write wins for duplicate ids or keys —
27    /// the [`crate::catalog`] tests guarantee no duplicates exist in the
28    /// default catalog.
29    pub fn register(&mut self, scheme: SchemeSpec) {
30        let key = scheme.key();
31        let idx = self.schemes.len();
32        self.by_id.insert(scheme.id, idx);
33        self.by_key.insert(key, idx);
34        self.schemes.push(scheme);
35    }
36
37    /// Look up a scheme by its unique id.
38    pub fn by_id(&self, id: SchemeId) -> Option<&SchemeSpec> {
39        self.by_id.get(&id).map(|&idx| &self.schemes[idx])
40    }
41
42    /// Look up a scheme by its snake_case key (derived from the name).
43    pub fn by_key(&self, key: &str) -> Option<&SchemeSpec> {
44        self.by_key.get(key).map(|&idx| &self.schemes[idx])
45    }
46
47    /// Return all schemes in a given category.
48    pub fn by_category(&self, category: SchemeCategory) -> Vec<&SchemeSpec> {
49        self.schemes
50            .iter()
51            .filter(|s| s.category == category)
52            .collect()
53    }
54
55    /// Return all registered schemes.
56    pub fn all(&self) -> &[SchemeSpec] {
57        &self.schemes
58    }
59
60    /// Number of registered schemes.
61    pub fn len(&self) -> usize {
62        self.schemes.len()
63    }
64
65    /// Whether the registry is empty.
66    pub fn is_empty(&self) -> bool {
67        self.schemes.is_empty()
68    }
69
70    /// Build a registry preloaded with the canonical Walton (2008)
71    /// argumentation-scheme catalog.
72    ///
73    /// Distinct from `Default::default()`, which returns an empty
74    /// registry. Reach for this when you want the full scheme library;
75    /// reach for `Default::default()` or `new()` when you want an
76    /// empty registry to populate yourself.
77    #[must_use]
78    pub fn with_walton_catalog() -> Self {
79        crate::catalog::default_catalog()
80    }
81
82    /// Look up a scheme by its canonical (human-readable) name.
83    ///
84    /// Names are matched exactly (case-sensitive). Use [`Self::by_key`] to
85    /// look up by the derived snake_case key instead.
86    #[must_use]
87    pub fn by_name(&self, name: &str) -> Option<&SchemeSpec> {
88        self.schemes.iter().find(|s| s.name == name)
89    }
90}
91
92impl Default for CatalogRegistry {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::critical::CriticalQuestion;
102    use crate::scheme::*;
103    use crate::types::*;
104
105    fn test_scheme(id: u32, name: &str, cat: SchemeCategory) -> SchemeSpec {
106        SchemeSpec {
107            id: SchemeId(id),
108            name: name.into(),
109            category: cat,
110            premises: vec![PremiseSlot::new("p", "premise", SlotRole::Proposition)],
111            conclusion: ConclusionTemplate::positive("c", "?p"),
112            critical_questions: vec![CriticalQuestion::new(
113                1,
114                "?p?",
115                Challenge::PremiseTruth("p".into()),
116            )],
117            metadata: SchemeMetadata {
118                citation: "test".into(),
119                domain_tags: vec![],
120                presumptive: true,
121                strength: SchemeStrength::Moderate,
122            },
123        }
124    }
125
126    #[test]
127    fn register_and_lookup_by_id() {
128        let mut reg = CatalogRegistry::new();
129        reg.register(test_scheme(1, "Test Scheme", SchemeCategory::Epistemic));
130        assert!(reg.by_id(SchemeId(1)).is_some());
131        assert!(reg.by_id(SchemeId(99)).is_none());
132    }
133
134    #[test]
135    fn lookup_by_key_uses_snake_case_name() {
136        let mut reg = CatalogRegistry::new();
137        reg.register(test_scheme(
138            1,
139            "Argument from Expert Opinion",
140            SchemeCategory::Epistemic,
141        ));
142        assert!(reg.by_key("argument_from_expert_opinion").is_some());
143        assert!(reg.by_key("Argument from Expert Opinion").is_none());
144    }
145
146    #[test]
147    fn filter_by_category_returns_only_matching() {
148        let mut reg = CatalogRegistry::new();
149        reg.register(test_scheme(1, "Scheme A", SchemeCategory::Epistemic));
150        reg.register(test_scheme(2, "Scheme B", SchemeCategory::Practical));
151        reg.register(test_scheme(3, "Scheme C", SchemeCategory::Epistemic));
152        assert_eq!(reg.by_category(SchemeCategory::Epistemic).len(), 2);
153        assert_eq!(reg.by_category(SchemeCategory::Practical).len(), 1);
154        assert_eq!(reg.by_category(SchemeCategory::Causal).len(), 0);
155    }
156
157    #[test]
158    fn len_and_is_empty_track_registrations() {
159        let mut reg = CatalogRegistry::new();
160        assert!(reg.is_empty());
161        assert_eq!(reg.len(), 0);
162        reg.register(test_scheme(1, "A", SchemeCategory::Causal));
163        reg.register(test_scheme(2, "B", SchemeCategory::Causal));
164        assert!(!reg.is_empty());
165        assert_eq!(reg.len(), 2);
166    }
167}