1pub mod cache;
15
16use std::collections::HashMap;
17use std::path::{Path, PathBuf};
18
19use eure_document::parse::{FromEure, ParseContext, ParseError, ParseErrorKind};
20use eure_macros::FromEure;
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, FromEure, 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, FromEure, 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, FromEure, 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, FromEure, 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 FromEure<'_> 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 result in targets_rec.unknown_fields() {
130 let (name, target_ctx) = result.map_err(|(key, ctx)| ParseError {
131 node_id: ctx.node_id(),
132 kind: ParseErrorKind::InvalidKeyType(key.clone()),
133 })?;
134 let target = target_ctx.parse::<Target>()?;
135 targets.insert(name.to_string(), target);
136 }
137 targets_rec.allow_unknown_fields()?;
138 targets
139 } else {
140 HashMap::new()
141 };
142
143 let security = rec
144 .field_optional("security")
145 .map(|ctx| ctx.parse::<SecurityConfig>())
146 .transpose()?;
147
148 #[cfg(feature = "cli")]
149 let cli = rec
150 .field_optional("cli")
151 .map(|ctx| ctx.parse::<CliConfig>())
152 .transpose()?;
153
154 #[cfg(feature = "ls")]
155 let ls = rec
156 .field_optional("ls")
157 .map(|ctx| ctx.parse::<LsConfig>())
158 .transpose()?;
159
160 rec.allow_unknown_fields()?;
161
162 Ok(EureConfig {
163 targets,
164 security,
165 #[cfg(feature = "cli")]
166 cli,
167 #[cfg(feature = "ls")]
168 ls,
169 })
170 }
171}
172
173impl EureConfig {
174 pub fn find_config_file(start_dir: &Path) -> Option<PathBuf> {
176 let mut current = start_dir.to_path_buf();
177 loop {
178 let config_path = current.join(CONFIG_FILENAME);
179 if config_path.exists() {
180 return Some(config_path);
181 }
182 if !current.pop() {
183 return None;
184 }
185 }
186 }
187
188 #[cfg(feature = "cli")]
190 pub fn default_targets(&self) -> &[String] {
191 self.cli
192 .as_ref()
193 .map(|c| c.default_targets.as_slice())
194 .unwrap_or(&[])
195 }
196
197 pub fn get_target(&self, name: &str) -> Option<&Target> {
199 self.targets.get(name)
200 }
201
202 pub fn target_names(&self) -> impl Iterator<Item = &str> {
204 self.targets.keys().map(|s| s.as_str())
205 }
206
207 pub fn schema_for_path(&self, file_path: &Path, config_dir: &Path) -> Option<String> {
211 let options = glob::MatchOptions {
213 case_sensitive: true,
214 require_literal_separator: true,
215 require_literal_leading_dot: false,
216 };
217
218 for target in self.targets.values() {
219 if let Some(ref schema) = target.schema {
220 for glob_pattern in &target.globs {
221 let full_pattern = config_dir.join(glob_pattern);
223 if let Ok(pattern) = glob::Pattern::new(&full_pattern.to_string_lossy())
224 && pattern.matches_path_with(file_path, options)
225 {
226 return Some(schema.clone());
228 }
229 }
230 }
231 }
232 None
233 }
234
235 pub fn allowed_hosts(&self) -> &[String] {
241 self.security
242 .as_ref()
243 .map(|s| s.allowed_hosts.as_slice())
244 .unwrap_or(&[])
245 }
246}