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
20pub(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];
30fn 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#[derive(Debug, Clone)]
113pub struct Elm {
116 project: Project,
117 types: Types,
119}
120
121impl 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 }
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#[derive(Debug, Clone)]
160pub struct Project {
161 src_dirs: Vec<PathBuf>,
162 }
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 })
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
262pub type ReferenceExports = HashMap<String, NamedDataType>;
265
266fn 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
404fn 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