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 use crate::meta::types::authoring::v1::AuthoringMeta;
165
166 const AUTHORING_META_V1_JSON: &str = r#"[{"word":"stack","description":"Copies an existing value from the stack.","operandParserOffset":16}]"#;
167
168 #[test]
171 fn test_build_empty() -> anyhow::Result<()> {
172 for global_magic in magic::KnownMagic::iter() {
173 let built_bytes = build_bytes(global_magic, vec![])?;
174 assert_eq!(built_bytes, global_magic.to_prefix_bytes());
175 }
176 Ok(())
177 }
178
179 #[test]
183 fn test_into_meta_document() -> anyhow::Result<()> {
184 let build_item = BuildItem {
185 data: "[]".as_bytes().to_vec(),
186 magic: KnownMagic::DotrainV1,
187 content_type: ContentType::Json,
188 content_encoding: ContentEncoding::None,
189 content_language: ContentLanguage::En,
190 };
191
192 let meta_document = RainMetaDocumentV1Item::try_from(&build_item)?;
193 let expected_meta_document = RainMetaDocumentV1Item {
194 payload: serde_bytes::ByteBuf::from("[]".as_bytes().to_vec()),
195 magic: KnownMagic::DotrainV1,
196 content_type: ContentType::Json,
197 content_encoding: ContentEncoding::None,
198 content_language: ContentLanguage::En,
199 schema: None,
200 };
201 assert_eq!(meta_document, expected_meta_document);
202 Ok(())
203 }
204
205 #[test]
209 fn test_empty_item() -> anyhow::Result<()> {
210 let build_item = BuildItem {
211 data: "[]".as_bytes().to_vec(),
212 magic: KnownMagic::DotrainV1,
213 content_type: ContentType::Json,
214 content_encoding: ContentEncoding::Identity,
215 content_language: ContentLanguage::En,
216 };
217
218 let bytes = super::build_bytes(KnownMagic::RainMetaDocumentV1, vec![build_item.clone()])?;
219
220 assert_eq!(
223 &bytes[0..8],
224 KnownMagic::RainMetaDocumentV1.to_prefix_bytes()
225 );
226 assert_eq!(bytes[8], 0xa5);
228 assert_eq!(bytes[9], 0x00);
230 assert_eq!(bytes[10], 0b010_00010);
232 assert_eq!(bytes[11..13], "[]".as_bytes()[..]);
234 assert_eq!(bytes[13], 0x01);
236 assert_eq!(bytes[14], 0b000_11011);
238 assert_eq!(&bytes[15..23], KnownMagic::DotrainV1.to_prefix_bytes());
240 assert_eq!(bytes[23], 0x02);
242 assert_eq!(bytes[24], 0b011_10000);
244 assert_eq!(&bytes[25..41], "application/json".as_bytes());
246 assert_eq!(bytes[41], 0x03);
248 assert_eq!(bytes[42], 0b011_01000);
250 assert_eq!(&bytes[43..51], "identity".as_bytes());
252 assert_eq!(bytes[51], 0x04);
254 assert_eq!(bytes[52], 0b011_00010);
256 assert_eq!(&bytes[53..55], "en".as_bytes());
258
259 assert_eq!(bytes.len(), 55);
260
261 Ok(())
262 }
263
264 #[test]
265 fn test_cbor_encoding_type() -> anyhow::Result<()> {
266 let build_item = BuildItem {
267 data: "[]".as_bytes().to_vec(),
268 magic: KnownMagic::DotrainV1,
269 content_type: ContentType::Cbor,
270 content_encoding: ContentEncoding::Identity,
271 content_language: ContentLanguage::En,
272 };
273
274 let bytes = super::build_bytes(KnownMagic::RainMetaDocumentV1, vec![build_item.clone()])?;
275
276 assert_eq!(
279 &bytes[0..8],
280 KnownMagic::RainMetaDocumentV1.to_prefix_bytes()
281 );
282 assert_eq!(bytes[8], 0xa5);
284 assert_eq!(bytes[9], 0x00);
286 assert_eq!(bytes[10], 0b010_00010);
288 assert_eq!(bytes[11..13], "[]".as_bytes()[..]);
290 assert_eq!(bytes[13], 0x01);
292 assert_eq!(bytes[14], 0b000_11011);
294 assert_eq!(&bytes[15..23], KnownMagic::DotrainV1.to_prefix_bytes());
296 assert_eq!(bytes[23], 0x02);
298 assert_eq!(bytes[24], 0b011_10000);
300 assert_eq!(&bytes[25..41], "application/cbor".as_bytes());
302 assert_eq!(bytes[41], 0x03);
304 assert_eq!(bytes[42], 0b011_01000);
306 assert_eq!(&bytes[43..51], "identity".as_bytes());
308 assert_eq!(bytes[51], 0x04);
310 assert_eq!(bytes[52], 0b011_00010);
312 assert_eq!(&bytes[53..55], "en".as_bytes());
314
315 assert_eq!(bytes.len(), 55);
316
317 Ok(())
318 }
319
320 use clap::Parser;
321 use std::io::Write;
322 use super::{Build, build};
323
324 #[test]
327 fn test_item_normalize_then_encode() -> anyhow::Result<()> {
328 let build_item = BuildItem {
330 data: AUTHORING_META_V1_JSON.as_bytes().to_vec(),
331 magic: KnownMagic::AuthoringMetaV1,
332 content_type: ContentType::Json,
333 content_encoding: ContentEncoding::Deflate,
334 content_language: ContentLanguage::En,
335 };
336 let meta_document = RainMetaDocumentV1Item::try_from(&build_item)?;
337 let abi =
338 serde_json::from_str::<AuthoringMeta>(AUTHORING_META_V1_JSON)?.abi_encode_validate()?;
339 assert_ne!(abi, build_item.data);
340 assert_eq!(
341 meta_document.payload.as_ref(),
342 ContentEncoding::Deflate.encode(&abi)
343 );
344
345 let invalid_item = BuildItem {
347 data: "not json".as_bytes().to_vec(),
348 ..build_item
349 };
350 assert!(RainMetaDocumentV1Item::try_from(&invalid_item).is_err());
351 Ok(())
352 }
353
354 fn parse_build(args: &[&str]) -> Build {
355 Build::try_parse_from(args).unwrap()
356 }
357
358 #[test]
361 fn test_build_arity_guards() {
362 let b = parse_build(&[
363 "build",
364 "-i",
365 "does-not-exist.json",
366 "-m",
367 "authoring-meta-v1",
368 "-m",
369 "authoring-meta-v1",
370 ]);
371 assert_eq!(
372 build(b).unwrap_err().to_string(),
373 "1 inputs does not match 2 magic numbers."
374 );
375
376 let b = parse_build(&[
377 "build",
378 "-i",
379 "does-not-exist.json",
380 "-m",
381 "authoring-meta-v1",
382 "-t",
383 "json",
384 "-t",
385 "json",
386 ]);
387 assert_eq!(
388 build(b).unwrap_err().to_string(),
389 "1 inputs does not match 2 content types."
390 );
391
392 let b = parse_build(&[
393 "build",
394 "-i",
395 "does-not-exist.json",
396 "-m",
397 "authoring-meta-v1",
398 "-t",
399 "json",
400 "-e",
401 "identity",
402 "-e",
403 "identity",
404 ]);
405 assert_eq!(
406 build(b).unwrap_err().to_string(),
407 "1 inputs does not match 2 content encodings."
408 );
409
410 let b = parse_build(&[
411 "build",
412 "-i",
413 "does-not-exist.json",
414 "-m",
415 "authoring-meta-v1",
416 "-t",
417 "json",
418 "-e",
419 "identity",
420 "-l",
421 "en",
422 "-l",
423 "en",
424 ]);
425 assert_eq!(
426 build(b).unwrap_err().to_string(),
427 "1 inputs does not match 2 content languages."
428 );
429 }
430
431 #[test]
435 fn test_build_reads_files_and_encodes_output() -> anyhow::Result<()> {
436 let mut input = tempfile::NamedTempFile::new()?;
437 input.write_all(AUTHORING_META_V1_JSON.as_bytes())?;
438 let output = tempfile::NamedTempFile::new()?;
439
440 let expected = build_bytes(
441 KnownMagic::RainMetaDocumentV1,
442 vec![BuildItem {
443 data: AUTHORING_META_V1_JSON.as_bytes().to_vec(),
444 magic: KnownMagic::AuthoringMetaV1,
445 content_type: ContentType::Json,
446 content_encoding: ContentEncoding::Identity,
447 content_language: ContentLanguage::En,
448 }],
449 )?;
450
451 let input_path = input.path().to_str().unwrap().to_string();
452 let output_path = output.path().to_str().unwrap().to_string();
453
454 let b = parse_build(&[
455 "build",
456 "-i",
457 &input_path,
458 "-m",
459 "authoring-meta-v1",
460 "-t",
461 "json",
462 "-e",
463 "identity",
464 "-l",
465 "en",
466 "-o",
467 &output_path,
468 ]);
469 build(b)?;
470 assert_eq!(std::fs::read(output.path())?, expected);
471
472 let b = parse_build(&[
473 "build",
474 "-i",
475 &input_path,
476 "-m",
477 "authoring-meta-v1",
478 "-t",
479 "json",
480 "-e",
481 "identity",
482 "-l",
483 "en",
484 "-o",
485 &output_path,
486 "-E",
487 "hex",
488 ]);
489 build(b)?;
490 assert_eq!(
491 std::fs::read_to_string(output.path())?,
492 alloy::primitives::hex::encode_prefixed(&expected)
493 );
494 Ok(())
495 }
496}