ron2_doc/
discovery.rs

1//! Schema file discovery and loading.
2
3use std::path::{Path, PathBuf};
4
5use anyhow::{Context, Result};
6use ron2::schema::{Schema, storage::read_schema};
7use walkdir::WalkDir;
8
9/// A discovered schema with its metadata.
10#[derive(Debug)]
11pub struct DiscoveredSchema {
12    /// Path to the schema file
13    pub path: PathBuf,
14    /// Derived type path (e.g., "my_crate::Config")
15    pub type_path: String,
16    /// Parsed schema
17    pub schema: Schema,
18}
19
20/// Discover all schema files in a directory or load a single file.
21pub fn discover_schemas(input: &Path) -> Result<Vec<DiscoveredSchema>> {
22    if input.is_file() {
23        // Single file mode
24        let schema = read_schema(input)
25            .with_context(|| format!("failed to read schema from {}", input.display()))?;
26        let type_path = file_path_to_type_path(input, input.parent().unwrap_or(input));
27        return Ok(vec![DiscoveredSchema {
28            path: input.to_path_buf(),
29            type_path,
30            schema,
31        }]);
32    }
33
34    // Directory mode - walk and find all .schema.ron files
35    let mut schemas = Vec::new();
36
37    for entry in WalkDir::new(input)
38        .follow_links(true)
39        .into_iter()
40        .filter_map(|e| e.ok())
41    {
42        let path = entry.path();
43        if path.is_file() && path.to_string_lossy().ends_with(".schema.ron") {
44            let schema = read_schema(path)
45                .with_context(|| format!("failed to read schema from {}", path.display()))?;
46            let type_path = file_path_to_type_path(path, input);
47            schemas.push(DiscoveredSchema {
48                path: path.to_path_buf(),
49                type_path,
50                schema,
51            });
52        }
53    }
54
55    // Sort by type path for consistent output
56    schemas.sort_by(|a, b| a.type_path.cmp(&b.type_path));
57
58    Ok(schemas)
59}
60
61/// Convert a file path to a type path.
62///
63/// Example: `base/my_crate/Config.schema.ron` -> `my_crate::Config`
64fn file_path_to_type_path(path: &Path, base: &Path) -> String {
65    // Get relative path from base
66    let relative = path
67        .strip_prefix(base)
68        .unwrap_or(path)
69        .with_extension("") // Remove .ron
70        .with_extension(""); // Remove .schema
71
72    // Convert path separators to ::
73    relative
74        .components()
75        .map(|c| c.as_os_str().to_string_lossy().to_string())
76        .collect::<Vec<_>>()
77        .join("::")
78}
79
80#[cfg(test)]
81mod tests {
82    use ron2::schema::{Field, TypeKind, write_schema};
83
84    use super::*;
85
86    #[test]
87    fn test_file_path_to_type_path() {
88        let base = Path::new("/schemas");
89        let path = Path::new("/schemas/my_crate/config/AppConfig.schema.ron");
90        assert_eq!(
91            file_path_to_type_path(path, base),
92            "my_crate::config::AppConfig"
93        );
94    }
95
96    #[test]
97    fn test_file_path_to_type_path_single_component() {
98        let base = Path::new("/schemas");
99        let path = Path::new("/schemas/Config.schema.ron");
100        assert_eq!(file_path_to_type_path(path, base), "Config");
101    }
102
103    #[test]
104    fn test_discover_schemas_preserves_generic_typeref() {
105        let temp_dir = tempfile::tempdir().expect("temp dir");
106        let schema = Schema::new(TypeKind::Struct {
107            fields: vec![Field::new(
108                "value",
109                TypeKind::TypeRef("my_crate::Wrapper<T>".to_string()),
110            )],
111        });
112
113        write_schema("my_crate::Generic", &schema, Some(temp_dir.path())).expect("write schema");
114
115        let discovered = discover_schemas(temp_dir.path()).expect("discover schemas");
116        assert_eq!(discovered.len(), 1);
117        assert_eq!(discovered[0].type_path, "my_crate::Generic");
118        assert_eq!(discovered[0].schema, schema);
119    }
120}