Skip to main content

fraiseql_cli/config/toml_schema/
domain.rs

1//! Domain-based schema organization types.
2
3use std::path::PathBuf;
4
5use anyhow::{Context, Result};
6use serde::{Deserialize, Serialize};
7
8/// Domain-based schema organization
9///
10/// Automatically discovers schema files in domain directories:
11/// ```toml
12/// [schema.domain_discovery]
13/// enabled = true
14/// root_dir = "schema"
15/// ```
16///
17/// Expects structure:
18/// ```text
19/// schema/
20/// ├── auth/
21/// │   ├── types.json
22/// │   ├── queries.json
23/// │   └── mutations.json
24/// ├── products/
25/// │   ├── types.json
26/// │   ├── queries.json
27/// │   └── mutations.json
28/// ```
29#[derive(Debug, Clone, Default, Deserialize, Serialize)]
30#[serde(default, deny_unknown_fields)]
31pub struct DomainDiscovery {
32    /// Enable automatic domain discovery
33    pub enabled:  bool,
34    /// Root directory containing domains
35    pub root_dir: String,
36}
37
38/// Represents a discovered domain
39#[derive(Debug, Clone)]
40pub struct Domain {
41    /// Domain name (directory name)
42    pub name: String,
43    /// Path to domain root
44    pub path: PathBuf,
45}
46
47impl DomainDiscovery {
48    /// Discover all domains in root_dir
49    ///
50    /// # Errors
51    ///
52    /// Returns an error if domain discovery is enabled but `root_dir` does not
53    /// exist, if the directory cannot be read, or if a domain entry has an
54    /// invalid (non-UTF-8) name.
55    pub fn resolve_domains(&self) -> Result<Vec<Domain>> {
56        if !self.enabled {
57            return Ok(Vec::new());
58        }
59
60        let root = PathBuf::from(&self.root_dir);
61        if !root.is_dir() {
62            anyhow::bail!("Domain discovery root not found: {}", self.root_dir);
63        }
64
65        let mut domains = Vec::new();
66
67        for entry in std::fs::read_dir(&root)
68            .context(format!("Failed to read domain root: {}", self.root_dir))?
69        {
70            let entry = entry.context("Failed to read directory entry")?;
71            let path = entry.path();
72
73            if path.is_dir() {
74                let name = path
75                    .file_name()
76                    .and_then(|n| n.to_str())
77                    .map(std::string::ToString::to_string)
78                    .ok_or_else(|| anyhow::anyhow!("Invalid domain name: {}", path.display()))?;
79
80                domains.push(Domain { name, path });
81            }
82        }
83
84        // Sort for deterministic ordering
85        domains.sort_by(|a, b| a.name.cmp(&b.name));
86
87        Ok(domains)
88    }
89}
90
91/// Schema includes for multi-file composition (glob patterns)
92///
93/// Supports glob patterns for flexible file inclusion:
94/// ```toml
95/// [schema.includes]
96/// types = ["schema/types/**/*.json"]
97/// queries = ["schema/queries/**/*.json"]
98/// mutations = ["schema/mutations/**/*.json"]
99/// ```
100#[derive(Debug, Clone, Default, Deserialize, Serialize)]
101#[serde(default, deny_unknown_fields)]
102pub struct SchemaIncludes {
103    /// Glob patterns for type files
104    pub types:     Vec<String>,
105    /// Glob patterns for query files
106    pub queries:   Vec<String>,
107    /// Glob patterns for mutation files
108    pub mutations: Vec<String>,
109}
110
111impl SchemaIncludes {
112    /// Check if any includes are specified
113    pub fn is_empty(&self) -> bool {
114        self.types.is_empty() && self.queries.is_empty() && self.mutations.is_empty()
115    }
116
117    /// Resolve glob patterns to actual file paths
118    ///
119    /// # Returns
120    /// `ResolvedIncludes` with expanded file paths.
121    ///
122    /// # Errors
123    ///
124    /// Returns an error if any glob pattern is syntactically invalid or if a
125    /// matched path cannot be accessed.
126    pub fn resolve_globs(&self) -> Result<ResolvedIncludes> {
127        use glob::glob as glob_pattern;
128
129        let mut type_paths = Vec::new();
130        let mut query_paths = Vec::new();
131        let mut mutation_paths = Vec::new();
132
133        // Resolve type globs
134        for pattern in &self.types {
135            for entry in glob_pattern(pattern)
136                .context(format!("Invalid glob pattern for types: {pattern}"))?
137            {
138                match entry {
139                    Ok(path) => type_paths.push(path),
140                    Err(e) => {
141                        anyhow::bail!("Error resolving type glob pattern '{pattern}': {e}");
142                    },
143                }
144            }
145        }
146
147        // Resolve query globs
148        for pattern in &self.queries {
149            for entry in glob_pattern(pattern)
150                .context(format!("Invalid glob pattern for queries: {pattern}"))?
151            {
152                match entry {
153                    Ok(path) => query_paths.push(path),
154                    Err(e) => {
155                        anyhow::bail!("Error resolving query glob pattern '{pattern}': {e}");
156                    },
157                }
158            }
159        }
160
161        // Resolve mutation globs
162        for pattern in &self.mutations {
163            for entry in glob_pattern(pattern)
164                .context(format!("Invalid glob pattern for mutations: {pattern}"))?
165            {
166                match entry {
167                    Ok(path) => mutation_paths.push(path),
168                    Err(e) => {
169                        anyhow::bail!("Error resolving mutation glob pattern '{pattern}': {e}");
170                    },
171                }
172            }
173        }
174
175        // Sort for deterministic ordering
176        type_paths.sort();
177        query_paths.sort();
178        mutation_paths.sort();
179
180        // Remove duplicates
181        type_paths.dedup();
182        query_paths.dedup();
183        mutation_paths.dedup();
184
185        Ok(ResolvedIncludes {
186            types:     type_paths,
187            queries:   query_paths,
188            mutations: mutation_paths,
189        })
190    }
191}
192
193/// Resolved glob patterns to actual file paths
194#[derive(Debug, Clone)]
195pub struct ResolvedIncludes {
196    /// Resolved type file paths
197    pub types:     Vec<PathBuf>,
198    /// Resolved query file paths
199    pub queries:   Vec<PathBuf>,
200    /// Resolved mutation file paths
201    pub mutations: Vec<PathBuf>,
202}