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