use std::collections::HashMap;
use std::fs::File;
use std::io::{Error, ErrorKind, Write};
use std::path::PathBuf;
use std::process::{Child, Command, ExitCode, Stdio};
use clap::Parser;
use slice_codec::decoder::Decoder;
use slice_codec::encoder::Encoder;
use slicec::ast::Ast;
use slicec::compilation_state::CompilationState;
use slicec::diagnostic_emitter::DiagnosticEmitter;
use slicec::diagnostics::Diagnostics;
use slicec::slice_file::SliceFile;
use slicec::slice_options::{DiagnosticFormat, Plugin, SliceOptions};
mod definition_types;
mod slice_file_converter;
fn encode_generate_code_request(parsed_files: &[slicec::slice_file::SliceFile]) -> Result<Vec<u8>, slice_codec::Error> {
let mut encoding_buffer: Vec<u8> = Vec::new();
let mut slice_encoder = Encoder::from(&mut encoding_buffer);
slice_encoder.encode("generateCode")?;
let mut source_files = Vec::new();
let mut reference_files = Vec::new();
for parsed_file in parsed_files {
let converted_file = definition_types::SliceFile::from(parsed_file);
match parsed_file.is_source {
true => source_files.push(converted_file),
false => reference_files.push(converted_file),
}
}
slice_encoder.encode(&source_files)?;
slice_encoder.encode(&reference_files)?;
Ok(encoding_buffer)
}
fn spawn_plugin_process(plugin: &Plugin, slice_payload: &[u8]) -> std::io::Result<Child> {
let mut subprocess = Command::new(&plugin.path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stdin = subprocess.stdin.as_mut().ok_or(ErrorKind::BrokenPipe)?;
stdin.write_all(slice_payload)?;
let mut arguments_payload = Vec::new();
let mut slice_encoder = Encoder::from(&mut arguments_payload);
slice_encoder.encode(definition_types::Options(plugin.args.clone()))?;
stdin.write_all(&arguments_payload)?;
Ok(subprocess)
}
fn collect_plugin_output(subprocess: Child) -> std::io::Result<Vec<u8>> {
let output = subprocess.wait_with_output()?;
if !output.stderr.is_empty() {
let mut error_string = String::from_utf8_lossy(&output.stderr).into_owned();
error_string.insert_str(0, "errors reported on 'stderr':\n");
return Err(Error::other(error_string));
}
match output.status.code() {
Some(0) => Ok(output.stdout),
Some(code) => Err(Error::other(format!("failed with status code '{code}'"))),
None => Err(Error::from(ErrorKind::Interrupted)),
}
}
fn decode_generator_response(
response_payload: Vec<u8>,
ast: &Ast,
files: &[SliceFile],
) -> std::io::Result<(Vec<definition_types::GeneratedFile>, Diagnostics)> {
let mut slice_decoder = Decoder::from(&response_payload);
let generated_files: Vec<definition_types::GeneratedFile> = slice_decoder.decode()?;
let generator_diagnostics: Vec<definition_types::Diagnostic> = slice_decoder.decode()?;
let mut converted_diagnostics = Diagnostics::new();
for generator_diagnostic in generator_diagnostics {
slice_file_converter::convert_diagnostic(generator_diagnostic, ast, files, &mut converted_diagnostics);
}
Ok((generated_files, converted_diagnostics))
}
fn write_generated_file(
generated_file: &definition_types::GeneratedFile,
output_dir: &Option<String>,
) -> std::io::Result<()> {
let generated_file_bytes = generated_file.contents.as_bytes();
let generated_file_path = match output_dir {
Some(dir) => PathBuf::from(dir).join(&generated_file.path),
None => PathBuf::from(&generated_file.path),
};
if let Ok(current_contents) = std::fs::read(&generated_file_path) {
if current_contents == generated_file_bytes {
return Ok(());
}
}
let mut file = File::create(&generated_file_path)?;
file.write_all(generated_file_bytes)?;
Ok(())
}
fn check_if_file_is_overwritten<'a>(
generated_file: &definition_types::GeneratedFile,
generator: &'a Plugin,
written_to_paths: &mut HashMap<PathBuf, &'a Plugin>,
) -> Result<(), slicec::diagnostics::Error> {
let canonical_path = std::fs::canonicalize(generated_file.path.as_str())
.unwrap_or_else(|_| PathBuf::from(generated_file.path.clone()));
if let Some(other_plugin) = written_to_paths.insert(canonical_path, generator) {
let message = format!(
"the path '{}' was already written to by '{}', and would be overwritten by '{}'",
generated_file.path, other_plugin.path, generator.path,
);
Err(slicec::diagnostics::Error::Other { message })
} else {
Ok(())
}
}
fn report_file_writing_error(file_path: &String, io_error: std::io::Error, diagnostics: &mut Diagnostics) {
let diagnostic = slicec::diagnostics::Error::IO {
action: "write generated file",
path: file_path.to_owned(),
error: io_error,
};
slicec::diagnostics::Diagnostic::from_error(diagnostic).push_into(diagnostics);
}
fn convert_generator_errors_to_diagnostics(generator: &Plugin, io_error: std::io::Error) -> Diagnostics {
let mapped_io_error = slicec::diagnostics::Error::IO {
action: "run code-generator",
path: generator.path.clone(),
error: io_error,
};
let mut diagnostics = Diagnostics::new();
slicec::diagnostics::Diagnostic::from_error(mapped_io_error).push_into(&mut diagnostics);
diagnostics
}
fn main() -> ExitCode {
let slice_options = SliceOptions::parse();
let mut compilation_state = slicec::compile_from_options(&slice_options);
let CompilationState {
ref ast,
ref mut diagnostics,
ref files,
} = compilation_state;
if !diagnostics.has_errors() {
let encoded_request = match encode_generate_code_request(files) {
Ok(result) => result,
Err(error) => {
eprintln!("Critical error: failed to encode request payload!\n{error:?}");
return ExitCode::from(79);
}
};
let generators = slice_options.generators.iter();
let generator_processes = generators
.map(|generator| (generator, spawn_plugin_process(generator, &encoded_request)))
.collect::<Vec<_>>();
let mut written_to_paths: HashMap<PathBuf, &Plugin> = HashMap::new();
for (generator, generator_process) in generator_processes {
let (generated_files, mut generator_diagnostics) = generator_process
.and_then(collect_plugin_output)
.and_then(|payload| decode_generator_response(payload, ast, files))
.unwrap_or_else(|err| (Vec::new(), convert_generator_errors_to_diagnostics(generator, err)));
if !generator_diagnostics.has_errors() {
for generated_file in &generated_files {
if let Err(error) = check_if_file_is_overwritten(generated_file, generator, &mut written_to_paths) {
slicec::diagnostics::Diagnostic::from_error(error).push_into(diagnostics);
}
if let Err(io_error) = write_generated_file(generated_file, &slice_options.output_dir) {
report_file_writing_error(&generated_file.path, io_error, &mut generator_diagnostics);
}
}
}
for mut generator_diagnostic in generator_diagnostics.into_inner() {
generator_diagnostic.plugin = Some(generator.path.clone());
diagnostics.push(generator_diagnostic);
}
}
}
let updated_diagnostics = compilation_state.get_annotated_diagnostics(&slice_options);
let (warning_count, error_count) = DiagnosticEmitter::get_totals(&updated_diagnostics);
let mut stderr = console::Term::stderr();
let mut emitter = DiagnosticEmitter::new(&mut stderr, &slice_options);
emitter.emit_diagnostics(&updated_diagnostics).expect("failed to emit");
if slice_options.diagnostic_format == DiagnosticFormat::Human {
DiagnosticEmitter::emit_totals(warning_count, error_count).expect("failed to emit totals");
}
match error_count == 0 {
true => ExitCode::SUCCESS,
false => ExitCode::FAILURE,
}
}