perspt_coding/
registry.rs1use std::collections::BTreeMap;
11
12use serde::{Deserialize, Serialize};
13
14use crate::lang::{LanguageAdapter, PythonAdapter, RustAdapter, TypeScriptAdapter};
15
16#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
18pub struct LanguageId(pub String);
19
20impl LanguageId {
21 pub fn new(id: impl Into<String>) -> Self {
22 Self(id.into().to_ascii_lowercase())
23 }
24}
25
26impl std::fmt::Display for LanguageId {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 f.write_str(&self.0)
29 }
30}
31
32pub struct CodingAdapterRegistry {
34 adapters: BTreeMap<LanguageId, Box<dyn LanguageAdapter>>,
35}
36
37impl std::fmt::Debug for CodingAdapterRegistry {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 f.debug_struct("CodingAdapterRegistry")
40 .field("languages", &self.language_ids())
41 .finish()
42 }
43}
44
45impl CodingAdapterRegistry {
46 pub fn empty() -> Self {
48 Self {
49 adapters: BTreeMap::new(),
50 }
51 }
52
53 pub fn with_builtins() -> Self {
55 let mut registry = Self::empty();
56 let builtins: [Box<dyn LanguageAdapter>; 3] = [
57 Box::new(RustAdapter),
58 Box::new(PythonAdapter),
59 Box::new(TypeScriptAdapter),
60 ];
61 for adapter in builtins {
62 registry.register(adapter.id(), adapter);
63 }
64 registry
65 }
66
67 pub fn register(&mut self, id: LanguageId, adapter: Box<dyn LanguageAdapter>) {
69 self.adapters.insert(id, adapter);
70 }
71
72 pub fn get(&self, id: &LanguageId) -> Option<&dyn LanguageAdapter> {
74 self.adapters.get(id).map(|a| a.as_ref())
75 }
76
77 pub fn language_ids(&self) -> Vec<LanguageId> {
79 self.adapters.keys().cloned().collect()
80 }
81}
82
83impl Default for CodingAdapterRegistry {
84 fn default() -> Self {
85 Self::with_builtins()
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use perspt_sdk::{CorrectionDirection, IndependenceRoute, ResidualEvent, SensorRef};
93
94 struct GleamAdapter;
98
99 impl LanguageAdapter for GleamAdapter {
100 fn id(&self) -> LanguageId {
101 LanguageId::new("gleam")
102 }
103 fn diagnostic_sensor(&self) -> SensorRef {
104 SensorRef::new("gleam-check", IndependenceRoute::DeterministicTool)
105 }
106 fn parse_diagnostics(&self, _n: &str, _g: u32, _raw: &str) -> Vec<ResidualEvent> {
107 Vec::new()
108 }
109 fn correction_for(&self, _r: &ResidualEvent) -> Option<CorrectionDirection> {
110 None
111 }
112 }
113
114 #[test]
115 fn builtins_resolve_by_id() {
116 let registry = CodingAdapterRegistry::with_builtins();
117 for id in ["rust", "python", "typescript"] {
118 assert!(registry.get(&LanguageId::new(id)).is_some(), "{id}");
119 }
120 assert!(registry.get(&LanguageId::new("cobol")).is_none());
121 }
122
123 #[test]
124 fn a_new_language_registers_without_editing_the_orchestrator() {
125 let mut registry = CodingAdapterRegistry::with_builtins();
126 registry.register(LanguageId::new("gleam"), Box::new(GleamAdapter));
127 assert!(registry.get(&LanguageId::new("gleam")).is_some());
128 assert_eq!(registry.language_ids().len(), 4);
129 }
130
131 #[test]
132 fn ids_are_case_insensitive_by_construction() {
133 assert_eq!(LanguageId::new("Rust"), LanguageId::new("rust"));
134 }
135}