Skip to main content

nu_command/random/
uuid.rs

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