1use 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, PartialEq)]
91pub struct EureConfig {
92 pub targets: HashMap<String, Target>,
94
95 #[cfg(feature = "cli")]
97 pub cli: Option<CliConfig>,
98
99 #[cfg(feature = "ls")]
101 pub ls: Option<LsConfig>,
102}
103
104impl ParseDocument<'_> for EureConfig {
105 type Error = ParseError;
106
107 fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
108 let rec = ctx.parse_record()?;
109
110 let targets = if let Some(targets_ctx) = rec.field_optional("targets") {
112 let targets_rec = targets_ctx.parse_record()?;
113 let mut targets = HashMap::new();
114 for (name, target_ctx) in targets_rec.unknown_fields() {
115 let target = target_ctx.parse::<Target>()?;
116 targets.insert(name.to_string(), target);
117 }
118 targets_rec.allow_unknown_fields()?;
119 targets
120 } else {
121 HashMap::new()
122 };
123
124 #[cfg(feature = "cli")]
125 let cli = rec
126 .field_optional("cli")
127 .map(|ctx| ctx.parse::<CliConfig>())
128 .transpose()?;
129
130 #[cfg(feature = "ls")]
131 let ls = rec
132 .field_optional("ls")
133 .map(|ctx| ctx.parse::<LsConfig>())
134 .transpose()?;
135
136 rec.allow_unknown_fields()?;
137
138 Ok(EureConfig {
139 targets,
140 #[cfg(feature = "cli")]
141 cli,
142 #[cfg(feature = "ls")]
143 ls,
144 })
145 }
146}
147
148impl EureConfig {
149 pub fn find_config_file(start_dir: &Path) -> Option<PathBuf> {
151 let mut current = start_dir.to_path_buf();
152 loop {
153 let config_path = current.join(CONFIG_FILENAME);
154 if config_path.exists() {
155 return Some(config_path);
156 }
157 if !current.pop() {
158 return None;
159 }
160 }
161 }
162
163 #[cfg(feature = "cli")]
165 pub fn default_targets(&self) -> &[String] {
166 self.cli
167 .as_ref()
168 .map(|c| c.default_targets.as_slice())
169 .unwrap_or(&[])
170 }
171
172 pub fn get_target(&self, name: &str) -> Option<&Target> {
174 self.targets.get(name)
175 }
176
177 pub fn target_names(&self) -> impl Iterator<Item = &str> {
179 self.targets.keys().map(|s| s.as_str())
180 }
181
182 pub fn schema_for_path(&self, file_path: &Path, config_dir: &Path) -> Option<PathBuf> {
186 let options = glob::MatchOptions {
188 case_sensitive: true,
189 require_literal_separator: true,
190 require_literal_leading_dot: false,
191 };
192
193 for target in self.targets.values() {
194 if let Some(ref schema) = target.schema {
195 for glob_pattern in &target.globs {
196 let full_pattern = config_dir.join(glob_pattern);
198 if let Ok(pattern) = glob::Pattern::new(&full_pattern.to_string_lossy())
199 && pattern.matches_path_with(file_path, options)
200 {
201 return Some(config_dir.join(schema));
203 }
204 }
205 }
206 }
207 None
208 }
209}