Skip to main content

reifydb_testing/testscript/
command.rs

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