mirror of
https://github.com/nushell/nushell.git
synced 2025-03-13 15:08:43 +01:00
feat(random uuid): add support for uuid versions other than 4. (#15239)
This PR implements the changes proposed in #15112 without any breaking changes. Should close #15112 post the review. # Description Added functionality to generate `uuid` versions 1, 3, 4, 5, 7 instead of just the version 4. - Users can now add a `-v n` flag to specify the version of uuid they want to generate and it maintains backward compatibility by returning a v4 uuid by default if no flags are passed. - Versions 3 and 5 have the additional but required namespace (`-s`) and name (`-n`) arguments too. Version 1 requires a mac address (`-m`). # User-Facing Changes - Added support for uuid versions 1, 3, 5 and 7. - For v3 and v5, the namespace and name arguments are required and hence there will be an error if those are not passed. Similarly the mac address for v1. - Full backward compatibility by setting v4 as default. # Tests + Formatting Tests added: in `nu-command::commands::random` - generates_valid_uuid4_by_default - generates_valid_uuid1 - generates_valid_uuid3_with_namespace_and_name - generates_valid_uuid4 - generates_valid_uuid5_with_namespace_and_name - generates_valid_uuid7
This commit is contained in:
parent
b1e591f84c
commit
f3982278e8
18
Cargo.lock
generated
18
Cargo.lock
generated
@ -293,6 +293,15 @@ version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4790f9e8961209112beb783d85449b508673cf4a6a419c8449b210743ac4dbe9"
|
||||
|
||||
[[package]]
|
||||
name = "atomic"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
@ -6547,6 +6556,12 @@ dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1_smol"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.8"
|
||||
@ -7711,8 +7726,11 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4"
|
||||
dependencies = [
|
||||
"atomic",
|
||||
"getrandom",
|
||||
"md-5",
|
||||
"serde",
|
||||
"sha1_smol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -40,13 +40,19 @@ byteorder = { workspace = true }
|
||||
bytesize = { workspace = true }
|
||||
calamine = { workspace = true, features = ["dates"] }
|
||||
chardetng = { workspace = true }
|
||||
chrono = { workspace = true, features = ["std", "unstable-locales", "clock"], default-features = false }
|
||||
chrono = { workspace = true, features = [
|
||||
"std",
|
||||
"unstable-locales",
|
||||
"clock",
|
||||
], default-features = false }
|
||||
chrono-humanize = { workspace = true }
|
||||
chrono-tz = { workspace = true }
|
||||
crossterm = { workspace = true, optional = true }
|
||||
csv = { workspace = true }
|
||||
devicons = { workspace = true }
|
||||
dialoguer = { workspace = true, default-features = false, features = ["fuzzy-select"] }
|
||||
dialoguer = { workspace = true, default-features = false, features = [
|
||||
"fuzzy-select",
|
||||
] }
|
||||
digest = { workspace = true, default-features = false }
|
||||
dtparse = { workspace = true }
|
||||
encoding_rs = { workspace = true }
|
||||
@ -58,7 +64,9 @@ indexmap = { workspace = true }
|
||||
indicatif = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
log = { workspace = true }
|
||||
lscolors = { workspace = true, default-features = false, features = ["nu-ansi-term"] }
|
||||
lscolors = { workspace = true, default-features = false, features = [
|
||||
"nu-ansi-term",
|
||||
] }
|
||||
md5 = { workspace = true }
|
||||
mime = { workspace = true }
|
||||
mime_guess = { workspace = true }
|
||||
@ -78,7 +86,11 @@ rand = { workspace = true, optional = true }
|
||||
getrandom = { workspace = true, optional = true }
|
||||
rayon = { workspace = true }
|
||||
roxmltree = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["bundled", "backup", "chrono"], optional = true }
|
||||
rusqlite = { workspace = true, features = [
|
||||
"bundled",
|
||||
"backup",
|
||||
"chrono",
|
||||
], optional = true }
|
||||
rmp = { workspace = true }
|
||||
scopeguard = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
@ -92,7 +104,12 @@ titlecase = { workspace = true }
|
||||
toml = { workspace = true, features = ["preserve_order"] }
|
||||
unicode-segmentation = { workspace = true }
|
||||
update-informer = { workspace = true, optional = true }
|
||||
ureq = { workspace = true, default-features = false, features = ["charset", "gzip", "json", "native-tls"], optional = true }
|
||||
ureq = { workspace = true, default-features = false, features = [
|
||||
"charset",
|
||||
"gzip",
|
||||
"json",
|
||||
"native-tls",
|
||||
], optional = true }
|
||||
url = { workspace = true }
|
||||
uu_cp = { workspace = true, optional = true }
|
||||
uu_mkdir = { workspace = true, optional = true }
|
||||
@ -101,7 +118,13 @@ uu_mv = { workspace = true, optional = true }
|
||||
uu_touch = { workspace = true, optional = true }
|
||||
uu_uname = { workspace = true, optional = true }
|
||||
uu_whoami = { workspace = true, optional = true }
|
||||
uuid = { workspace = true, features = ["v4"], optional = true }
|
||||
uuid = { workspace = true, features = [
|
||||
"v1",
|
||||
"v3",
|
||||
"v4",
|
||||
"v5",
|
||||
"v7",
|
||||
], optional = true }
|
||||
v_htmlescape = { workspace = true }
|
||||
wax = { workspace = true }
|
||||
which = { workspace = true, optional = true }
|
||||
@ -117,7 +140,11 @@ uucore = { workspace = true, features = ["mode"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
umask = { workspace = true }
|
||||
nix = { workspace = true, default-features = false, features = ["user", "resource", "pthread"] }
|
||||
nix = { workspace = true, default-features = false, features = [
|
||||
"user",
|
||||
"resource",
|
||||
"pthread",
|
||||
] }
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
|
||||
procfs = { workspace = true }
|
||||
@ -164,12 +191,7 @@ os = [
|
||||
# The dependencies listed below need 'getrandom'.
|
||||
# They work with JS (usually with wasm-bindgen) or regular OS support.
|
||||
# Hence they are also put under the 'os' feature to avoid repetition.
|
||||
js = [
|
||||
"getrandom",
|
||||
"getrandom/js",
|
||||
"rand",
|
||||
"uuid",
|
||||
]
|
||||
js = ["getrandom", "getrandom/js", "rand", "uuid"]
|
||||
|
||||
# These dependencies require networking capabilities, especially the http
|
||||
# interface requires openssl which is not easy to embed into wasm,
|
||||
@ -182,10 +204,7 @@ network = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
plugin = [
|
||||
"nu-parser/plugin",
|
||||
"os",
|
||||
]
|
||||
plugin = ["nu-parser/plugin", "os"]
|
||||
sqlite = ["rusqlite"]
|
||||
trash-support = ["trash"]
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
use nu_engine::command_prelude::*;
|
||||
use uuid::Uuid;
|
||||
use uuid::{Timestamp, Uuid};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SubCommand;
|
||||
@ -13,41 +13,316 @@ impl Command for SubCommand {
|
||||
Signature::build("random uuid")
|
||||
.category(Category::Random)
|
||||
.input_output_types(vec![(Type::Nothing, Type::String)])
|
||||
.named(
|
||||
"version",
|
||||
SyntaxShape::Int,
|
||||
"The UUID version to generate (1, 3, 4, 5, 7). Defaults to 4 if not specified.",
|
||||
Some('v'),
|
||||
)
|
||||
.named(
|
||||
"namespace",
|
||||
SyntaxShape::String,
|
||||
"The namespace for v3 and v5 UUIDs (dns, url, oid, x500). Required for v3 and v5.",
|
||||
Some('n'),
|
||||
)
|
||||
.named(
|
||||
"name",
|
||||
SyntaxShape::String,
|
||||
"The name string for v3 and v5 UUIDs. Required for v3 and v5.",
|
||||
Some('s'),
|
||||
)
|
||||
.named(
|
||||
"mac",
|
||||
SyntaxShape::String,
|
||||
"The MAC address (node ID) used to generate v1 UUIDs. Required for v1.",
|
||||
Some('m'),
|
||||
)
|
||||
.allow_variants_without_examples(true)
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Generate a random uuid4 string."
|
||||
"Generate a random uuid string of the specified version."
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["generate", "uuid4"]
|
||||
vec!["generate", "uuid4", "uuid1", "uuid3", "uuid5", "uuid7"]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
uuid(call)
|
||||
uuid(engine_state, stack, call)
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Generate a random uuid4 string",
|
||||
example: "random uuid",
|
||||
result: None,
|
||||
}]
|
||||
vec![
|
||||
Example {
|
||||
description: "Generate a random uuid v4 string (default)",
|
||||
example: "random uuid",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Generate a uuid v1 string (timestamp-based)",
|
||||
example: "random uuid -v 1 -m 00:11:22:33:44:55",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Generate a uuid v3 string (namespace with MD5)",
|
||||
example: "random uuid -v 3 -n dns -s example.com",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Generate a uuid v4 string (random).",
|
||||
example: "random uuid -v 4",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Generate a uuid v5 string (namespace with SHA1)",
|
||||
example: "random uuid -v 5 -n dns -s example.com",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Generate a uuid v7 string (timestamp + random)",
|
||||
example: "random uuid -v 7",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn uuid(call: &Call) -> Result<PipelineData, ShellError> {
|
||||
fn uuid(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let span = call.head;
|
||||
let uuid_4 = Uuid::new_v4().hyphenated().to_string();
|
||||
|
||||
Ok(PipelineData::Value(Value::string(uuid_4, span), None))
|
||||
let version: Option<i64> = call.get_flag(engine_state, stack, "version")?;
|
||||
let version = version.unwrap_or(4);
|
||||
|
||||
validate_flags(engine_state, stack, call, span, version)?;
|
||||
|
||||
let uuid_str = match version {
|
||||
1 => {
|
||||
let ts = Timestamp::now(uuid::timestamp::context::NoContext);
|
||||
let node_id = get_mac_address(engine_state, stack, call, span)?;
|
||||
let uuid = Uuid::new_v1(ts, &node_id);
|
||||
uuid.hyphenated().to_string()
|
||||
}
|
||||
3 => {
|
||||
let (namespace, name) = get_namespace_and_name(engine_state, stack, call, span)?;
|
||||
let uuid = Uuid::new_v3(&namespace, name.as_bytes());
|
||||
uuid.hyphenated().to_string()
|
||||
}
|
||||
4 => {
|
||||
let uuid = Uuid::new_v4();
|
||||
uuid.hyphenated().to_string()
|
||||
}
|
||||
5 => {
|
||||
let (namespace, name) = get_namespace_and_name(engine_state, stack, call, span)?;
|
||||
let uuid = Uuid::new_v5(&namespace, name.as_bytes());
|
||||
uuid.hyphenated().to_string()
|
||||
}
|
||||
7 => {
|
||||
let ts = Timestamp::now(uuid::timestamp::context::NoContext);
|
||||
let uuid = Uuid::new_v7(ts);
|
||||
uuid.hyphenated().to_string()
|
||||
}
|
||||
_ => {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: format!(
|
||||
"Unsupported UUID version: {}. Supported versions are 1, 3, 4, 5, and 7.",
|
||||
version
|
||||
),
|
||||
val_span: span,
|
||||
call_span: span,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Ok(PipelineData::Value(Value::string(uuid_str, span), None))
|
||||
}
|
||||
|
||||
fn validate_flags(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
span: Span,
|
||||
version: i64,
|
||||
) -> Result<(), ShellError> {
|
||||
match version {
|
||||
1 => {
|
||||
if call
|
||||
.get_flag::<Option<String>>(engine_state, stack, "namespace")?
|
||||
.is_some()
|
||||
{
|
||||
return Err(ShellError::IncompatibleParametersSingle {
|
||||
msg: "version 1 uuid does not take namespace as a parameter".to_string(),
|
||||
span,
|
||||
});
|
||||
}
|
||||
if call
|
||||
.get_flag::<Option<String>>(engine_state, stack, "name")?
|
||||
.is_some()
|
||||
{
|
||||
return Err(ShellError::IncompatibleParametersSingle {
|
||||
msg: "version 1 uuid does not take name as a parameter".to_string(),
|
||||
span,
|
||||
});
|
||||
}
|
||||
}
|
||||
3 | 5 => {
|
||||
if call
|
||||
.get_flag::<Option<String>>(engine_state, stack, "mac")?
|
||||
.is_some()
|
||||
{
|
||||
return Err(ShellError::IncompatibleParametersSingle {
|
||||
msg: "version 3 and 5 uuids do not take mac as a parameter".to_string(),
|
||||
span,
|
||||
});
|
||||
}
|
||||
}
|
||||
v => {
|
||||
if v != 4 && v != 7 {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: format!(
|
||||
"Unsupported UUID version: {}. Supported versions are 1, 3, 4, 5, and 7.",
|
||||
v
|
||||
),
|
||||
val_span: span,
|
||||
call_span: span,
|
||||
});
|
||||
}
|
||||
if call
|
||||
.get_flag::<Option<String>>(engine_state, stack, "mac")?
|
||||
.is_some()
|
||||
{
|
||||
return Err(ShellError::IncompatibleParametersSingle {
|
||||
msg: format!("version {} uuid does not take mac as a parameter", v),
|
||||
span,
|
||||
});
|
||||
}
|
||||
if call
|
||||
.get_flag::<Option<String>>(engine_state, stack, "namespace")?
|
||||
.is_some()
|
||||
{
|
||||
return Err(ShellError::IncompatibleParametersSingle {
|
||||
msg: format!("version {} uuid does not take namespace as a parameter", v),
|
||||
span,
|
||||
});
|
||||
}
|
||||
if call
|
||||
.get_flag::<Option<String>>(engine_state, stack, "name")?
|
||||
.is_some()
|
||||
{
|
||||
return Err(ShellError::IncompatibleParametersSingle {
|
||||
msg: format!("version {} uuid does not take name as a parameter", v),
|
||||
span,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_mac_address(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
span: Span,
|
||||
) -> Result<[u8; 6], ShellError> {
|
||||
let mac_str: Option<String> = call.get_flag(engine_state, stack, "mac")?;
|
||||
|
||||
let mac_str = match mac_str {
|
||||
Some(mac) => mac,
|
||||
None => {
|
||||
return Err(ShellError::MissingParameter {
|
||||
param_name: "mac".to_string(),
|
||||
span,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let mac_parts = mac_str.split(':').collect::<Vec<&str>>();
|
||||
if mac_parts.len() != 6 {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: "MAC address must be in the format XX:XX:XX:XX:XX:XX".to_string(),
|
||||
val_span: span,
|
||||
call_span: span,
|
||||
});
|
||||
}
|
||||
|
||||
let mac: [u8; 6] = mac_parts
|
||||
.iter()
|
||||
.map(|x| u8::from_str_radix(x, 16))
|
||||
.collect::<Result<Vec<u8>, _>>()
|
||||
.map_err(|_| ShellError::IncorrectValue {
|
||||
msg: "MAC address must be in the format XX:XX:XX:XX:XX:XX".to_string(),
|
||||
val_span: span,
|
||||
call_span: span,
|
||||
})?
|
||||
.try_into()
|
||||
.map_err(|_| ShellError::IncorrectValue {
|
||||
msg: "MAC address must be in the format XX:XX:XX:XX:XX:XX".to_string(),
|
||||
val_span: span,
|
||||
call_span: span,
|
||||
})?;
|
||||
|
||||
Ok(mac)
|
||||
}
|
||||
|
||||
fn get_namespace_and_name(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
span: Span,
|
||||
) -> Result<(Uuid, String), ShellError> {
|
||||
let namespace_str: Option<String> = call.get_flag(engine_state, stack, "namespace")?;
|
||||
let name: Option<String> = call.get_flag(engine_state, stack, "name")?;
|
||||
|
||||
let namespace_str = match namespace_str {
|
||||
Some(ns) => ns,
|
||||
None => {
|
||||
return Err(ShellError::MissingParameter {
|
||||
param_name: "namespace".to_string(),
|
||||
span,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let name = match name {
|
||||
Some(n) => n,
|
||||
None => {
|
||||
return Err(ShellError::MissingParameter {
|
||||
param_name: "name".to_string(),
|
||||
span,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let namespace = match namespace_str.to_lowercase().as_str() {
|
||||
"dns" => Uuid::NAMESPACE_DNS,
|
||||
"url" => Uuid::NAMESPACE_URL,
|
||||
"oid" => Uuid::NAMESPACE_OID,
|
||||
"x500" => Uuid::NAMESPACE_X500,
|
||||
_ => match Uuid::parse_str(&namespace_str) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => {
|
||||
return Err(ShellError::IncorrectValue {
|
||||
msg: "Namespace must be one of: dns, url, oid, x500, or a valid UUID string"
|
||||
.to_string(),
|
||||
val_span: span,
|
||||
call_span: span,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Ok((namespace, name))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -2,10 +2,75 @@ use nu_test_support::nu;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test]
|
||||
fn generates_valid_uuid4() {
|
||||
fn generates_valid_uuid4_by_default() {
|
||||
let actual = nu!("random uuid");
|
||||
|
||||
let result = Uuid::parse_str(actual.out.as_str());
|
||||
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Ok(uuid) = result {
|
||||
assert_eq!(uuid.get_version_num(), 4);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generates_valid_uuid1() {
|
||||
let actual = nu!("random uuid -v 1 -m 00:11:22:33:44:55");
|
||||
let result = Uuid::parse_str(actual.out.as_str());
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Ok(uuid) = result {
|
||||
assert_eq!(uuid.get_version_num(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generates_valid_uuid3_with_namespace_and_name() {
|
||||
let actual = nu!("random uuid -v 3 -n dns -s example.com");
|
||||
let result = Uuid::parse_str(actual.out.as_str());
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Ok(uuid) = result {
|
||||
assert_eq!(uuid.get_version_num(), 3);
|
||||
|
||||
let namespace = Uuid::NAMESPACE_DNS;
|
||||
let expected = Uuid::new_v3(&namespace, "example.com".as_bytes());
|
||||
assert_eq!(uuid, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generates_valid_uuid4() {
|
||||
let actual = nu!("random uuid -v 4");
|
||||
let result = Uuid::parse_str(actual.out.as_str());
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Ok(uuid) = result {
|
||||
assert_eq!(uuid.get_version_num(), 4);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generates_valid_uuid5_with_namespace_and_name() {
|
||||
let actual = nu!("random uuid -v 5 -n dns -s example.com");
|
||||
let result = Uuid::parse_str(actual.out.as_str());
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Ok(uuid) = result {
|
||||
assert_eq!(uuid.get_version_num(), 5);
|
||||
|
||||
let namespace = Uuid::NAMESPACE_DNS;
|
||||
let expected = Uuid::new_v5(&namespace, "example.com".as_bytes());
|
||||
assert_eq!(uuid, expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generates_valid_uuid7() {
|
||||
let actual = nu!("random uuid -v 7");
|
||||
let result = Uuid::parse_str(actual.out.as_str());
|
||||
assert!(result.is_ok());
|
||||
|
||||
if let Ok(uuid) = result {
|
||||
assert_eq!(uuid.get_version_num(), 7);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user