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}