Skip to main content

specta_elm/
elm.rs

1use std::{
2    collections::{HashMap, HashSet},
3    fmt::{self, Debug},
4    fs::File,
5    io::BufReader,
6    path::{Path, PathBuf},
7};
8
9use serde_json::Value;
10use specta::{
11    Types,
12    datatype::{DataType, Fields, NamedDataType},
13};
14
15use crate::{
16    Error,
17    export::{Exporter, IntoExporter},
18};
19
20// Constants {{{
21pub(crate) const COMMENT_SYMBOL: &'static str = "-- ";
22pub(crate) const EXTENSION: &'static str = "elm";
23pub(crate) const ELM_ENUM_SYMBOL: &'static str = "type ";
24pub(crate) const ELM_STRUCT_SYMBOL: &'static str = "type alias ";
25pub(crate) const PRELUDE: &'static str = "generated by specta-elm";
26pub(crate) const RESERVED_TYPE_NAMES: &[&str] = &[
27    "if", "then", "else", "case", "of", "let", "in", "type", "module", "where", "import",
28    "exposing", "as", "port",
29];
30// }}}
31// Types {{{
32fn recurse_fields(fields: &Fields) {
33    match fields {
34        specta::datatype::Fields::Unit => (),
35        specta::datatype::Fields::Unnamed(unnamed_fields) => {
36            for field in unnamed_fields.fields.iter() {
37                if let Some(dt) = &field.ty {
38                    recurse_dt_and_panic(&dt);
39                }
40            }
41        }
42        specta::datatype::Fields::Named(named_fields) => {
43            for (_, field) in named_fields.fields.iter() {
44                if let Some(dt) = &field.ty {
45                    recurse_dt_and_panic(&dt);
46                }
47            }
48        }
49    }
50}
51fn recurse_dt_and_panic(dt: &DataType) {
52    match dt {
53        DataType::List(list) => recurse_dt_and_panic(&list.ty),
54        DataType::Map(map) => {
55            recurse_dt_and_panic(map.key_ty());
56            recurse_dt_and_panic(map.value_ty());
57        }
58        DataType::Struct(st) => recurse_fields(&st.fields),
59        DataType::Enum(en) => {
60            for (_, variant) in &en.variants {
61                recurse_fields(&variant.fields);
62            }
63        }
64        DataType::Tuple(tuple) => {
65            for dt in &tuple.elements {
66                recurse_dt_and_panic(&dt);
67            }
68        }
69        DataType::Nullable(data_type) => recurse_dt_and_panic(&data_type),
70        DataType::Intersection(data_types) => {
71            for dt in data_types {
72                recurse_dt_and_panic(&dt);
73            }
74        }
75        DataType::Reference(reference) => match reference {
76            specta::datatype::Reference::Named(named_reference) => {
77                match &named_reference.inner {
78                    specta::datatype::NamedReferenceType::Recursive(_recursive_inline_type) => {
79                        panic!("recursivity")
80                    }
81                    specta::datatype::NamedReferenceType::Inline { dt, .. } => {
82                        recurse_dt_and_panic(&dt)
83                    }
84                    specta::datatype::NamedReferenceType::Reference { generics, .. } => {
85                        if !generics.is_empty() {
86                            panic!("generics")
87                        }
88                    }
89                };
90            }
91            specta::datatype::Reference::Opaque(_opaque_reference) => panic!("opaque ref"),
92        },
93        DataType::Generic(_generic) => panic!("generic"),
94        _ => (),
95    }
96}
97fn guard_panic_on_unsupported_types<'a>(types: &Types) {
98    for ndt in types.into_unsorted_iter() {
99        if ndt.name.is_empty() {
100            panic!("unnamed")
101        }
102
103        if let Some(dt) = &ndt.ty {
104            recurse_dt_and_panic(&dt);
105        }
106    }
107}
108//
109// }}}
110// Elm {{{
111
112#[derive(Debug, Clone)]
113// #[non_exhaustive]
114// pub struct Elm<E: Exporter> {
115pub struct Elm {
116    project: Project,
117    // exporter: E,
118    types: Types,
119}
120
121// impl<E: Exporter> Elm<E> {
122impl Elm {
123    pub fn init(types: Types, path: &str) -> Self {
124        guard_panic_on_unsupported_types(&types);
125        let project = Project::try_from(path).expect("no elm.json in path or path chidren");
126        Elm { project, types }
127        // let exporter = IntoExporter::into(output, &project);
128
129        // Elm { exporter, project }
130    }
131
132    pub fn export<E: Exporter, O: IntoExporter<Output = E>>(
133        &mut self,
134        output: O,
135    ) -> Result<(), Error> {
136        let mut exporter = output.into(&self.project);
137        exporter.export(&self.types);
138        self.project.cleanup_stale_files()
139    }
140}
141
142#[derive(Clone, PartialEq, Eq, Hash, Debug)]
143pub enum ElmCoreLibImport {
144    Dict,
145    Set,
146}
147
148impl fmt::Display for ElmCoreLibImport {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        f.write_str(match self {
151            ElmCoreLibImport::Dict => "Dict",
152            ElmCoreLibImport::Set => "Set",
153        })
154    }
155}
156
157// }}}
158// Project {{{
159#[derive(Debug, Clone)]
160pub struct Project {
161    src_dirs: Vec<PathBuf>,
162    // config: Value,
163}
164
165impl Project {
166    pub fn source_directories(&self) -> &[PathBuf] {
167        self.src_dirs.as_slice()
168    }
169    fn cleanup_stale_files(&mut self) -> Result<(), Error> {
170        for dir in &self.src_dirs {
171            if dir.exists() {
172                return Ok(());
173            }
174        }
175
176        for dir in &self.src_dirs {
177            for path in collect_existing_files(dir)? {
178                if !is_generated_specta_file(&path)? {
179                    continue;
180                }
181
182                std::fs::remove_file(&path).or_else(|source| {
183                    if source.kind() == std::io::ErrorKind::NotFound {
184                        Ok(())
185                    } else {
186                        Err(Error::remove_file(path.clone(), source))
187                    }
188                })?;
189            }
190            remove_empty_dirs(dir, dir)?;
191        }
192
193        Ok(())
194    }
195}
196
197impl TryFrom<&str> for Project {
198    type Error = Error;
199
200    fn try_from(path: &str) -> Result<Self, Self::Error> {
201        let path = PathBuf::from(path);
202
203        let start: PathBuf = if path.is_file() {
204            path.parent()
205                .map(Path::to_path_buf)
206                .unwrap_or_else(|| PathBuf::from("."))
207        } else {
208            path
209        };
210
211        let elm_json_path = search_down(&start, 2).expect("couldn't find elm.json file");
212
213        let file =
214            File::open(&elm_json_path).map_err(|e| Error::read_file(elm_json_path.clone(), e))?;
215
216        let config: Value =
217            serde_json::from_reader(BufReader::new(file)).expect("couldn't deserialize elm.json");
218
219        let src_dirs = config
220            .get("source-directories")
221            .and_then(Value::as_array)
222            .map(|arr| {
223                arr.iter()
224                    .filter_map(Value::as_str)
225                    .map(|rel_dir| {
226                        PathBuf::from(
227                            elm_json_path
228                                .parent()
229                                .expect("elm project is batman (somehow elm.json exists butt no parent :l)")
230                                .join(rel_dir),
231                        )
232                    })
233                    .collect()
234            })
235            .unwrap_or_default();
236
237        // Ok(Project { src_dirs, config })
238        Ok(Project { src_dirs })
239    }
240}
241
242fn search_down(root: &Path, max_depth: usize) -> Option<PathBuf> {
243    let candidate = root.join("elm.json");
244    if candidate.is_file() {
245        return Some(candidate);
246    }
247    if max_depth == 0 {
248        return None;
249    }
250    let entries = std::fs::read_dir(root).ok()?;
251    for entry in entries.filter_map(Result::ok) {
252        let entry_path = entry.path();
253        if entry_path.is_dir() {
254            if let Some(found) = search_down(&entry_path, max_depth - 1) {
255                return Some(found);
256            }
257        }
258    }
259    None
260}
261
262// }}}
263// References {{{
264pub type ReferenceExports = HashMap<String, NamedDataType>;
265
266// #[derive(Default, Debug)]
267// pub struct References<'a> {
268//     pool: HashSet<&'a NamedReference>,
269// }
270//
271// impl<'a> References<'a> {
272//     pub fn collect(types: &'a Types) -> Self {
273//         let mut refs = References::default();
274//
275//         for ndt in types.into_sorted_iter() {
276//             if let Some(dt) = &ndt.ty {
277//                 refs.collect_refs(&dt);
278//                 // refs.exports.insert(ndt.name.to_string(), ndt.clone());
279//             }
280//         }
281//
282//         refs
283//     }
284//
285//     fn collect_fields(&mut self, fields: &'a Fields) {
286//         match fields {
287//             Fields::Unit => {}
288//             Fields::Unnamed(u) => {
289//                 for f in &u.fields {
290//                     if let Some(ty) = &f.ty {
291//                         self.collect_refs(ty);
292//                     }
293//                 }
294//                 panic!("unnamed fields are not supported in Elm");
295//             }
296//             Fields::Named(n) => {
297//                 for (_, f) in &n.fields {
298//                     if let Some(ty) = &f.ty {
299//                         eprintln!("field export: {:?}", ty);
300//                         self.collect_refs(ty);
301//                     }
302//                 }
303//             }
304//         }
305//     }
306//
307//     fn collect_refs(&mut self, dt: &'a DataType) {
308//         match dt {
309//             DataType::Primitive(_) | DataType::Generic(_) => {}
310//
311//             DataType::List(list) => self.collect_refs(&list.ty),
312//
313//             DataType::Map(map) => {
314//                 self.collect_refs(map.key_ty());
315//                 self.collect_refs(map.value_ty());
316//             }
317//
318//             DataType::Tuple(tuple) => {
319//                 for ty in &tuple.elements {
320//                     self.collect_refs(ty);
321//                 }
322//             }
323//
324//             DataType::Nullable(inner) => self.collect_refs(inner),
325//
326//             DataType::Intersection(types) => {
327//                 for t in types {
328//                     self.collect_refs(t);
329//                 }
330//             }
331//
332//             DataType::Struct(s) => self.collect_fields(&s.fields),
333//
334//             DataType::Enum(e) => {
335//                 for (_, variant) in &e.variants {
336//                     self.collect_fields(&variant.fields);
337//                 }
338//             }
339//
340//             DataType::Reference(reference) => match reference {
341//                 Reference::Named(nref) => match &nref.inner {
342//                     // stdlib "named" wrappers around primitives (String, CString, OsString, ...)
343//                     // aren't real declarations — recurse into the inline body and don't pool them.
344//                     NamedReferenceType::Inline { dt, .. } => self.collect_refs(dt),
345//                     NamedReferenceType::Reference { generics, .. } => {
346//                         // panic!("Elm don't like generics");
347//                         // self.pool.insert(nref);
348//                         if !generics.is_empty() {
349//                             panic!("NO GENERICSS!!");
350//                         }
351//                     }
352//                     NamedReferenceType::Recursive(_) => {
353//                         panic!("dont recurseee!!")
354//                         // self.pool.insert(nref);
355//                     }
356//                 },
357//                 Reference::Opaque(_) => panic!("we don't like opaque refs"),
358//             },
359//         }
360//     }
361// }
362//
363// }}}
364// Utils: file managing {{{
365fn collect_existing_files(root: &Path) -> Result<HashSet<PathBuf>, Error> {
366    if !root.exists() {
367        return Ok(HashSet::new());
368    }
369
370    let mut files = HashSet::new();
371    let entries =
372        std::fs::read_dir(root).map_err(|source| Error::read_dir(root.to_path_buf(), source))?;
373    for entry in entries {
374        let entry = entry.map_err(|source| Error::read_dir(root.to_path_buf(), source))?;
375        let path = entry.path();
376        let file_type = entry
377            .file_type()
378            .map_err(|source| Error::metadata(path.clone(), source))?;
379
380        if file_type.is_symlink() {
381            continue;
382        }
383
384        if file_type.is_dir() {
385            files.extend(collect_existing_files(&path)?);
386        } else if matches!(path.extension().and_then(|e| e.to_str()), Some(EXTENSION)) {
387            files.insert(path);
388        }
389    }
390
391    Ok(files)
392}
393
394fn is_generated_specta_file(path: &Path) -> Result<bool, Error> {
395    match std::fs::read_to_string(path) {
396        Ok(contents) => {
397            Ok((contents.contains("generated by Specta")) || contents.contains(PRELUDE))
398        }
399        Err(err) if err.kind() == std::io::ErrorKind::InvalidData => Ok(false),
400        Err(source) => Err(Error::read_file(path.to_path_buf(), source)),
401    }
402}
403
404/// Remove empty directories recursively, stopping at the root
405fn remove_empty_dirs(path: &Path, root: &Path) -> Result<(), Error> {
406    let entries =
407        std::fs::read_dir(path).map_err(|source| Error::read_dir(path.to_path_buf(), source))?;
408    for entry in entries {
409        let entry = entry.map_err(|source| Error::read_dir(path.to_path_buf(), source))?;
410        let entry_path = entry.path();
411        let file_type = entry
412            .file_type()
413            .map_err(|source| Error::metadata(entry_path.clone(), source))?;
414        if file_type.is_symlink() {
415            continue;
416        }
417        if file_type.is_dir() {
418            remove_empty_dirs(&entry_path, root)?;
419        }
420    }
421
422    let is_empty = path
423        .read_dir()
424        .map_err(|source| Error::read_dir(path.to_path_buf(), source))?
425        .next()
426        .is_none();
427
428    if path != root && is_empty {
429        match std::fs::remove_dir(path) {
430            Ok(()) => {}
431            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
432            Err(source) => {
433                return Err(Error::remove_dir(path.to_path_buf(), source));
434            }
435        }
436    }
437    Ok(())
438}
439
440// }}}