use std::{
collections::{HashMap, HashSet},
fmt::{self, Debug},
fs::File,
io::BufReader,
path::{Path, PathBuf},
};
use serde_json::Value;
use specta::{
Types,
datatype::{DataType, Fields, NamedDataType},
};
use crate::{
Error,
export::{Exporter, IntoExporter},
};
pub(crate) const COMMENT_SYMBOL: &'static str = "-- ";
pub(crate) const EXTENSION: &'static str = "elm";
pub(crate) const ELM_ENUM_SYMBOL: &'static str = "type ";
pub(crate) const ELM_STRUCT_SYMBOL: &'static str = "type alias ";
pub(crate) const PRELUDE: &'static str = "generated by specta-elm";
pub(crate) const RESERVED_TYPE_NAMES: &[&str] = &[
"if", "then", "else", "case", "of", "let", "in", "type", "module", "where", "import",
"exposing", "as", "port",
];
fn recurse_fields(fields: &Fields) {
match fields {
specta::datatype::Fields::Unit => (),
specta::datatype::Fields::Unnamed(unnamed_fields) => {
for field in unnamed_fields.fields.iter() {
if let Some(dt) = &field.ty {
recurse_dt_and_panic(&dt);
}
}
}
specta::datatype::Fields::Named(named_fields) => {
for (_, field) in named_fields.fields.iter() {
if let Some(dt) = &field.ty {
recurse_dt_and_panic(&dt);
}
}
}
}
}
fn recurse_dt_and_panic(dt: &DataType) {
match dt {
DataType::List(list) => recurse_dt_and_panic(&list.ty),
DataType::Map(map) => {
recurse_dt_and_panic(map.key_ty());
recurse_dt_and_panic(map.value_ty());
}
DataType::Struct(st) => recurse_fields(&st.fields),
DataType::Enum(en) => {
for (_, variant) in &en.variants {
recurse_fields(&variant.fields);
}
}
DataType::Tuple(tuple) => {
for dt in &tuple.elements {
recurse_dt_and_panic(&dt);
}
}
DataType::Nullable(data_type) => recurse_dt_and_panic(&data_type),
DataType::Intersection(data_types) => {
for dt in data_types {
recurse_dt_and_panic(&dt);
}
}
DataType::Reference(reference) => match reference {
specta::datatype::Reference::Named(named_reference) => {
match &named_reference.inner {
specta::datatype::NamedReferenceType::Recursive(_recursive_inline_type) => {
panic!("recursivity")
}
specta::datatype::NamedReferenceType::Inline { dt, .. } => {
recurse_dt_and_panic(&dt)
}
specta::datatype::NamedReferenceType::Reference { generics, .. } => {
if !generics.is_empty() {
panic!("generics")
}
}
};
}
specta::datatype::Reference::Opaque(_opaque_reference) => panic!("opaque ref"),
},
DataType::Generic(_generic) => panic!("generic"),
_ => (),
}
}
fn guard_panic_on_unsupported_types<'a>(types: &Types) {
for ndt in types.into_unsorted_iter() {
if ndt.name.is_empty() {
panic!("unnamed")
}
if let Some(dt) = &ndt.ty {
recurse_dt_and_panic(&dt);
}
}
}
#[derive(Debug, Clone)]
pub struct Elm {
project: Project,
types: Types,
}
impl Elm {
pub fn init(types: Types, path: &str) -> Self {
guard_panic_on_unsupported_types(&types);
let project = Project::try_from(path).expect("no elm.json in path or path chidren");
Elm { project, types }
}
pub fn export<E: Exporter, O: IntoExporter<Output = E>>(
&mut self,
output: O,
) -> Result<(), Error> {
let mut exporter = output.into(&self.project);
exporter.export(&self.types);
self.project.cleanup_stale_files()
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub enum ElmCoreLibImport {
Dict,
Set,
}
impl fmt::Display for ElmCoreLibImport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
ElmCoreLibImport::Dict => "Dict",
ElmCoreLibImport::Set => "Set",
})
}
}
#[derive(Debug, Clone)]
pub struct Project {
src_dirs: Vec<PathBuf>,
}
impl Project {
pub fn source_directories(&self) -> &[PathBuf] {
self.src_dirs.as_slice()
}
fn cleanup_stale_files(&mut self) -> Result<(), Error> {
for dir in &self.src_dirs {
if dir.exists() {
return Ok(());
}
}
for dir in &self.src_dirs {
for path in collect_existing_files(dir)? {
if !is_generated_specta_file(&path)? {
continue;
}
std::fs::remove_file(&path).or_else(|source| {
if source.kind() == std::io::ErrorKind::NotFound {
Ok(())
} else {
Err(Error::remove_file(path.clone(), source))
}
})?;
}
remove_empty_dirs(dir, dir)?;
}
Ok(())
}
}
impl TryFrom<&str> for Project {
type Error = Error;
fn try_from(path: &str) -> Result<Self, Self::Error> {
let path = PathBuf::from(path);
let start: PathBuf = if path.is_file() {
path.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."))
} else {
path
};
let elm_json_path = search_down(&start, 2).expect("couldn't find elm.json file");
let file =
File::open(&elm_json_path).map_err(|e| Error::read_file(elm_json_path.clone(), e))?;
let config: Value =
serde_json::from_reader(BufReader::new(file)).expect("couldn't deserialize elm.json");
let src_dirs = config
.get("source-directories")
.and_then(Value::as_array)
.map(|arr| {
arr.iter()
.filter_map(Value::as_str)
.map(|rel_dir| {
PathBuf::from(
elm_json_path
.parent()
.expect("elm project is batman (somehow elm.json exists butt no parent :l)")
.join(rel_dir),
)
})
.collect()
})
.unwrap_or_default();
Ok(Project { src_dirs })
}
}
fn search_down(root: &Path, max_depth: usize) -> Option<PathBuf> {
let candidate = root.join("elm.json");
if candidate.is_file() {
return Some(candidate);
}
if max_depth == 0 {
return None;
}
let entries = std::fs::read_dir(root).ok()?;
for entry in entries.filter_map(Result::ok) {
let entry_path = entry.path();
if entry_path.is_dir() {
if let Some(found) = search_down(&entry_path, max_depth - 1) {
return Some(found);
}
}
}
None
}
pub type ReferenceExports = HashMap<String, NamedDataType>;
fn collect_existing_files(root: &Path) -> Result<HashSet<PathBuf>, Error> {
if !root.exists() {
return Ok(HashSet::new());
}
let mut files = HashSet::new();
let entries =
std::fs::read_dir(root).map_err(|source| Error::read_dir(root.to_path_buf(), source))?;
for entry in entries {
let entry = entry.map_err(|source| Error::read_dir(root.to_path_buf(), source))?;
let path = entry.path();
let file_type = entry
.file_type()
.map_err(|source| Error::metadata(path.clone(), source))?;
if file_type.is_symlink() {
continue;
}
if file_type.is_dir() {
files.extend(collect_existing_files(&path)?);
} else if matches!(path.extension().and_then(|e| e.to_str()), Some(EXTENSION)) {
files.insert(path);
}
}
Ok(files)
}
fn is_generated_specta_file(path: &Path) -> Result<bool, Error> {
match std::fs::read_to_string(path) {
Ok(contents) => {
Ok((contents.contains("generated by Specta")) || contents.contains(PRELUDE))
}
Err(err) if err.kind() == std::io::ErrorKind::InvalidData => Ok(false),
Err(source) => Err(Error::read_file(path.to_path_buf(), source)),
}
}
fn remove_empty_dirs(path: &Path, root: &Path) -> Result<(), Error> {
let entries =
std::fs::read_dir(path).map_err(|source| Error::read_dir(path.to_path_buf(), source))?;
for entry in entries {
let entry = entry.map_err(|source| Error::read_dir(path.to_path_buf(), source))?;
let entry_path = entry.path();
let file_type = entry
.file_type()
.map_err(|source| Error::metadata(entry_path.clone(), source))?;
if file_type.is_symlink() {
continue;
}
if file_type.is_dir() {
remove_empty_dirs(&entry_path, root)?;
}
}
let is_empty = path
.read_dir()
.map_err(|source| Error::read_dir(path.to_path_buf(), source))?
.next()
.is_none();
if path != root && is_empty {
match std::fs::remove_dir(path) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(source) => {
return Err(Error::remove_dir(path.to_path_buf(), source));
}
}
}
Ok(())
}