1use super::pattern::NamePattern;
10use super::schema::{SchemaError, SpecFile};
11use super::types::FunctionSpec;
12use std::collections::BTreeMap;
13use std::path::{Path, PathBuf};
14use thiserror::Error;
15use tracing::{debug, warn};
16
17#[derive(Debug, Default)]
22pub struct SpecRegistry {
23 exact_specs: BTreeMap<String, FunctionSpec>,
25
26 pattern_specs: Vec<(NamePattern, FunctionSpec)>,
28
29 loaded_paths: Vec<PathBuf>,
31
32 warnings: Vec<String>,
34}
35
36impl SpecRegistry {
37 #[must_use]
39 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn load() -> Result<Self, RegistryError> {
54 let mut registry = Self::new();
55 let paths = Self::discovery_paths();
56
57 for path in paths {
58 if path.exists() {
59 registry.load_directory(&path)?;
60 }
61 }
62
63 Ok(registry)
64 }
65
66 pub fn load_from(paths: &[PathBuf]) -> Result<Self, RegistryError> {
73 let mut registry = Self::new();
74
75 for path in paths {
76 if path.is_dir() {
77 registry.load_directory(path)?;
78 } else if path.is_file() {
79 registry.load_file(path)?;
80 } else {
81 return Err(RegistryError::PathNotFound(path.display().to_string()));
82 }
83 }
84
85 Ok(registry)
86 }
87
88 fn load_directory(&mut self, dir: &Path) -> Result<(), RegistryError> {
90 let pattern = dir.join("**/*.yaml");
91 let pattern_str = pattern.display().to_string();
92
93 let entries = glob::glob(&pattern_str).map_err(|e| RegistryError::GlobError {
94 pattern: pattern_str.clone(),
95 message: e.to_string(),
96 })?;
97
98 let mut files: Vec<PathBuf> = entries.filter_map(Result::ok).collect();
100 files.sort();
101
102 for file in files {
103 self.load_file(&file)?;
104 }
105
106 Ok(())
107 }
108
109 fn load_file(&mut self, path: &Path) -> Result<(), RegistryError> {
111 debug!("loading spec file: {}", path.display());
112
113 let spec_file = SpecFile::load(path).map_err(RegistryError::Schema)?;
114
115 for spec in spec_file.specs {
116 self.insert(spec)?;
117 }
118
119 self.loaded_paths.push(path.to_path_buf());
120 Ok(())
121 }
122
123 fn insert(&mut self, spec: FunctionSpec) -> Result<(), RegistryError> {
128 let pattern = NamePattern::parse(&spec.name).map_err(|e| RegistryError::PatternError {
129 name: spec.name.clone(),
130 message: e.to_string(),
131 })?;
132
133 if pattern.is_exact() {
134 if self.exact_specs.contains_key(&spec.name) {
136 self.warnings.push(format!(
137 "duplicate spec for '{}', later definition overrides",
138 spec.name
139 ));
140 }
141
142 self.exact_specs
144 .entry(spec.name.clone())
145 .and_modify(|existing| existing.merge(&spec))
146 .or_insert(spec);
147 } else {
148 self.pattern_specs.push((pattern, spec));
150 }
151
152 Ok(())
153 }
154
155 #[must_use]
163 pub fn lookup(&self, name: &str) -> Option<&FunctionSpec> {
164 if let Some(spec) = self.exact_specs.get(name) {
166 if spec.is_disabled() {
167 return None;
168 }
169 return Some(spec);
170 }
171
172 for (pattern, spec) in self.pattern_specs.iter().rev() {
176 if pattern.matches(name) {
177 if spec.is_disabled() {
178 continue;
179 }
180 return Some(spec);
181 }
182 }
183
184 None
185 }
186
187 pub fn iter(&self) -> impl Iterator<Item = &FunctionSpec> {
189 self.exact_specs.values()
190 }
191
192 pub fn patterns(&self) -> impl Iterator<Item = &FunctionSpec> {
194 self.pattern_specs.iter().map(|(_, s)| s)
195 }
196
197 #[must_use]
199 pub fn len(&self) -> usize {
200 self.exact_specs.len()
201 }
202
203 #[must_use]
205 pub fn is_empty(&self) -> bool {
206 self.exact_specs.is_empty() && self.pattern_specs.is_empty()
207 }
208
209 #[must_use]
211 pub fn loaded_paths(&self) -> &[PathBuf] {
212 &self.loaded_paths
213 }
214
215 #[must_use]
217 pub fn warnings(&self) -> &[String] {
218 &self.warnings
219 }
220
221 fn discovery_paths() -> Vec<PathBuf> {
223 let mut paths = Vec::new();
224
225 if let Ok(exe) = std::env::current_exe() {
227 if let Some(parent) = exe.parent().and_then(|p| p.parent()) {
228 let share_path = parent.join("share/saf/specs");
229 paths.push(share_path);
230 }
231 }
232
233 if let Some(home) = std::env::var_os("HOME") {
235 let home_path = PathBuf::from(home).join(".saf/specs");
236 paths.push(home_path);
237 }
238
239 paths.push(PathBuf::from("./saf-specs"));
241
242 paths.push(PathBuf::from("./share/saf/specs"));
246
247 if let Ok(specs_path) = std::env::var("SAF_SPECS_PATH") {
249 for path in specs_path.split(':') {
250 if !path.is_empty() {
251 paths.push(PathBuf::from(path));
252 }
253 }
254 }
255
256 paths
257 }
258
259 pub fn add(&mut self, spec: FunctionSpec) -> Result<(), RegistryError> {
265 self.insert(spec)
266 }
267
268 pub fn from_yaml(yaml: &str) -> Result<Self, RegistryError> {
274 let file = SpecFile::parse(yaml)?;
275 let mut registry = Self::new();
276 for spec in file.specs {
277 registry.insert(spec)?;
278 }
279 Ok(registry)
280 }
281
282 pub fn from_yaml_strs(yamls: &[String]) -> Result<Self, RegistryError> {
291 let mut registry = Self::new();
292 for yaml_str in yamls {
293 let file = SpecFile::parse(yaml_str)?;
294 for spec in file.specs {
295 registry.insert(spec)?;
296 }
297 }
298 Ok(registry)
299 }
300
301 pub fn warn_missing(&self, name: &str) {
303 warn!("no spec for '{}', using conservative assumptions", name);
304 }
305}
306
307#[derive(Debug, Error)]
309pub enum RegistryError {
310 #[error("{0}")]
312 Schema(#[from] SchemaError),
313
314 #[error("path not found: {0}")]
316 PathNotFound(String),
317
318 #[error("glob error for '{pattern}': {message}")]
320 GlobError {
321 pattern: String,
323 message: String,
325 },
326
327 #[error("invalid pattern in '{name}': {message}")]
329 PatternError {
330 name: String,
332 message: String,
334 },
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340 use crate::spec::types::{Nullness, Pointer, ReturnSpec, Role};
341 use std::io::Write;
342 use tempfile::TempDir;
343
344 fn create_spec_file(dir: &Path, name: &str, content: &str) {
345 let path = dir.join(name);
346 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
347 let mut file = std::fs::File::create(path).unwrap();
348 file.write_all(content.as_bytes()).unwrap();
349 }
350
351 #[test]
352 fn test_empty_registry() {
353 let registry = SpecRegistry::new();
354 assert!(registry.is_empty());
355 assert_eq!(registry.len(), 0);
356 assert!(registry.lookup("malloc").is_none());
357 }
358
359 #[test]
360 fn test_load_single_file() {
361 let dir = TempDir::new().unwrap();
362 create_spec_file(
363 dir.path(),
364 "alloc.yaml",
365 r#"
366version: "1.0"
367specs:
368 - name: malloc
369 role: allocator
370 returns:
371 pointer: fresh_heap
372"#,
373 );
374
375 let registry = SpecRegistry::load_from(&[dir.path().to_path_buf()]).unwrap();
376 assert_eq!(registry.len(), 1);
377
378 let spec = registry.lookup("malloc").unwrap();
379 assert_eq!(spec.role, Some(Role::Allocator));
380 assert_eq!(
381 spec.returns.as_ref().unwrap().pointer,
382 Some(Pointer::FreshHeap)
383 );
384 }
385
386 #[test]
387 fn test_load_multiple_files_merge() {
388 let dir = TempDir::new().unwrap();
389
390 create_spec_file(
392 dir.path(),
393 "a_base.yaml",
394 r#"
395version: "1.0"
396specs:
397 - name: malloc
398 role: allocator
399"#,
400 );
401
402 create_spec_file(
404 dir.path(),
405 "b_extra.yaml",
406 r#"
407version: "1.0"
408specs:
409 - name: malloc
410 returns:
411 nullness: maybe_null
412"#,
413 );
414
415 let registry = SpecRegistry::load_from(&[dir.path().to_path_buf()]).unwrap();
416
417 let spec = registry.lookup("malloc").unwrap();
418 assert_eq!(spec.role, Some(Role::Allocator));
420 assert_eq!(
422 spec.returns.as_ref().unwrap().nullness,
423 Some(Nullness::MaybeNull)
424 );
425 }
426
427 #[test]
428 fn test_pattern_lookup() {
429 let dir = TempDir::new().unwrap();
430 create_spec_file(
431 dir.path(),
432 "string.yaml",
433 r#"
434version: "1.0"
435specs:
436 - name: "glob:str*"
437 role: string_operation
438"#,
439 );
440
441 let registry = SpecRegistry::load_from(&[dir.path().to_path_buf()]).unwrap();
442
443 let strlen = registry.lookup("strlen").unwrap();
445 assert_eq!(strlen.role, Some(Role::StringOperation));
446
447 let strcpy = registry.lookup("strcpy").unwrap();
448 assert_eq!(strcpy.role, Some(Role::StringOperation));
449
450 assert!(registry.lookup("malloc").is_none());
452 }
453
454 #[test]
455 fn test_exact_overrides_pattern() {
456 let dir = TempDir::new().unwrap();
457 create_spec_file(
458 dir.path(),
459 "specs.yaml",
460 r#"
461version: "1.0"
462specs:
463 - name: "glob:str*"
464 role: string_operation
465 - name: strlen
466 pure: true
467"#,
468 );
469
470 let registry = SpecRegistry::load_from(&[dir.path().to_path_buf()]).unwrap();
471
472 let strlen = registry.lookup("strlen").unwrap();
474 assert!(strlen.is_pure());
475 assert!(strlen.role.is_none());
477
478 let strcpy = registry.lookup("strcpy").unwrap();
480 assert_eq!(strcpy.role, Some(Role::StringOperation));
481 }
482
483 #[test]
484 fn test_disabled_spec() {
485 let dir = TempDir::new().unwrap();
486 create_spec_file(
487 dir.path(),
488 "specs.yaml",
489 r#"
490version: "1.0"
491specs:
492 - name: malloc
493 role: allocator
494 disabled: true
495"#,
496 );
497
498 let registry = SpecRegistry::load_from(&[dir.path().to_path_buf()]).unwrap();
499 assert!(registry.lookup("malloc").is_none());
501 }
502
503 #[test]
504 fn test_nested_directories() {
505 let dir = TempDir::new().unwrap();
506
507 create_spec_file(
508 dir.path(),
509 "libc/alloc.yaml",
510 r#"
511version: "1.0"
512specs:
513 - name: malloc
514 role: allocator
515"#,
516 );
517
518 create_spec_file(
519 dir.path(),
520 "libc/string.yaml",
521 r#"
522version: "1.0"
523specs:
524 - name: strlen
525 pure: true
526"#,
527 );
528
529 let registry = SpecRegistry::load_from(&[dir.path().to_path_buf()]).unwrap();
530 assert!(registry.lookup("malloc").is_some());
531 assert!(registry.lookup("strlen").is_some());
532 }
533
534 #[test]
535 fn test_add_programmatic() {
536 let mut registry = SpecRegistry::new();
537
538 let mut spec = FunctionSpec::new("my_alloc");
539 spec.role = Some(Role::Allocator);
540 spec.returns = Some(ReturnSpec {
541 pointer: Some(Pointer::FreshHeap),
542 ..ReturnSpec::default()
543 });
544
545 registry.add(spec).unwrap();
546
547 let found = registry.lookup("my_alloc").unwrap();
548 assert_eq!(found.role, Some(Role::Allocator));
549 }
550
551 #[test]
552 fn test_invalid_file() {
553 let dir = TempDir::new().unwrap();
554 create_spec_file(dir.path(), "bad.yaml", "not: valid: yaml:");
555
556 let result = SpecRegistry::load_from(&[dir.path().to_path_buf()]);
557 assert!(result.is_err());
558 }
559
560 #[test]
561 fn test_path_not_found() {
562 let result = SpecRegistry::load_from(&[PathBuf::from("/nonexistent/path")]);
563 assert!(matches!(
564 result.unwrap_err(),
565 RegistryError::PathNotFound(_)
566 ));
567 }
568}