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            .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}