Chapter 18

A Creative Break

👋 Anyone can read and edit this exercise. Sign up to save your progress.

You might not have noticed yet, but, quietly, like a beautiful magpie collecting shiny things, you've acquired the skills to write a lot of helpful Rust programs by now!

This chapter is an open-ended project rather than a focused lesson. You already have the tools you need: structs, enums, iterators, Option, Result, vectors, and strings. The fun part is putting them together.

From this chapter onward the files get longer, and the in-browser editor starts feeling cramped. You have two upgrades available:

A few patterns that come up

Counting with iterators. The .filter(...).count() combo is a quick way to ask "how many of these match?":

let digit_count = password.chars().filter(|c| c.is_ascii_digit()).count();

Building a list of feedback messages. A Vec<String> you push into as you check each rule keeps the validator readable:

let mut feedback = Vec::new();
if password.len() < 8 {
    feedback.push("Use at least 8 characters".to_string());
}

Mapping a score to a category. A simple match on a range works nicely for "weak / medium / strong":

let strength = match score {
    0..30 => PasswordStrength::Weak,
    30..70 => PasswordStrength::Medium,
    _ => PasswordStrength::Strong,
};

The 0..30 here is a range pattern. It's the same .. syntax you saw in chapter 3 for ranges as values, but used inside a match arm to mean "any value in 0..30." ..= (inclusive) works in patterns too.

Cycling through characters for the generator. If you want to avoid external crates, you can use the current time's nanoseconds as a poor person's randomness. Not cryptographically secure, but enough for an exercise:

use std::time::{SystemTime, UNIX_EPOCH};
let seed = SystemTime::now()
    .duration_since(UNIX_EPOCH)
    .unwrap()
    .subsec_nanos() as usize;

For real randomness, the rand crate is the standard answer; out of scope here, but worth knowing it exists.

Take your time with this one. It's deliberately less guided.

Warm-up: `is_strong`

Welcome to the open-ended chapter. The whole exercise revolves around a PasswordReport value: a structured verdict about a password, with a numeric score, some human-readable feedback, and a coarse strength label.

We start small. Before tackling the actual scoring, get a feel for the data by implementing the one-line is_strong method on PasswordReport. By convention in this exercise, "strong" means the score is at least 70.

This step also introduces the shared PasswordStrength enum and PasswordReport struct that every later step will reuse (each step re-declares them so it can stand on its own).

Useful from the standard library

  • The body is one comparison expression: self.score >= 70. No semicolon needed since the expression is the function's return.
  • #[derive(Debug, Clone)] on the struct is what makes the test helper compile. You don't need to add anything else.
Exercise 1 of 5
Open in Web Editor

Results

    Compiler / runtime output
    
                

    Character-class helpers

    The validator's scoring rules all boil down to questions like "does this password contain an uppercase letter?" Before assembling the orchestrator, write the small predicates that answer each one.

    All four functions take a &str and return bool. The "special" character set for this exercise is !@#$%^&*. Feel free to expand it if you want a stricter validator later.

    Hint: str::chars() plus Iterator::any is the natural way to check for the presence of a character class.

    Useful from the standard library

    • str::chars produces an iterator of chars. The standard entry point for any per-character check.
    • Iterator::any returns true as soon as one item satisfies the predicate. Reads as password.chars().any(|c| c.is_ascii_digit()).
    • char::is_ascii_uppercase, is_ascii_lowercase, and is_ascii_digit are the per-character classifiers for three of the four checks.
    • For the special-character set, str::contains on the literal "!@#$%^&*" (with a char argument) gives you a one-line membership test inside the closure.
    Exercise 2 of 5
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  

      Generating a secure password

      Implement generate_secure_password(length) that returns a String of the requested length, mixing uppercase letters, lowercase letters, digits, and special characters so it would pass a strict validator.

      For variability without pulling in rand, you can use std::time::SystemTime::now().duration_since(UNIX_EPOCH)?.subsec_nanos() as a seed and cycle through your character sets. This is not cryptographically secure, but it's plenty for this exercise. In real code, use the rand crate.

      Tips:

      Useful from the standard library

      • std::time::SystemTime::now and Duration::subsec_nanos give you a quick pseudo-random u32 for the cycling index.
      • String::with_capacity pre-allocates the buffer if you know the final length up front.
      • String::push appends a char one at a time. Combine with a for _ in 0..length loop and an index that walks the alphabets.
      • For "pick the i-th character of an alphabet", a &str plus .chars().nth(i) works, or index a &[u8] and cast back to char.
      Exercise 3 of 5
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    

        Turning feedback into suggestions

        The validator (next step) produces a PasswordReport with a list of short complaints in feedback, like "too short" or "missing digit". This step turns those complaints into actionable, human-readable suggestions.

        Decide on your own format. A reasonable approach is to scan each feedback string for keywords ("short", "uppercase", "digit", ...) and emit a matching suggestion ("Add at least 4 more characters", "Mix in an uppercase letter like A-Z", ...).

        The shared types are duplicated here so this step compiles on its own.

        Useful from the standard library

        • str::contains takes either a &str or a char and answers yes/no. Perfect for keyword sniffing inside each feedback line.
        • <[T]>::iter on report.feedback walks the messages without consuming the report. The closure inside an if/else chain can decide what suggestion (if any) to emit.
        • Vec::push appends each suggestion. A Vec<String> is a fine return value.
        • format! lets you splice the input password into a suggestion when that helps the user understand what to fix.
        Exercise 4 of 5
        Open in Web Editor

        Results

          Compiler / runtime output
          
                      

          The orchestrator: `validate`

          Time to combine everything. PasswordValidator::validate(password) returns a PasswordReport with a numeric score, a list of feedback messages, and a PasswordStrength label.

          Because each step in this chapter stands on its own, you'll re-implement the helpers from earlier steps here so this step can stand on its own. The shared types and the four has_* helpers are stubbed below. Fill them in (or copy your earlier solutions) and then write validate on top of them.

          Suggested scoring (feel free to tweak; the tests only check broad ranges):

          Map the final score to PasswordStrength:

          Push a short message into feedback for every rule that fails. That way PasswordAdvisor (or your own future code) has something to react to. The length-related complaint should mention "characters", "length", "short", "longer", or "at least" so the test below can recognise it.

          Useful from the standard library

          • str::len is byte length, but for an ASCII-only check it's also the character count. Good enough for the length thresholds.
          • Vec::new for the feedback accumulator; push a String for every failed rule.
          • A match on the final score with range patterns (0..30 => Weak, 30..70 => Medium, _ => Strong) keeps the classification clean. Range patterns are end-exclusive by default; use ..= if you want the upper bound included.
          • The character-class helpers are exactly what you wrote in step 4, so the body of validate is mostly bookkeeping: add to score, push to feedback, then build the report.
          Exercise 5 of 5
          Open in Web Editor

          Results

            Compiler / runtime output
            
                        
            Stuck? Show a hint No spoilers, just a nudge
            fn validate(password: &str) -> PasswordReport {
                let mut score: u8 = 0;
                let mut feedback: Vec<String> = Vec::new();
            
                // 1. length checks (each adds to score; failure adds feedback)
                // 2. character-class checks (uppercase/lowercase/digit/special)
                // 3. compute strength from score
                // 4. construct PasswordReport { input: password.to_string(), ... }
            
                todo!()
            }
            

            Wrapping up the password validator

            You've put the whole standard toolkit to work in one project: a struct, an enum, methods, character iteration, vectors of feedback, a match over score ranges, and a tiny reflection of how the pieces talk to each other.

            What we learned

            • Open-ended projects are where the chapters since chapter 1 start to feel cohesive. The same handful of types (struct, enum, Vec, String, Option) keep showing up.
            • Per-character checks are almost always s.chars().any(|c| c.is_ascii_*()) or s.chars().filter(...).count(). Internalise this call chain.
            • Membership in a small set of literal characters is one "!@#$%^&*".contains(c) call. No need for a HashSet.
            • A Vec<String> you push into as you check each rule is the idiomatic way to accumulate validation feedback.
            • Range patterns inside match arms (0..30 => Weak) are the cleanest way to bucket a number into categories.
            • Splitting a domain across small types (PasswordReport, PasswordStrength, PasswordValidator, PasswordAdvisor) keeps each piece focused on one job and easy to test.
            • For real randomness, reach for the rand crate. The clock-based trick is fine for an exercise, never for a password generator that ships.
            Stuck? Show a hint No spoilers, just a nudge

            This chapter is open-ended. The hints below are scaffolding, not a solution. They're here to keep you moving when you're stuck on where to start, not on which trick to use.

            Where to start

            1. Implement PasswordReport::is_strong first. It's a one-liner: self.score >= 70.
            2. Implement PasswordValidator::validate with the base requirements only (length + uppercase + lowercase + digit + special). Skip the advanced ideas until the four base tests pass.

            Useful one-liners

            • Length: password.len() or, more correctly, password.chars().count().
            • Has uppercase: password.chars().any(|c| c.is_ascii_uppercase()).
            • Has digit: password.chars().any(|c| c.is_ascii_digit()).
            • Has special: password.chars().any(|c| "!@#$%^&*".contains(c)).

            Strength enum

            let strength = match score {
                0..=29 => PasswordStrength::Weak,
                30..=69 => PasswordStrength::Medium,
                _      => PasswordStrength::Strong,
            };
            

            On PasswordGenerator::generate_secure_password

            The exercise text suggests using SystemTime::now().duration_since(UNIX_EPOCH)?.subsec_nanos() as a source of variability. That's enough to pass the test; in real code, reach for the rand crate.

            Next chapter 19The `?` Operator