Skip to content

Commit

Permalink
refactor: move to a pipeline-based code processing system
Browse files Browse the repository at this point in the history
  • Loading branch information
threadexio committed Dec 27, 2024
1 parent 9885cb6 commit 02bef08
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 121 deletions.
90 changes: 90 additions & 0 deletions src/banner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use std::fmt::{self, Write};

use chrono::{DateTime, Local};
use const_format::formatcp;

use crate::consts::{CRATE_NAME, CRATE_REPOSITORY, SHORT_VERSION};
use crate::display::format_date;
use crate::pipeline::Stage;
use crate::quotes::Quotes;

#[derive(Debug, Clone)]
pub struct Banner {
pub quotes: Quotes,
pub deterministic: bool,
}

impl Banner {
fn write_banner<W: Write + ?Sized>(&self, out: &mut W) -> fmt::Result {
const MIN_WIDTH: usize = 56;
const PADDING: usize = 4;

const ART: &[&str] = &[
r#" ) ( ("#,
r#" ( /( ( )\ ) )\"#,
r#" ( )\()) ))\ ( (()/(((_)"#,
r#" )\ ((_)\ /((_) )\ ) ((_))_"#,
r#" ((_)| |(_)(_))( _(_/( _| || |"#,
r#"/ _| | '_ \| || || ' \))/ _` || |"#,
r#"\__| |_.__/ \_,_||_||_| \__,_||_|"#,
];

let generated_at = if self.deterministic {
format_date(DateTime::UNIX_EPOCH)
} else {
format_date(Local::now())
};

let quote = if self.deterministic {
self.quotes.get(0).expect("we dont have a single quote :'(")
} else {
self.quotes.random()
};

let line1 = formatcp!("{CRATE_NAME} {SHORT_VERSION}");
let line2 = formatcp!("{CRATE_REPOSITORY}");
let line3 = format!("Generated at: {}", generated_at);

let art_width = ART.iter().map(|x| x.len()).max().unwrap();
let banner_width = MIN_WIDTH.max(art_width).max(line1.len()).max(line2.len()) + PADDING;

writeln!(out, "/**")?;
writeln!(out, " *")?;
for line in ART {
writeln!(out, " * {:^1$}", line, banner_width)?;
}
writeln!(out, " * ")?;

writeln!(out, " * {:^1$}", line1, banner_width)?;
writeln!(out, " * {:^1$}", line2, banner_width)?;
writeln!(out, " *")?;
writeln!(out, " * {:^1$}", line3, banner_width)?;
writeln!(out, " *")?;
writeln!(out, " *")?;
quote.lines().try_for_each(|x| writeln!(out, " * {x}"))?;
writeln!(out, " * - {}", quote.author())?;
writeln!(out, " *")?;
writeln!(out, " */")?;
writeln!(out)?;

Ok(())
}
}

impl Stage for Banner {
fn name() -> &'static str {
"banner"
}

fn process(&mut self, code: String) -> eyre::Result<String> {
const ESTIMATED_BANNER_SIZE: usize = 1024;

let mut out = String::with_capacity(ESTIMATED_BANNER_SIZE + code.len());

self.write_banner(&mut out)
.expect("writing to String should never fail");

out.push_str(&code);
Ok(out)
}
}
130 changes: 23 additions & 107 deletions src/bundler.rs
Original file line number Diff line number Diff line change
@@ -1,123 +1,39 @@
use std::fmt::{self, Write};
use std::fmt::Write;

use chrono::{DateTime, Local};
use const_format::formatcp;
use eyre::{Context, Result};
use eyre::Result;

use crate::consts::{CRATE_NAME, CRATE_REPOSITORY, SHORT_VERSION};
use crate::display::format_date;
use crate::formatter::Formatter;
use crate::quotes::Quotes;
use crate::source::Source;
use crate::source::Sources;

#[derive(Debug, Clone)]
pub struct Bundler {
pub formatter: Option<Formatter>,
pub deterministic: bool,
pub quotes: Quotes,
}
pub struct Bundler {}

impl Bundler {
fn make_bundle<'a, I>(&self, sources: I) -> String
where
I: Iterator<Item = &'a Source>,
{
pub fn bundle(&self, sources: &Sources) -> Result<String> {
let mut out = String::new();

self.write_banner(&mut out)
.expect("writing to a String should never fail");

let mut write_source = |source: &Source| -> fmt::Result {
let file_name = source
.path
.file_name()
.expect("source file paths should always have a last component");

let header = format!("bundled from \"{}\"", file_name.to_string_lossy());
sources
.dependency_order()?
.try_for_each(|source| -> Result<()> {
let file_name = source
.path
.file_name()
.expect("source file paths should always have a last component");

writeln!(out, "/**")?;
writeln!(out, " * {}", header)?;
writeln!(out, " */")?;
writeln!(out)?;
let header = format!("bundled from \"{}\"", file_name.to_string_lossy());

out.write_str(&source.content)?;
if !source.content.ends_with("\n\n") {
writeln!(out, "/**")?;
writeln!(out, " * {}", header)?;
writeln!(out, " */")?;
writeln!(out)?;
}

Ok(())
};

sources.for_each(|x| write_source(x).expect("writing to a String should never fail"));

out
}

fn write_banner<W: Write + ?Sized>(&self, out: &mut W) -> fmt::Result {
const MIN_WIDTH: usize = 56;
const PADDING: usize = 4;

const ART: &[&str] = &[
r#" ) ( ("#,
r#" ( /( ( )\ ) )\"#,
r#" ( )\()) ))\ ( (()/(((_)"#,
r#" )\ ((_)\ /((_) )\ ) ((_))_"#,
r#" ((_)| |(_)(_))( _(_/( _| || |"#,
r#"/ _| | '_ \| || || ' \))/ _` || |"#,
r#"\__| |_.__/ \_,_||_||_| \__,_||_|"#,
];

let generated_at = if self.deterministic {
format_date(DateTime::UNIX_EPOCH)
} else {
format_date(Local::now())
};

let quote = if self.deterministic {
self.quotes.get(0).expect("we dont have a single quote :'(")
} else {
self.quotes.random()
};

let line1 = formatcp!("{CRATE_NAME} {SHORT_VERSION}");
let line2 = formatcp!("{CRATE_REPOSITORY}");
let line3 = format!("Generated at: {}", generated_at);

let art_width = ART.iter().map(|x| x.len()).max().unwrap();
let banner_width = MIN_WIDTH.max(art_width).max(line1.len()).max(line2.len()) + PADDING;

writeln!(out, "/**")?;
writeln!(out, " *")?;
for line in ART {
writeln!(out, " * {:^1$}", line, banner_width)?;
}
writeln!(out, " * ")?;

writeln!(out, " * {:^1$}", line1, banner_width)?;
writeln!(out, " * {:^1$}", line2, banner_width)?;
writeln!(out, " *")?;
writeln!(out, " * {:^1$}", line3, banner_width)?;
writeln!(out, " *")?;
writeln!(out, " *")?;
quote.lines().try_for_each(|x| writeln!(out, " * {x}"))?;
writeln!(out, " * - {}", quote.author())?;
writeln!(out, " *")?;
writeln!(out, " */")?;
writeln!(out)?;

Ok(())
}

pub fn bundle<'a, I>(&self, sources: I) -> Result<String>
where
I: Iterator<Item = &'a Source>,
{
let mut out = self.make_bundle(sources);
out.write_str(&source.content)?;
if !source.content.ends_with("\n\n") {
writeln!(out)?;
}

if let Some(formatter) = self.formatter.as_ref() {
out = formatter.format(out).context("failed to format bundle")?;
out.push('\n'); // clang-format removes the final newline for some reason
}
Ok(())
})
.expect("writing to String should never fail");

Ok(out)
}
Expand Down
15 changes: 9 additions & 6 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ use std::path::{Path, PathBuf};
use clap::Parser;
use eyre::{Context, Result};

use crate::banner::Banner;
use crate::bundler::Bundler;
use crate::consts::{CRATE_DESCRIPTION, LONG_VERSION, SHORT_VERSION};
use crate::display::display_path;
use crate::formatter::Formatter;
use crate::pipeline::Pipeline;
use crate::quotes::Quotes;
use crate::source::Sources;

Expand Down Expand Up @@ -62,17 +64,18 @@ pub fn run() -> Result<()> {

let sources = Sources::new(args.entry)?;

let bundler = Bundler {
let mut pipeline = Pipeline {
bundler: Bundler {},
banner: Banner {
quotes: Quotes {},
deterministic: args.deterministic,
},
formatter: some_if(!args.no_format, || Formatter {
exe: args.formatter,
}),
deterministic: args.deterministic,
quotes: Quotes {},
};

let bundle = bundler
.bundle(sources.dependency_order()?)
.context("failed to make bundle")?;
let bundle = pipeline.process(&sources)?;

write_bundle(bundle, &args.output_file).with_context(|| {
format!(
Expand Down
19 changes: 11 additions & 8 deletions src/formatter.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::str::from_utf8;

use eyre::{bail, Context, Result};

use crate::pipeline::Stage;

#[derive(Debug, Clone)]
pub struct Formatter {
pub exe: PathBuf,
}

impl Formatter {
pub fn format(&self, code: String) -> Result<String> {
impl Stage for Formatter {
fn name() -> &'static str {
"format"
}

fn process(&mut self, code: String) -> Result<String> {
let mut p = Command::new(&self.exe)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
Expand All @@ -29,11 +34,9 @@ impl Formatter {
bail!("formatter exited with non-zero code");
}

let formatterd_code = from_utf8(&p.stdout)
.context("formatter stdout contains invalid UTF8")?
.trim()
.to_owned();
let formatted_code =
String::from_utf8(p.stdout).context("formatter stdout contains invalid UTF8")?;

Ok(formatterd_code)
Ok(formatted_code)
}
}
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ use owo_colors::OwoColorize;
#[macro_use]
extern crate log;

mod banner;
mod bundler;
mod cli;
mod consts;
mod display;
mod formatter;
mod parse;
mod pipeline;
mod quotes;
mod source;

Expand Down
48 changes: 48 additions & 0 deletions src/pipeline.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use eyre::{Context, Result};

use crate::banner::Banner;
use crate::bundler::Bundler;
use crate::formatter::Formatter;
use crate::source::Sources;

pub trait Stage: Sized {
fn name() -> &'static str;
fn process(&mut self, code: String) -> Result<String>;
}

impl<S: Stage> Stage for Option<S> {
fn name() -> &'static str {
S::name()
}

fn process(&mut self, code: String) -> Result<String> {
match self.as_mut() {
None => Ok(code),
Some(stage) => stage.process(code),
}
}
}

#[derive(Debug, Clone)]
pub struct Pipeline {
pub bundler: Bundler,
pub banner: Banner,
pub formatter: Option<Formatter>,
}

impl Pipeline {
pub fn process(&mut self, sources: &Sources) -> Result<String> {
let mut out = self.bundler.bundle(sources)?;

out = run_stage(&mut self.banner, out)?;
out = run_stage(&mut self.formatter, out)?;

Ok(out)
}
}

fn run_stage<S: Stage>(stage: &mut S, code: String) -> Result<String> {
stage
.process(code)
.with_context(|| format!("stage '{}' failed", S::name()))
}

0 comments on commit 02bef08

Please sign in to comment.