nu_command/random/
uuid.rs

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}