1use std::fs;
9use std::path::{Path, PathBuf};
10
11use anyhow::{Context, Result};
12use blockworx_doc::document::Document;
13use blockworx_doc::fixtures::scale::build_scale;
14use blockworx_store::handle::{Clock, Store};
15use blockworx_store::record::Identity;
16use blockworx_store::storage::{Memory, Name, ready_now};
17use blockworx_store::transfer;
18use clap::{Args, Subcommand};
19
20#[derive(Subcommand)]
21pub enum AutogenCmd {
22 Scale(ScaleArgs),
26 Pack(PackArgs),
29}
30
31#[derive(Args)]
32pub struct PackArgs {
33 input: PathBuf,
35 output: PathBuf,
37}
38
39#[derive(Args)]
40pub struct ScaleArgs {
41 n: usize,
43 output: PathBuf,
46}
47
48enum Form {
50 Json,
51 Archive,
52}
53
54impl Form {
55 fn of(output: &Path) -> Self {
56 let zipped = output
57 .extension()
58 .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"));
59 if zipped { Self::Archive } else { Self::Json }
60 }
61}
62
63pub fn run(cmd: &AutogenCmd) -> Result<()> {
64 match cmd {
65 AutogenCmd::Scale(args) => scale(args),
66 AutogenCmd::Pack(args) => pack(args),
67 }
68}
69
70fn pack(args: &PackArgs) -> Result<()> {
71 let read =
72 fs::read(&args.input).with_context(|| format!("reading {}", args.input.display()))?;
73 let document: Document = serde_json::from_slice(&read)
74 .with_context(|| format!("{} is not a document", args.input.display()))?;
75 let bytes = archive(&document, &container_name(&args.output)?)?;
76 write(&args.output, &bytes)?;
77 eprintln!(
78 "packed {} ({} blocks, {} bytes) into {}",
79 args.input.display(),
80 document.blocks().count(),
81 bytes.len(),
82 args.output.display()
83 );
84 Ok(())
85}
86
87fn write(output: &Path, bytes: &[u8]) -> Result<()> {
88 if let Some(dir) = output.parent().filter(|dir| !dir.as_os_str().is_empty()) {
89 fs::create_dir_all(dir).with_context(|| format!("creating {}", dir.display()))?;
90 }
91 fs::write(output, bytes).with_context(|| format!("writing {}", output.display()))
92}
93
94fn scale(args: &ScaleArgs) -> Result<()> {
95 let n = args.n;
96 if n == 0 {
97 anyhow::bail!("grid dimension N must be at least 1");
98 }
99
100 let document = settled(build_scale(n))?;
101 let bytes = match Form::of(&args.output) {
102 Form::Json => {
103 let mut json = serde_json::to_string_pretty(&document)
104 .context("serializing the generated document")?;
105 json.push('\n');
106 json.into_bytes()
107 }
108 Form::Archive => archive(&document, &container_name(&args.output)?)?,
109 };
110 write(&args.output, &bytes)?;
111 let blocks = n * n;
112 eprintln!(
113 "wrote autogen scale ({n}×{n} = {blocks} blocks, {} bytes) to {}",
114 bytes.len(),
115 args.output.display()
116 );
117 Ok(())
118}
119
120fn settled(document: Document) -> Result<Document> {
123 blockworx_editor::widget::drawing::settle_corners(document)
124 .map_err(|refusal| anyhow::anyhow!("storing the wires' corners: {refusal}"))
125}
126
127fn container_name(output: &Path) -> Result<Name> {
130 output
131 .file_name()
132 .and_then(|file| file.to_str())
133 .and_then(Name::of_archive)
134 .with_context(|| format!("{} names no diagram", output.display()))
135}
136
137fn archive(document: &Document, name: &Name) -> Result<Vec<u8>> {
140 let label = format!("Generated {name}");
141 let commits: Vec<_> = document.creating_commit(&label).into_iter().collect();
142 let storage = Memory::new(name.as_str());
143 Store::seeded(
144 storage.clone(),
145 Clock::System,
146 &commits,
147 &Identity::new("cargo xtask autogen"),
148 )
149 .context("seeding the container")?;
150 ready_now(transfer::pack(&storage)).context("packing the container")
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn a_scale_archive_unpacks_into_a_container_holding_the_grid() {
159 let n = 3;
160 let generated = build_scale(n);
161 assert_eq!(
162 generated.blocks().count(),
163 n * n + 1,
164 "precondition: the grid's blocks and the sheet wrapping them",
165 );
166 let name = container_name(Path::new("target/autogen/scale-3.bwx.zip")).expect("a name");
167 assert_eq!(name.as_str(), "scale-3.bwx");
168
169 let packed = archive(&generated, &name).expect("the archive");
170 let arrived = Memory::new(name.as_str());
171 ready_now(transfer::unpack(&arrived, &packed)).expect("the archive lays down");
172 let opened = Store::open(arrived, Clock::System).expect("a container");
173
174 assert!(
175 opened.read_only_reason().is_none(),
176 "the container verifies"
177 );
178 assert_eq!(opened.document().blocks().count(), n * n + 1);
179 assert_eq!(opened.document(), &generated);
180 }
181
182 #[test]
183 fn the_destination_extension_picks_the_form() {
184 assert!(matches!(
185 Form::of(Path::new("scale.bwx.zip")),
186 Form::Archive
187 ));
188 assert!(matches!(Form::of(Path::new("scale.ZIP")), Form::Archive));
189 assert!(matches!(Form::of(Path::new("scale.json")), Form::Json));
190 }
191}