use crate::{Config, Flow, TypeVisitor};
use std::any::TypeId;
use std::collections::HashSet;
use std::path::Path;
#[derive(Debug, thiserror::Error)]
pub enum ExportError {
#[error("type `{0}` cannot be exported")]
CannotBeExported(&'static str),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Fmt(#[from] std::fmt::Error),
}
pub fn export_to<T: Flow + 'static + ?Sized>(cfg: &Config, path: &Path) -> Result<(), ExportError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let type_name = T::ident(cfg);
if path.exists() {
let existing = std::fs::read_to_string(path)?;
let marker = format!("type {type_name}");
if existing.contains(&marker) {
return Ok(());
}
let decl_line = format_decl::<T>(cfg);
let mut merged = existing;
if !merged.ends_with('\n') {
merged.push('\n');
}
merged.push('\n');
if let Some(docs) = T::docs() {
merged.push_str(&format_docs(&docs));
}
merged.push_str(&decl_line);
merged.push('\n');
std::fs::write(path, merged)?;
} else {
let content = export_to_string::<T>(cfg)?;
std::fs::write(path, content)?;
}
Ok(())
}
pub fn export_all_into<T: Flow + ?Sized + 'static>(cfg: &Config) -> Result<(), ExportError> {
let mut seen = HashSet::new();
export_recursive::<T>(cfg, &mut seen)
}
struct Visit<'a> {
cfg: &'a Config,
seen: &'a mut HashSet<TypeId>,
error: Option<ExportError>,
}
impl TypeVisitor for Visit<'_> {
fn visit<T: Flow + 'static + ?Sized>(&mut self) {
if self.error.is_some() || <T as crate::Flow>::output_path().is_none() {
return;
}
self.error = export_recursive::<T>(self.cfg, self.seen).err();
}
}
fn export_recursive<T: Flow + ?Sized + 'static>(
cfg: &Config,
seen: &mut HashSet<TypeId>,
) -> Result<(), ExportError> {
if !seen.insert(TypeId::of::<T>()) {
return Ok(());
}
let mut visitor = Visit {
cfg,
seen,
error: None,
};
<T as crate::Flow>::visit_dependencies(&mut visitor);
if let Some(e) = visitor.error {
return Err(e);
}
let relative = <T as crate::Flow>::output_path()
.ok_or(ExportError::CannotBeExported(std::any::type_name::<T>()))?;
let path = cfg.out_dir().join(relative);
export_to::<T>(cfg, &path)
}
pub fn export_to_string<T: Flow + ?Sized + 'static>(cfg: &Config) -> Result<String, ExportError> {
let mut content = String::from("// @flow\n// Generated by flowjs-rs. Do not edit.\n\n");
let self_id = TypeId::of::<T>();
let self_path = <T as crate::Flow>::output_path();
let deps = T::dependencies(cfg);
let mut seen_imports = HashSet::new();
for dep in &deps {
if dep.type_id == self_id || !seen_imports.insert(dep.type_id) {
continue;
}
if let Some(ref self_p) = self_path {
if &dep.output_path == self_p {
continue;
}
}
let import_path = dep.output_path.to_str().unwrap_or(&dep.flow_name);
content.push_str(&format!(
"import type {{ {} }} from './{import_path}';\n",
dep.flow_name
));
}
if !seen_imports.is_empty() {
content.push('\n');
}
if let Some(docs) = T::docs() {
content.push_str(&format_docs(&docs));
}
content.push_str(&format_decl::<T>(cfg));
content.push('\n');
Ok(content)
}
fn format_decl<T: Flow + ?Sized>(cfg: &Config) -> String {
let decl = T::decl(cfg);
if decl.starts_with("declare export") {
decl
} else {
format!("export {decl}")
}
}
fn format_docs(docs: &str) -> String {
let mut out = String::from("/**\n");
for line in docs.lines() {
out.push_str(&format!(" * {line}\n"));
}
out.push_str(" */\n");
out
}