Skip to main content

reifydb_testing/testscript/
command.rs

1// SPDX-License-Identifier: Apache-2.0
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	macro_rules! cmd {
186		($input:expr) => {{ crate::testscript::parser::parse_command(&format!("{}\n", $input)).expect("invalid command") }};
187	}
188
189	#[test]
190	fn test_argument_name() {
191		assert_eq!(arg!("value").name(), "value");
192		assert_eq!(arg!("key" => "value").name(), "key");
193	}
194
195	#[test]
196	fn test_argument_parse() {
197		// Not comprehensive: parse() only wraps the target type's own FromStr, so this pins the
198		// error-message wrapping rather than the parsing.
199		assert_eq!(arg!("-1").parse::<i64>().unwrap(), -1_i64);
200		assert_eq!(arg!("0").parse::<i64>().unwrap(), 0_i64);
201		assert_eq!(arg!("1").parse::<i64>().unwrap(), 1_i64);
202
203		assert_eq!(
204			arg!("").parse::<i64>().unwrap_err().to_string(),
205			"invalid argument '': cannot parse integer from empty string"
206		);
207		assert_eq!(
208			arg!("foo").parse::<i64>().unwrap_err().to_string(),
209			"invalid argument 'foo': invalid digit found in string"
210		);
211
212		assert!(!arg!("false").parse::<bool>().unwrap());
213		assert!(arg!("true").parse::<bool>().unwrap());
214
215		assert_eq!(
216			arg!("").parse::<bool>().unwrap_err().to_string(),
217			"invalid argument '': provided string was not `true` or `false`"
218		);
219	}
220
221	#[test]
222	fn test_command_consume_args() {
223		let cmd = cmd!("cmd foo key=value bar");
224		assert_eq!(cmd.consume_args().rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[2]]);
225	}
226
227	#[test]
228	fn test_argument_consumer_lookup() {
229		let cmd = cmd!("cmd value key=value foo=bar key=other");
230
231		// A positional argument whose value matches the key must not be found by lookup().
232		let mut args = cmd.consume_args();
233		assert_eq!(args.lookup("unknown"), None);
234		assert_eq!(args.lookup("value"), None);
235		assert_eq!(args.rest().len(), 4);
236
237		// Duplicate keys collapse to the last, and all of them are consumed.
238		let mut args = cmd.consume_args();
239		assert_eq!(args.lookup("key"), Some(&cmd.args[3]));
240		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[2]]);
241
242		let mut args = cmd.consume_args();
243		assert_eq!(args.lookup("foo"), Some(&cmd.args[2]));
244		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[3]]);
245	}
246
247	#[test]
248	fn test_argument_consumer_lookup_parse() {
249		let cmd = cmd!("cmd value key=1 foo=bar key=2");
250
251		// A positional argument whose value matches the key must not be found.
252		let mut args = cmd.consume_args();
253		assert_eq!(args.lookup_parse::<String>("unknown").unwrap(), None);
254		assert_eq!(args.lookup_parse::<String>("value").unwrap(), None);
255		assert_eq!(args.rest().len(), 4);
256
257		// Duplicate keys collapse to the last, and all of them are consumed.
258		let mut args = cmd.consume_args();
259		assert_eq!(args.lookup_parse("key").unwrap(), Some(2));
260		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[2]]);
261
262		let mut args = cmd.consume_args();
263		assert_eq!(args.lookup_parse("foo").unwrap(), Some("bar".to_string()));
264		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[3]]);
265
266		// A parse error must leave every argument in place, duplicates included.
267		let mut args = cmd.consume_args();
268		assert!(args.lookup_parse::<bool>("key").is_err());
269		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[2], &cmd.args[3]]);
270	}
271
272	#[test]
273	fn test_argument_consumer_next() {
274		let cmd = cmd!("cmd foo key=1 key=2 bar");
275
276		// next() walks every argument regardless of kind.
277		let mut args = cmd.consume_args();
278		assert_eq!(args.next(), Some(&cmd.args[0]));
279		assert_eq!(args.next(), Some(&cmd.args[1]));
280		assert_eq!(args.next(), Some(&cmd.args[2]));
281		assert_eq!(args.next(), Some(&cmd.args[3]));
282		assert_eq!(args.next(), None);
283		assert!(args.rest().is_empty());
284
285		// next_key() takes only key/value arguments, leaving the positional ones for next().
286		let mut args = cmd.consume_args();
287		assert_eq!(args.next_key(), Some(&cmd.args[1]));
288		assert_eq!(args.next_key(), Some(&cmd.args[2]));
289		assert_eq!(args.next_key(), None);
290		assert_eq!(args.next(), Some(&cmd.args[0]));
291		assert_eq!(args.next(), Some(&cmd.args[3]));
292		assert_eq!(args.next(), None);
293		assert!(args.rest().is_empty());
294
295		// next_pos() takes only positional arguments, leaving the key/value ones for next().
296		let mut args = cmd.consume_args();
297		assert_eq!(args.next_pos(), Some(&cmd.args[0]));
298		assert_eq!(args.next_pos(), Some(&cmd.args[3]));
299		assert_eq!(args.next_pos(), None);
300		assert_eq!(args.next(), Some(&cmd.args[1]));
301		assert_eq!(args.next(), Some(&cmd.args[2]));
302		assert_eq!(args.next(), None);
303		assert!(args.rest().is_empty());
304	}
305
306	#[test]
307	fn test_argument_consumer_reject_rest() {
308		let cmd = cmd!("cmd");
309		assert!(cmd.consume_args().reject_rest().is_ok());
310
311		// Rejection must not consume the offending argument.
312		let cmd = cmd!("cmd value");
313		let mut args = cmd.consume_args();
314		assert_eq!(args.reject_rest().unwrap_err().to_string(), "invalid argument 'value'");
315		assert!(!args.rest().is_empty());
316
317		// A key/value argument reports its key, not its value.
318		let cmd = cmd!("cmd key=value");
319		let mut args = cmd.consume_args();
320		assert_eq!(args.reject_rest().unwrap_err().to_string(), "invalid argument 'key'");
321		assert!(!args.rest().is_empty());
322	}
323
324	#[test]
325	fn test_argument_consumer_rest() {
326		let cmd = cmd!("cmd foo key=1 key=2 bar");
327
328		// Each variant must drain what it returns, so a second call comes back empty.
329		let mut args = cmd.consume_args();
330		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[2], &cmd.args[3]]);
331		assert!(args.rest().is_empty());
332
333		let mut args = cmd.consume_args();
334		assert_eq!(args.rest_pos(), vec![&cmd.args[0], &cmd.args[3]]);
335		assert!(args.rest_pos().is_empty());
336		assert_eq!(args.rest(), vec![&cmd.args[1], &cmd.args[2]]);
337
338		let mut args = cmd.consume_args();
339		assert_eq!(args.rest_key(), vec![&cmd.args[1], &cmd.args[2]]);
340		assert!(args.rest_key().is_empty());
341		assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[3]]);
342	}
343}