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