daedalus_data/
typing.rs

1use crate::model::{EnumVariant, TypeExpr, ValueType};
2use std::any::{TypeId, type_name};
3use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
4use std::sync::OnceLock;
5use std::sync::RwLock;
6
7/// Registered mapping between a Rust type name and a `TypeExpr`.
8///
9/// ```
10/// use daedalus_data::model::{TypeExpr, ValueType};
11/// use daedalus_data::typing::register_type;
12///
13/// register_type::<u32>(TypeExpr::Scalar(ValueType::U32));
14/// ```
15#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
16pub struct RegisteredType {
17    pub rust: String,
18    pub expr: TypeExpr,
19}
20
21struct TypeRegistry {
22    by_type_id: HashMap<TypeId, RegisteredType>,
23    by_rust_name: HashMap<String, TypeExpr>,
24}
25
26fn registry() -> &'static RwLock<TypeRegistry> {
27    static REGISTRY: OnceLock<RwLock<TypeRegistry>> = OnceLock::new();
28    REGISTRY.get_or_init(|| {
29        RwLock::new(TypeRegistry {
30            by_type_id: HashMap::new(),
31            by_rust_name: HashMap::new(),
32        })
33    })
34}
35
36#[derive(Clone, Debug)]
37struct CompatRegistry {
38    edges: BTreeMap<TypeExpr, BTreeSet<TypeExpr>>,
39    resolved: BTreeMap<(TypeExpr, TypeExpr), bool>,
40}
41
42fn compat_registry() -> &'static RwLock<CompatRegistry> {
43    static REGISTRY: OnceLock<RwLock<CompatRegistry>> = OnceLock::new();
44    REGISTRY.get_or_init(|| {
45        RwLock::new(CompatRegistry {
46            edges: BTreeMap::new(),
47            resolved: BTreeMap::new(),
48        })
49    })
50}
51
52fn normalize_rust_type_name(raw: &str) -> String {
53    raw.chars().filter(|c| !c.is_whitespace()).collect()
54}
55
56fn rust_type_key<T: 'static>() -> String {
57    normalize_rust_type_name(type_name::<T>())
58}
59
60/// Register a concrete `TypeExpr` for a Rust type `T`.
61///
62/// This is how applications/plugins can attach richer type information to
63/// structured payloads without requiring proc-macro hard-coding.
64///
65/// ```
66/// use daedalus_data::model::{TypeExpr, ValueType};
67/// use daedalus_data::typing::{register_type, lookup_type};
68///
69/// register_type::<u16>(TypeExpr::Scalar(ValueType::U32));
70/// assert!(lookup_type::<u16>().is_some());
71/// ```
72pub fn register_type<T: 'static>(expr: TypeExpr) {
73    let rust = rust_type_key::<T>();
74    let expr = expr.normalize();
75
76    let mut guard = registry()
77        .write()
78        .expect("daedalus_data::typing registry lock poisoned");
79    guard.by_rust_name.insert(rust.clone(), expr.clone());
80    guard
81        .by_type_id
82        .insert(TypeId::of::<T>(), RegisteredType { rust, expr });
83}
84
85/// Register a type-compatibility edge from `from` to `to`.
86///
87/// Compatibility is directional; call twice for bidirectional compatibility.
88///
89/// ```
90/// use daedalus_data::model::{TypeExpr, ValueType};
91/// use daedalus_data::typing::{register_compatibility, can_convert_typeexpr};
92///
93/// register_compatibility(
94///     TypeExpr::Scalar(ValueType::I32),
95///     TypeExpr::Scalar(ValueType::Int),
96/// );
97/// assert!(can_convert_typeexpr(
98///     &TypeExpr::Scalar(ValueType::I32),
99///     &TypeExpr::Scalar(ValueType::Int),
100/// ));
101/// ```
102pub fn register_compatibility(from: TypeExpr, to: TypeExpr) {
103    let from = from.normalize();
104    let to = to.normalize();
105    let mut guard = compat_registry()
106        .write()
107        .expect("daedalus_data::typing compatibility registry lock poisoned");
108    guard.edges.entry(from).or_default().insert(to);
109    guard.resolved.clear();
110}
111
112/// Return true if `from` can be coerced into `to` based on registered compatibility.
113///
114/// ```
115/// use daedalus_data::model::{TypeExpr, ValueType};
116/// use daedalus_data::typing::can_convert_typeexpr;
117///
118/// assert!(can_convert_typeexpr(
119///     &TypeExpr::Scalar(ValueType::Int),
120///     &TypeExpr::Scalar(ValueType::Int),
121/// ));
122/// ```
123pub fn can_convert_typeexpr(from: &TypeExpr, to: &TypeExpr) -> bool {
124    let from = from.clone().normalize();
125    let to = to.clone().normalize();
126    if from == to {
127        return true;
128    }
129    let mut guard = compat_registry()
130        .write()
131        .expect("daedalus_data::typing compatibility registry lock poisoned");
132    if let Some(cached) = guard.resolved.get(&(from.clone(), to.clone())) {
133        return *cached;
134    }
135    let mut queue: VecDeque<TypeExpr> = VecDeque::new();
136    let mut seen: BTreeSet<TypeExpr> = BTreeSet::new();
137    queue.push_back(from.clone());
138    seen.insert(from.clone());
139    let mut found = false;
140    while let Some(cur) = queue.pop_front() {
141        if cur == to {
142            found = true;
143            break;
144        }
145        if let Some(nexts) = guard.edges.get(&cur) {
146            for next in nexts {
147                if seen.insert(next.clone()) {
148                    queue.push_back(next.clone());
149                }
150            }
151        }
152    }
153    guard.resolved.insert((from, to), found);
154    found
155}
156
157/// Register an enum (variants only) for Rust type `T`.
158///
159/// ```
160/// use daedalus_data::typing::{register_enum, lookup_type};
161/// register_enum::<bool>(["Yes", "No"]);
162/// assert!(lookup_type::<bool>().is_some());
163/// ```
164pub fn register_enum<T: 'static>(variants: impl IntoIterator<Item = impl Into<String>>) {
165    let variants = variants
166        .into_iter()
167        .map(|name| EnumVariant {
168            name: name.into(),
169            ty: None,
170        })
171        .collect();
172    register_type::<T>(TypeExpr::Enum(variants));
173}
174
175/// Look up a previously registered `TypeExpr` for a Rust type `T`.
176///
177/// ```
178/// use daedalus_data::model::{TypeExpr, ValueType};
179/// use daedalus_data::typing::{register_type, lookup_type};
180/// register_type::<u8>(TypeExpr::Scalar(ValueType::U32));
181/// let found = lookup_type::<u8>().unwrap();
182/// assert!(matches!(found, TypeExpr::Scalar(_)));
183/// ```
184pub fn lookup_type<T: 'static>() -> Option<TypeExpr> {
185    let guard = registry()
186        .read()
187        .expect("daedalus_data::typing registry lock poisoned");
188    guard
189        .by_type_id
190        .get(&TypeId::of::<T>())
191        .map(|v| v.expr.clone())
192}
193
194fn builtin_type_expr<T: 'static>() -> Option<TypeExpr> {
195    let tid = TypeId::of::<T>();
196    let scalar = |v| TypeExpr::Scalar(v);
197
198    if tid == TypeId::of::<()>() {
199        return Some(scalar(ValueType::Unit));
200    }
201    if tid == TypeId::of::<bool>() {
202        return Some(scalar(ValueType::Bool));
203    }
204
205    if tid == TypeId::of::<i8>()
206        || tid == TypeId::of::<i16>()
207        || tid == TypeId::of::<i32>()
208        || tid == TypeId::of::<i64>()
209        || tid == TypeId::of::<i128>()
210        || tid == TypeId::of::<isize>()
211        || tid == TypeId::of::<u8>()
212        || tid == TypeId::of::<u16>()
213        || tid == TypeId::of::<u32>()
214        || tid == TypeId::of::<u64>()
215        || tid == TypeId::of::<u128>()
216        || tid == TypeId::of::<usize>()
217    {
218        return Some(scalar(ValueType::Int));
219    }
220
221    if tid == TypeId::of::<f32>() || tid == TypeId::of::<f64>() {
222        return Some(scalar(ValueType::Float));
223    }
224
225    if tid == TypeId::of::<String>() {
226        return Some(scalar(ValueType::String));
227    }
228
229    if tid == TypeId::of::<Vec<u8>>() {
230        return Some(scalar(ValueType::Bytes));
231    }
232
233    None
234}
235
236/// Return an explicit type expression if `T` has either been registered or is
237/// covered by built-in mappings (without falling back to `Opaque`).
238///
239/// ```
240/// use daedalus_data::typing::override_type_expr;
241/// use daedalus_data::model::TypeExpr;
242/// let ty = override_type_expr::<u32>().unwrap();
243/// assert!(matches!(ty, TypeExpr::Scalar(_)));
244/// ```
245pub fn override_type_expr<T: 'static>() -> Option<TypeExpr> {
246    lookup_type::<T>().or_else(builtin_type_expr::<T>)
247}
248
249/// Returns the best-effort `TypeExpr` for a Rust type `T`.
250///
251/// Resolution order:
252/// 1) `register_type::<T>(...)`
253/// 2) Built-in primitives and common shims (e.g. `Vec<u8>` as `Bytes`)
254/// 3) `Opaque("rust:<type_name>")` fallback
255///
256/// ```
257/// use daedalus_data::typing::type_expr;
258/// let ty = type_expr::<String>();
259/// assert!(format!("{ty:?}").contains("String"));
260/// ```
261pub fn type_expr<T: 'static>() -> TypeExpr {
262    if let Some(expr) = override_type_expr::<T>() {
263        return expr;
264    }
265    TypeExpr::Opaque(format!("rust:{}", rust_type_key::<T>()))
266}
267
268/// Look up a previously registered `TypeExpr` by Rust type name (whitespace is ignored).
269///
270/// This is stable across dylib/plugin boundaries where `TypeId` differs but `type_name::<T>()`
271/// is identical (compiled from the same sources).
272pub fn lookup_type_by_rust_name(raw: &str) -> Option<TypeExpr> {
273    let key = normalize_rust_type_name(raw);
274    let guard = registry()
275        .read()
276        .expect("daedalus_data::typing registry lock poisoned");
277    guard.by_rust_name.get(&key).cloned()
278}
279
280/// Snapshot the current registry as a list keyed by Rust type name.
281///
282/// Intended for UIs and tooling (e.g. exposing enum/struct definitions registered by plugins).
283pub fn snapshot_by_rust_name() -> Vec<RegisteredType> {
284    let guard = registry()
285        .read()
286        .expect("daedalus_data::typing registry lock poisoned");
287    let mut out: Vec<RegisteredType> = guard
288        .by_rust_name
289        .iter()
290        .map(|(rust, expr)| RegisteredType {
291            rust: rust.clone(),
292            expr: expr.clone(),
293        })
294        .collect();
295    out.sort_by(|a, b| a.rust.cmp(&b.rust));
296    out
297}