refactor(mock-render): split main.rs 227 LOC into 4 cubes (F5a Constructor Pattern)

main.rs 227→55 + cli_args.rs + cmd_screenshot.rs + cmd_lock.rs + cmd_verify.rs (each <100 LOC).
This commit is contained in:
Parfii-bot 2026-04-22 13:36:17 +08:00
parent ff10f76469
commit 37c8e857d7
5 changed files with 217 additions and 179 deletions

View file

@ -0,0 +1,33 @@
//! Shared CLI-arg helpers for every mock-render subcommand.
//!
//! Extracted from `main.rs` in v0.14.1 to keep that dispatcher ≤40 LOC
//! per Constructor Pattern.
use std::path::PathBuf;
/// Look up a `--name <value>` pair in the arg slice.
pub fn flag<'a>(args: &'a [String], name: &str) -> Option<&'a str> {
args.windows(2)
.find(|w| w[0] == name)
.map(|w| w[1].as_str())
}
/// Parse `WxH` viewport (e.g. `1280x800`).
pub fn parse_viewport(s: &str) -> Option<(u32, u32)> {
let (w, h) = s.split_once('x')?;
Some((w.parse().ok()?, h.parse().ok()?))
}
/// Require `--project` (default `.`) and `--section <existing-file>`.
pub fn require_project_section(args: &[String]) -> Result<(PathBuf, PathBuf), String> {
let project = flag(args, "--project")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
let section = flag(args, "--section")
.map(PathBuf::from)
.ok_or_else(|| "--section <file> required".to_string())?;
if !section.exists() {
return Err(format!("section file not found: {}", section.display()));
}
Ok((project, section))
}

View file

@ -0,0 +1,51 @@
//! `mock-render lock --project <dir> --section <src> [--screenshot <png>]`
//!
//! Extracted from `main.rs` in v0.14.1 per Constructor Pattern.
use crate::cli_args::{flag, require_project_section};
use crate::hash;
use crate::state::{Section, SiteState};
use std::process::ExitCode;
pub fn run(args: &[String]) -> ExitCode {
let (project, section) = match require_project_section(args) {
Ok(v) => v,
Err(e) => {
eprintln!("lock: {e}");
return ExitCode::from(1);
}
};
let screenshot = flag(args, "--screenshot");
let Ok(hash_now) = hash::hash_file(&section) else {
eprintln!("lock: cannot hash {}", section.display());
return ExitCode::from(2);
};
let mut st = match SiteState::load(&project) {
Ok(s) => s,
Err(e) => {
eprintln!("lock: {e}");
return ExitCode::from(2);
}
};
let key = SiteState::key_for(&section);
st.sections.insert(
key.clone(),
Section {
path: section.display().to_string(),
sha256: hash_now.clone(),
locked: true,
screenshot: screenshot.map(String::from),
},
);
if let Err(e) = st.save(&project) {
eprintln!("lock: {e}");
return ExitCode::from(2);
}
println!("locked {key} ({})", &hash_now[..12]);
ExitCode::SUCCESS
}

View file

@ -0,0 +1,34 @@
//! `mock-render screenshot <url> --out <png> [--viewport WxH]`
//!
//! Extracted from `main.rs` in v0.14.1 per Constructor Pattern.
use crate::cli_args::{flag, parse_viewport};
use crate::render;
use std::path::PathBuf;
use std::process::ExitCode;
pub fn run(args: &[String]) -> ExitCode {
let Some(url) = args.first().cloned() else {
eprintln!("screenshot: <url> required");
return ExitCode::from(1);
};
let out = match flag(args, "--out") {
Some(p) => PathBuf::from(p),
None => {
eprintln!("screenshot: --out <png> required");
return ExitCode::from(1);
}
};
let viewport = flag(args, "--viewport").and_then(parse_viewport);
match render::screenshot(&url, &out, viewport) {
Ok(()) => {
println!("{}", out.display());
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("mock-render: {e}");
ExitCode::from(1)
}
}
}

View file

@ -0,0 +1,91 @@
//! `mock-render verify --project <dir> --section <src>`
//! `mock-render status --project <dir>`
//!
//! Two closely-related subcommands extracted from `main.rs` in v0.14.1.
//! They share state-loading + hash-comparison logic.
use crate::cli_args::{flag, require_project_section};
use crate::hash;
use crate::state::SiteState;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
pub fn run_verify(args: &[String]) -> ExitCode {
let (project, section) = match require_project_section(args) {
Ok(v) => v,
Err(e) => {
eprintln!("verify: {e}");
return ExitCode::from(1);
}
};
let st = match SiteState::load(&project) {
Ok(s) => s,
Err(e) => {
eprintln!("verify: {e}");
return ExitCode::from(2);
}
};
let key = SiteState::key_for(&section);
let Some(entry) = st.sections.get(&key) else {
eprintln!("verify: section '{key}' not in site-state.json (not locked yet)");
return ExitCode::SUCCESS;
};
if !entry.locked {
return ExitCode::SUCCESS;
}
let Ok(hash_now) = hash::hash_file(&section) else {
eprintln!("verify: cannot hash {}", section.display());
return ExitCode::from(2);
};
if hash_now != entry.sha256 {
eprintln!(
"WYSIWYD VIOLATION: {key} drifted\n locked : {}\n current: {}\nThe screenshot user approved no longer matches the source.\nRerun render + user-approval before deploy.",
&entry.sha256[..12],
&hash_now[..12]
);
return ExitCode::from(2);
}
println!("ok {key} ({})", &hash_now[..12]);
ExitCode::SUCCESS
}
pub fn run_status(args: &[String]) -> ExitCode {
let project = flag(args, "--project")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
let st = match SiteState::load(&project) {
Ok(s) => s,
Err(e) => {
eprintln!("status: {e}");
return ExitCode::from(2);
}
};
if st.sections.is_empty() {
println!("(no sections tracked)");
return ExitCode::SUCCESS;
}
for (name, sec) in &st.sections {
let lock = if sec.locked { "LOCKED" } else { "open" };
let drift = match hash::hash_file(Path::new(&sec.path)) {
Ok(h) if h == sec.sha256 => "clean",
Ok(_) => "DRIFT",
Err(_) => "missing",
};
println!(
"{:<20} {:>6} {:<7} {} ({})",
name,
lock,
drift,
sec.path,
&sec.sha256[..12]
);
}
ExitCode::SUCCESS
}

View file

@ -8,22 +8,24 @@
//! mock-render verify --project <dir> --section <src>
//! mock-render status --project <dir>
mod cli_args;
mod cmd_lock;
mod cmd_screenshot;
mod cmd_verify;
mod hash;
mod render;
mod state;
use state::{Section, SiteState};
use std::env;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
fn main() -> ExitCode {
let args: Vec<String> = env::args().skip(1).collect();
match args.first().map(String::as_str) {
Some("screenshot") => cmd_screenshot(&args[1..]),
Some("lock") => cmd_lock(&args[1..]),
Some("verify") => cmd_verify(&args[1..]),
Some("status") => cmd_status(&args[1..]),
Some("screenshot") => cmd_screenshot::run(&args[1..]),
Some("lock") => cmd_lock::run(&args[1..]),
Some("verify") => cmd_verify::run_verify(&args[1..]),
Some("status") => cmd_verify::run_status(&args[1..]),
Some("--help") | Some("-h") | None => {
print_help();
ExitCode::SUCCESS
@ -51,176 +53,3 @@ EXIT
2 WYSIWYD invariant violated (file drift / hash mismatch)"
);
}
fn cmd_screenshot(args: &[String]) -> ExitCode {
let Some(url) = args.first().cloned() else {
eprintln!("screenshot: <url> required");
return ExitCode::from(1);
};
let out = match flag(args, "--out") {
Some(p) => PathBuf::from(p),
None => {
eprintln!("screenshot: --out <png> required");
return ExitCode::from(1);
}
};
let viewport = flag(args, "--viewport").and_then(parse_viewport);
match render::screenshot(&url, &out, viewport) {
Ok(()) => {
println!("{}", out.display());
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("mock-render: {e}");
ExitCode::from(1)
}
}
}
fn cmd_lock(args: &[String]) -> ExitCode {
let (project, section) = match require_project_section(args) {
Ok(v) => v,
Err(e) => {
eprintln!("lock: {e}");
return ExitCode::from(1);
}
};
let screenshot = flag(args, "--screenshot");
let Ok(hash_now) = hash::hash_file(&section) else {
eprintln!("lock: cannot hash {}", section.display());
return ExitCode::from(2);
};
let mut st = match SiteState::load(&project) {
Ok(s) => s,
Err(e) => {
eprintln!("lock: {e}");
return ExitCode::from(2);
}
};
let key = SiteState::key_for(&section);
st.sections.insert(
key.clone(),
Section {
path: section.display().to_string(),
sha256: hash_now.clone(),
locked: true,
screenshot: screenshot.map(String::from),
},
);
if let Err(e) = st.save(&project) {
eprintln!("lock: {e}");
return ExitCode::from(2);
}
println!("locked {key} ({})", &hash_now[..12]);
ExitCode::SUCCESS
}
fn cmd_verify(args: &[String]) -> ExitCode {
let (project, section) = match require_project_section(args) {
Ok(v) => v,
Err(e) => {
eprintln!("verify: {e}");
return ExitCode::from(1);
}
};
let st = match SiteState::load(&project) {
Ok(s) => s,
Err(e) => {
eprintln!("verify: {e}");
return ExitCode::from(2);
}
};
let key = SiteState::key_for(&section);
let Some(entry) = st.sections.get(&key) else {
eprintln!("verify: section '{key}' not in site-state.json (not locked yet)");
return ExitCode::SUCCESS;
};
if !entry.locked {
return ExitCode::SUCCESS;
}
let Ok(hash_now) = hash::hash_file(&section) else {
eprintln!("verify: cannot hash {}", section.display());
return ExitCode::from(2);
};
if hash_now != entry.sha256 {
eprintln!(
"WYSIWYD VIOLATION: {key} drifted\n locked : {}\n current: {}\nThe screenshot user approved no longer matches the source.\nRerun render + user-approval before deploy.",
&entry.sha256[..12],
&hash_now[..12]
);
return ExitCode::from(2);
}
println!("ok {key} ({})", &hash_now[..12]);
ExitCode::SUCCESS
}
fn cmd_status(args: &[String]) -> ExitCode {
let project = flag(args, "--project")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
let st = match SiteState::load(&project) {
Ok(s) => s,
Err(e) => {
eprintln!("status: {e}");
return ExitCode::from(2);
}
};
if st.sections.is_empty() {
println!("(no sections tracked)");
return ExitCode::SUCCESS;
}
for (name, sec) in &st.sections {
let lock = if sec.locked { "LOCKED" } else { "open" };
let drift = match hash::hash_file(Path::new(&sec.path)) {
Ok(h) if h == sec.sha256 => "clean",
Ok(_) => "DRIFT",
Err(_) => "missing",
};
println!(
"{:<20} {:>6} {:<7} {} ({})",
name,
lock,
drift,
sec.path,
&sec.sha256[..12]
);
}
ExitCode::SUCCESS
}
fn flag<'a>(args: &'a [String], name: &str) -> Option<&'a str> {
args.windows(2)
.find(|w| w[0] == name)
.map(|w| w[1].as_str())
}
fn parse_viewport(s: &str) -> Option<(u32, u32)> {
let (w, h) = s.split_once('x')?;
Some((w.parse().ok()?, h.parse().ok()?))
}
fn require_project_section(args: &[String]) -> Result<(PathBuf, PathBuf), String> {
let project = flag(args, "--project")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
let section = flag(args, "--section")
.map(PathBuf::from)
.ok_or_else(|| "--section <file> required".to_string())?;
if !section.exists() {
return Err(format!("section file not found: {}", section.display()));
}
Ok((project, section))
}