← Back to list

Learn Rust Through the Photo Framer App — Enums, Pattern Matching & Option/Result

Key concept: Rust enums are algebraic data types — each variant can carry its own data. Paired with match, they replace null checks…

Denys Periel · 2026-04-17 08:10 · 20 claps · 4.1 min read
#rustlang #pattern-matching #rust-tutorial #enum #error-handling
Open on Medium ↗
Wiki topics: TLS · Design Tools & Workflow 📐 · Mathematics

Learn Rust Through the Photo Framer App — Enums, Pattern Matching & Option/Result

Key concept: Rust enums are algebraic data types — each variant can carry its own data. Paired with match, they replace null checks, exception handling, and a lot of polymorphism-by-class-hierarchy in one stroke.

Previous: Ownership, Borrowing & Lifetimes

generated based on the context of the article

generated based on the context of the article

Enums in Photo Framer

Simple Enum

#[derive(Clone, Copy, PartialEq, Debug)]
pub enum BorderMode {
    Percentage,
    Pixels,
}

This looks like a C/C++ enum or a TypeScript string union, but Rust enums are more powerful — each variant can hold data. Here, BorderMode is just a tag.

The derive gives us:

  • Clone + Copy — the value is bit-copyable (no heap data, just a tag).
  • PartialEq — we can compare with ==, which is what selectable_value in egui needs.
  • Debug — we get {:?} printing for free.

Enum with Data

Here’s where Rust gets interesting. Our Border type actually does carry data — either a percentage or a pixel count:

#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Border {
    Percent(f32),
    Pixels(u32),
}

Each variant holds a different payload. When we write Border::Percent(5.0), we’re saying "five percent of the shortest side." When we write Border::Pixels(20), we’re saying "exactly twenty pixels." We can never accidentally mix them up — they're different values of the same type.

This is the Rust equivalent of a TypeScript discriminated union:

type Border =
  | { kind: "percent"; value: number }
  | { kind: "pixels"; value: number };

But in Rust, the compiler guarantees exhaustive handling. If we ever add a Border::Auto variant, every match on Border becomes a compile error until we handle it. That's a refactoring safety net we lean on hard.

Our Error Type Is Also an Enum

#[derive(Debug)]
pub enum FrameError {
    Open { path: PathBuf, source: image::ImageError },
    Save { path: PathBuf, source: image::ImageError },
    Io { path: PathBuf, source: std::io::Error },
    Encode(image::ImageError),
}

Three struct-like variants and one tuple variant, all in one type. This is covered in depth in the following article, but the point I want to make here is that enums scale — they’re not just “a better C enum”, they’re the primary tool for expressing “this value is one of a fixed set of shapes.”

Pattern Matching with match

Basic Match

Here’s how Border resolves itself to pixels:

impl Border {
    fn resolve(self, min_side: u32) -> u32 {
        match self {
            Border::Percent(p) => ((min_side as f32) * p / 100.0).round().max(1.0) as u32,
            Border::Pixels(px) => px.max(1),
        }
    }
}

match is a switch that:

  • Must be exhaustive — every variant must be handled, or I use _ as a catch-all.
  • Is an expression — the whole thing returns a value.
  • Destructures — the p in Percent(p) binds the inner float for us.

Match on a String Slice

pub fn detect_format(path: &Path) -> Option<ImageFormat> {
    let ext = path.extension().and_then(|e| e.to_str())?.to_ascii_lowercase();
    match ext.as_str() {
        "jpg" | "jpeg" => Some(ImageFormat::Jpeg),
        "png" => Some(ImageFormat::Png),
        "tif" | "tiff" => Some(ImageFormat::Tiff),
        "bmp" => Some(ImageFormat::Bmp),
        "webp" => Some(ImageFormat::WebP),
        _ => None,
    }
}

The | inside a pattern is an "or" — "jpg" | "jpeg" matches either string. Much cleaner than a chain of if guards.

Notice the ? on the first line: .and_then(|e| e.to_str()) gives us Option<&str>. If that's None (no extension or non-UTF-8 bytes), the whole function early-returns None. Then we can work with a plain &str inside the match.

The matches! Macro

A macro in Rust is a metaprogramming construct that generates code at compile time. matches!(value, pattern) is a convenience macro that returns a bool. We use it when we only care about "does this match?" and don't need to bind any inner data. The | inside the pattern works the same as in a regular match.

Option — Rust’s Replacement for Null

Rust has no null. A value that might not exist is wrapped in Option<T>:

enum Option<T> {
    Some(T),
    None,
}

It lives in the prelude, so we never import it —we just write Some(x) and None directly.

Option in Photo Framer

path.extension().and_then(|e| e.to_str())

Two chained fallible steps:

  1. extension()Option<&OsStr> (the path might not have an extension at all)
  2. .and_then(|e| e.to_str())Option<&str> (the OsStr might not be valid UTF-8)

If any step returns None, the whole chain short-circuits to None. It's like optional chaining in TypeScript (path.extension?.toString()), but the types are explicit the whole way through.

Common Option Methods

we picked up .map_or_else while cleaning up the file list panel:

let display = path.file_name().map_or_else(
    || path.to_string_lossy().to_string(),
    |n| n.to_string_lossy().to_string(),
);

Reads as: “if there’s a file name, use it; otherwise fall back to the whole path.” One expression, no temporaries.

if let — Conditional Destructuring

if let Some(rx) = &self.done_rx {
    if rx.try_recv().is_ok() {
        self.processing = false;
        self.done_rx = None;
    }
}

if let is the Rust idiom for "do something if the value is present." It's cleaner than a full match when we only care about one variant.

Result<T, E> — Errors as Values

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Rust has no exceptions. All fallible operations return Result. I cover error handling in depth in [t](06 - Error Handling)he following article, but the connection to enums is the key point: Result is just a two-variant enum, and we handle it with the same match / if let / method chains I use for Option.

Exercises

  1. Add a new variant Border::Auto that picks a percentage for large images and a pixel count for small ones. Watch how the compiler marches you through every match on Border until you've handled it.
  2. Rewrite detect_format using if let chains instead of match on ext.as_str(). Which do you think reads better?
  3. Write a function fn parse_dimension(s: &str) -> Option<Border> that parses strings like "5%" or "20px" into a Border. Practice combining Option methods instead of reaching for match.

Next: Structs Traits & Impl Blocks


메타데이터
post_id
0249299d79db
slug
learn-rust-through-the-photo-framer-app-enums-pattern-matching-option-result-0249299d79db
url
https://medium.com/@DPeriel/learn-rust-through-the-photo-framer-app-enums-pattern-matching-option-result-0249299d79db
canonical_url
https://medium.com/@DPeriel/learn-rust-through-the-photo-framer-app-enums-pattern-matching-option-result-0249299d79db
author_url
https://medium.com/@DPeriel
status
ok
fetched_at
2026-06-12 18:14:10