SideQuest 1:

student-log
rust
advent-of-code
Errors
crates
It’s dangerous to go alone, take this…
Author

The MidWit

Published

May 31, 2026

Note

If you just want to jump straight to the whole implementation you can skip here

Problem

So far, all of the AOC puzzles have given me long strings of text as input, and the easiest way for me to use them has been to put them into a file called “./input.txt” and then read that file in. Now, obviously Rust has standard implementations for that but, in the process of solving the day 1 puzzle it occurred to me that it would be a good idea for me to practice things like writing my own types, errors, and functions, and setting up my own modules/crates to use, as is my prerogative.

Also, it seemed like an opportunity to practice a little Test Driven Development (TDD) which comes recommended in The Book. It would cost me nothing to try it out right?

Right!?

Spoiler: was TDD fine? Yes. It was fine. Calm down.

Aims

Limitations (cause like, I know where I’m at)

  • I’m just going to use read_to_string as the core of my new function, I ain’t got the skills to roll my own from scratch… yet.
  • The crate must be useable in future problem sets
  • Claude is allowed to help, but can’t generate any code.
  • I’m going to really try TDD, but I give myself an eject button if I find that doesn’t help.

Phase one - The Setupening

This one is easy, and I’m taking the win early. Rust’s package manager Cargo has one-liner for getting this up and running.

$Cargo new aoc_common --lib

This creates a new directory containing everything needed for a library crate, the main difference being that instead of ./src/main.rs we get ./src/lib.rs. The code below is the boilerplate that Cargo produces.

#| label: lib-1

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

Honestly, this is great because it gives me a lot of what I need regarding the tests. So yeah, easy win on step one. I feel like a genius!!

Phase two - ComfortablEnum1

Without blinding myself with science an `Enum` is a particular kind of Struct that allows us to specify variants inside of them. They are a relatively simple kind of object that allows us to hold much more complex data. Results and Option are both kinds of Enums and so they’re great for matching.

So the first thing to do is just define the Enum which will carry my specific expected cases:

  • Where we can’t find the file
  • Where we don’t have permission to read the file
  • Where the file already exist (although that not realy relevant for this case)
  • And a general case to capture whole universe of other BS that I don’t know I don’t know.
#| label: enum-1
enum FileIoError{ // defining the enum 
    FileNotFound(String),// note that each variant can carry a type within it
    NoPermission(String), 
    FileExists(String),
    Unknown(String),
}

And there we go, we have an enum for us to do with as we please2. Now we just need to think of things to do with it.

Aims of the enum

At the end of the day this enum is intended to form the return type in my own read_challenge_input(path: &str) -> Result<String, FileIoError> function and I want to work up to that. Maybe the best place to start is with a FileIoError::new() implementation, and so I’ll write a test and try to get it to pass.


enum FileIoError{
    FileNotFound(String),
    NoPermission(String), 
    FileExists(String),
    Unknown(String),
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_new() {
        let test_error = FileIoError::new();
        assert_eq!(test_error, FileIoError::FileNotFound);
    }
}

and let’s run Cargo test and see wha’happen

#| label: test-1
warning: enum `FileIoError` is never used
 --> src/lib.rs:1:6
  |
1 | enum FileIoError{
  |      ^^^^^^^^^^^
  |
  = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default

warning: `aoc_common` (lib) generated 1 warning
error[E0599]: no variant or associated item named `new` found for enum `FileIoError` in the current scope
  --> src/lib.rs:14:39
   |
 1 | enum FileIoError{
   | ---------------- variant or associated item `new` not found for this enum
...
14 |         let test_error = FileIoError::new();
   |                                       ^^^ variant or associated item not found in `FileIoError`

For more information about this error, try `rustc --explain E0599`.
error: could not compile `aoc_common` (lib test) due to 1 previous error

shell returned 101

Unsurprisingly… ya can’t test something you don’t have.

Implementing new()

The point of the new() function is to just give me back a FileIoError and so let’s wire that up first.

#| label: impl-new-1

impl FileIoError {
    fn new() -> FileIoError {
        FileIoError::FileNotFound(String::from("testing"))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_new() {
        let test_error = FileIoError::new();
        assert_eq!(test_error, FileIoError::FileNotFound(String::from("testing")));
    }
}
#| label: test-2
  Compiling aoc_common v0.1.0 (/home/sp1d3r-z3r0/MyProjects-tmp/midwitsanonymous/scratch/aoc_common)
warning: enum `FileIoError` is never used
 --> src/lib.rs:1:6
  |
1 | enum FileIoError{
  |      ^^^^^^^^^^^
  |
  = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default

warning: associated function `new` is never used
 --> src/lib.rs:9:8
  |
8 | impl FileIoError {
  | ---------------- associated function in this implementation
9 |     fn new() -> FileIoError {
  |        ^^^

warning: `aoc_common` (lib) generated 2 warnings
error[E0369]: binary operation `==` cannot be applied to type `FileIoError`
  --> src/lib.rs:21:9
   |
21 |         assert_eq!(test_error, FileIoError::FileNotFound(String::from("testing")));
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |         |
   |         FileIoError
   |         FileIoError
   |
note: an implementation of `PartialEq` might be missing for `FileIoError`
  --> src/lib.rs:1:1
  |
  |
note: an implementation of `PartialEq` might be missing for `FileIoError`
  --> src/lib.rs:1:1
   |
 1 | enum FileIoError{
   | ^^^^^^^^^^^^^^^^ must implement `PartialEq`
help: consider annotating `FileIoError` with `#[derive(PartialEq)]`
   |
 1 + #[derive(PartialEq)]
 2 | enum FileIoError{
   |

error[E0277]: `FileIoError` doesn't implement `Debug`
  --> src/lib.rs:21:9
   |
21 |         assert_eq!(test_error, FileIoError::FileNotFound(String::from("testing")));
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Debug` is not implemented for
`FileIoError`
   |
   = note: add `#[derive(Debug)]` to `FileIoError` or manually `impl Debug for FileIoError`
help: consider annotating `FileIoError` with `#[derive(Debug)]`
   |
 1 + #[derive(Debug)]
 2 | enum FileIoError{
   |

Some errors have detailed explanations: E0277, E0369.
For more information about an error, try `rustc --explain E0277`.
error: could not compile `aoc_common` (lib test) due to 3 previous error

Sam Becket: “Oh Boy…”

OK so let’s go through that from top to bottom:

  1. dead_code and never_used warnings; ignore for now, nothing is wired up yet.
  2. E0369; == can’t be applied to FileIoError because PartialEq isn’t implemented.
  3. E0277; FileIoError doesn’t implement Debug.

So, there’s two Traits (other than Display, which I mentioned earlier) that apparently need to be there for testing to work. The compiler (Friend Computer) has helpfully told us what we need and how to fix it: derive. Rather than needing to hand-roll my own implementation for every Trait we can use a decorator to just derive them (if possible) for that Struct

#| label: derive-1
// -- snip --
#[derive(Debug, PartialEq)]
enum FileIoError{
    FileNotFound(String),
    NoPermission(String), 
    FileExists(String),
    Unknown(String),
}
// -- snip --
#| label: derive-out-1
// -- snip -- skipping all the unused warnings
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.42s
     Running unittests src/lib.rs (target/debug/deps/aoc_common-7c1db3e116eaa741)

running 1 test
test tests::test_new ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests aoc_common

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Fellow Humans! We have our first Boom!3 We wrote a test and it passed!

Now we just need to make it actually useful.

Using existing Errors

Unsurprisingly rust has actually has all this functionality already; in the std::io crate. Really I’m just writing a wrapper around that. We need to bring that crate into our scope first with a use statement

#| label: use-io
use std::io::ErrorKind; // This holds the std library's IO Errors
// -- snip --

And then wire up the various ErrorKinds to my FileIoError variants

#| label: errorkind-fileioerr
// -- snip --
impl FileIoError {
    fn new(e: ErrorKind) -> FileIoError {
        match e {// taking the e matching on it. 
            ErrorKind::NotFound => FileIoError::FileNotFound(format!("No file at path specified {}", e).to_string()), 
            ErrorKind::PermissionDenied => FileIoError::NoPermission(format!("Permission Denied {}", e).to_string()), 
            ErrorKind::AlreadyExists => FileIoError::FileExists(format!("There is already a file at the path specified {}", e).to_string()), 
            _ => FileIoError::Unknown(format!("{}", e).to_string()),
        }
    }
}
// -- snip --

    #[test]
    fn test_new() {
        let test_error = FileIoError::new(ErrorKind::NotFound);
        assert_eq!(test_error, FileIoError::FileNotFound());
    }
// -- snip --

Now, I’m pretty sure this won’t work properly because there’s no String instide the FileNotFound in the test, but I have an idea for how I might fix that, and I’m just going to run the tests first.

#| label: test-3
// -- snip --
warning: `aoc_common` (lib) generated 2 warnings
   Compiling aoc_common v0.1.0 (/home/sp1d3r-z3r0/MyProjects-tmp/midwitsanonymous/scratch/aoc_common)
error[E0061]: this enum variant takes 1 argument but 0 arguments were supplied
  --> src/lib.rs:29:32
   |
29 |         assert_eq!(test_error, FileIoError::FileNotFound());
   |                                ^^^^^^^^^^^^^^^^^^^^^^^^^-- argument #1 of type `String` is missing
   |
note: tuple variant defined here
  --> src/lib.rs:5:5
   |
 5 |     FileNotFound(String),
   |     ^^^^^^^^^^^^
help: provide the argument
   |
29 |         assert_eq!(test_error, FileIoError::FileNotFound(/* String */));
   |                                                          ++++++++++++

For more information about this error, try `rustc --explain E0061`.
error: could not compile `aoc_common` (lib test) due to 1 previous error

Yeah, just as I thought; but it’s nice to be Schrodinger’s correct instead of just wrong.

My idea is to use format! inside the test the same way I have up in the new() function.

#| label: format-test
// -- snip --
    #[test]
    fn test_new() {
        let test_error = FileIoError::new(ErrorKind::NotFound);
        assert_eq!(
            test_error,
            FileIoError::FileNotFound(
                    format!("No file at path specified {:?}", ErrorKind::NotFound).to_string()
                )
            );
    }
#| label: format-test1
// -- snip --
running 1 test
test tests::test_new ... FAILED

failures:

---- tests::test_new stdout ----

thread 'tests::test_new' (17597) panicked at src/lib.rs:29:9:
assertion `left == right` failed
  left: FileNotFound("No file at path specified entity not found")
 right: FileNotFound("No file at path specified NotFound")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::test_new

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`
// -- snip --

Ok! So the test is failing, but maybe it’s because we’re using the debug syntax in the format! call, but lets see what happens if we just remove that.

#| label: format-test2
// -- snip --
running 1 test
test tests::test_new ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
// -- snip --

And there you have it! Boom 2: Electric Boomaloo!

Checkpoint 1

Gotta say, I’m pretty stoked with that:

  • Write a test to drive implementing new()
  • Implement new() iteratively until the test passed (while cheating a little because I didn’t fully write the test at the outset)

So the next thing to do I write the Display Trait

Implementing Traits

We already know that some Traits can be derived with a decorator but others are more complex and require us to manually write the implementation. According to the docs Display can’t be derived because it’s for “user facing output”, which makes sense in the case of my FileIoError as I want to get this back if there’s something wrong with my input reading in later AOC puzzles. Fortunately it’s really easy to wire up. Just gotta write a test first, as per my own rules.

// -- snip --
#| label: display-test-1
    #[test]
    fn test_display() {
        let test_error = FileIoError::new(ErrorKind::NotFound);
        assert_eq!(
            format!("{}", test_error),
            format!("{}", FileIoError::FileNotFound(
                    format!("No file at path specified {}", ErrorKind::NotFound).to_string()
                )
            )
        )
    }

So I don’t know that the test works but hey, let’s test a test!

#| label: display-test-out-1
// -- snip --
warning: enum `FileIoError` is never used
 --> src/lib.rs:4:6
  |
4 | enum FileIoError{
  |      ^^^^^^^^^^^
  |
  = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default

warning: associated function `new` is never used
  --> src/lib.rs:12:8
   |
11 | impl FileIoError {
   | ---------------- associated function in this implementation
12 |     fn new(e: ErrorKind) -> FileIoError {
   |        ^^^

warning: `aoc_common` (lib) generated 2 warnings
error[E0277]: `FileIoError` doesn't implement `std::fmt::Display`
  --> src/lib.rs:41:27
   |
41 |             format!("{}", test_error),
   |                      --   ^^^^^^^^^^ `FileIoError` cannot be formatted with the default formatter
   |                      |
   |                      required by this formatting parameter
   |
help: the trait `std::fmt::Display` is not implemented for `FileIoError`
  --> src/lib.rs:4:1
   |
   |
 4 | enum FileIoError{
   | ^^^^^^^^^^^^^^^^
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

error[E0277]: `FileIoError` doesn't implement `std::fmt::Display`
  --> src/lib.rs:42:27
   |
42 |               format!("{}", FileIoError::FileNotFound(
   |  ______________________--___^
   | |                      |
   | |                      required by this formatting parameter
43 | |                     format!("No file at path specified {}", ErrorKind::NotFound).to_string()
44 | |                 )
   | |_________________^ `FileIoError` cannot be formatted with the default formatter
   |
help: the trait `std::fmt::Display` is not implemented for `FileIoError`
  --> src/lib.rs:4:1
   |
 4 | enum FileIoError{
   | ^^^^^^^^^^^^^^^^
   = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead

For more information about this error, try `rustc --explain E0277`.
error: could not compile `aoc_common` (lib test) due to 2 previous errors

Welp! The test it telling me that it’s failing because we haven’t done the thing that would be needed to make it pass, which… is great(?). Also, it’s telling us two ways to fix it, either use the format syntax or to implement Display so lets do that (copying directly from the docs).

#| label: impl-display
use std::io::ErrorKind; // This holds the std library's IO Errors
// -- snip --

impl fmt::Display for FileIoError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self)
    }

}
// -- snip --

We have a fresh Error I’ve never seen before

#| label: stack-overflow
// -- snip --
running 2 tests
test tests::test_new ... ok

thread 'tests::test_display' (20470) has overflowed its stack
fatal runtime error: stack overflow, aborting
error: test failed, to rerun pass `--lib`

Caused by:
  process didn't exit successfully: `/home/user/aoc_common/target/debug/deps/aoc_c
ommon-7c1db3e116eaa741` (signal: 6, SIGABRT: process abort signal)

I’ve never actually seen a stack overflow before, and I have no idea what it means in this context so I’m going to practice a little google fu Aaaand if that doesn’t work I’m going to call in Claude.

Google fu results

So this this post(skip to the end) tells me that I need to enable a debuger on my system, and right now I don’t want to risk falling down a rabbit hole in the case that that doesn’t work so let’s ask Claude what’s up.

Claude response

Claude responded: The Display implementation for FileIoError is calling itself recursively and infinitely. In the fmt method, you wrote write!(f, “{}”, self). The {} format specifier invokes Display on self — but self is a FileIoError, so it calls fmt again, which calls write!(f, “{}”, self) again, and so on forever until the stack overflows.

Huh, just copying from the docs didn’t help… who’da thunk!

Ok, so calling write(f, "{}", self) is causing a recursion because Display is getting called on self ad infinitum which, I believe, caused it to use all the memory and get aborted; my first Stack Overflow4. Instead of just trying to easily write the self, we need to get the String out of the FileIoError variants.

You thinkin’ what I’m thinkin’?

Let’s match this MFer

#| label: impl-display-2
// -- snip --
impl fmt::Display for FileIoError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FileIoError::FileNotFound(s) => write!(f, "{}", s),
            FileIoError::NoPermission(s) => write!(f, "{}", s),
            FileIoError::FileExists(s) => write!(f, "{}", s),
            FileIoError::Unknown(s) => write!(f, "{}", s),
        }
    }
}
// -- snip --

Hold on to your butts

#| label: display-test-out-2
// -- snip --
running 2 tests
test tests::test_display ... ok
test tests::test_new ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
// -- snip --

There it is!! La troisieme Boom! as the French say. I’m going to call that…

Checkpoint 2

I’ll admit I was absolutely winging it with test_display but as Napoleon once said

If yer gonna be dumb, ya gotta be tough5.

So now it’s time to write the read_challenge_input function.

Phase three - Reading input

As per the rules we must (must) write a test before we go forward, I’m just going to batch out the three I want now. Oh, also, I’ve created two files in the same directory as our code:

  • an ./input.txt file that just contains the text “It works!”.
  • a ./notyoudont.txt file that we don’t have permission to read.

Three tests outside Enum Missouri

#| label: read-input-tests
// -- snip --
    #[test]
    fn it_works() {
        let result = read_challenge_input("./input.txt");
        assert_eq!(result.unwrap(), String::from("It works!\n"));
    }
    #[test]
    fn returns_error_for_missing_file() {
        let result = read_challenge_input("nonexistent.txt");
        assert!(matches!(result, Err(FileIoError::FileNotFound(_))));
    }

    #[test]
    fn returns_error_for_no_permission() {
        let result = read_challenge_input("./noyoudont.txt");
        assert!(matches!(result,Err(FileIoError::NoPermission(_))));
    }

and now to write the function

#| label: read_input
use std::fs::read_to_string;
// -- snip --
fn read_challenge_input(path:&str) -> Result<String, FileIoError> {
    let input = read_to_string(path)?; // using the ? opporator to propagate the error back up
    Ok(input)
}
// -- snip --

and run it back

#| label: trait-missing
// -- snip --
error[E0277]: `?` couldn't convert the error to `FileIoError`
  --> src/lib.rs:37:46
   |
36 | fn read_challenge_input(path:&str) -> Result<String, FileIoError> {
   |                                       --------------------------- expected `FileIoError` because of this
37 |     let input = read_to_string("./input.txt")?;
   |                 -----------------------------^ the trait `From<std::io::Error>` is not implemented for `FileIoError`
   |                 |
   |                 this can't be annotated with `?` because it has type `Result<_, std::io::Error>`
   |
note: `FileIoError` needs to implement `From<std::io::Error>`
  --> src/lib.rs:6:1
   |
 6 | enum FileIoError{
   | ^^^^^^^^^^^^^^^^
   = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait

For more information about this error, try `rustc --explain E0277`.
error: could not compile `aoc_common` (lib) due to 1 previous error
warning: build failed, waiting for other jobs to finish...
error: could not compile `aoc_common` (lib test) due to 1 previous error

Ladies, Gentlemen and pals beyond the binary, I thought I might get through this without one but we’ve reached a say it with me

Rust Rookie Mistake!

So, I thought I was being clever jumping straight to propagating the Error with the ? operator, and maybe if I’d not made this rookie error I would have been, but it can’t work if the thing I want to use as E in Result<T,E> doesn’t have the Error Trait.

Which I actually knew already! See? Rookie move!.

As well as adding the Error Trait, I also need to convert the Errors from that are produced by read_to_string() into FileIoErrors before my test will pass.

#| label: read-input-err
use std::error::Error;// bring Error into scope
use std::fs::read_to_string;
// -- snip --
impl Error for FileIoError {}// The implementation is really simple
// -- snip --
fn read_challenge_input(path:&str) -> Result<String, FileIoError> {
    let input = read_to_string(path).map_err(// map_err lets me convert errors from one kind to another
        |e| match e.kind() {
            ErrorKind::NotFound => FileIoError::FileNotFound(e.to_string()),
            ErrorKind::PermissionDenied => FileIoError::NoPermission(e.to_string()),
            ErrorKind::AlreadyExists => FileIoError::FileExists(e.to_string()),
            _ => FileIoError::Unknown(e.to_string())
        }
        )?;// closures man! 
    Ok(input)
}
}
// -- snip --

Ok, so we’ve wired up Error and plugged in the map_err() to handle conversion. Let’s run it.

#| label: final-tests
// -- snip --

running 5 tests
test tests::returns_error_for_missing_file ... ok
test tests::returns_error_for_no_permission ... ok
test tests::test_display ... ok
test tests::test_new ... ok
test tests::it_works ... ok

test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Penultimate Boom!

There we have it, five passing tests, covering my own enum and file reading wrapper function. Pretty chuffed with that. There’s one thing left to do.

Phase four - Plumbing

The eagled-eyed among you will have noticed that as it stands nothing in this module is accessible to outside code. Everything in a module is Private by default, for…reasons. However, if we want to be able to use our code in other modules we need to mark them as pub; you’ll see that in the final implementation down below.

In the meantime there are a few other bits of plumbing that needs doing in the various Cargo.toml files in the project.

Workspaces

Rust gives us a way to organise sub-directories together into a cohesive crate: workspaces. In essence we just need to tell the system what to include in our crate, which sub-directories to group together. Thankfully, this is really easy to organise, we just need to put a Cargo.toml in the overall root folder and fill it with the following text:

#| label: workspace-toml
// -- snip --
[workspace]
members = [
  "aoc_common",
  "day_1",
  ]

Dependencies

Once that’s done we need to add our module as a dependency in any others we want to use it in. Where would use Cargo add \<crate\> to add remote dependencies, in this case we need to manually add it to all the sub-dir/Cargo.toml, specifying the path like so

#| label: day-1-toml
// -- snip --
[package]
name = "day_1"
version = "0.1.0"
edition = "2024"

[dependencies]
aoc_common = {path = "../aoc_common/"}

Viola6! We have a fully functioning module that we can pull into anything else in our AOC crate and handle reading in input like a boss.

Ultimate Boom!

Full Implementation

As promised, here’s the full contents

#| label: lib-rs
use std::error::Error;
use std::fs::read_to_string;
use std::io::ErrorKind; 
use std::fmt;

#[derive(Debug, PartialEq)]
pub enum FileIoError{
    FileNotFound(String),
    NoPermission(String), 
    FileExists(String),
    Unknown(String),
}

impl FileIoError {
    pub fn new(e: ErrorKind) -> FileIoError {
        match e {
            ErrorKind::NotFound => FileIoError::FileNotFound(format!("No file at path specified {}", e).to_string()), 
            ErrorKind::PermissionDenied => FileIoError::NoPermission(format!("Permission Denied {}", e).to_string()), 
            ErrorKind::AlreadyExists => FileIoError::FileExists(format!("There is already a file at the path specified {}", e).to_string()), 
            _ => FileIoError::Unknown(format!("{}", e).to_string()),
        }
    }
}

impl fmt::Display for FileIoError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FileIoError::FileNotFound(s) => write!(f, "{}", s),
            FileIoError::NoPermission(s) => write!(f, "{}", s),
            FileIoError::FileExists(s) => write!(f, "{}", s),
            FileIoError::Unknown(s) => write!(f, "{}", s),
        }
    }

}

impl Error for FileIoError {}

pub fn read_challenge_input(path:&str) -> Result<String, FileIoError> {
    let input = read_to_string(path).map_err(
        |e| match e.kind() {
            ErrorKind::NotFound => FileIoError::FileNotFound(e.to_string()),
            ErrorKind::PermissionDenied => FileIoError::NoPermission(e.to_string()),
            ErrorKind::AlreadyExists => FileIoError::FileExists(e.to_string()),
            _ => FileIoError::Unknown(e.to_string())
        }
        )?;
    Ok(input)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_new() {
        let test_error = FileIoError::new(ErrorKind::NotFound);
        assert_eq!(
            test_error,
            FileIoError::FileNotFound(
                    format!("No file at path specified {}", ErrorKind::NotFound).to_string()
                )
            );
    }

    #[test]
    fn test_display() {
        let test_error = FileIoError::new(ErrorKind::NotFound);
        assert_eq!(
            format!("{}", test_error),
            format!("{}", FileIoError::FileNotFound(
                    format!("No file at path specified {}", ErrorKind::NotFound).to_string()
                )
            )

        )
    }

    #[test]
    fn it_works() {
        let result = read_challenge_input("./input.txt");
        assert_eq!(result.unwrap(), String::from("It works!\n"));
    }
    #[test]
    fn returns_error_for_missing_file() {
        let result = read_challenge_input("nonexistent.txt");
        assert!(matches!(result, Err(FileIoError::FileNotFound(_))));
    }

    #[test]
    fn returns_error_for_no_permission() {
        let result = read_challenge_input("./noyoudont.txt");
        assert!(matches!(result,Err(FileIoError::NoPermission(_))));
    }

}

What do I need to take from this?

  • Friend Computer is really helpful. This exercise really drove this home for me.
  • Recursion is something to watch out for, it’s not easy for untrained eyes to spot where the overflow might be coming from and I couldn’t have parsed that without Claude. It sucks to admit it but it’s just true.
  • Implementing Error is really easy, but it has to be there to give access to the power of Result<T, E>
  • TDD is fine, and I can see why it would be really useful to well-heeled devs.
    • Honestly I got lucky with at least one of the tests. Maybe, in the case where the problem was small, or where I was already pretty sure of the expected output then it would have felt better.
    • But maybe that’s how TDD is meant to feel and actually the compiler feed back from the tests is an example of the power of TDD. I ain’t experienced enough to make that call from here.

All in all though I do feel like I got where I was aiming with this, and I did learn a lot. I think on day_2 I’ll try to write up some Structs and functions that are relevant to the puzzle from the outset. Although I’m not committed to TDD there too. Who knows.

If you’ve read this far, I appreciate your time and I hope it was at least a little entertaining, even if the sentiment was mainly Schadenfreude.

God’s Speed

The MidWit

Footnotes

  1. You better believe I’m delighted with that!↩︎

  2. The power!↩︎

  3. I know that we could have called the Cargo new from earlier a boom, but it just didn’t feel right y’know? You know.↩︎

  4. I’m probably wrong about the explanation, call it a MidWit moment↩︎

  5. I know this isn’t a Napoleon quote (don’t @ me). The quote “I would rather have a general who was lucky than one who was good” is often attributed to him, but it’s probably apocryphal. I was going to quote that here in reference to the fact that I stumbled into a working test, but then the Roger Miller quote was funnier. And nothing polishes off a joke like explaining it.↩︎

  6. In a Bugs Bunny voice↩︎