SW Design Bites: TypeErase Structure

"You can't depend on your eyes when your imagination is out of focus."
- Mark Twain

1.0 Introduction

This DesignBite sequence was inspired by project TextFinder. As that project is designed and implemented, a number of design decisions are made, consciously or unconsciously. Each of these pages addresses one answer to questions about fundamental decisions of structure. To make discussion pragmatic and concrete, we implement a program that evaluates the number of lines in text files. Processing is quite simple so it allows us to see how each structure alternative is implemented. We consider both package structure and logical structure, e.g., functions and structs used to order design and implementation. In this TypeErase Structure page, code is implemented in a set of packages Executive, Input, Compute, and Output and their structs. That provides all of the organization for processing. An additional package, Fileutils, defines a series of file-handling functions used to implement unit tests.

2. Application Structure - TypeErase

This structure is modular with a data flow structure. It differs from the previous dataflow structure in that:
  • Its Input and Compute parts are parameterized on the types to which they send output.
  • Each parameterized part depends only on the trait defined by its up-stream component part.
  • That means that all non-Executive parts are immune to build breakage when other parts change.
  • Its dependencies are opposite of the data flow structure. Each part depends on its upstream package where the trait it uses is defined. For that reason it is often said to implement the Dependency Inversion Principle.
Figure 4. Type Erasure Structure

Type Erasure Structure

Type erasure can change dependency relationships. In Figure 4, Input holds a "type erased" instance of the compute block. Input defines an interface it will call, Compute, that ComputeImpl implements. The Executive creates an instance of Input<ComputeImpl<OutputImpl>>. Input, internally, just uses the Compute interface, Compute. That means that both InputImpl and ComputeImpl depend on Compute. Since InputImpl defines Compute, the Compute package now depends on the Input package - the opposite of the basic data flow structure. Because of that, we say this design uses dependency inversion. Now, Input can be instantiated by Executive with any type that implements Comp. It does not depend on any of the downstream implementation details, and so can be reusable. Similarly, Input instantiates an instance of ComputeImpl<Output> and ComputeImpl<Output> instantiates an instance of Output. Note that Executive is the only component that knows the concrete types of the other components. Each of the other components know only the trait interface it implements.

Pros:

  1. Same as data flow
  2. The Input, Compute, and Output parts are now decoupled, each depending only on the interfaces it defines and the interfaces of upstream components.

Cons:

  1. The building process becomes more complex. Each component needs to use a factory function to create its downstream component (to maintain type ignorance).
Project Notes: TypeErase structures use generic types and traits:
  • Each component, except for the last in the dataflow chain, declares an interface trait it will use to communicate with its successor. Each successor is obligated to implement its predecessor's defined trait.
  • Each trait is required to implement a factory function fn new() that a component uses to instantiate its successor, even though it doesn't know its concrete type.
  • The main type for each component is generic, with parameter required to implement its trait. The parameter is a placeholder for the type of its successor, and the constraint ensures that it can pass its results to the successor. So, for example, the ComputeImpl type is defined as ComputeImpl<Out: Output>. It does not know the concrete type of its successor, just that it implements the Output trait.

TypeErase Code Repository
Executive Output
/////////////////////////////////////////////////////////////
// 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!

Cargo.toml
[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" }
            
Comments:
  • Executive code resides in the left panel. It creates and uses instances of the types OutputImpl, ComputeImpl, and InputImpl, all stored in heap memory.
  • The instances are wired into a dataflow pipeline: input to compute to output.
  • InputImpl provides function fn do_input(&mut self, name: &str). That attempts to open a named file and return its handle.
  • ComputeImple reads the file into a string and counts its lines. It then calls OutputImpl to display the name and lines.
  • Only Executive knows the concrete type names. The other components simply use trait objects through their trait specified interface.
Input Library Build and Test
/////////////////////////////////////////////////////////////
// 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
Cargo.toml
[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
Comments:
  • Input library defines the trait Compute and type InputImpl containing one String data member, and a smart Box pointer to trait object Compute in the heap.
  • It defines a fn new(compute: Box<dyn Computer>) that returns an InputImpl holding a smart Box pointer to the trait object Compute.
  • It defines another method fn do_input(&mut self, name:&str) -> Option<File>
  • That attempts to open a named file for reading, and, if successful, calls compute.do_compute(name, file).
  • The Input library depends on the library file_utils.
  • It also depends on a crate, tempfile from crates.io, supporting creation of temporary files that self destruct when no longer referenced.
Comp Library Build and Test
/////////////////////////////////////////////////////////////
// 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

Cargo.toml
[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"
Comments:
  • The Comp library defines the type ComputeImpl with two data members, lines and out, a generic parameter that is required to implement the Output trait, as specified by ComputeImpl<Out: Output>.
  • It provides thre functions fn new(), fn do_compute(&mut self, name: &str, mut file: File) and fn lines(&self) -> usize.
  • do_compute attempts to read contents of file handle file into a String. If successful it counts its lines and stores in self.lines.
  • lines returns the value stored in self.lines.
  • Cargo.toml [dependencies] identifies crates the library depends on.
  • Cargo.toml [dev-dependencies] identifies crates library tests depend on.
  • This library defines unit tests for counting lines in a file, considering several cases indicated with function name and comments.
Output Library Build
/////////////////////////////////////////////////////////////
// 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
Cargo.toml
[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" }
Comments:
  • Output library defines the type OutputImpl with no data members.
  • It provides two functions new() and fn do_output(&mut self, name: &str, lines: usize).
  • do_output displays a file name and its line count.
  • This library implements 2 simple unit tests.
  • Output depends only on Compute, as show in its Cargo.toml file.
Fileutils Library Build and Test
/////////////////////////////////////////////////////////////
// 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
Cargo.toml
[package]
name = "file_utils"
version = "0.1.0"
edition = "2024"

[dependencies]


[dev-dependencies]
tempfile = "3"
Comments:
  • Fileutils library has no dependencies other than the Rust standard libraries, as shown in the Cargo.toml above.
  • It does not define any new types, but does define several functions:
    1. fn open_file_for_read(file_name: &str) -> Result<File: std::io::Error>
      Attempts to open named file and return a handle to its String contents. The return type is Result<File, std::io::Error>
    2. fn read_file_to_string(f: &mut File) -> Result<String, std::io::Error>
      Attempts to read contents referred to by file handle and return the string of contents wrapped in Ok(contents).
    3. fn open_file_for_write(file_name: &str) -> Result<File, std::io::Error>
      Attempts to open named file and return a File handle to the opened named file.
    4. fn write_string_to_file_handle(s: &str, mut f: std::fs:::File) -> Result<()>
      Attempts to write string to file referred to by the File handle and flush the handle.
    5. fn write_string_to_file(s: &str, file_name: &str) -> Result<()>
      Attempts to write string to file referred to named file.
  • The Build and Test view above shows successful build and execution of two unit test functions.

3. Epilogue

The fourh design alternatives considered here:
  1. Monolithic Structure
  2. Factored Structure
  3. DataFlow Structure
  4. TypeErase Structure
  5. PlugIn Structure
are progressively more flexible, eventually resulting in reusable components, but also increasingly complex. Where you settle in these alternatives is determined by design context. Is this a one-of-a-kind project that you want to finish quickly or is it heading for production code that will be maintained by more than one developer?