1use nu_engine::command_prelude::*;
2use uuid::{Timestamp, Uuid};
3
4#[derive(Clone)]
5pub struct RandomUuid;
6
7impl Command for RandomUuid {
8 fn name(&self) -> &str {
9 "random uuid"
10 }
11
12 fn signature(&self) -> Signature {
13 Signature::build("random uuid")
14 .category(Category::Random)
15 .input_output_types(vec![(Type::Nothing, Type::String)])
16 .param(
17 Flag::new("version")
18 .short('v')
19 .arg(SyntaxShape::Int)
20 .desc(
21 "The UUID version to generate (1, 3, 4, 5, 7). Defaults to 4 if not \
22 specified.",
23 )
24 .completion(Completion::new_list(&["1", "3", "4", "5", "7"])),
25 )
26 .param(
27 Flag::new("namespace")
28 .short('n')
29 .arg(SyntaxShape::String)
30 .desc(
31 "The namespace for v3 and v5 UUIDs (dns, url, oid, x500). Required for v3 \
32 and v5.",
33 )
34 .completion(Completion::new_list(&["dns", "url", "oid", "x500"])),
35 )
36 .named(
37 "name",
38 SyntaxShape::String,
39 "The name string for v3 and v5 UUIDs. Required for v3 and v5.",
40 Some('s'),
41 )
42 .named(
43 "mac",
44 SyntaxShape::String,
45 "The MAC address (node ID) used to generate v1 UUIDs. Required for v1.",
46 Some('m'),
47 )
48 .allow_variants_without_examples(true)
49 }
50
51 fn description(&self) -> &str {
52 "Generate a random uuid string of the specified version."
53 }
54
55 fn search_terms(&self) -> Vec<&str> {
56 vec!["generate", "uuid4", "uuid1", "uuid3", "uuid5", "uuid7"]
57 }
58
59 fn run(
60 &self,
61 engine_state: &EngineState,
62 stack: &mut Stack,
63 call: &Call,
64 _input: PipelineData,
65 ) -> Result<PipelineData, ShellError> {
66 uuid(engine_state, stack, call)
67 }
68
69 fn examples(&self) -> Vec<Example<'_>> {
70 vec![
71 Example {
72 description: "Generate a random uuid v4 string (default)",
73 example: "random uuid",
74 result: None,
75 },
76 Example {
77 description: "Generate a uuid v1 string (timestamp-based)",
78 example: "random uuid -v 1 -m 00:11:22:33:44:55",
79 result: None,
80 },
81 Example {
82 description: "Generate a uuid v3 string (namespace with MD5)",
83 example: "random uuid -v 3 -n dns -s example.com",
84 result: None,
85 },
86 Example {
87 description: "Generate a uuid v4 string (random).",
88 example: "random uuid -v 4",
89 result: None,
90 },
91 Example {
92 description: "Generate a uuid v5 string (namespace with SHA1)",
93 example: "random uuid -v 5 -n dns -s example.com",
94 result: None,
95 },
96 Example {
97 description: "Generate a uuid v7 string (timestamp + random)",
98 example: "random uuid -v 7",
99 result: None,
100 },
101 ]
102 }
103}
104
105fn uuid(
106 engine_state: &EngineState,
107 stack: &mut Stack,
108 call: &Call,
109) -> Result<PipelineData, ShellError> {
110 let span = call.head;
111
112 let version: Option<i64> = call.get_flag(engine_state, stack, "version")?;
113 let version = version.unwrap_or(4);
114
115 validate_flags(engine_state, stack, call, span, version)?;
116
117 let uuid_str = match version {
118 1 => {
119 let ts = Timestamp::now(uuid::timestamp::context::NoContext);
120 let node_id = get_mac_address(engine_state, stack, call, span)?;
121 let uuid = Uuid::new_v1(ts, &node_id);
122 uuid.hyphenated().to_string()
123 }
124 3 => {
125 let (namespace, name) = get_namespace_and_name(engine_state, stack, call, span)?;
126 let uuid = Uuid::new_v3(&namespace, name.as_bytes());
127 uuid.hyphenated().to_string()
128 }
129 4 => {
130 let uuid = Uuid::new_v4();
131 uuid.hyphenated().to_string()
132 }
133 5 => {
134 let (namespace, name) = get_namespace_and_name(engine_state, stack, call, span)?;
135 let uuid = Uuid::new_v5(&namespace, name.as_bytes());
136 uuid.hyphenated().to_string()
137 }
138 7 => {
139 let ts = Timestamp::now(uuid::timestamp::context::NoContext);
140 let uuid = Uuid::new_v7(ts);
141 uuid.hyphenated().to_string()
142 }
143 _ => {
144 return Err(ShellError::IncorrectValue {
145 msg: format!(
146 "Unsupported UUID version: {version}. Supported versions are 1, 3, 4, 5, and 7."
147 ),
148 val_span: span,
149 call_span: span,
150 });
151 }
152 };
153
154 Ok(PipelineData::value(Value::string(uuid_str, span), None))
155}
156
157fn validate_flags(
158 engine_state: &EngineState,
159 stack: &mut Stack,
160 call: &Call,
161 span: Span,
162 version: i64,
163) -> Result<(), ShellError> {
164 match version {
165 1 => {
166 if call
167 .get_flag::<Option<String>>(engine_state, stack, "namespace")?
168 .is_some()
169 {
170 return Err(ShellError::IncompatibleParametersSingle {
171 msg: "version 1 uuid does not take namespace as a parameter".to_string(),
172 span,
173 });
174 }
175 if call
176 .get_flag::<Option<String>>(engine_state, stack, "name")?
177 .is_some()
178 {
179 return Err(ShellError::IncompatibleParametersSingle {
180 msg: "version 1 uuid does not take name as a parameter".to_string(),
181 span,
182 });
183 }
184 }
185 3 | 5 => {
186 if call
187 .get_flag::<Option<String>>(engine_state, stack, "mac")?
188 .is_some()
189 {
190 return Err(ShellError::IncompatibleParametersSingle {
191 msg: "version 3 and 5 uuids do not take mac as a parameter".to_string(),
192 span,
193 });
194 }
195 }
196 v => {
197 if v != 4 && v != 7 {
198 return Err(ShellError::IncorrectValue {
199 msg: format!(
200 "Unsupported UUID version: {v}. Supported versions are 1, 3, 4, 5, and 7."
201 ),
202 val_span: span,
203 call_span: span,
204 });
205 }
206 if call
207 .get_flag::<Option<String>>(engine_state, stack, "mac")?
208 .is_some()
209 {
210 return Err(ShellError::IncompatibleParametersSingle {
211 msg: format!("version {v} uuid does not take mac as a parameter"),
212 span,
213 });
214 }
215 if call
216 .get_flag::<Option<String>>(engine_state, stack, "namespace")?
217 .is_some()
218 {
219 return Err(ShellError::IncompatibleParametersSingle {
220 msg: format!("version {v} uuid does not take namespace as a parameter"),
221 span,
222 });
223 }
224 if call
225 .get_flag::<Option<String>>(engine_state, stack, "name")?
226 .is_some()
227 {
228 return Err(ShellError::IncompatibleParametersSingle {
229 msg: format!("version {v} uuid does not take name as a parameter"),
230 span,
231 });
232 }
233 }
234 }
235 Ok(())
236}
237
238fn get_mac_address(
239 engine_state: &EngineState,
240 stack: &mut Stack,
241 call: &Call,
242 span: Span,
243) -> Result<[u8; 6], ShellError> {
244 let mac_str: Option<String> = call.get_flag(engine_state, stack, "mac")?;
245
246 let mac_str = match mac_str {
247 Some(mac) => mac,
248 None => {
249 return Err(ShellError::MissingParameter {
250 param_name: "mac".to_string(),
251 span,
252 });
253 }
254 };
255
256 let mac_parts = mac_str.split(':').collect::<Vec<&str>>();
257 if mac_parts.len() != 6 {
258 return Err(ShellError::IncorrectValue {
259 msg: "MAC address must be in the format XX:XX:XX:XX:XX:XX".to_string(),
260 val_span: span,
261 call_span: span,
262 });
263 }
264
265 let mac: [u8; 6] = mac_parts
266 .iter()
267 .map(|x| u8::from_str_radix(x, 16))
268 .collect::<Result<Vec<u8>, _>>()
269 .map_err(|_| ShellError::IncorrectValue {
270 msg: "MAC address must be in the format XX:XX:XX:XX:XX:XX".to_string(),
271 val_span: span,
272 call_span: span,
273 })?
274 .try_into()
275 .map_err(|_| ShellError::IncorrectValue {
276 msg: "MAC address must be in the format XX:XX:XX:XX:XX:XX".to_string(),
277 val_span: span,
278 call_span: span,
279 })?;
280
281 Ok(mac)
282}
283
284fn get_namespace_and_name(
285 engine_state: &EngineState,
286 stack: &mut Stack,
287 call: &Call,
288 span: Span,
289) -> Result<(Uuid, String), ShellError> {
290 let namespace_str: Option<String> = call.get_flag(engine_state, stack, "namespace")?;
291 let name: Option<String> = call.get_flag(engine_state, stack, "name")?;
292
293 let namespace_str = match namespace_str {
294 Some(ns) => ns,
295 None => {
296 return Err(ShellError::MissingParameter {
297 param_name: "namespace".to_string(),
298 span,
299 });
300 }
301 };
302
303 let name = match name {
304 Some(n) => n,
305 None => {
306 return Err(ShellError::MissingParameter {
307 param_name: "name".to_string(),
308 span,
309 });
310 }
311 };
312
313 let namespace = match namespace_str.to_lowercase().as_str() {
314 "dns" => Uuid::NAMESPACE_DNS,
315 "url" => Uuid::NAMESPACE_URL,
316 "oid" => Uuid::NAMESPACE_OID,
317 "x500" => Uuid::NAMESPACE_X500,
318 _ => match Uuid::parse_str(&namespace_str) {
319 Ok(uuid) => uuid,
320 Err(_) => {
321 return Err(ShellError::IncorrectValue {
322 msg: "Namespace must be one of: dns, url, oid, x500, or a valid UUID string"
323 .to_string(),
324 val_span: span,
325 call_span: span,
326 });
327 }
328 },
329 };
330
331 Ok((namespace, name))
332}
333
334#[cfg(test)]
335mod test {
336 use super::*;
337
338 #[test]
339 fn test_examples() {
340 use crate::test_examples;
341
342 test_examples(RandomUuid {})
343 }
344}