nichlink/registry_core/requirements/
requirements.rs1use std::collections::BTreeSet;
10
11use crate::registry_core::diagnostic::BuildDiagnostic;
12use crate::registry_core::identity::NodeId;
13
14#[derive(Clone, Debug)]
17pub struct CapabilityDeclaration {
18 pub id: NodeId,
21 pub parent: Option<NodeId>,
24 pub kind: String,
27 pub provides: Vec<String>,
30}
31
32#[derive(Clone, Debug)]
35pub struct CapabilityRequirement {
36 pub node: NodeId,
39 pub kind: String,
42 pub function: String,
45 pub branch: String,
48 pub capability: String,
51 pub provider: String,
54 pub parent: Option<NodeId>,
58 pub source: String,
61 pub line: usize,
64}
65
66pub fn missing_capabilities(
69 requirements: Vec<CapabilityRequirement>,
70 declarations: &[CapabilityDeclaration],
71) -> Vec<BuildDiagnostic> {
72 requirements
73 .into_iter()
74 .filter_map(|requirement| missing(requirement, declarations))
75 .collect()
76}
77
78fn missing(
79 requirement: CapabilityRequirement,
80 declarations: &[CapabilityDeclaration],
81) -> Option<BuildDiagnostic> {
82 let provided = requirement.parent.is_some_and(|parent| {
83 has_ancestor_provider(
84 parent,
85 &requirement.capability,
86 &requirement.provider,
87 declarations,
88 )
89 });
90 if provided {
91 return None;
92 }
93 let detail = requirement
94 .parent
95 .and_then(|parent| find_ancestor_capability(parent, &requirement.capability, declarations))
96 .map(|declaration| declaration.kind.clone());
97 let mut diagnostic = BuildDiagnostic::new(
98 "requirements",
99 format!("missing capability `{}`", requirement.capability),
100 )
101 .branch(requirement.branch)
102 .node(requirement.node.to_string(), requirement.kind)
103 .at(requirement.source, requirement.line)
104 .function(requirement.function)
105 .field(requirement.capability)
106 .expected(requirement.provider.clone());
107 if let Some(provider) = detail {
108 diagnostic = diagnostic.provider(provider);
109 }
110 Some(diagnostic)
111}
112
113fn has_ancestor_provider(
114 mut target: NodeId,
115 capability: &str,
116 expected_kind: &str,
117 declarations: &[CapabilityDeclaration],
118) -> bool {
119 let mut visited = BTreeSet::new();
120 loop {
121 if !visited.insert(target) {
122 return false;
123 }
124 if declarations.iter().any(|declaration| {
125 declaration.id == target
126 && declaration.kind == expected_kind
127 && declaration.provides.iter().any(|item| item == capability)
128 }) {
129 return true;
130 }
131 let Some(parent) = declarations
132 .iter()
133 .find(|declaration| declaration.id == target)
134 .and_then(|declaration| declaration.parent)
135 else {
136 return false;
137 };
138 if parent == target {
139 return false;
140 }
141 target = parent;
142 }
143}
144
145fn find_ancestor_capability<'a>(
146 mut target: NodeId,
147 capability: &str,
148 declarations: &'a [CapabilityDeclaration],
149) -> Option<&'a CapabilityDeclaration> {
150 let mut visited = BTreeSet::new();
151 loop {
152 if !visited.insert(target) {
153 return None;
154 }
155 if let Some(declaration) = declarations.iter().find(|declaration| {
156 declaration.id == target && declaration.provides.iter().any(|item| item == capability)
157 }) {
158 return Some(declaration);
159 }
160 let parent = declarations
161 .iter()
162 .find(|declaration| declaration.id == target)
163 .and_then(|declaration| declaration.parent)?;
164 if parent == target {
165 return None;
166 }
167 target = parent;
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::{CapabilityDeclaration, CapabilityRequirement, missing_capabilities};
174 use crate::registry_core::identity::{NodeId, ROOT_NODE_ID};
175
176 fn id(byte: u8) -> NodeId {
177 NodeId::from_raw([byte, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
178 }
179
180 fn declaration(
181 byte: u8,
182 parent: Option<u8>,
183 kind: &str,
184 provides: &[&str],
185 ) -> CapabilityDeclaration {
186 CapabilityDeclaration {
187 id: id(byte),
188 parent: parent.map(id),
189 kind: kind.to_owned(),
190 provides: provides.iter().map(|item| item.to_string()).collect(),
191 }
192 }
193
194 fn requirement(
195 byte: u8,
196 parent: Option<u8>,
197 capability: &str,
198 provider: &str,
199 ) -> CapabilityRequirement {
200 CapabilityRequirement {
201 node: id(byte),
202 kind: "Leaf".to_owned(),
203 function: "register".to_owned(),
204 branch: "a".to_owned(),
205 capability: capability.to_owned(),
206 provider: provider.to_owned(),
207 parent: parent.map(id),
208 source: "a/leaf.rs".to_owned(),
209 line: 3,
210 }
211 }
212
213 #[test]
214 fn satisfied_requirement_produces_no_diagnostic() {
215 let declarations = vec![
216 declaration(1, None, "CanvasProvider", &["render"]),
217 declaration(2, Some(1), "Leaf", &[]),
218 ];
219 let requirements = vec![requirement(2, Some(1), "render", "CanvasProvider")];
220 assert!(missing_capabilities(requirements, &declarations).is_empty());
221 }
222
223 #[test]
224 fn missing_capability_is_reported_with_ancestor_detail() {
225 let declarations = vec![
226 declaration(1, None, "WrongProvider", &["render"]),
227 declaration(2, Some(1), "Leaf", &[]),
228 ];
229 let requirements = vec![requirement(2, Some(1), "render", "CanvasProvider")];
230 let missing = missing_capabilities(requirements, &declarations);
231 assert_eq!(missing.len(), 1);
232 let rendered = missing[0].clone().message;
233 assert!(rendered.contains("missing capability `render`"));
234 }
235
236 #[test]
237 fn root_requirement_without_parent_is_reported() {
238 let declarations = vec![declaration(1, None, "Root", &[])];
239 let requirements = vec![requirement(1, None, "render", "CanvasProvider")];
240 assert_eq!(missing_capabilities(requirements, &declarations).len(), 1);
241 }
242
243 #[test]
244 fn parent_cycle_does_not_hang() {
245 let mut root = declaration(1, Some(2), "A", &[]);
246 root.parent = Some(id(2));
247 let mut other = declaration(2, Some(1), "B", &[]);
248 other.parent = Some(id(1));
249 let declarations = vec![root, other];
250 let requirements = vec![requirement(2, Some(1), "render", "CanvasProvider")];
251 let missing = missing_capabilities(requirements, &declarations);
252 assert_eq!(missing.len(), 1);
253 let _ = ROOT_NODE_ID;
254 }
255}