1pub mod error;
5pub mod language;
6pub mod source;
7
8use std::{
9 collections::HashMap,
10 fmt,
11 path::{Path, PathBuf},
12};
13
14use language::{Language, LanguageConfiguration};
15use nickel_lang_core::{
16 error::NullReporter, eval::cache::CacheImpl, program::Program, term::RichTerm,
17};
18use serde::Deserialize;
19
20#[cfg(not(target_arch = "wasm32"))]
21use crate::error::TopiaryConfigFetchingError;
22#[cfg(not(target_arch = "wasm32"))]
23use tempfile::tempdir;
24
25use crate::error::{TopiaryConfigError, TopiaryConfigResult};
26
27pub use source::Source;
28
29#[derive(Debug)]
35pub struct Configuration {
36 languages: Vec<Language>,
37}
38
39#[derive(Debug, serde::Deserialize, PartialEq, serde::Serialize, Clone)]
41struct SerdeConfiguration {
42 languages: HashMap<String, LanguageConfiguration>,
43}
44
45impl Configuration {
46 #[allow(clippy::result_large_err)]
56 pub fn fetch(merge: bool, file: &Option<PathBuf>) -> TopiaryConfigResult<(Self, RichTerm)> {
57 if let Some(path) = file
59 && !path.exists()
60 {
61 return Err(TopiaryConfigError::FileNotFound(path.to_path_buf()));
62 }
63
64 if merge {
65 let sources: Vec<Source> = Source::fetch_all(file);
67
68 Self::parse_and_merge(&sources)
70 } else {
71 match Source::fetch_one(file) {
73 Source::Builtin => Self::parse(Source::Builtin),
74 source => Self::parse_and_merge(&[source, Source::Builtin]),
75 }
76 }
77 }
78
79 #[allow(clippy::result_large_err)]
86 pub fn get_language<T>(&self, name: T) -> TopiaryConfigResult<&Language>
87 where
88 T: AsRef<str> + fmt::Display,
89 {
90 self.languages
91 .iter()
92 .find(|language| language.name == name.as_ref())
93 .ok_or(TopiaryConfigError::UnknownLanguage(name.to_string()))
94 }
95
96 #[cfg(not(target_arch = "wasm32"))]
102 fn fetch_language(
103 language: &Language,
104 force: bool,
105 tmp_dir: &Path,
106 ) -> Result<(), TopiaryConfigFetchingError> {
107 match &language.config.grammar.source {
108 language::GrammarSource::Git(git_source) => {
109 let library_path = language.library_path()?;
110
111 log::info!(
112 "Fetch \"{}\": Configured via Git ({} ({})); to {}",
113 language.name,
114 git_source.git,
115 git_source.rev,
116 library_path.display()
117 );
118
119 git_source.fetch_and_compile_with_dir(
120 &language.name,
121 library_path,
122 force,
123 tmp_dir.to_path_buf(),
124 )
125 }
126
127 language::GrammarSource::Path(path) => {
128 log::info!(
129 "Fetch \"{}\": Configured via filesystem ({}); nothing to do",
130 language.name,
131 path.display(),
132 );
133
134 if !path.exists() {
135 Err(TopiaryConfigFetchingError::GrammarFileNotFound(
136 path.to_path_buf(),
137 ))
138 } else {
139 Ok(())
140 }
141 }
142 }
143 }
144
145 #[cfg(not(target_arch = "wasm32"))]
152 #[allow(clippy::result_large_err)]
153 pub fn prefetch_language<T>(&self, language: T, force: bool) -> TopiaryConfigResult<()>
154 where
155 T: AsRef<str> + fmt::Display,
156 {
157 let tmp_dir = tempdir()?;
158 let tmp_dir_path = tmp_dir.path().to_owned();
159 let l = self.get_language(language)?;
160 Configuration::fetch_language(l, force, &tmp_dir_path)?;
161 Ok(())
162 }
163
164 #[cfg(not(target_arch = "wasm32"))]
171 #[allow(clippy::result_large_err)]
172 pub fn prefetch_languages(&self, force: bool) -> TopiaryConfigResult<()> {
173 let tmp_dir = tempdir()?;
174 let tmp_dir_path = tmp_dir.path().to_owned();
175
176 #[cfg(all(feature = "parallel", not(windows)))]
181 {
182 use rayon::prelude::*;
183 self.languages
184 .par_iter()
185 .map(|l| Configuration::fetch_language(l, force, &tmp_dir_path))
186 .collect::<Result<Vec<_>, TopiaryConfigFetchingError>>()?;
187 }
188
189 #[cfg(any(not(feature = "parallel"), windows))]
190 {
191 self.languages
192 .iter()
193 .map(|l| Configuration::fetch_language(l, force, &tmp_dir_path))
194 .collect::<Result<Vec<_>, TopiaryConfigFetchingError>>()?;
195 }
196
197 tmp_dir.close()?;
198 Ok(())
199 }
200
201 #[allow(clippy::result_large_err)]
207 pub fn detect<P: AsRef<Path>>(&self, path: P) -> TopiaryConfigResult<&Language> {
208 let pb = &path.as_ref().to_path_buf();
209 if let Some(extension) = pb.extension().and_then(|ext| ext.to_str()) {
210 for lang in &self.languages {
211 if lang.config.extensions.contains(extension) {
212 return Ok(lang);
213 }
214 }
215 return Err(TopiaryConfigError::UnknownExtension(extension.to_string()));
216 }
217 Err(TopiaryConfigError::NoExtension(pb.clone()))
218 }
219
220 #[allow(clippy::result_large_err)]
221 fn parse_and_merge(sources: &[Source]) -> TopiaryConfigResult<(Self, RichTerm)> {
222 let inputs = sources.iter().map(|s| s.clone().into());
223
224 let mut program =
225 Program::<CacheImpl>::new_from_inputs(inputs, std::io::stderr(), NullReporter {})?;
226
227 let term = program.eval_full_for_export()?;
228
229 let serde_config = SerdeConfiguration::deserialize(term.clone())?;
230
231 Ok((serde_config.into(), term))
232 }
233
234 #[allow(clippy::result_large_err)]
235 fn parse(source: Source) -> TopiaryConfigResult<(Self, RichTerm)> {
236 let mut program = Program::<CacheImpl>::new_from_input(
237 source.into(),
238 std::io::stderr(),
239 NullReporter {},
240 )?;
241
242 let term = program.eval_full_for_export()?;
243
244 let serde_config = SerdeConfiguration::deserialize(term.clone())?;
245
246 Ok((serde_config.into(), term))
247 }
248}
249
250impl Default for Configuration {
251 fn default() -> Self {
254 let mut program = Program::<CacheImpl>::new_from_source(
255 Source::Builtin
256 .read()
257 .expect("Evaluating the builtin configuration should be safe")
258 .as_slice(),
259 "built-in",
260 std::io::empty(),
261 NullReporter {},
262 )
263 .expect("Evaluating the builtin configuration should be safe");
264 let term = program
265 .eval_full_for_export()
266 .expect("Evaluating the builtin configuration should be safe");
267 let serde_config = SerdeConfiguration::deserialize(term)
268 .expect("Evaluating the builtin configuration should be safe");
269
270 serde_config.into()
271 }
272}
273
274impl From<&Configuration> for HashMap<String, Language> {
276 fn from(config: &Configuration) -> Self {
277 HashMap::from_iter(
278 config
279 .languages
280 .iter()
281 .map(|language| (language.name.clone(), language.clone())),
282 )
283 }
284}
285
286impl PartialEq for Configuration {
288 fn eq(&self, other: &Self) -> bool {
289 let lhs: HashMap<String, Language> = self.into();
290 let rhs: HashMap<String, Language> = other.into();
291
292 lhs == rhs
293 }
294}
295
296impl From<SerdeConfiguration> for Configuration {
297 fn from(value: SerdeConfiguration) -> Self {
298 let languages = value
299 .languages
300 .into_iter()
301 .map(|(name, config)| Language::new(name, config))
302 .collect();
303
304 Self { languages }
305 }
306}
307
308pub(crate) fn project_dirs() -> directories::ProjectDirs {
309 directories::ProjectDirs::from("", "", "topiary")
310 .expect("Could not access the OS's Home directory")
311}