use crate::exit_codes::ExitCode;
use copybook_core::{ParseOptions, Schema};
use std::io::{self, Read, Write};
use std::path::Path;
#[cfg(test)]
use std::path::PathBuf;
use tempfile::NamedTempFile;
use tracing::{debug, info};
pub fn parse_selectors(select_args: &[String]) -> Vec<String> {
use std::collections::BTreeSet;
select_args
.iter()
.flat_map(|s| s.split(','))
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
pub fn apply_field_projection(schema: Schema, select_args: &[String]) -> anyhow::Result<Schema> {
if select_args.is_empty() {
return Ok(schema);
}
let selectors = parse_selectors(select_args);
info!(
"Applying field projection with {} selectors",
selectors.len()
);
copybook_core::project_schema(&schema, &selectors).map_err(|err| {
anyhow::anyhow!("Failed to apply field projection with selectors {selectors:?}: {err}")
})
}
pub struct ParseOptionsConfig<'a> {
pub strict: bool,
pub strict_comments: bool,
pub codepage: &'a str,
pub emit_filler: bool,
pub dialect: copybook_core::dialect::Dialect,
}
pub fn build_parse_options(config: &ParseOptionsConfig) -> ParseOptions {
ParseOptions {
strict_comments: config.strict_comments,
strict: config.strict,
codepage: config.codepage.to_string(),
emit_filler: config.emit_filler,
allow_inline_comments: !config.strict_comments,
dialect: config.dialect,
}
}
pub fn atomic_write<P: AsRef<Path>, F>(path: P, write_fn: F) -> io::Result<()>
where
F: FnOnce(&mut dyn Write) -> io::Result<()>,
{
let path = path.as_ref();
let temp_dir = path.parent().unwrap_or_else(|| Path::new("."));
let mut temp_file = NamedTempFile::new_in(temp_dir)?;
debug!("Writing to temporary file: {:?}", temp_file.path());
write_fn(&mut temp_file)?;
temp_file.flush()?;
temp_file.as_file().sync_all()?;
debug!("Renaming {:?} to {:?}", temp_file.path(), path);
temp_file.persist(path)?;
Ok(())
}
#[cfg(test)]
#[allow(clippy::expect_used)]
#[allow(clippy::unwrap_used)]
fn temp_path_for(target: &Path) -> PathBuf {
let mut temp_name = target
.file_name()
.unwrap_or_else(|| std::ffi::OsStr::new("output"))
.to_os_string();
temp_name.push(".tmp");
if let Some(parent) = target.parent() {
parent.join(temp_name)
} else {
PathBuf::from(temp_name)
}
}
pub fn determine_exit_code(
has_warnings: bool,
has_errors: bool,
failure_code: ExitCode,
) -> ExitCode {
let _ = has_warnings; if has_errors {
failure_code
} else {
ExitCode::Ok
}
}
pub fn read_file_or_stdin<P: AsRef<Path>>(path: P) -> io::Result<String> {
let path = path.as_ref();
if path == Path::new("-") {
debug!("Reading from stdin");
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
Ok(buffer)
} else {
debug!("Reading from file: {:?}", path);
std::fs::read_to_string(path)
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use anyhow::Result;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_atomic_write_success() -> Result<()> {
let temp_dir = tempdir()?;
let target_path = temp_dir.path().join("test.txt");
let result = atomic_write(&target_path, |writer| writer.write_all(b"Hello, world!"));
assert!(result.is_ok());
assert!(target_path.exists());
let content = fs::read_to_string(&target_path)?;
assert_eq!(content, "Hello, world!");
Ok(())
}
#[test]
fn test_atomic_write_failure_leaves_no_file() -> Result<()> {
let temp_dir = tempdir()?;
let target_path = temp_dir.path().join("test.txt");
let result = atomic_write(&target_path, |_writer| {
Err(io::Error::other("Simulated error"))
});
assert!(result.is_err());
assert!(!target_path.exists());
Ok(())
}
#[test]
fn test_determine_exit_code() {
assert_eq!(
determine_exit_code(false, false, ExitCode::Data),
ExitCode::Ok
); assert_eq!(
determine_exit_code(true, false, ExitCode::Data),
ExitCode::Ok
); assert_eq!(
determine_exit_code(false, true, ExitCode::Data),
ExitCode::Data
); assert_eq!(
determine_exit_code(true, true, ExitCode::Encode),
ExitCode::Encode
); }
#[test]
fn test_temp_path_for() {
let target = Path::new("/path/to/output.jsonl");
let temp = temp_path_for(target);
assert_eq!(temp, Path::new("/path/to/output.jsonl.tmp"));
let target = Path::new("output.jsonl");
let temp = temp_path_for(target);
assert_eq!(temp, Path::new("output.jsonl.tmp"));
}
}