ipld_schema/
lib.rs

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// TODO: clean up unwraps
14
15// TODO: create a suitable error type
16
17#[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    /// By default seeds are non-deterministic.
31    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 IPLD schemas and data
65    Validate {
66        /// Path to IPLD schema file to validate.
67        #[cfg_attr(feature = "build-binary", structopt(parse(from_os_str)))]
68        schema_file: PathBuf,
69
70        /// Path to IPLD data file to validate against the specified schema.
71        #[cfg_attr(feature = "build-binary", structopt(parse(from_os_str)))]
72        data_file: Option<PathBuf>,
73    },
74    /// Generate IPLD schemas and data
75    Generate {
76        /// Explicitly seed the PRNG for deterministic output.
77        ///
78        /// If unspecified, a random seed is used.
79        #[cfg_attr(feature = "build-binary", structopt(long, parse(try_from_str)))]
80        seed: Option<Seed>,
81
82        /// Path to IPLD schema file to use when generating data.
83        ///
84        /// If unspecified, generates a schema instead of data.
85        #[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    // TODO: write
129    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(); // TODO: consider emitting a CID for the schema file's contents too
207    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                    // schema_file: Some(schema_file.path().into())
304                },
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        // TODO: assertions about output
323
324        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        // TODO: assertions about output
367
368        schema_file.close()?;
369        data_file.close()?;
370    }
371}