Skip to main content

reifydb_testing/testscript/
command.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4// This file includes and modifies code from the toydb project (https://github.com/erikgrinaker/toydb),
5// originally licensed under the Apache License, Version 2.0.
6// Original copyright:
7//   Copyright (c) 2024 Erik Grinaker
8//
9// The original Apache License can be found at:
10//   http://www.apache.org/licenses/LICENSE-2.0
11
12use std::{
13	collections::{BTreeSet, HashSet, VecDeque},
14	error::Error,
15	fmt,
16	str::FromStr,
17};
18
19#[derive(Clone, Debug, PartialEq)]
20#[non_exhaustive]
21pub(crate) struct Block {
22	pub commands: Vec<Command>,
23
24	pub literal: String,
25
26	pub line_number: u32,
27}
28
29#[derive(Clone, PartialEq)]
30#[non_exhaustive]
31pub struct Command {
32	pub name: String,
33
34	pub args: Vec<Argument>,
35
36	pub prefix: Option<String>,
37
38	pub tags: HashSet<String>,
39
40	pub silent: bool,
41
42	pub fail: bool,
43
44	pub line_number: u32,
45}
46
47impl fmt::Debug for Command {
48	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49		f.debug_struct("Command")
50			.field("name", &self.name)
51			.field("args", &self.args)
52			.field("prefix", &self.prefix)
53			.field("tags", &BTreeSet::from_iter(&self.tags))
54			.field("silent", &self.silent)
55			.field("fail", &self.fail)
56			.field("line_number", &self.line_number)
57			.finish()
58	}
59}
60
61impl Command {
62	pub fn consume_args(&self) -> ArgumentConsumer<'_> {
63		ArgumentConsumer::new(&self.args)
64	}
65}
66
67#[derive(Clone, Debug, PartialEq)]
68#[non_exhaustive]
69pub struct Argument {
70	pub key: Option<String>,
71
72	pub value: String,
73}
74
75impl Argument {
76	pub fn name(&self) -> &str {
77		match self.key.as_deref() {
78			Some(key) => key,
79			None => &self.value,
80		}
81	}
82
83	pub fn parse<T>(&self) -> Result<T, Box<dyn Error>>
84	where
85		T: FromStr,
86		<T as FromStr>::Err: fmt::Display,
87	{
88		self.value.parse().map_err(|e| format!("invalid argument '{}': {e}", self.value).into())
89	}
90}
91
92pub struct ArgumentConsumer<'a> {
93	args: VecDeque<&'a Argument>,
94}
95
96impl<'a> Iterator for ArgumentConsumer<'a> {
97	type Item = &'a Argument;
98
99	fn next(&mut self) -> Option<Self::Item> {
100		self.args.pop_front()
101	}
102}
103
104impl<'a> ArgumentConsumer<'a> {
105	fn new(args: &'a [Argument]) -> Self {
106		Self {
107			args: VecDeque::from_iter(args.iter()),
108		}
109	}
110
111	pub fn lookup(&mut self, key: &str) -> Option<&'a Argument> {
112		let arg = self.args.iter().rev().find(|a| a.key.as_deref() == Some(key)).copied();
113		if arg.is_some() {
114			self.args.retain(|a| a.key.as_deref() != Some(key))
115		}
116		arg
117	}
118
119	pub fn lookup_parse<T>(&mut self, key: &str) -> Result<Option<T>, Box<dyn Error>>
120	where
121		T: FromStr,
122		<T as FromStr>::Err: fmt::Display,
123	{
124		let value = self
125			.args
126			.iter()
127			.rev()
128			.find(|a| a.key.as_deref() == Some(key))
129			.map(|a| a.parse())
130			.transpose()?;
131		if value.is_some() {
132			self.args.retain(|a| a.key.as_deref() != Some(key))
133		}
134		Ok(value)
135	}
136
137	pub fn next_key(&mut self) -> Option<&'a Argument> {
138		self.args.iter().position(|a| a.key.is_some()).map(|i| self.args.remove(i).unwrap())
139	}
140
141	pub fn next_pos(&mut self) -> Option<&'a Argument> {
142		self.args.iter().position(|a| a.key.is_none()).map(|i| self.args.remove(i).unwrap())
143	}
144
145	pub fn reject_rest(&self) -> Result<(), Box<dyn Error>> {
146		if let Some(arg) = self.args.front() {
147			return Err(format!("invalid argument '{}'", arg.name()).into());
148		}
149		Ok(())
150	}
151
152	pub fn rest(&mut self) -> Vec<&'a Argument> {
153		self.args.drain(..).collect()
154	}
155
156	pub fn rest_key(&mut self) -> Vec<&'a Argument> {
157		let keyed: Vec<_> = self.args.iter().filter(|a| a.key.is_some()).copied().collect();
158		if !keyed.is_empty() {
159			self.args.retain(|a| a.key.is_none());
160		}
161		keyed
162	}
163
164	pub fn rest_pos(&mut self) -> Vec<&'a Argument> {
165		let pos: Vec<_> = self.args.iter().filter(|a| a.key.is_none()).copied().collect();
166		if !pos.is_empty() {
167			self.args.retain(|a| a.key.is_some());
168		}
169		pos
170	}
171}
172
173#[cfg(test)]
174pub mod tests {
175	use super::*;
176
177	/// Constructs an Argument from a string value or key => value.
178	macro_rules! arg {
179		($value:expr) => {
180			Argument {
181				key: None,
182				value: $value.to_string(),
183			}
184		};
185		($key:expr => $value:expr) => {
186			Argument {
187				key: Some($key.to_string()),
188				value: $value.to_string(),
189			}
190		};
191	}
192
193	/// Constructs a Command by parsing the given input string.
194	macro_rules! cmd {
195		($input:expr) => {{ crate::testscript::parser::parse_command(&format!("{}\n", $input)).expect("invalid command") }};
196	}
197
198	/// Tests Argument.name().
199	#[test]
200	fn test_argument_name() {
201		assert_eq!(arg!("value").name(), "value");
202		assert_eq!(arg!("key" => "value").name(), "key");
203	}
204
205	/// Basic tests of Argument.parse(). Not comprehensive, since it
206	/// dispatches to flow::str::parse().
207	#[test]
208	fn test_argument_parse() {
209		assert_eq!(arg!("-1").parse::<i64>().unwrap(), -1_i64);
210		assert_eq!(arg!("0").parse::<i64>().unwrap(), 0_i64);
211		assert_eq!(arg!("1").parse::<i64>().unwrap(), 1_i64);
212
213		assert_eq!(
214			arg!("").parse::<i64>().unwrap_err().to_string(),
215			"invalid argument '': cannot parse integer from empty string"
216		);
217		assert_eq!(
218			arg!("foo").parse::<i64>().unwrap_err().to_string(),
219			"invalid argument 'foo': invalid digit found in string"
220		);
221
222		assert!(!arg!("false").parse::<bool>().unwrap());
223		assert!(arg!("true").parse::<bool>().unwrap());
224
225		assert_eq!(
226			arg!("").parse::<bool>().unwrap_err().to_string(),
227			"invalid argument '': provided string was not `true` or `false`"
228		);
229	}
230
231	/// Tests Command.consume_args(). ArgumentConsumer is tested separately.
232	#[test]
233	fn test_command_consume_args() {
234		let cmd = cmd!("cmd foo key=value bar");
235		assert_eq!(cmd.consume_args().rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[2]]);
236	}
237
238	/// Tests ArgumentConsumer.lookup().
239	#[test]
240	fn test_argument_consumer_lookup() {
241		let cmd = cmd!("cmd value key=value foo=bar key=other");
242
243		// lookup() returns None on unknown keys, including ones that
244		// match a value argument.
245		let mut args = cmd.consume_args();
246		assert_eq!(args.lookup("unknown"), None);
247		assert_eq!(args.lookup("value"), None);
248		assert_eq!(args.rest().len(), 4);
249
250		// lookup() removes duplicate keys, returning the last.
251		let mut args = cmd.consume_args();
252		assert_eq!(args.lookup("key"), Some(&cmd.args[3]));
253		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[2]]);
254
255		// lookup() removes single keys.
256		let mut args = cmd.consume_args();
257		assert_eq!(args.lookup("foo"), Some(&cmd.args[2]));
258		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[3]]);
259	}
260
261	/// Tests ArgumentConsumer.lookup_parse().
262	#[test]
263	fn test_argument_consumer_lookup_parse() {
264		let cmd = cmd!("cmd value key=1 foo=bar key=2");
265
266		// lookup_parse() returns None on unknown keys, including ones
267		// that match a value argument.
268		let mut args = cmd.consume_args();
269		assert_eq!(args.lookup_parse::<String>("unknown").unwrap(), None);
270		assert_eq!(args.lookup_parse::<String>("value").unwrap(), None);
271		assert_eq!(args.rest().len(), 4);
272
273		// lookup_parse() parses and removes duplicate keys, returning
274		// the last.
275		let mut args = cmd.consume_args();
276		assert_eq!(args.lookup_parse("key").unwrap(), Some(2));
277		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[2]]);
278
279		// lookup_parse() parses and removes single keys, with string
280		// parsing being a noop.
281		let mut args = cmd.consume_args();
282		assert_eq!(args.lookup_parse("foo").unwrap(), Some("bar".to_string()));
283		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[3]]);
284
285		// lookup_parse() does not remove arguments on parse errors,
286		// even with duplicate keys.
287		let mut args = cmd.consume_args();
288		assert!(args.lookup_parse::<bool>("key").is_err());
289		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[2], &cmd.args[3]]);
290	}
291
292	/// Tests ArgumentConsumer.next(), next_pos(), and next_key().
293	#[test]
294	fn test_argument_consumer_next() {
295		let cmd = cmd!("cmd foo key=1 key=2 bar");
296
297		// next() returns references to all arguments and consumes them.
298		let mut args = cmd.consume_args();
299		assert_eq!(args.next(), Some(&cmd.args[0]));
300		assert_eq!(args.next(), Some(&cmd.args[1]));
301		assert_eq!(args.next(), Some(&cmd.args[2]));
302		assert_eq!(args.next(), Some(&cmd.args[3]));
303		assert_eq!(args.next(), None);
304		assert!(args.rest().is_empty());
305
306		// next_key() returns references to key/value arguments and
307		// consumes them.
308		let mut args = cmd.consume_args();
309		assert_eq!(args.next_key(), Some(&cmd.args[1]));
310		assert_eq!(args.next_key(), Some(&cmd.args[2]));
311		assert_eq!(args.next_key(), None);
312		assert_eq!(args.next(), Some(&cmd.args[0]));
313		assert_eq!(args.next(), Some(&cmd.args[3]));
314		assert_eq!(args.next(), None);
315		assert!(args.rest().is_empty());
316
317		// next_pos() returns references to key/value arguments and
318		// consumes them.
319		let mut args = cmd.consume_args();
320		assert_eq!(args.next_pos(), Some(&cmd.args[0]));
321		assert_eq!(args.next_pos(), Some(&cmd.args[3]));
322		assert_eq!(args.next_pos(), None);
323		assert_eq!(args.next(), Some(&cmd.args[1]));
324		assert_eq!(args.next(), Some(&cmd.args[2]));
325		assert_eq!(args.next(), None);
326		assert!(args.rest().is_empty());
327	}
328
329	/// Tests ArgumentConsumer.reject_rest().
330	#[test]
331	fn test_argument_consumer_reject_rest() {
332		// Empty args return Ok.
333		let cmd = cmd!("cmd");
334		assert!(cmd.consume_args().reject_rest().is_ok());
335
336		// Positional argument fails. It does not consume the arg.
337		let cmd = cmd!("cmd value");
338		let mut args = cmd.consume_args();
339		assert_eq!(args.reject_rest().unwrap_err().to_string(), "invalid argument 'value'");
340		assert!(!args.rest().is_empty());
341
342		// Key/value argument fails.
343		let cmd = cmd!("cmd key=value");
344		let mut args = cmd.consume_args();
345		assert_eq!(args.reject_rest().unwrap_err().to_string(), "invalid argument 'key'");
346		assert!(!args.rest().is_empty());
347	}
348
349	/// Tests ArgumentConsumer.rest(), rest_pos() and rest_key().
350	#[test]
351	fn test_argument_consumer_rest() {
352		let cmd = cmd!("cmd foo key=1 key=2 bar");
353
354		// rest() returns references to all arguments and consumes them.
355		let mut args = cmd.consume_args();
356		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[2], &cmd.args[3]]);
357		assert!(args.rest().is_empty());
358
359		// rest_pos() returns and consumes positional arguments.
360		let mut args = cmd.consume_args();
361		assert_eq!(args.rest_pos(), vec![&cmd.args[0], &cmd.args[3]]);
362		assert!(args.rest_pos().is_empty());
363		assert_eq!(args.rest(), vec![&cmd.args[1], &cmd.args[2]]);
364
365		// rest_key() returns and consumes key/value arguments.
366		let mut args = cmd.consume_args();
367		assert_eq!(args.rest_key(), vec![&cmd.args[1], &cmd.args[2]]);
368		assert!(args.rest_key().is_empty());
369		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[3]]);
370	}
371}