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:
parent
ff10f76469
commit
37c8e857d7
5 changed files with 217 additions and 179 deletions
33
_primitives/_rust/mock-render/src/cli_args.rs
Normal file
33
_primitives/_rust/mock-render/src/cli_args.rs
Normal 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))
|
||||
}
|
||||
51
_primitives/_rust/mock-render/src/cmd_lock.rs
Normal file
51
_primitives/_rust/mock-render/src/cmd_lock.rs
Normal 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(§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
|
||||
}
|
||||
34
_primitives/_rust/mock-render/src/cmd_screenshot.rs
Normal file
34
_primitives/_rust/mock-render/src/cmd_screenshot.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
91
_primitives/_rust/mock-render/src/cmd_verify.rs
Normal file
91
_primitives/_rust/mock-render/src/cmd_verify.rs
Normal 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(§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
|
||||
}
|
||||
|
|
@ -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(§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 <file> required".to_string())?;
|
||||
if !section.exists() {
|
||||
return Err(format!("section file not found: {}", section.display()));
|
||||
}
|
||||
Ok((project, section))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue