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