use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
fs,
path::{Path, PathBuf},
};
use anyhow::Context;
use itertools::Itertools;
use typeshare_model::prelude::*;
use crate::{args::OutputLocation, parser::ParsedData, topsort::topsort};
pub fn write_output<'c>(
lang: &impl Language<'c>,
crate_parsed_data: HashMap<Option<CrateName>, ParsedData>,
dest: &OutputLocation<'_>,
) -> anyhow::Result<()> {
match dest {
OutputLocation::File(file) => {
let mut parsed_data = crate_parsed_data
.into_values()
.reduce(|mut data, new_data| {
data.merge(new_data);
data
})
.context("called `write_output` with no data")?;
parsed_data.sort_contents();
write_single_file(lang, file, &parsed_data)
}
OutputLocation::Folder(directory) => {
let crate_parsed_data = crate_parsed_data
.into_iter()
.map(|(crate_name, mut data)| match crate_name {
Some(crate_name) => {
data.sort_contents();
Ok((crate_name, data))
}
None => anyhow::bail!(
"got files with unknown crates; all files \
must be in crates in multi-file mode"
),
})
.try_collect()?;
write_multiple_files(lang, directory, &crate_parsed_data)
}
}
}
pub fn write_multiple_files<'c>(
lang: &impl Language<'c>,
output_folder: &Path,
crate_parsed_data: &HashMap<CrateName, ParsedData>,
) -> anyhow::Result<()> {
let mut output_files = Vec::with_capacity(crate_parsed_data.len());
for (crate_name, parsed_data) in crate_parsed_data {
let file_path = output_folder.join(&lang.output_filename_for_crate(&crate_name));
let mut output = Vec::new();
generate_types(
lang,
&mut output,
parsed_data,
FilesMode::Multi(&crate_name),
)
.with_context(|| format!("error generating typeshare types for crate {crate_name}"))?;
check_write_file(&file_path, output).with_context(|| {
format!(
"error writing generated typeshare types for crate {crate_name} to '{}'",
file_path.display()
)
})?;
output_files.push((crate_name, file_path));
}
output_files.sort_by_key(|&(crate_name, _)| crate_name);
lang.write_additional_files(
output_folder,
output_files
.iter()
.map(|(crate_name, file_path)| (*crate_name, file_path.as_path())),
)
.context("failed to write additional files")?;
Ok(())
}
pub fn write_single_file<'c>(
lang: &impl Language<'c>,
file_name: &Path,
parsed_data: &ParsedData,
) -> Result<(), anyhow::Error> {
let mut output = Vec::new();
generate_types(lang, &mut output, parsed_data, FilesMode::Single)
.context("error generating typeshare types")?;
let outfile = Path::new(file_name).to_path_buf();
check_write_file(&outfile, output)
.context("error writing generated typeshare types to file")?;
Ok(())
}
fn check_write_file(outfile: &PathBuf, output: Vec<u8>) -> anyhow::Result<()> {
match fs::read(outfile) {
Ok(buf) if buf == output => {
eprintln!("Skipping writing to {outfile:?} no changes");
return Ok(());
}
_ => {}
}
if !output.is_empty() {
let out_dir = outfile
.parent()
.context(format!("Could not get parent for {outfile:?}"))?;
if !out_dir.exists() {
fs::create_dir_all(out_dir).context("failed to create output directory")?;
}
fs::write(outfile, output).context("failed to write output")?;
}
Ok(())
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub enum BorrowedRustItem<'a> {
Struct(&'a RustStruct),
Enum(&'a RustEnum),
Alias(&'a RustTypeAlias),
Const(&'a RustConst),
}
impl BorrowedRustItem<'_> {
pub fn id(&self) -> &TypeName {
&match *self {
BorrowedRustItem::Struct(item) => &item.id,
BorrowedRustItem::Enum(item) => &item.shared().id,
BorrowedRustItem::Alias(item) => &item.id,
BorrowedRustItem::Const(item) => &item.id,
}
.original
}
}
fn generate_types<'c>(
lang: &impl Language<'c>,
out: &mut Vec<u8>,
data: &ParsedData,
mode: FilesMode<&CrateName>,
) -> anyhow::Result<()> {
lang.begin_file(out, mode)
.context("error writing file header")?;
if let FilesMode::Multi(crate_name) = mode {
let all_types = HashMap::new();
lang.write_imports(out, crate_name, used_imports(&data, crate_name, &all_types))
.context("error writing imports")?;
}
let ParsedData {
structs,
enums,
aliases,
consts,
..
} = data;
let mut items = Vec::from_iter(
aliases
.iter()
.map(BorrowedRustItem::Alias)
.chain(structs.iter().map(BorrowedRustItem::Struct))
.chain(enums.iter().map(BorrowedRustItem::Enum))
.chain(consts.iter().map(BorrowedRustItem::Const)),
);
topsort(&mut items);
for thing in &items {
let name = thing.id();
match thing {
BorrowedRustItem::Enum(e) => lang
.write_enum(out, e)
.with_context(|| format!("error writing enum {name}"))?,
BorrowedRustItem::Struct(s) => lang
.write_struct(out, s)
.with_context(|| format!("error writing struct {name}"))?,
BorrowedRustItem::Alias(a) => lang
.write_type_alias(out, a)
.with_context(|| format!("error writing type alias {name}"))?,
BorrowedRustItem::Const(c) => lang
.write_const(out, c)
.with_context(|| format!("error writing const {name}"))?,
}
}
lang.end_file(out, mode)
.context("error writing file trailer")
}
fn used_imports<'a, 'b: 'a>(
data: &'b ParsedData,
crate_name: &CrateName,
all_types: &'a HashMap<CrateName, HashSet<TypeName>>,
) -> BTreeMap<&'a CrateName, BTreeSet<&'a TypeName>> {
let mut used_imports: BTreeMap<&'a CrateName, BTreeSet<&'a TypeName>> = BTreeMap::new();
let fallback = |referenced_import: &'a ImportedType,
used: &mut BTreeMap<&'a CrateName, BTreeSet<&'a TypeName>>| {
if let Some((crate_name, ty)) = all_types
.iter()
.flat_map(|(k, v)| {
v.iter()
.find(|&t| *t == referenced_import.type_name && k != crate_name)
.map(|t| (k, t))
})
.next()
{
println!("Warning: Using {crate_name} as module for {ty} which is not in referenced crate {}", referenced_import.base_crate);
used.entry(crate_name).or_default().insert(ty);
} else {
}
};
for referenced_import in data
.import_types
.iter()
.filter(|imp| imp.base_crate != *crate_name)
{
if let Some(type_names) = all_types.get(&referenced_import.base_crate) {
if referenced_import.type_name == "*" {
used_imports
.entry(&referenced_import.base_crate)
.and_modify(|names| names.extend(type_names.iter()));
} else if let Some(ty_name) = type_names.get(&referenced_import.type_name) {
used_imports
.entry(&referenced_import.base_crate)
.or_default()
.insert(ty_name);
} else {
fallback(referenced_import, &mut used_imports);
}
} else {
fallback(referenced_import, &mut used_imports);
}
}
used_imports
}