1#![deny(clippy::all)]
2#![deny(clippy::pedantic)]
3
4use std::{convert::TryInto, fmt, path::PathBuf, str::FromStr};
5
6use proptest::{arbitrary::Arbitrary, strategy::Strategy};
7
8#[cfg(feature = "build-binary")]
9use structopt::StructOpt;
10
11pub mod schema;
12
13#[derive(Clone, Copy, Debug, test_strategy::Arbitrary)]
18pub struct Seed {
19 inner: [u8; 32],
20}
21
22impl Seed {
23 #[must_use]
24 pub const fn fixed() -> Self {
25 Self { inner: [0_u8; 32] }
26 }
27}
28
29impl Default for Seed {
30 fn default() -> Self {
32 Self {
33 inner: rand::random(),
34 }
35 }
36}
37
38impl FromStr for Seed {
39 type Err = &'static str;
40
41 fn from_str(s: &str) -> Result<Self, Self::Err> {
42 let inner = {
43 let bytes = base64::decode(s).unwrap();
44 if bytes.len() == 32 {
45 bytes.try_into().unwrap()
46 } else {
47 return Err("invalid input");
48 }
49 };
50
51 Ok(Seed { inner })
52 }
53}
54
55impl fmt::Display for Seed {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
57 write!(f, "{}", base64::encode(self.inner))
58 }
59}
60
61#[derive(Debug)]
62#[cfg_attr(feature = "build-binary", derive(StructOpt))]
63pub enum Command {
64 Validate {
66 #[cfg_attr(feature = "build-binary", structopt(parse(from_os_str)))]
68 schema_file: PathBuf,
69
70 #[cfg_attr(feature = "build-binary", structopt(parse(from_os_str)))]
72 data_file: Option<PathBuf>,
73 },
74 Generate {
76 #[cfg_attr(feature = "build-binary", structopt(long, parse(try_from_str)))]
80 seed: Option<Seed>,
81
82 #[cfg_attr(feature = "build-binary", structopt(parse(from_os_str)))]
86 schema_file: Option<PathBuf>,
87 },
88}
89
90#[derive(Debug)]
91#[cfg_attr(feature = "build-binary", derive(StructOpt))]
92#[cfg_attr(feature = "build-binary", structopt(name = env!("CARGO_PKG_NAME"), version = env!("CARGO_PKG_VERSION"), author = env!("CARGO_PKG_AUTHORS"), about = env!("CARGO_PKG_DESCRIPTION")))]
93pub struct Opt {
94 #[cfg_attr(feature = "build-binary", structopt(subcommand))]
95 cmd: Command,
96}
97
98#[allow(clippy::result_unit_err)]
99#[allow(clippy::missing_errors_doc)]
100pub fn run<W: std::io::Write>(opt: &Opt, output: &mut W) -> Result<(), ()> {
101 match &opt.cmd {
102 Command::Validate {
103 schema_file,
104 data_file,
105 } => validate(schema_file, data_file, output),
106 Command::Generate { seed, schema_file } => {
107 generate(&seed.unwrap_or_default(), schema_file, output)
108 }
109 }
110}
111
112fn validate<P: AsRef<std::path::Path> + std::fmt::Debug, W: std::io::Write>(
113 schema_file: &P,
114 data_file: &Option<P>,
115 out: &mut W,
116) -> Result<(), ()> {
117 match data_file {
118 None => validate_schema(schema_file, out),
119 Some(data) => validate_data(schema_file, data, out),
120 }
121}
122
123fn validate_schema<P: AsRef<std::path::Path> + std::fmt::Debug, W: std::io::Write>(
124 schema_file: &P,
125 _out: &mut W,
126) -> Result<(), ()> {
127 schema::schema_dsl::parse(&std::fs::read_to_string(schema_file).unwrap()).unwrap();
128 Ok(())
130}
131
132fn validate_data<P: AsRef<std::path::Path> + std::fmt::Debug, W: std::io::Write>(
133 schema_file: &P,
134 data_file: &P,
135 _out: &mut W,
136) -> Result<(), ()> {
137 validate_schema(schema_file, &mut std::io::sink())?;
138
139 todo!(
140 "validate data ({:?}) using schema ({:?})",
141 data_file,
142 schema_file
143 );
144}
145
146fn generate<P, W>(seed: &Seed, schema_file: &Option<P>, out: &mut W) -> Result<(), ()>
147where
148 P: AsRef<std::path::Path> + std::fmt::Debug,
149 W: std::io::Write,
150{
151 let mut out = std::io::BufWriter::new(out);
152
153 match schema_file {
154 None => generate_schema(seed, &mut out),
155 Some(schema) => generate_data(seed, schema, &mut out),
156 }
157}
158
159fn generate_schema<W: std::io::Write>(seed: &Seed, out: &mut W) -> Result<(), ()> {
160 let config = proptest::test_runner::Config::default();
161 let rng = proptest::test_runner::TestRng::from_seed(
162 proptest::test_runner::RngAlgorithm::ChaCha,
163 &seed.inner,
164 );
165 let mut runner = proptest::test_runner::TestRunner::new_with_rng(config, rng);
166
167 let schema = schema::Schema::arbitrary()
168 .new_tree(&mut runner)
169 .unwrap()
170 .current();
171
172 writeln!(out, "##").unwrap();
173 writeln!(
174 out,
175 "## Deterministically generated with {} {}",
176 env!("CARGO_PKG_NAME"),
177 env!("CARGO_PKG_VERSION")
178 )
179 .unwrap();
180 writeln!(out, "##").unwrap();
181 writeln!(out, "## - reproduction seed: '{}'", seed).unwrap();
182 writeln!(out, "##").unwrap();
183 writeln!(out).unwrap();
184 writeln!(out, "{}", schema).unwrap();
185
186 Ok(())
187}
188
189fn generate_data<P: AsRef<std::path::Path> + std::fmt::Debug, W: std::io::Write>(
190 seed: &Seed,
191 schema_file: &P,
192 out: &mut W,
193) -> Result<(), ()> {
194 validate_schema(schema_file, &mut std::io::sink())?;
195
196 writeln!(out, "##").unwrap();
197 writeln!(
198 out,
199 "## Deterministically generated with {} {}",
200 env!("CARGO_PKG_NAME"),
201 env!("CARGO_PKG_VERSION")
202 )
203 .unwrap();
204 writeln!(out, "##").unwrap();
205 writeln!(out, "## - reproduction seed: '{}'", seed).unwrap();
206 writeln!(out, "## - schema file: {:?}", schema_file).unwrap(); writeln!(out, "##").unwrap();
208 writeln!(out).unwrap();
209
210 todo!(
211 "generate data using seed '{}' and schema {:?}",
212 seed,
213 schema_file
214 );
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 use test_strategy::proptest;
222
223 #[cfg(feature = "fast-test")]
224 const CASES: u32 = 10;
225 #[cfg(not(feature = "fast-test"))]
226 const CASES: u32 = 1000;
227
228 #[cfg(feature = "fast-test")]
229 const MAX_SHRINK_ITERS: u32 = 2;
230 #[cfg(not(feature = "fast-test"))]
231 const MAX_SHRINK_ITERS: u32 = 10000;
232
233 #[cfg(not(feature = "fast-test"))]
234 use insta::assert_debug_snapshot;
235
236 #[test]
237 #[cfg(not(feature = "fast-test"))]
238 fn snapshot_of_schema_generated_from_fixed_seed() {
239 let seed = Some(Seed::fixed());
240
241 let mut schema_buffer = std::io::Cursor::new(vec![]);
242 run(
243 &Opt {
244 cmd: Command::Generate {
245 seed,
246 schema_file: None,
247 },
248 },
249 &mut schema_buffer,
250 )
251 .unwrap();
252
253 assert_debug_snapshot!(schema::schema_dsl::parse(&String::from_utf8_lossy(
254 &schema_buffer.into_inner()
255 ))
256 .unwrap());
257 }
258
259 #[test]
260 #[cfg(not(feature = "fast-test"))]
261 #[ignore = "TODO: implement data generator based on a schema"]
262 fn snapshot_of_data_generated_from_fixed_seed() {
263 let seed = Some(Seed::fixed());
264
265 let mut schema_file = tempfile::NamedTempFile::new().unwrap();
266 run(
267 &Opt {
268 cmd: Command::Generate {
269 seed,
270 schema_file: None,
271 },
272 },
273 &mut schema_file,
274 )
275 .unwrap();
276
277 let mut data_buffer = std::io::Cursor::new(vec![]);
278 run(
279 &Opt {
280 cmd: Command::Generate {
281 seed,
282 schema_file: Some(schema_file.path().into()),
283 },
284 },
285 &mut data_buffer,
286 )
287 .unwrap();
288
289 assert_debug_snapshot!(schema::schema_dsl::parse(&String::from_utf8_lossy(
290 &data_buffer.into_inner()
291 ))
292 .unwrap());
293 }
294
295 #[proptest(cases = CASES, max_shrink_iters = MAX_SHRINK_ITERS)]
296 fn generated_schemas_are_valid(seed: Seed) {
297 let mut schema_file = tempfile::NamedTempFile::new()?;
298 run(
299 &Opt {
300 cmd: Command::Generate {
301 seed: Some(seed),
302 schema_file: None,
303 },
305 },
306 &mut schema_file,
307 )
308 .unwrap();
309
310 let mut output = std::io::Cursor::new(vec![]);
311 run(
312 &Opt {
313 cmd: Command::Validate {
314 schema_file: schema_file.path().into(),
315 data_file: None,
316 },
317 },
318 &mut output,
319 )
320 .unwrap();
321
322 schema_file.close()?;
325 }
326
327 #[proptest(cases = CASES, max_shrink_iters = MAX_SHRINK_ITERS)]
328 #[ignore = "TODO: implement data generator based on a schema"]
329 fn generated_data_are_valid(seed: Seed) {
330 let mut schema_file = tempfile::NamedTempFile::new()?;
331 run(
332 &Opt {
333 cmd: Command::Generate {
334 seed: Some(seed),
335 schema_file: None,
336 },
337 },
338 &mut schema_file,
339 )
340 .unwrap();
341
342 let mut data_file = tempfile::NamedTempFile::new()?;
343 run(
344 &Opt {
345 cmd: Command::Generate {
346 seed: Some(seed),
347 schema_file: Some(schema_file.path().into()),
348 },
349 },
350 &mut data_file,
351 )
352 .unwrap();
353
354 let mut output = std::io::Cursor::new(vec![]);
355 run(
356 &Opt {
357 cmd: Command::Validate {
358 schema_file: schema_file.path().into(),
359 data_file: Some(data_file.path().into()),
360 },
361 },
362 &mut output,
363 )
364 .unwrap();
365
366 schema_file.close()?;
369 data_file.close()?;
370 }
371}