1use clap::Parser;
2use anyhow::anyhow;
3use itertools::izip;
4use std::path::PathBuf;
5use crate::cli::output::SupportedOutputEncoding;
6use crate::meta::{
7 RainMetaDocumentV1Item, KnownMeta, ContentType, ContentEncoding, ContentLanguage,
8 magic::KnownMagic,
9};
10
11#[derive(Parser)]
13pub struct Build {
14 #[arg(short, long)]
16 output_path: Option<PathBuf>,
17 #[arg(short = 'E', long, default_value = "binary")]
19 output_encoding: SupportedOutputEncoding,
20 #[arg(short = 'M', long, default_value = "rain-meta-document-v1")]
24 global_magic: KnownMagic,
25 #[arg(short, long, num_args = 1..)]
29 input_path: Vec<PathBuf>,
30 #[arg(short, long, num_args = 1..)]
35 magic: Vec<KnownMagic>,
36 #[arg(short = 't', long, num_args = 1..)]
40 content_type: Vec<ContentType>,
41 #[arg(short = 'e', long, num_args = 1..)]
45 content_encoding: Vec<ContentEncoding>,
46 #[arg(short = 'l', long, num_args = 1..)]
50 content_language: Vec<ContentLanguage>,
51}
52
53#[derive(Clone, Debug)]
55pub struct BuildItem {
56 pub data: Vec<u8>,
58 pub magic: KnownMagic,
60 pub content_type: ContentType,
62 pub content_encoding: ContentEncoding,
64 pub content_language: ContentLanguage,
66}
67
68impl TryFrom<&BuildItem> for RainMetaDocumentV1Item {
71 type Error = anyhow::Error;
72 fn try_from(item: &BuildItem) -> anyhow::Result<Self> {
73 let normalized = TryInto::<KnownMeta>::try_into(item.magic)?.normalize(&item.data)?;
74 let encoded = item.content_encoding.encode(&normalized);
75 Ok(RainMetaDocumentV1Item {
76 payload: serde_bytes::ByteBuf::from(encoded),
77 magic: item.magic,
78 content_type: item.content_type,
79 content_encoding: item.content_encoding,
80 content_language: item.content_language,
81 schema: None,
82 })
83 }
84}
85
86pub fn build_bytes(magic: KnownMagic, items: Vec<BuildItem>) -> anyhow::Result<Vec<u8>> {
88 let mut metas: Vec<RainMetaDocumentV1Item> = vec![];
89 for item in items {
90 metas.push(RainMetaDocumentV1Item::try_from(&item)?);
91 }
92 Ok(RainMetaDocumentV1Item::cbor_encode_seq(&metas, magic)?)
93}
94
95pub fn build(b: Build) -> anyhow::Result<()> {
101 if b.input_path.len() != b.magic.len() {
102 return Err(anyhow!(
103 "{} inputs does not match {} magic numbers.",
104 b.input_path.len(),
105 b.magic.len()
106 ));
107 }
108
109 if b.input_path.len() != b.content_type.len() {
110 return Err(anyhow!(
111 "{} inputs does not match {} content types.",
112 b.input_path.len(),
113 b.content_type.len()
114 ));
115 }
116
117 if b.input_path.len() != b.content_encoding.len() {
118 return Err(anyhow!(
119 "{} inputs does not match {} content encodings.",
120 b.input_path.len(),
121 b.content_encoding.len()
122 ));
123 }
124
125 if b.input_path.len() != b.content_language.len() {
126 return Err(anyhow!(
127 "{} inputs does not match {} content languages.",
128 b.input_path.len(),
129 b.content_language.len()
130 ));
131 }
132 let mut items: Vec<BuildItem> = vec![];
133 for (input_path, magic, content_type, content_encoding, content_language) in izip!(
134 b.input_path.iter(),
135 b.magic.iter(),
136 b.content_type.iter(),
137 b.content_encoding.iter(),
138 b.content_language.iter()
139 ) {
140 items.push(BuildItem {
141 data: std::fs::read(input_path)?,
142 magic: *magic,
143 content_type: *content_type,
144 content_encoding: *content_encoding,
145 content_language: *content_language,
146 });
147 }
148 crate::cli::output::output(
149 &b.output_path,
150 b.output_encoding,
151 &build_bytes(b.global_magic, items)?,
152 )
153}
154
155#[cfg(all(test, not(target_family = "wasm")))]
156mod tests {
157 use strum::IntoEnumIterator;
158 use crate::meta::{
159 magic::{self, KnownMagic},
160 ContentType, ContentEncoding, ContentLanguage, RainMetaDocumentV1Item,
161 };
162 use super::BuildItem;
163 use super::build_bytes;
164
165 #[test]
168 fn test_build_empty() -> anyhow::Result<()> {
169 for global_magic in magic::KnownMagic::iter() {
170 let built_bytes = build_bytes(global_magic, vec![])?;
171 assert_eq!(built_bytes, global_magic.to_prefix_bytes());
172 }
173 Ok(())
174 }
175
176 #[test]
180 fn test_into_meta_document() -> anyhow::Result<()> {
181 let build_item = BuildItem {
182 data: "[]".as_bytes().to_vec(),
183 magic: KnownMagic::SolidityAbiV2,
184 content_type: ContentType::Json,
185 content_encoding: ContentEncoding::None,
186 content_language: ContentLanguage::En,
187 };
188
189 let meta_document = RainMetaDocumentV1Item::try_from(&build_item)?;
190 let expected_meta_document = RainMetaDocumentV1Item {
191 payload: serde_bytes::ByteBuf::from("[]".as_bytes().to_vec()),
192 magic: KnownMagic::SolidityAbiV2,
193 content_type: ContentType::Json,
194 content_encoding: ContentEncoding::None,
195 content_language: ContentLanguage::En,
196 schema: None,
197 };
198 assert_eq!(meta_document, expected_meta_document);
199 Ok(())
200 }
201
202 #[test]
205 fn test_empty_item() -> anyhow::Result<()> {
206 let build_item = BuildItem {
207 data: "[]".as_bytes().to_vec(),
208 magic: KnownMagic::SolidityAbiV2,
209 content_type: ContentType::Json,
210 content_encoding: ContentEncoding::Identity,
211 content_language: ContentLanguage::En,
212 };
213
214 let bytes = super::build_bytes(KnownMagic::RainMetaDocumentV1, vec![build_item.clone()])?;
215
216 assert_eq!(
219 &bytes[0..8],
220 KnownMagic::RainMetaDocumentV1.to_prefix_bytes()
221 );
222 assert_eq!(bytes[8], 0xa5);
224 assert_eq!(bytes[9], 0x00);
226 assert_eq!(bytes[10], 0b010_00010);
228 assert_eq!(bytes[11..13], "[]".as_bytes()[..]);
230 assert_eq!(bytes[13], 0x01);
232 assert_eq!(bytes[14], 0b000_11011);
234 assert_eq!(&bytes[15..23], KnownMagic::SolidityAbiV2.to_prefix_bytes());
236 assert_eq!(bytes[23], 0x02);
238 assert_eq!(bytes[24], 0b011_10000);
240 assert_eq!(&bytes[25..41], "application/json".as_bytes());
242 assert_eq!(bytes[41], 0x03);
244 assert_eq!(bytes[42], 0b011_01000);
246 assert_eq!(&bytes[43..51], "identity".as_bytes());
248 assert_eq!(bytes[51], 0x04);
250 assert_eq!(bytes[52], 0b011_00010);
252 assert_eq!(&bytes[53..55], "en".as_bytes());
254
255 assert_eq!(bytes.len(), 55);
256
257 Ok(())
258 }
259
260 #[test]
261 fn test_cbor_encoding_type() -> anyhow::Result<()> {
262 let build_item = BuildItem {
263 data: "[]".as_bytes().to_vec(),
264 magic: KnownMagic::DotrainV1,
265 content_type: ContentType::Cbor,
266 content_encoding: ContentEncoding::Identity,
267 content_language: ContentLanguage::En,
268 };
269
270 let bytes = super::build_bytes(KnownMagic::RainMetaDocumentV1, vec![build_item.clone()])?;
271
272 assert_eq!(
275 &bytes[0..8],
276 KnownMagic::RainMetaDocumentV1.to_prefix_bytes()
277 );
278 assert_eq!(bytes[8], 0xa5);
280 assert_eq!(bytes[9], 0x00);
282 assert_eq!(bytes[10], 0b010_00010);
284 assert_eq!(bytes[11..13], "[]".as_bytes()[..]);
286 assert_eq!(bytes[13], 0x01);
288 assert_eq!(bytes[14], 0b000_11011);
290 assert_eq!(&bytes[15..23], KnownMagic::DotrainV1.to_prefix_bytes());
292 assert_eq!(bytes[23], 0x02);
294 assert_eq!(bytes[24], 0b011_10000);
296 assert_eq!(&bytes[25..41], "application/cbor".as_bytes());
298 assert_eq!(bytes[41], 0x03);
300 assert_eq!(bytes[42], 0b011_01000);
302 assert_eq!(&bytes[43..51], "identity".as_bytes());
304 assert_eq!(bytes[51], 0x04);
306 assert_eq!(bytes[52], 0b011_00010);
308 assert_eq!(&bytes[53..55], "en".as_bytes());
310
311 assert_eq!(bytes.len(), 55);
312
313 Ok(())
314 }
315
316 use clap::Parser;
317 use std::io::Write;
318 use super::{Build, build};
319
320 #[test]
323 fn test_item_normalize_then_encode() -> anyhow::Result<()> {
324 let build_item = BuildItem {
326 data: "[ ]".as_bytes().to_vec(),
327 magic: KnownMagic::SolidityAbiV2,
328 content_type: ContentType::Json,
329 content_encoding: ContentEncoding::Deflate,
330 content_language: ContentLanguage::En,
331 };
332 let meta_document = RainMetaDocumentV1Item::try_from(&build_item)?;
333 assert_eq!(
334 meta_document.payload.as_ref(),
335 ContentEncoding::Deflate.encode("[]".as_bytes())
336 );
337
338 let invalid_item = BuildItem {
340 data: "not json".as_bytes().to_vec(),
341 ..build_item
342 };
343 assert!(RainMetaDocumentV1Item::try_from(&invalid_item).is_err());
344 Ok(())
345 }
346
347 fn parse_build(args: &[&str]) -> Build {
348 Build::try_parse_from(args).unwrap()
349 }
350
351 #[test]
354 fn test_build_arity_guards() {
355 let b = parse_build(&[
356 "build",
357 "-i",
358 "does-not-exist.json",
359 "-m",
360 "solidity-abi-v2",
361 "-m",
362 "solidity-abi-v2",
363 ]);
364 assert_eq!(
365 build(b).unwrap_err().to_string(),
366 "1 inputs does not match 2 magic numbers."
367 );
368
369 let b = parse_build(&[
370 "build",
371 "-i",
372 "does-not-exist.json",
373 "-m",
374 "solidity-abi-v2",
375 "-t",
376 "json",
377 "-t",
378 "json",
379 ]);
380 assert_eq!(
381 build(b).unwrap_err().to_string(),
382 "1 inputs does not match 2 content types."
383 );
384
385 let b = parse_build(&[
386 "build",
387 "-i",
388 "does-not-exist.json",
389 "-m",
390 "solidity-abi-v2",
391 "-t",
392 "json",
393 "-e",
394 "identity",
395 "-e",
396 "identity",
397 ]);
398 assert_eq!(
399 build(b).unwrap_err().to_string(),
400 "1 inputs does not match 2 content encodings."
401 );
402
403 let b = parse_build(&[
404 "build",
405 "-i",
406 "does-not-exist.json",
407 "-m",
408 "solidity-abi-v2",
409 "-t",
410 "json",
411 "-e",
412 "identity",
413 "-l",
414 "en",
415 "-l",
416 "en",
417 ]);
418 assert_eq!(
419 build(b).unwrap_err().to_string(),
420 "1 inputs does not match 2 content languages."
421 );
422 }
423
424 #[test]
428 fn test_build_reads_files_and_encodes_output() -> anyhow::Result<()> {
429 let mut input = tempfile::NamedTempFile::new()?;
430 input.write_all("[ ]".as_bytes())?;
431 let output = tempfile::NamedTempFile::new()?;
432
433 let expected = build_bytes(
434 KnownMagic::RainMetaDocumentV1,
435 vec![BuildItem {
436 data: "[ ]".as_bytes().to_vec(),
437 magic: KnownMagic::SolidityAbiV2,
438 content_type: ContentType::Json,
439 content_encoding: ContentEncoding::Identity,
440 content_language: ContentLanguage::En,
441 }],
442 )?;
443
444 let input_path = input.path().to_str().unwrap().to_string();
445 let output_path = output.path().to_str().unwrap().to_string();
446
447 let b = parse_build(&[
448 "build",
449 "-i",
450 &input_path,
451 "-m",
452 "solidity-abi-v2",
453 "-t",
454 "json",
455 "-e",
456 "identity",
457 "-l",
458 "en",
459 "-o",
460 &output_path,
461 ]);
462 build(b)?;
463 assert_eq!(std::fs::read(output.path())?, expected);
464
465 let b = parse_build(&[
466 "build",
467 "-i",
468 &input_path,
469 "-m",
470 "solidity-abi-v2",
471 "-t",
472 "json",
473 "-e",
474 "identity",
475 "-l",
476 "en",
477 "-o",
478 &output_path,
479 "-E",
480 "hex",
481 ]);
482 build(b)?;
483 assert_eq!(
484 std::fs::read_to_string(output.path())?,
485 alloy::primitives::hex::encode_prefixed(&expected)
486 );
487 Ok(())
488 }
489}