1pub mod cache;
15
16use std::collections::HashMap;
17use std::path::{Path, PathBuf};
18
19use eure_document::parse::{ParseContext, ParseDocument, ParseError};
20use eure_macros::ParseDocument;
21use eure_parol::EureParseError;
22
23pub const CONFIG_FILENAME: &str = "Eure.eure";
25
26#[derive(Debug, thiserror::Error)]
30pub enum ConfigError {
31 #[error("IO error: {0}")]
32 Io(#[from] std::io::Error),
33
34 #[error("Syntax error: {0}")]
35 Syntax(EureParseError),
36
37 #[error("Config error: {0}")]
38 Parse(#[from] ParseError),
39}
40
41impl PartialEq for ConfigError {
42 fn eq(&self, other: &Self) -> bool {
43 match (self, other) {
44 (ConfigError::Io(a), ConfigError::Io(b)) => a.kind() == b.kind(),
45 (ConfigError::Syntax(a), ConfigError::Syntax(b)) => a.to_string() == b.to_string(),
46 (ConfigError::Parse(a), ConfigError::Parse(b)) => a == b,
47 _ => false,
48 }
49 }
50}
51
52impl From<EureParseError> for ConfigError {
53 fn from(err: EureParseError) -> Self {
54 ConfigError::Syntax(err)
55 }
56}
57
58#[derive(Debug, Clone, ParseDocument, PartialEq, Eq, Hash)]
60#[eure(crate = eure_document, allow_unknown_fields)]
61pub struct Target {
62 pub globs: Vec<String>,
64 #[eure(default)]
66 pub schema: Option<String>,
67}
68
69#[cfg(feature = "cli")]
71#[derive(Debug, Clone, Default, ParseDocument, PartialEq)]
72#[eure(crate = eure_document, rename_all = "kebab-case", allow_unknown_fields)]
73pub struct CliConfig {
74 #[eure(default)]
76 pub default_targets: Vec<String>,
77}
78
79#[cfg(feature = "ls")]
81#[derive(Debug, Clone, Default, ParseDocument, PartialEq)]
82#[eure(crate = eure_document, rename_all = "kebab-case", allow_unknown_fields)]
83pub struct LsConfig {
84 #[eure(default)]
86 pub format_on_save: bool,
87}
88
89#[derive(Debug, Clone, Default, ParseDocument, PartialEq)]
91#[eure(crate = eure_document, rename_all = "kebab-case", allow_unknown_fields)]
92pub struct SecurityConfig {
93 #[eure(default)]
98 pub allowed_hosts: Vec<String>,
99}
100
101#[derive(Debug, Clone, Default, PartialEq)]
103pub struct EureConfig {
104 pub targets: HashMap<String, Target>,
106
107 pub security: Option<SecurityConfig>,
109
110 #[cfg(feature = "cli")]
112 pub cli: Option<CliConfig>,
113
114 #[cfg(feature = "ls")]
116 pub ls: Option<LsConfig>,
117}
118
119impl ParseDocument<'_> for EureConfig {
120 type Error = ParseError;
121
122 fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
123 let rec = ctx.parse_record()?;
124
125 let targets = if let Some(targets_ctx) = rec.field_optional("targets") {
127 let targets_rec = targets_ctx.parse_record()?;
128 let mut targets = HashMap::new();
129 for (name, target_ctx) in targets_rec.unknown_fields() {
130 let target = target_ctx.parse::<Target>()?;
131 targets.insert(name.to_string(), target);
132 }
133 targets_rec.allow_unknown_fields()?;
134 targets
135 } else {
136 HashMap::new()
137 };
138
139 let security = rec
140 .field_optional("security")
141 .map(|ctx| ctx.parse::<SecurityConfig>())
142 .transpose()?;
143
144 #[cfg(feature = "cli")]
145 let cli = rec
146 .field_optional("cli")
147 .map(|ctx| ctx.parse::<CliConfig>())
148 .transpose()?;
149
150 #[cfg(feature = "ls")]
151 let ls = rec
152 .field_optional("ls")
153 .map(|ctx| ctx.parse::<LsConfig>())
154 .transpose()?;
155
156 rec.allow_unknown_fields()?;
157
158 Ok(EureConfig {
159 targets,
160 security,
161 #[cfg(feature = "cli")]
162 cli,
163 #[cfg(feature = "ls")]
164 ls,
165 })
166 }
167}
168
169impl EureConfig {
170 pub fn find_config_file(start_dir: &Path) -> Option<PathBuf> {
172 let mut current = start_dir.to_path_buf();
173 loop {
174 let config_path = current.join(CONFIG_FILENAME);
175 if config_path.exists() {
176 return Some(config_path);
177 }
178 if !current.pop() {
179 return None;
180 }
181 }
182 }
183
184 #[cfg(feature = "cli")]
186 pub fn default_targets(&self) -> &[String] {
187 self.cli
188 .as_ref()
189 .map(|c| c.default_targets.as_slice())
190 .unwrap_or(&[])
191 }
192
193 pub fn get_target(&self, name: &str) -> Option<&Target> {
195 self.targets.get(name)
196 }
197
198 pub fn target_names(&self) -> impl Iterator<Item = &str> {
200 self.targets.keys().map(|s| s.as_str())
201 }
202
203 pub fn schema_for_path(&self, file_path: &Path, config_dir: &Path) -> Option<String> {
207 let options = glob::MatchOptions {
209 case_sensitive: true,
210 require_literal_separator: true,
211 require_literal_leading_dot: false,
212 };
213
214 for target in self.targets.values() {
215 if let Some(ref schema) = target.schema {
216 for glob_pattern in &target.globs {
217 let full_pattern = config_dir.join(glob_pattern);
219 if let Ok(pattern) = glob::Pattern::new(&full_pattern.to_string_lossy())
220 && pattern.matches_path_with(file_path, options)
221 {
222 return Some(schema.clone());
224 }
225 }
226 }
227 }
228 None
229 }
230
231 pub fn allowed_hosts(&self) -> &[String] {
237 self.security
238 .as_ref()
239 .map(|s| s.allowed_hosts.as_slice())
240 .unwrap_or(&[])
241 }
242}