From 37c8e857d7f46da4455ec413ba8dadd33dd0ad9b Mon Sep 17 00:00:00 2001 From: Parfii-bot Date: Wed, 22 Apr 2026 13:36:17 +0800 Subject: [PATCH] refactor(mock-render): split main.rs 227 LOC into 4 cubes (F5a Constructor Pattern) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main.rs 227→55 + cli_args.rs + cmd_screenshot.rs + cmd_lock.rs + cmd_verify.rs (each <100 LOC). --- _primitives/_rust/mock-render/src/cli_args.rs | 33 ++++ _primitives/_rust/mock-render/src/cmd_lock.rs | 51 +++++ .../_rust/mock-render/src/cmd_screenshot.rs | 34 ++++ .../_rust/mock-render/src/cmd_verify.rs | 91 +++++++++ _primitives/_rust/mock-render/src/main.rs | 187 +----------------- 5 files changed, 217 insertions(+), 179 deletions(-) create mode 100644 _primitives/_rust/mock-render/src/cli_args.rs create mode 100644 _primitives/_rust/mock-render/src/cmd_lock.rs create mode 100644 _primitives/_rust/mock-render/src/cmd_screenshot.rs create mode 100644 _primitives/_rust/mock-render/src/cmd_verify.rs diff --git a/_primitives/_rust/mock-render/src/cli_args.rs b/_primitives/_rust/mock-render/src/cli_args.rs new file mode 100644 index 0000000..e6cfdbd --- /dev/null +++ b/_primitives/_rust/mock-render/src/cli_args.rs @@ -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 ` 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 `. +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 required".to_string())?; + if !section.exists() { + return Err(format!("section file not found: {}", section.display())); + } + Ok((project, section)) +} diff --git a/_primitives/_rust/mock-render/src/cmd_lock.rs b/_primitives/_rust/mock-render/src/cmd_lock.rs new file mode 100644 index 0000000..4523b86 --- /dev/null +++ b/_primitives/_rust/mock-render/src/cmd_lock.rs @@ -0,0 +1,51 @@ +//! `mock-render lock --project --section [--screenshot ]` +//! +//! 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(§ion) 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(§ion); + 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 +} diff --git a/_primitives/_rust/mock-render/src/cmd_screenshot.rs b/_primitives/_rust/mock-render/src/cmd_screenshot.rs new file mode 100644 index 0000000..3505ab5 --- /dev/null +++ b/_primitives/_rust/mock-render/src/cmd_screenshot.rs @@ -0,0 +1,34 @@ +//! `mock-render screenshot --out [--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: required"); + return ExitCode::from(1); + }; + let out = match flag(args, "--out") { + Some(p) => PathBuf::from(p), + None => { + eprintln!("screenshot: --out 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) + } + } +} diff --git a/_primitives/_rust/mock-render/src/cmd_verify.rs b/_primitives/_rust/mock-render/src/cmd_verify.rs new file mode 100644 index 0000000..5adfe4e --- /dev/null +++ b/_primitives/_rust/mock-render/src/cmd_verify.rs @@ -0,0 +1,91 @@ +//! `mock-render verify --project --section ` +//! `mock-render status --project ` +//! +//! 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(§ion); + 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(§ion) 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 +} diff --git a/_primitives/_rust/mock-render/src/main.rs b/_primitives/_rust/mock-render/src/main.rs index ed9a091..6510162 100644 --- a/_primitives/_rust/mock-render/src/main.rs +++ b/_primitives/_rust/mock-render/src/main.rs @@ -8,22 +8,24 @@ //! mock-render verify --project --section //! mock-render status --project +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 = 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: required"); - return ExitCode::from(1); - }; - let out = match flag(args, "--out") { - Some(p) => PathBuf::from(p), - None => { - eprintln!("screenshot: --out 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(§ion) 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(§ion); - 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(§ion); - 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(§ion) 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 required".to_string())?; - if !section.exists() { - return Err(format!("section file not found: {}", section.display())); - } - Ok((project, section)) -}