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: {version}. Supported versions are 1, 3, 4, 5, and 7."
139 ),
140 val_span: span,
141 call_span: span,
142 });
143 }
144 };
145
146 Ok(PipelineData::Value(Value::string(uuid_str, span), None))
147}
148
149fn validate_flags(
150 engine_state: &EngineState,
151 stack: &mut Stack,
152 call: &Call,
153 span: Span,
154 version: i64,
155) -> Result<(), ShellError> {
156 match version {
157 1 => {
158 if call
159 .get_flag::<Option<String>>(engine_state, stack, "namespace")?
160 .is_some()
161 {
162 return Err(ShellError::IncompatibleParametersSingle {
163 msg: "version 1 uuid does not take namespace as a parameter".to_string(),
164 span,
165 });
166 }
167 if call
168 .get_flag::<Option<String>>(engine_state, stack, "name")?
169 .is_some()
170 {
171 return Err(ShellError::IncompatibleParametersSingle {
172 msg: "version 1 uuid does not take name as a parameter".to_string(),
173 span,
174 });
175 }
176 }
177 3 | 5 => {
178 if call
179 .get_flag::<Option<String>>(engine_state, stack, "mac")?
180 .is_some()
181 {
182 return Err(ShellError::IncompatibleParametersSingle {
183 msg: "version 3 and 5 uuids do not take mac as a parameter".to_string(),
184 span,
185 });
186 }
187 }
188 v => {
189 if v != 4 && v != 7 {
190 return Err(ShellError::IncorrectValue {
191 msg: format!(
192 "Unsupported UUID version: {v}. Supported versions are 1, 3, 4, 5, and 7."
193 ),
194 val_span: span,
195 call_span: span,
196 });
197 }
198 if call
199 .get_flag::<Option<String>>(engine_state, stack, "mac")?
200 .is_some()
201 {
202 return Err(ShellError::IncompatibleParametersSingle {
203 msg: format!("version {v} uuid does not take mac as a parameter"),
204 span,
205 });
206 }
207 if call
208 .get_flag::<Option<String>>(engine_state, stack, "namespace")?
209 .is_some()
210 {
211 return Err(ShellError::IncompatibleParametersSingle {
212 msg: format!("version {v} uuid does not take namespace as a parameter"),
213 span,
214 });
215 }
216 if call
217 .get_flag::<Option<String>>(engine_state, stack, "name")?
218 .is_some()
219 {
220 return Err(ShellError::IncompatibleParametersSingle {
221 msg: format!("version {v} uuid does not take name as a parameter"),
222 span,
223 });
224 }
225 }
226 }
227 Ok(())
228}
229
230fn get_mac_address(
231 engine_state: &EngineState,
232 stack: &mut Stack,
233 call: &Call,
234 span: Span,
235) -> Result<[u8; 6], ShellError> {
236 let mac_str: Option<String> = call.get_flag(engine_state, stack, "mac")?;
237
238 let mac_str = match mac_str {
239 Some(mac) => mac,
240 None => {
241 return Err(ShellError::MissingParameter {
242 param_name: "mac".to_string(),
243 span,
244 });
245 }
246 };
247
248 let mac_parts = mac_str.split(':').collect::<Vec<&str>>();
249 if mac_parts.len() != 6 {
250 return Err(ShellError::IncorrectValue {
251 msg: "MAC address must be in the format XX:XX:XX:XX:XX:XX".to_string(),
252 val_span: span,
253 call_span: span,
254 });
255 }
256
257 let mac: [u8; 6] = mac_parts
258 .iter()
259 .map(|x| u8::from_str_radix(x, 16))
260 .collect::<Result<Vec<u8>, _>>()
261 .map_err(|_| ShellError::IncorrectValue {
262 msg: "MAC address must be in the format XX:XX:XX:XX:XX:XX".to_string(),
263 val_span: span,
264 call_span: span,
265 })?
266 .try_into()
267 .map_err(|_| ShellError::IncorrectValue {
268 msg: "MAC address must be in the format XX:XX:XX:XX:XX:XX".to_string(),
269 val_span: span,
270 call_span: span,
271 })?;
272
273 Ok(mac)
274}
275
276fn get_namespace_and_name(
277 engine_state: &EngineState,
278 stack: &mut Stack,
279 call: &Call,
280 span: Span,
281) -> Result<(Uuid, String), ShellError> {
282 let namespace_str: Option<String> = call.get_flag(engine_state, stack, "namespace")?;
283 let name: Option<String> = call.get_flag(engine_state, stack, "name")?;
284
285 let namespace_str = match namespace_str {
286 Some(ns) => ns,
287 None => {
288 return Err(ShellError::MissingParameter {
289 param_name: "namespace".to_string(),
290 span,
291 });
292 }
293 };
294
295 let name = match name {
296 Some(n) => n,
297 None => {
298 return Err(ShellError::MissingParameter {
299 param_name: "name".to_string(),
300 span,
301 });
302 }
303 };
304
305 let namespace = match namespace_str.to_lowercase().as_str() {
306 "dns" => Uuid::NAMESPACE_DNS,
307 "url" => Uuid::NAMESPACE_URL,
308 "oid" => Uuid::NAMESPACE_OID,
309 "x500" => Uuid::NAMESPACE_X500,
310 _ => match Uuid::parse_str(&namespace_str) {
311 Ok(uuid) => uuid,
312 Err(_) => {
313 return Err(ShellError::IncorrectValue {
314 msg: "Namespace must be one of: dns, url, oid, x500, or a valid UUID string"
315 .to_string(),
316 val_span: span,
317 call_span: span,
318 });
319 }
320 },
321 };
322
323 Ok((namespace, name))
324}
325
326#[cfg(test)]
327mod test {
328 use super::*;
329
330 #[test]
331 fn test_examples() {
332 use crate::test_examples;
333
334 test_examples(RandomUuid {})
335 }
336}