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:
examples/NN_slug/, and run cargo test --example NN_slug (or
cargo check for a faster compile-only loop). Local gets you
rust-analyzer, on-save formatting, and the proper Rust workflow
you'll want once you start writing real projects.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.
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.
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::charsproduces an iterator ofchars. The standard entry point for any per-character check.Iterator::anyreturnstrueas soon as one item satisfies the predicate. Reads aspassword.chars().any(|c| c.is_ascii_digit()).char::is_ascii_uppercase,is_ascii_lowercase, andis_ascii_digitare the per-character classifiers for three of the four checks.- For the special-character set,
str::containson the literal"!@#$%^&*"(with acharargument) gives you a one-line membership test inside the closure.
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:
&[u8] (or &str) per character class.length (assume length >= 4).Useful from the standard library
std::time::SystemTime::nowandDuration::subsec_nanosgive you a quick pseudo-randomu32for the cycling index.String::with_capacitypre-allocates the buffer if you know the final length up front.String::pushappends acharone at a time. Combine with afor _ in 0..lengthloop and an index that walks the alphabets.- For "pick the i-th character of an alphabet", a
&strplus.chars().nth(i)works, or index a&[u8]and cast back tochar.
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::containstakes either a&stror acharand answers yes/no. Perfect for keyword sniffing inside each feedback line.<[T]>::iteronreport.feedbackwalks the messages without consuming the report. The closure inside anif/elsechain can decide what suggestion (if any) to emit.Vec::pushappends each suggestion. AVec<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.
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):
!@#$%^&*: +15Map the final score to PasswordStrength:
< 30 → Weak30..70 → Medium>= 70 → StrongPush 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::lenis byte length, but for an ASCII-only check it's also the character count. Good enough for the length thresholds.Vec::newfor thefeedbackaccumulator; push aStringfor every failed rule.- A
matchon 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
validateis mostly bookkeeping: add toscore, push tofeedback, then build the report.
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!()
}
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_*())ors.chars().filter(...).count(). Internalise this call chain.- Membership in a small set of literal characters is one
"!@#$%^&*".contains(c)call. No need for aHashSet.- A
Vec<String>youpushinto as you check each rule is the idiomatic way to accumulate validation feedback.- Range patterns inside
matcharms (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
randcrate. The clock-based trick is fine for an exercise, never for a password generator that ships.
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.
PasswordReport::is_strong first. It's a one-liner: self.score >= 70.PasswordValidator::validate with the base requirements
only (length + uppercase + lowercase + digit + special). Skip the
advanced ideas until the four base tests pass.password.len() or, more correctly, password.chars().count().password.chars().any(|c| c.is_ascii_uppercase()).password.chars().any(|c| c.is_ascii_digit()).password.chars().any(|c| "!@#$%^&*".contains(c)).let strength = match score {
0..=29 => PasswordStrength::Weak,
30..=69 => PasswordStrength::Medium,
_ => PasswordStrength::Strong,
};
PasswordGenerator::generate_secure_passwordThe 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.