stellar_xdr/cli/generate/
arbitrary.rs1use arbitrary::Unstructured;
2use clap::{Args, ValueEnum};
3use std::{
4 io::{stdout, Write},
5 str::FromStr,
6};
7
8use crate::cli::util;
9
10#[derive(thiserror::Error, Debug)]
11pub enum Error {
12 #[error("unknown type {0}, choose one of {1:?}")]
13 UnknownType(String, &'static [&'static str]),
14 #[error("error reading file: {0}")]
15 ReadFile(#[from] std::io::Error),
16 #[error("error generating XDR: {0}")]
17 WriteXdr(#[from] crate::Error),
18 #[error("error generating JSON: {0}")]
19 GenerateJson(#[from] serde_json::Error),
20 #[error("error generating arbitrary value: {0}")]
21 Arbitrary(#[from] arbitrary::Error),
22 #[error("type doesn't have a text representation, use 'json' as output")]
23 TextUnsupported,
24 #[error("could not generate a value whose JSON contains all of {hints:?} within {attempts} attempts")]
25 HintNotFound { hints: Vec<String>, attempts: u64 },
26}
27
28#[derive(Args, Debug, Clone)]
30#[command()]
31pub struct Cmd {
32 #[arg(long)]
34 pub r#type: String,
35
36 #[arg(long = "output", value_enum, default_value_t)]
38 pub output_format: OutputFormat,
39
40 #[arg(long)]
45 pub hint: Vec<String>,
46
47 #[arg(long, default_value_t = 20_000)]
50 pub hint_attempts: u64,
51}
52
53#[derive(Default, Clone, Copy, Debug, Eq, Hash, PartialEq, ValueEnum)]
54pub enum OutputFormat {
55 Single,
56 #[default]
57 SingleBase64,
58 Json,
62 JsonFormatted,
63 Text,
64}
65
66macro_rules! run_x {
69 ($f:ident) => {
70 fn $f(&self) -> Result<(), Error> {
71 use crate::WriteXdr;
72 let r#type = crate::TypeVariant::from_str(&self.r#type).map_err(|_| {
73 Error::UnknownType(self.r#type.clone(), &crate::TypeVariant::VARIANTS_STR)
74 })?;
75 let (v, hint_json) = self.generate(r#type)?;
76 match self.output_format {
77 OutputFormat::Single => {
78 let l = crate::Limits::none();
79 stdout().write_all(&v.to_xdr(l)?)?
80 }
81 OutputFormat::SingleBase64 => {
82 let l = crate::Limits::none();
83 println!("{}", v.to_xdr_base64(l)?)
84 }
85 OutputFormat::Json => match hint_json {
86 Some(json) => println!("{json}"),
87 None => println!("{}", serde_json::to_string(&v)?),
88 },
89 OutputFormat::JsonFormatted => {
90 println!("{}", serde_json::to_string_pretty(&v)?);
91 }
92 OutputFormat::Text => {
93 let v = serde_json::to_value(v)?;
94 let text = util::serde_json_value_to_text(v).ok_or(Error::TextUnsupported)?;
95 println!("{text}")
96 }
97 }
98 Ok(())
99 }
100 };
101}
102
103impl Cmd {
104 pub fn run(&self) -> Result<(), Error> {
110 self.run_inner()
111 }
112
113 run_x!(run_inner);
114
115 fn generate_one(type_: crate::TypeVariant) -> Result<crate::Type, Error> {
118 let r = rand::random::<[u8; 10_240]>();
119 let mut u = Unstructured::new(&r);
120 Ok(crate::Type::arbitrary(type_, &mut u)?)
121 }
122
123 fn generate(&self, type_: crate::TypeVariant) -> Result<(crate::Type, Option<String>), Error> {
136 if self.hint.is_empty() {
137 return Ok((Self::generate_one(type_)?, None));
138 }
139 for _ in 0..self.hint_attempts {
140 let Ok(v) = Self::generate_one(type_) else {
141 continue;
142 };
143 let Ok(json) = serde_json::to_string(&v) else {
144 continue;
145 };
146 if self.hint.iter().all(|hint| json.contains(hint)) {
147 return Ok((v, Some(json)));
148 }
149 }
150 Err(Error::HintNotFound {
151 hints: self.hint.clone(),
152 attempts: self.hint_attempts,
153 })
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn hint_matches() {
163 let cmd = Cmd {
164 r#type: "TimeBounds".to_string(),
165 output_format: OutputFormat::Json,
166 hint: vec!["min_time".into()],
167 hint_attempts: 1000,
168 };
169 let type_ = crate::TypeVariant::from_str("TimeBounds").unwrap();
170 let (v, json) = cmd.generate(type_).unwrap();
171 let json = json.expect("a hinted search returns the computed json");
172 assert!(json.contains("min_time"));
173 assert_eq!(json, serde_json::to_string(&v).unwrap());
174 }
175
176 #[test]
177 fn all_hints_match() {
178 let cmd = Cmd {
179 r#type: "TimeBounds".to_string(),
180 output_format: OutputFormat::Json,
181 hint: vec!["min_time".into(), "max_time".into()],
182 hint_attempts: 1000,
183 };
184 let type_ = crate::TypeVariant::from_str("TimeBounds").unwrap();
185 let (_v, json) = cmd.generate(type_).unwrap();
186 let json = json.expect("a hinted search returns the computed json");
187 assert!(json.contains("min_time"));
188 assert!(json.contains("max_time"));
189 }
190
191 #[test]
192 fn hint_not_found() {
193 let cmd = Cmd {
194 r#type: "TimeBounds".to_string(),
195 output_format: OutputFormat::Json,
196 hint: vec!["zzz_does_not_exist_xyzzy".into()],
197 hint_attempts: 5,
198 };
199 let type_ = crate::TypeVariant::from_str("TimeBounds").unwrap();
200 assert!(matches!(
201 cmd.generate(type_),
202 Err(Error::HintNotFound { .. })
203 ));
204 }
205
206 #[test]
207 fn not_all_hints_found() {
208 let cmd = Cmd {
209 r#type: "TimeBounds".to_string(),
210 output_format: OutputFormat::Json,
211 hint: vec!["min_time".into(), "zzz_does_not_exist_xyzzy".into()],
212 hint_attempts: 5,
213 };
214 let type_ = crate::TypeVariant::from_str("TimeBounds").unwrap();
215 assert!(matches!(
216 cmd.generate(type_),
217 Err(Error::HintNotFound { .. })
218 ));
219 }
220
221 #[test]
222 fn no_hint_generates_once() {
223 let cmd = Cmd {
224 r#type: "TimeBounds".to_string(),
225 output_format: OutputFormat::Json,
226 hint: vec![],
227 hint_attempts: 1,
228 };
229 let type_ = crate::TypeVariant::from_str("TimeBounds").unwrap();
230 let (_v, json) = cmd.generate(type_).unwrap();
231 assert!(json.is_none());
232 }
233}