flexgen/
config.rs

1use std::collections::HashMap;
2use std::io::Read;
3use std::path::{Path, PathBuf};
4use std::{fs, io};
5
6use flexstr::SharedStr;
7
8use crate::var::Vars;
9use crate::{CodeFragments, Error, TokenVars};
10
11const BUF_SIZE: usize = u16::MAX as usize;
12
13const DEFAULT_FILENAME: &str = "flexgen.toml";
14
15// *** FragmentItem ***
16
17/// An enum that is either a reference to a code fragment or a fragment list
18#[derive(Clone, Debug, serde::Deserialize, PartialEq)]
19#[serde(untagged)]
20pub enum FragmentItem {
21    // Must be first so Serde uses this one always
22    /// A single code fragment
23    Fragment(SharedStr),
24    /// A reference to a list of code fragments
25    FragmentListRef(SharedStr),
26}
27
28// *** Fragment Lists ***
29
30#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq)]
31struct FragmentLists(HashMap<SharedStr, Vec<FragmentItem>>);
32
33impl FragmentLists {
34    pub fn build(&self) -> Self {
35        let mut lists = HashMap::with_capacity(self.0.len());
36
37        for (key, fragments) in &self.0 {
38            let mut new_fragments = Vec::with_capacity(fragments.len());
39
40            for fragment in fragments {
41                match fragment {
42                    FragmentItem::Fragment(s) | FragmentItem::FragmentListRef(s) => {
43                        // If it is also a key, that means it is a list reference
44                        if self.0.contains_key(s) {
45                            new_fragments.push(FragmentItem::FragmentListRef(s.clone()));
46                        } else {
47                            new_fragments.push(FragmentItem::Fragment(s.clone()));
48                        }
49                    }
50                }
51            }
52
53            lists.insert(key.clone(), new_fragments);
54        }
55
56        Self(lists)
57    }
58
59    pub fn validate_code_fragments(&self, code: &CodeFragments) -> Result<(), Error> {
60        let mut missing = Vec::new();
61
62        // Loop over each fragment list searching for each item in the code fragments
63        for fragments in self.0.values() {
64            let v: Vec<_> = fragments
65                .iter()
66                .filter_map(|fragment| match fragment {
67                    FragmentItem::Fragment(name) if !code.contains_key(name) => Some(name.clone()),
68                    _ => None,
69                })
70                .collect();
71
72            // Store all missing fragments
73            missing.extend(v);
74        }
75
76        if missing.is_empty() {
77            Ok(())
78        } else {
79            Err(Error::MissingFragments(missing))
80        }
81    }
82
83    pub fn validate_file(&self, name: &SharedStr, f: &File) -> Result<(), Error> {
84        // Ensure the file's fragment list exists
85        if !self.0.contains_key(&f.fragment_list) {
86            return Err(Error::MissingFragmentList(
87                f.fragment_list.clone(),
88                name.clone(),
89            ));
90        }
91
92        let mut missing = Vec::new();
93
94        'top: for exception in &f.fragment_list_exceptions {
95            // If it is the name of a list, we can bypass the 2nd scan entirely
96            if self.0.contains_key(exception) {
97                continue;
98            }
99
100            // If it might be the name of an actual fragment we will need to scan them all
101            for fragment_list in self.0.values() {
102                // As soon as we find a match jump to looking for next exception
103                if fragment_list.iter().any(|fragment| match fragment {
104                    FragmentItem::Fragment(name) => name == exception,
105                    _ => false,
106                }) {
107                    continue 'top;
108                }
109            }
110
111            // If we didn't find as a list or via scan, it is missing
112            missing.push(exception.clone());
113        }
114
115        if missing.is_empty() {
116            Ok(())
117        } else {
118            Err(Error::MissingFragmentListExceptions(missing, name.clone()))
119        }
120    }
121
122    #[inline]
123    pub fn fragment_list(&self, name: &SharedStr) -> Result<&Vec<FragmentItem>, Error> {
124        self.0
125            .get(name)
126            .ok_or_else(|| Error::FragmentListNotFound(name.clone()))
127    }
128}
129
130// *** Config ***
131
132#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq)]
133struct General {
134    #[serde(default)]
135    base_path: PathBuf,
136    #[serde(default)]
137    rust_fmt: RustFmt,
138    #[serde(default)]
139    vars: Vars,
140}
141
142impl General {
143    #[inline]
144    fn build_rust_fmt(&self) -> Option<rust_format::RustFmt> {
145        self.rust_fmt.build_rust_fmt()
146    }
147}
148
149#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq)]
150struct RustFmt {
151    #[serde(default)]
152    omit_final_format: bool,
153    #[serde(default)]
154    path: Option<PathBuf>,
155    #[serde(default)]
156    options: HashMap<SharedStr, SharedStr>,
157}
158
159impl RustFmt {
160    fn build_rust_fmt(&self) -> Option<rust_format::RustFmt> {
161        if !self.omit_final_format {
162            let mut config = if !self.options.is_empty() {
163                let map = self.options.iter().map(|(k, v)| (&**k, &**v)).collect();
164                rust_format::Config::from_hash_map(map)
165            } else {
166                rust_format::Config::new()
167            };
168            if let Some(path) = &self.path {
169                config = config.rust_fmt_path(path.clone())
170            }
171
172            Some(rust_format::RustFmt::from_config(config))
173        } else {
174            None
175        }
176    }
177}
178
179#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq)]
180struct File {
181    path: PathBuf,
182    fragment_list: SharedStr,
183    #[serde(default)]
184    fragment_list_exceptions: Vec<SharedStr>,
185    vars: Vars,
186}
187
188/// The `flexgen` configuration
189#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq)]
190pub struct Config {
191    #[serde(default)]
192    general: General,
193    fragment_lists: FragmentLists,
194    files: HashMap<SharedStr, File>,
195}
196
197impl Config {
198    /// Try to load the `Config` from the given TOML reader
199    pub fn from_toml_reader(r: impl io::Read) -> Result<Config, Error> {
200        let mut reader = io::BufReader::new(r);
201        let mut buffer = String::with_capacity(BUF_SIZE);
202        reader.read_to_string(&mut buffer)?;
203
204        Ok(toml::from_str(&buffer)?)
205    }
206
207    /// Try to load the `Config` from the default TOML file (flexgen.toml)
208    pub fn from_default_toml_file() -> Result<Config, Error> {
209        let f = fs::File::open(DEFAULT_FILENAME)?;
210        Self::from_toml_reader(f)
211    }
212
213    /// Try to load the `Config` from the given TOML file
214    pub fn from_toml_file(cfg_name: impl AsRef<Path>) -> Result<Config, Error> {
215        let f = fs::File::open(cfg_name)?;
216        Self::from_toml_reader(f)
217    }
218
219    pub(crate) fn build_and_validate(&mut self, code: &CodeFragments) -> Result<(), Error> {
220        // Build and validate fragment lists against code fragments and files
221        self.fragment_lists = self.fragment_lists.build();
222
223        self.fragment_lists.validate_code_fragments(code)?;
224        for (name, file) in &self.files {
225            self.fragment_lists.validate_file(name, file)?;
226        }
227
228        Ok(())
229    }
230
231    /// Return all the files names specified in the config
232    #[inline]
233    pub fn file_names(&self) -> Vec<&SharedStr> {
234        self.files.keys().collect()
235    }
236
237    /// Return the specified file configuration
238    #[inline]
239    fn file(&self, name: &SharedStr) -> Result<&File, Error> {
240        self.files
241            .get(name)
242            .ok_or_else(|| Error::FileNotFound(name.clone()))
243    }
244
245    /// Build the full file path to the file given as a parameter
246    pub fn file_path(&self, name: &SharedStr) -> Result<PathBuf, Error> {
247        let file = self.file(name)?;
248        let base_path = self.general.base_path.as_os_str();
249
250        let mut path = PathBuf::with_capacity(base_path.len() + file.path.as_os_str().len());
251        path.push(base_path);
252        path.push(&file.path);
253        Ok(path)
254    }
255
256    #[inline]
257    fn convert_vars(vars: &Vars) -> Result<TokenVars, Error> {
258        vars.iter()
259            .map(|(key, value)| match value.to_token_item() {
260                Ok(value) => Ok((key.clone(), value)),
261                Err(err) => Err(err),
262            })
263            .collect()
264    }
265
266    #[inline]
267    fn general_vars(&self) -> Result<TokenVars, Error> {
268        Self::convert_vars(&self.general.vars)
269    }
270
271    #[inline]
272    fn file_vars(&self, name: &SharedStr) -> Result<TokenVars, Error> {
273        Self::convert_vars(&self.file(name)?.vars)
274    }
275
276    /// Return the complete vars for the file name given as a parameter
277    #[inline]
278    pub fn vars(&self, name: &SharedStr) -> Result<TokenVars, Error> {
279        let mut vars = self.general_vars()?;
280        vars.extend(self.file_vars(name)?);
281        Ok(vars)
282    }
283
284    /// Return the given named fragment list
285    #[inline]
286    pub fn fragment_list(&self, name: &SharedStr) -> Result<&Vec<FragmentItem>, Error> {
287        self.fragment_lists.fragment_list(name)
288    }
289
290    /// Return the fragment list used by the file given a parameter
291    #[inline]
292    pub fn file_fragment_list(&self, name: &SharedStr) -> Result<&Vec<FragmentItem>, Error> {
293        let name = &self.file(name)?.fragment_list;
294        self.fragment_list(name)
295    }
296
297    /// Return all the fragment exceptions for the given file
298    #[inline]
299    pub fn file_fragment_exceptions(&self, name: &SharedStr) -> Result<&Vec<SharedStr>, Error> {
300        Ok(&self.file(name)?.fragment_list_exceptions)
301    }
302
303    /// Return a [RustFmt](rust_format::RustFmt) instance configured as specified in this configuration
304    #[inline]
305    pub fn build_rust_fmt(&self) -> Option<rust_format::RustFmt> {
306        self.general.build_rust_fmt()
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use std::collections::HashMap;
313    use std::path::PathBuf;
314    use std::str::FromStr;
315
316    use flexstr::{shared_str, SharedStr};
317    use pretty_assertions::assert_eq;
318
319    use crate::config::{Config, File, FragmentItem, FragmentLists, General, RustFmt};
320    use crate::var::{CodeValue, VarItem, VarValue};
321
322    const CONFIG: &str = r#"
323        [general]
324        base_path = "src/"
325        
326        [general.rust_fmt]
327        path = "rustfmt"
328        
329        [general.vars]
330        product = "FlexStr"
331        generate = true
332        count = 5
333        suffix = "$ident$Str"
334        list = [ "FlexStr", true, 5, "$ident$Str" ]
335                
336        [fragment_lists]
337        impl = [ "impl_struct", "impl_core_ref" ]
338        impl_struct = [ "empty", "from_ref" ]
339        
340        [files.str]
341        path = "strings/generated/std_str.rs"
342        fragment_list = "impl"
343        fragment_list_exceptions = [ "impl_core_ref" ]
344        
345        [files.str.vars]
346        str_type = "str"
347    "#;
348
349    fn general() -> General {
350        let mut vars = HashMap::new();
351
352        let product = VarValue::String(shared_str!("FlexStr"));
353        vars.insert(shared_str!("product"), VarItem::Single(product.clone()));
354
355        let generate = VarValue::Bool(true);
356        vars.insert(shared_str!("generate"), VarItem::Single(generate.clone()));
357
358        let count = VarValue::Number(5);
359        vars.insert(shared_str!("count"), VarItem::Single(count.clone()));
360
361        let suffix = VarValue::CodeValue(CodeValue::from_str("$ident$Str").unwrap());
362        vars.insert(shared_str!("suffix"), VarItem::Single(suffix.clone()));
363
364        vars.insert(
365            shared_str!("list"),
366            VarItem::List(vec![product, generate, count, suffix]),
367        );
368
369        let rust_fmt = RustFmt {
370            omit_final_format: false,
371            path: Some("rustfmt".into()),
372            options: Default::default(),
373        };
374
375        General {
376            base_path: PathBuf::from("src/"),
377            rust_fmt,
378            vars,
379        }
380    }
381
382    fn fragment_lists() -> FragmentLists {
383        use FragmentItem::*;
384
385        let mut lists = HashMap::new();
386        lists.insert(
387            shared_str!("impl"),
388            vec![
389                Fragment(shared_str!("impl_struct")),
390                Fragment(shared_str!("impl_core_ref")),
391            ],
392        );
393        lists.insert(
394            shared_str!("impl_struct"),
395            vec![
396                Fragment(shared_str!("empty")),
397                Fragment(shared_str!("from_ref")),
398            ],
399        );
400        FragmentLists(lists)
401    }
402
403    fn files() -> HashMap<SharedStr, File> {
404        let mut str_vars = HashMap::new();
405        str_vars.insert(
406            shared_str!("str_type"),
407            VarItem::Single(VarValue::String(shared_str!("str"))),
408        );
409
410        let files_str = File {
411            path: PathBuf::from("strings/generated/std_str.rs"),
412            fragment_list: shared_str!("impl"),
413            fragment_list_exceptions: vec![shared_str!("impl_core_ref")],
414            vars: str_vars,
415        };
416
417        let mut files = HashMap::new();
418        files.insert(shared_str!("str"), files_str);
419        files
420    }
421
422    #[test]
423    fn from_reader() {
424        let actual = Config::from_toml_reader(CONFIG.as_bytes()).unwrap();
425        let expected = Config {
426            general: general(),
427            fragment_lists: fragment_lists(),
428            files: files(),
429        };
430
431        assert_eq!(expected, actual);
432    }
433}