/////////////////////////////////////////////////////////////
// TypeErasureDataFlowStructure::Executive::main.rs //
// - Executive creates and uses all lower level parts //
// Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 //
/////////////////////////////////////////////////////////////
/*
Note:
Executive only creates Input instance. The rest of
the pipeline self installs, e.g., Input creates Compute,
and Compute creates Output.
*/
use compute::*;
use input::*;
use output::*;
fn main() {
print!("\n -- TypeErasureDataFlowStructure::Executive --\n");
type Comp = ComputeImpl<OutputImpl>;
let mut input = Input::<Comp>::new(); // factory function
// Use it generically:
let mut total = 0;
for path in &[
"./src/main.rs",
"../Input/src/lib.rs",
"../Compute/src/lib.rs",
"../Output/src/lib.rs",
"../Fileutils/src/lib.rs"
] {
total += input.do_input(path);
}
print!("\n total lines: {}", total);
print!("\n\n That's all Folks!\n\n");
}
> cargo run -q
-- TypeErasureDataFlowStructure::Executive --
file "./src/main.rs": 39 lines
file "../Input/src/lib.rs": 109 lines
file "../Compute/src/lib.rs": 63 lines
file "../Output/src/lib.rs": 26 lines
file "../Fileutils/src/lib.rs": 94 lines
total lines: 331
That's all Folks!
[package]
name = "executive"
version = "0.1.0"
authors = ["James W. Fawcett "]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
input = { path = "../Input" }
compute = { path = "../Compute" }
output = { path = "../Output" }
/////////////////////////////////////////////////////////////
// TypeErasureDataFlowStructure::Input::lib.rs //
// - Attempts to return line count from file //
// Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 //
/////////////////////////////////////////////////////////////
/*
Note:
- Input owns and instantiates Compute.
- It attempts to open file and pass to Compute for
processing.
- Returns line count if successful
*/
use file_utils::open_file_for_read;
use std::fs::*;
pub trait Compute {
fn new() -> Self; // factory function
fn do_compute(&mut self, name: &str, file: File);
fn lines(&self) -> usize;
}
#[derive(Debug)]
pub struct Input<T: Compute> {
name: String,
compute: T,
}
impl<T: Compute> Default for Input<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Compute> Input<T> {
pub fn new() -> Input<T> {
Input {
name: String::new(),
compute: T::new(), // factory function
}
}
pub fn do_input(&mut self, name: &str) -> usize {
let mut lines: usize = 0;
self.name = name.to_string();
let rslt = open_file_for_read(name);
if let Ok(file) = rslt {
self.compute.do_compute(name, file);
lines = self.compute.lines();
} else {
print!("\n can't open file {:?}", name);
}
lines
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
/// A Compute stub: tracks if do_compute() was invoked,
/// and always reports 42 lines.
struct StubCompute {
called: bool,
return_lines: usize,
}
impl Compute for StubCompute {
fn new() -> Self {
StubCompute {
called: false,
return_lines: 42,
}
}
fn do_compute(&mut self, _name: &str, _file: File) {
self.called = true;
}
fn lines(&self) -> usize {
self.return_lines
}
}
#[test]
fn missing_file_returns_zero_and_skips_compute() {
let mut inp: Input<StubCompute> = Input::new();
let count = inp.do_input("definitely_not_a_file.txt");
assert_eq!(count, 0, "should return 0 when file open fails");
assert!(!inp.compute.called, "do_compute must not be called");
}
#[test]
fn existing_file_invokes_compute_and_returns_stub_value() {
// create a real temp file so open_file_for_read succeeds
let mut tmp = NamedTempFile::new().expect("create temp file");
write!(tmp, "ignored contents").expect("write to temp file");
tmp.flush().expect("flush temp file");
let path = tmp.path().to_str().unwrap();
let mut inp: Input<StubCompute> = Input::new();
let count = inp.do_input(path);
assert_eq!(count, 42, "should return the stub's return_lines value");
assert!(
inp.compute.called,
"do_compute must be called for existing file"
);
}
}
> cargo build --lib
Compiling file_utils v0.1.0 (C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\Fileutils)
Compiling input v0.1.0 (C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\Input)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\Input
> cargo test --lib
Compiling windows_x86_64_msvc v0.52.6
Compiling getrandom v0.3.3
Compiling cfg-if v1.0.1
Compiling once_cell v1.21.3
Compiling fastrand v2.3.0
Compiling windows-targets v0.52.6
Compiling windows-sys v0.59.0
Compiling tempfile v3.20.0
Compiling input v0.1.0 (C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\Input)
Finished `test` profile [unoptimized + debuginfo] target(s) in 2.06s
Running unittests src\lib.rs (target\debug\deps\input-f4e2ad4738f6bc68.exe)
running 2 tests
test tests::missing_file_returns_zero_and_skips_compute ... ok
test tests::existing_file_invokes_compute_and_returns_stub_value ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
[package]
name = "input"
version = "0.1.0"
authors = ["James W. Fawcett "]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
file_utils = { path = "../Fileutils" }
[dev-dependencies]
tempfile = "3" # tempfile crate from crates.io
/////////////////////////////////////////////////////////////
// TypeErasureDataFlowStructure::Compute::lib.rs //
// - Attempts to read opened file to string, count lines //
// Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 //
/////////////////////////////////////////////////////////////
/*
Note:
- creates instance of Output
- attempts to read file to string and count its lines
- sends results to Output
*/
use file_utils::read_file_to_string;
use input::Compute;
use std::fs::*;
pub trait Output {
fn new() -> Self;
fn do_output(&self, name: &str, lines: usize);
}
#[derive(Debug)]
pub struct ComputeImpl<Out: Output> {
lines: usize,
out: Out,
}
impl<Out: Output> Compute for ComputeImpl<Out> {
fn new() -> ComputeImpl<Out> {
ComputeImpl {
lines: 0,
out: Out::new(),
}
}
fn do_compute(&mut self, name: &str, mut file: File) {
let rslt = read_file_to_string(&mut file);
if let Ok(contents) = rslt {
if contents.is_empty() {
self.lines = 0;
} else {
self.lines = 1;
}
for ch in contents.chars() {
if ch == '\n' {
self.lines += 1;
}
}
self.out.do_output(name, self.lines);
} else {
print!("\n could not read {:?}", name);
}
}
fn lines(&self) -> usize {
self.lines
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use std::fs::File;
use std::io::Write;
use std::rc::Rc;
use tempfile::NamedTempFile;
/// A stub Output that records the last (name, lines) it was given.
struct StubOutput {
last: Rc<RefCell<Option<(String, usize)>>>,
}
impl Output for StubOutput {
fn new() -> Self {
// We never call this in tests; we construct StubOutput by hand.
unreachable!("StubOutput::new should not be used in tests");
}
fn do_output(&self, name: &str, lines: usize) {
*self.last.borrow_mut() = Some((name.to_string(), lines));
}
}
/// Write `contents` into a temp file and reopen it so reads start at the beginning.
fn make_file(contents: &str) -> File {
let mut tmp = NamedTempFile::new().expect("create temp file");
write!(tmp, "{}", contents).expect("write to temp file");
tmp.flush().expect("flush temp file");
tmp.reopen().expect("reopen temp file")
}
#[test]
fn empty_file_emits_zero_and_reports_zero() {
let record = Rc::new(RefCell::new(None));
let stub = StubOutput {
last: Rc::clone(&record),
};
// Construct ComputeImpl directly, bypassing ComputeImpl::new()
let mut comp = ComputeImpl {
lines: 0,
out: stub,
};
let file = make_file("");
comp.do_compute("empty", file);
// internal count
assert_eq!(comp.lines(), 0);
// output called with ("empty", 0)
assert_eq!(*record.borrow(), Some(("empty".to_string(), 0)));
}
#[test]
fn no_newline_emits_one_and_reports_one() {
let record = Rc::new(RefCell::new(None));
let stub = StubOutput {
last: Rc::clone(&record),
};
let mut comp = ComputeImpl {
lines: 0,
out: stub,
};
let file = make_file("single line");
comp.do_compute("single", file);
assert_eq!(comp.lines(), 1);
assert_eq!(*record.borrow(), Some(("single".to_string(), 1)));
}
#[test]
fn multiple_lines_counted_correctly() {
let record = Rc::new(RefCell::new(None));
let stub = StubOutput {
last: Rc::clone(&record),
};
let mut comp = ComputeImpl {
lines: 0,
out: stub,
};
let file = make_file("l1\nl2\nl3");
comp.do_compute("multi", file);
assert_eq!(comp.lines(), 3);
assert_eq!(*record.borrow(), Some(("multi".to_string(), 3)));
}
#[test]
fn trailing_newline_adds_empty_line() {
let record = Rc::new(RefCell::new(None));
let stub = StubOutput {
last: Rc::clone(&record),
};
let mut comp = ComputeImpl {
lines: 0,
out: stub,
};
let file = make_file("a\nb\n");
comp.do_compute("trail", file);
assert_eq!(comp.lines(), 3);
assert_eq!(*record.borrow(), Some(("trail".to_string(), 3)));
}
}
> cargo build --lib
Compiling file_utils v0.1.0 (C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\Fileutils)
Compiling input v0.1.0 (C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\Input)
Compiling compute v0.1.0 (C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\compute)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\compute
> cargo test --lib
Compiling windows_x86_64_msvc v0.52.6
Compiling getrandom v0.3.3
Compiling cfg-if v1.0.1
Compiling once_cell v1.21.3
Compiling fastrand v2.3.0
Compiling windows-targets v0.52.6
Compiling windows-sys v0.59.0
Compiling tempfile v3.20.0
Compiling compute v0.1.0 (C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\compute)
Finished `test` profile [unoptimized + debuginfo] target(s) in 2.53s
Running unittests src\lib.rs (target\debug\deps\compute-076bb42754ebb50e.exe)
running 4 tests
test tests::empty_file_emits_zero_and_reports_zero ... ok
test tests::multiple_lines_counted_correctly ... ok
test tests::no_newline_emits_one_and_reports_one ... ok
test tests::trailing_newline_adds_empty_line ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
[package]
name = "compute"
version = "0.1.0"
authors = ["James W. Fawcett "]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
input = { path = "../Input" }
file_utils = { path = "../Fileutils" }
[dev-dependencies]
tempfile = "3"
/////////////////////////////////////////////////////////////
// TypeErasureDataFlowStructure::Output::lib.rs //
// - Sends results to console //
// Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 //
/////////////////////////////////////////////////////////////
use compute::Output;
#[derive(Debug)]
pub struct OutputImpl {}
impl Output for OutputImpl {
fn new() -> OutputImpl {
OutputImpl {}
}
fn do_output(&self, name: &str, lines: usize) {
print!("\n file {:?}: {:?} lines\n", name, lines);
}
}
#[cfg(test)]
mod tests {
use super::*;
/// `new()` + `Debug` should yield the type name.
#[test]
fn new_and_debug() {
let out = OutputImpl::new();
// The derived Debug for an empty struct prints exactly "OutputImpl"
assert_eq!(format!("{:?}", out), "OutputImpl");
}
/// `do_output()` returns unit and never panics.
#[test]
fn do_output_returns_unit() {
let out = OutputImpl::new();
// We don't capture stdout here; we just ensure it runs successfully and returns ()
let ret = out.do_output("example.rs", 123);
assert_eq!(ret, ());
}
}
> cargo build --lib
Compiling file_utils v0.1.0 (C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\Fileutils)
Compiling input v0.1.0 (C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\Input)
Compiling compute v0.1.0 (C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\Compute)
Compiling output v0.1.0 (C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\Output)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.34s
C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\Output
> cargo test --lib
Compiling output v0.1.0 (C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\Output)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.38s
Running unittests src\lib.rs (target\debug\deps\output-e8639558e699949f.exe)
running 2 tests
test tests::do_output_returns_unit ... ok
test tests::new_and_debug ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
[package]
name = "output"
version = "0.1.0"
authors = ["James W. Fawcett "]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
compute = { path = "../Compute" }
/////////////////////////////////////////////////////////////
// FactoredStructure::Fileutils::file_utilities.rs //
// - Input attempts to open named file and return File //
// Jim Fawcett, https://JimFawcett.github.io, 04 Mar 2021 //
/////////////////////////////////////////////////////////////
/*
This code may be useful for other programs so it is
factored into a module here.
*/
#![allow(dead_code)]
use std::fs::*;
use std::io::{Error, ErrorKind, Read, Write};
pub fn open_file_for_read(file_name: &str) -> Result<File, std::io::Error> {
let rfile = OpenOptions::new().read(true).open(file_name);
rfile
}
pub fn read_file_to_string(f: &mut File) -> Result<String, std::io::Error> {
let mut contents = String::new();
let bytes_rslt = f.read_to_string(&mut contents);
if bytes_rslt.is_ok() {
Ok(contents)
} else {
Err(Error::new(ErrorKind::Other, "read error"))
}
}
pub fn open_file_for_write(file_name: &str) -> Result<File, std::io::Error> {
let wfile = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(file_name);
wfile
}
pub fn write_string_to_file_handle(s: &str, mut f: std::fs::File) -> std::io::Result<()> {
f.write_all(s.as_bytes())?;
f.flush()?;
Ok(())
}
pub fn write_string_to_file(s: &str, file_name: &str) -> std::io::Result<()> {
std::fs::write(file_name, s)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
/// Test the filename-based API: write_string_to_file + open_file_for_read + read_file_to_string
#[test]
fn write_and_read_via_filename() {
// Create a temporary file and get its path as a String
let tmp = NamedTempFile::new().expect("failed to create temp file");
let path = tmp.path().to_str().unwrap().to_owned();
let test_string = "hello via filename";
// Write via the file_name API
write_string_to_file(test_string, &path).expect("write_string_to_file failed");
// Read back
let mut f = open_file_for_read(&path).expect("open_file_for_read failed");
let contents = read_file_to_string(&mut f).expect("read_file_to_string failed");
assert_eq!(contents, test_string);
}
/// Test the handle-based API: open_file_for_write + write_string_to_file_handle + open/read
#[test]
fn write_and_read_via_handle() {
let tmp = NamedTempFile::new()
.expect("failed to create temp file");
let path = tmp.path().to_str().unwrap().to_owned();
let test_string = "hello via handle";
// Open for write, then write via handle
let wfile = open_file_for_write(&path)
.expect("open_file_for_write failed");
write_string_to_file_handle(test_string, wfile)
.expect("write_string_to_file_handle failed");
// Read back
let mut f2 = open_file_for_read(&path)
.expect("open_file_for_read failed");
let contents2 = read_file_to_string(&mut f2)
.expect("read_file_to_string failed");
assert_eq!(contents2, test_string);
}
}
> cargo build --lib
Compiling file_utils v0.1.0 (C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\Fileutils)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\Fileutils
> cargo test --lib
Compiling windows_x86_64_msvc v0.52.6
Compiling getrandom v0.3.3
Compiling cfg-if v1.0.1
Compiling once_cell v1.21.3
Compiling fastrand v2.3.0
Compiling windows-targets v0.52.6
Compiling windows-sys v0.59.0
Compiling tempfile v3.20.0
Compiling file_utils v0.1.0 (C:\github\JimFawcett\NewSite\Code\DesignStructure\TypeEraseDataFlowStructure\Fileutils)
Finished `test` profile [unoptimized + debuginfo] target(s) in 2.02s
Running unittests src\lib.rs (target\debug\deps\file_utils-43bed36694d2543d.exe)
running 2 tests
test tests::write_and_read_via_handle ... ok
test tests::write_and_read_via_filename ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
[package]
name = "file_utils"
version = "0.1.0"
edition = "2024"
[dependencies]
[dev-dependencies]
tempfile = "3"