Skip to main content

derive_defs/
resolver.rs

1//! Definition resolver with inheritance support.
2//!
3//! This module handles resolving `extends` relationships between definitions,
4//! including cycle detection and merging of traits and attributes.
5//!
6//! # Resolution Algorithm
7//!
8//! Uses topological sort (Kahn's algorithm) for:
9//! 1. Cycle detection
10//! 2. Deterministic resolution order (parents before children)
11//! 3. Efficient O(V + E) complexity
12//!
13//! # Example
14//!
15//! ```toml
16//! [defs.base]
17//! traits = ["Debug", "Clone"]
18//!
19//! [defs.child]
20//! extends = "base"
21//! traits = ["PartialEq", "Clone"]  # Clone will be deduplicated
22//! ```
23//!
24//! After resolution, `child` will have traits: `["Debug", "Clone", "PartialEq"]`
25
26use std::collections::{HashMap, HashSet, VecDeque};
27
28use crate::parser::{Config, Def};
29use crate::validation;
30use crate::{Error, Result};
31
32/// A fully resolved definition with all inheritance applied.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct ResolvedDef {
35    /// The name of the definition.
36    pub name: String,
37
38    /// List of derive traits (including inherited).
39    pub traits: Vec<String>,
40
41    /// Additional attributes (including inherited).
42    pub attrs: Vec<String>,
43}
44
45/// Resolved configuration with all definitions fully expanded.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct ResolvedConfig {
48    /// All resolved definitions.
49    pub defs: HashMap<String, ResolvedDef>,
50}
51
52/// Resolve all definitions using topological sort.
53///
54/// # Algorithm
55///
56/// 1. Build dependency graph (def -> parent)
57/// 2. Calculate in-degrees
58/// 3. Kahn's algorithm for topological sort
59/// 4. If nodes remain → cycle detected
60/// 5. Resolve in topological order
61///
62/// # Errors
63///
64/// Returns an error if:
65/// - A circular dependency is detected
66/// - A parent definition does not exist
67///
68/// # Example
69///
70/// ```
71/// use derive_defs::parser;
72/// use derive_defs::resolver;
73///
74/// let toml = r#"
75/// [defs.base]
76/// traits = ["Debug", "Clone"]
77///
78/// [defs.child]
79/// extends = "base"
80/// traits = ["PartialEq"]
81/// "#;
82///
83/// let config = parser::parse(toml).unwrap();
84/// let resolved = resolver::resolve(&config).unwrap();
85///
86/// let child = resolved.defs.get("child").unwrap();
87/// assert_eq!(child.traits, vec!["Debug", "Clone", "PartialEq"]);
88/// ```
89/// # Panics
90///
91/// Panics if the internal graph structure is corrupted (should never happen).
92pub fn resolve(config: &Config) -> Result<ResolvedConfig> {
93    let defs = &config.defs;
94
95    // Validate all parents exist before building graph
96    for (name, def) in defs {
97        if let Some(ref parent) = def.extends
98            && !defs.contains_key(parent)
99        {
100            return Err(Error::UndefinedParent {
101                def: name.clone(),
102                parent: parent.clone(),
103            });
104        }
105    }
106
107    // Build adjacency list: parent -> [children]
108    let mut graph: HashMap<&str, Vec<&str>> = HashMap::new();
109    let mut in_degree: HashMap<&str, usize> = HashMap::new();
110
111    for name in defs.keys() {
112        in_degree.insert(name, 0);
113    }
114
115    for (name, def) in defs {
116        if let Some(ref parent) = def.extends {
117            graph.entry(parent).or_default().push(name);
118            *in_degree.entry(name).or_insert(0) += 1;
119        }
120    }
121
122    // Kahn's algorithm
123    let mut queue: VecDeque<&str> = in_degree
124        .iter()
125        .filter(|(_, deg)| **deg == 0)
126        .map(|(name, _)| *name)
127        .collect();
128
129    let mut topo_order: Vec<&str> = Vec::with_capacity(defs.len());
130
131    while let Some(node) = queue.pop_front() {
132        topo_order.push(node);
133
134        if let Some(children) = graph.get(node) {
135            for &child in children {
136                let deg = in_degree.get_mut(child).unwrap();
137                *deg -= 1;
138                if *deg == 0 {
139                    queue.push_back(child);
140                }
141            }
142        }
143    }
144
145    // Check for cycles
146    if topo_order.len() != defs.len() {
147        let remaining: Vec<_> = in_degree
148            .keys()
149            .filter(|&&name| !topo_order.contains(&name))
150            .copied()
151            .collect();
152        return Err(Error::CircularDependency(format_cycle(&remaining, defs)));
153    }
154
155    // Resolve in topological order
156    let mut resolved = HashMap::with_capacity(defs.len());
157
158    for name in topo_order {
159        let def = defs.get(name).unwrap();
160        let resolved_def = resolve_single(name, def, defs, &resolved)?;
161        resolved.insert(name.to_string(), resolved_def);
162    }
163
164    Ok(ResolvedConfig { defs: resolved })
165}
166
167/// Resolve a single definition by merging with parent.
168fn resolve_single(
169    name: &str,
170    def: &Def,
171    _defs: &HashMap<String, Def>,
172    resolved: &HashMap<String, ResolvedDef>,
173) -> Result<ResolvedDef> {
174    let (traits, attrs) = if let Some(ref parent_name) = def.extends {
175        let parent = resolved
176            .get(parent_name)
177            .ok_or_else(|| Error::Validation(format!("parent '{parent_name}' not resolved")))?;
178
179        (
180            merge_unique(&parent.traits, &def.traits),
181            merge_unique(&parent.attrs, &def.attrs),
182        )
183    } else {
184        (def.traits.clone(), def.attrs.clone())
185    };
186
187    // Validate trait names
188    validation::validate_trait_names(&traits)?;
189
190    Ok(ResolvedDef {
191        name: name.to_string(),
192        traits,
193        attrs,
194    })
195}
196
197/// Merge two slices, keeping unique items with parent items first.
198fn merge_unique(parent: &[String], child: &[String]) -> Vec<String> {
199    let mut seen = HashSet::with_capacity(parent.len() + child.len());
200    let mut result = Vec::with_capacity(parent.len() + child.len());
201
202    for item in parent.iter().chain(child.iter()) {
203        if seen.insert(item.clone()) {
204            result.push(item.clone());
205        }
206    }
207
208    result
209}
210
211/// Format cycle for error message.
212fn format_cycle(nodes: &[&str], _defs: &HashMap<String, Def>) -> String {
213    if nodes.is_empty() {
214        return "unknown".to_string();
215    }
216
217    // For now, just list involved nodes
218    // A full cycle reconstruction would require more complex graph traversal
219    nodes.join(" → ")
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use crate::parser;
226
227    #[test]
228    fn test_resolve_basic() {
229        let toml = r#"
230[defs.base]
231traits = ["Debug", "Clone"]
232
233[defs.child]
234extends = "base"
235traits = ["PartialEq"]
236"#;
237
238        let config = parser::parse(toml).unwrap();
239        let resolved = resolve(&config).unwrap();
240
241        let child = resolved.defs.get("child").unwrap();
242        assert_eq!(child.traits, vec!["Debug", "Clone", "PartialEq"]);
243    }
244
245    #[test]
246    fn test_resolve_chain() {
247        let toml = r#"
248[defs.a]
249traits = ["Debug"]
250
251[defs.b]
252extends = "a"
253traits = ["Clone"]
254
255[defs.c]
256extends = "b"
257traits = ["PartialEq"]
258"#;
259
260        let config = parser::parse(toml).unwrap();
261        let resolved = resolve(&config).unwrap();
262
263        let c = resolved.defs.get("c").unwrap();
264        assert_eq!(c.traits, vec!["Debug", "Clone", "PartialEq"]);
265    }
266
267    #[test]
268    fn test_resolve_diamond() {
269        // Diamond inheritance pattern
270        let toml = r#"
271[defs.base]
272traits = ["Debug"]
273
274[defs.left]
275extends = "base"
276traits = ["Clone"]
277
278[defs.right]
279extends = "base"
280traits = ["PartialEq"]
281
282[defs.bottom]
283extends = "left"
284traits = ["Hash"]
285"#;
286
287        let config = parser::parse(toml).unwrap();
288        let resolved = resolve(&config).unwrap();
289
290        let bottom = resolved.defs.get("bottom").unwrap();
291        // Should have: Debug (from base), Clone (from left), Hash (from bottom)
292        assert_eq!(bottom.traits, vec!["Debug", "Clone", "Hash"]);
293    }
294
295    #[test]
296    fn test_resolve_circular() {
297        let toml = r#"
298[defs.a]
299extends = "b"
300traits = ["Debug"]
301
302[defs.b]
303extends = "c"
304traits = ["Clone"]
305
306[defs.c]
307extends = "a"
308traits = ["PartialEq"]
309"#;
310
311        let config = parser::parse(toml).unwrap();
312        let result = resolve(&config);
313
314        assert!(result.is_err());
315        let err = result.unwrap_err().to_string();
316        assert!(err.contains("circular"));
317    }
318
319    #[test]
320    fn test_resolve_self_reference() {
321        let toml = r#"
322[defs.a]
323extends = "a"
324traits = ["Debug"]
325"#;
326
327        let config = parser::parse(toml).unwrap();
328        let result = resolve(&config);
329
330        assert!(result.is_err());
331        let err = result.unwrap_err().to_string();
332        assert!(err.contains("circular"));
333    }
334
335    #[test]
336    fn test_resolve_undefined_parent() {
337        let toml = r#"
338[defs.child]
339extends = "nonexistent"
340traits = ["Debug"]
341"#;
342
343        let config = parser::parse(toml).unwrap();
344        let result = resolve(&config);
345
346        assert!(result.is_err());
347        let err = result.unwrap_err().to_string();
348        assert!(err.contains("nonexistent"));
349    }
350
351    #[test]
352    fn test_resolve_deduplication() {
353        let toml = r#"
354[defs.base]
355traits = ["Debug", "Clone"]
356
357[defs.child]
358extends = "base"
359traits = ["Clone", "PartialEq"]
360"#;
361
362        let config = parser::parse(toml).unwrap();
363        let resolved = resolve(&config).unwrap();
364
365        let child = resolved.defs.get("child").unwrap();
366        // Clone should appear only once
367        assert_eq!(child.traits, vec!["Debug", "Clone", "PartialEq"]);
368    }
369
370    #[test]
371    fn test_resolve_with_attrs() {
372        let toml = r#"
373[defs.base]
374traits = ["Debug"]
375attrs = ['#[serde(rename_all = "camelCase")]']
376
377[defs.child]
378extends = "base"
379traits = ["Clone"]
380attrs = ['#[serde(default)]']
381"#;
382
383        let config = parser::parse(toml).unwrap();
384        let resolved = resolve(&config).unwrap();
385
386        let child = resolved.defs.get("child").unwrap();
387        assert_eq!(child.traits, vec!["Debug", "Clone"]);
388        assert_eq!(child.attrs.len(), 2);
389        assert!(
390            child
391                .attrs
392                .contains(&"#[serde(rename_all = \"camelCase\")]".to_string())
393        );
394        assert!(child.attrs.contains(&"#[serde(default)]".to_string()));
395    }
396
397    #[test]
398    fn test_resolve_empty() {
399        let config = parser::parse("").unwrap();
400        let resolved = resolve(&config).unwrap();
401        assert!(resolved.defs.is_empty());
402    }
403
404    #[test]
405    fn test_resolve_multiple_roots() {
406        let toml = r#"
407[defs.a]
408traits = ["Debug"]
409
410[defs.b]
411traits = ["Clone"]
412
413[defs.c]
414extends = "a"
415traits = ["PartialEq"]
416"#;
417
418        let config = parser::parse(toml).unwrap();
419        let resolved = resolve(&config).unwrap();
420
421        assert_eq!(resolved.defs.len(), 3);
422        assert_eq!(resolved.defs["a"].traits, vec!["Debug"]);
423        assert_eq!(resolved.defs["b"].traits, vec!["Clone"]);
424        assert_eq!(resolved.defs["c"].traits, vec!["Debug", "PartialEq"]);
425    }
426}