use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand};
use nu_protocol::{
Category, Example, LabeledError, PipelineData, Signature, Span, SyntaxShape, Type, Value,
};
use crate::{SecurityWarnings, UlidEngine, UlidPlugin};
pub struct UlidGenerateCommand;
impl PluginCommand for UlidGenerateCommand {
type Plugin = UlidPlugin;
fn name(&self) -> &str {
"ulid generate"
}
fn description(&self) -> &str {
"Generate a new ULID (Universally Unique Lexicographically Sortable Identifier)"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.named(
"count",
SyntaxShape::Int,
"Number of ULIDs to generate (max 10,000)",
Some('c'),
)
.named(
"timestamp",
SyntaxShape::Int,
"Custom timestamp in milliseconds",
Some('t'),
)
.input_output_types(vec![
(Type::Nothing, Type::String),
(Type::Nothing, Type::List(Box::new(Type::String))),
])
.category(Category::Generators)
}
fn examples(&self) -> Vec<Example<'_>> {
vec![
Example {
example: "ulid generate",
description: "Generate a single ULID",
result: None,
},
Example {
example: "ulid generate --count 5",
description: "Generate 5 ULIDs",
result: None,
},
Example {
example: "ulid generate --timestamp 1640995200000",
description: "Generate a ULID with specific timestamp",
result: None,
},
]
}
fn run(
&self,
_plugin: &Self::Plugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
_input: PipelineData,
) -> Result<PipelineData, LabeledError> {
let count: Option<i64> = call.get_flag("count")?;
let timestamp: Option<i64> = call.get_flag("timestamp")?;
match count {
Some(c) => generate_bulk_ulids(c, timestamp, call.head),
None => generate_single_ulid(timestamp, call.head),
}
}
}
pub struct UlidValidateCommand;
impl PluginCommand for UlidValidateCommand {
type Plugin = UlidPlugin;
fn name(&self) -> &str {
"ulid validate"
}
fn description(&self) -> &str {
"Validate if a string is a valid ULID"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.required("ulid", SyntaxShape::String, "The ULID string to validate")
.input_output_types(vec![(Type::Nothing, Type::Bool)])
.category(Category::Strings)
}
fn examples(&self) -> Vec<Example<'_>> {
vec![
Example {
example: "ulid validate '01AN4Z07BY79KA1307SR9X4MV3'",
description: "Validate a ULID string",
result: Some(Value::bool(true, Span::test_data())),
},
Example {
example: "ulid validate 'invalid-ulid'",
description: "Validate an invalid ULID string",
result: Some(Value::bool(false, Span::test_data())),
},
]
}
fn run(
&self,
_plugin: &Self::Plugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
_input: PipelineData,
) -> Result<PipelineData, LabeledError> {
let ulid_str: String = call.req(0)?;
let is_valid = UlidEngine::validate(&ulid_str);
Ok(PipelineData::Value(Value::bool(is_valid, call.head), None))
}
}
pub struct UlidParseCommand;
impl PluginCommand for UlidParseCommand {
type Plugin = UlidPlugin;
fn name(&self) -> &str {
"ulid parse"
}
fn description(&self) -> &str {
"Parse a ULID string and extract its components"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.required("ulid", SyntaxShape::String, "The ULID string to parse")
.input_output_types(vec![(Type::Nothing, Type::Record(vec![].into()))])
.category(Category::Strings)
}
fn examples(&self) -> Vec<Example<'_>> {
vec![Example {
example: "ulid parse '01AN4Z07BY79KA1307SR9X4MV3'",
description: "Parse a ULID and show its components",
result: None,
}]
}
fn run(
&self,
_plugin: &Self::Plugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
_input: PipelineData,
) -> Result<PipelineData, LabeledError> {
let ulid_str: String = call.req(0)?;
match UlidEngine::parse(&ulid_str) {
Ok(components) => {
let value = UlidEngine::components_to_value(&components, call.head);
Ok(PipelineData::Value(value, None))
}
Err(e) => Err(LabeledError::new("Parse failed").with_label(e.to_string(), call.head)),
}
}
}
pub struct UlidSecurityAdviceCommand;
impl PluginCommand for UlidSecurityAdviceCommand {
type Plugin = UlidPlugin;
fn name(&self) -> &str {
"ulid security-advice"
}
fn description(&self) -> &str {
"Show comprehensive security advice for ULID usage"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.input_output_types(vec![(Type::Nothing, Type::Record(vec![].into()))])
.category(Category::Misc)
}
fn examples(&self) -> Vec<Example<'_>> {
vec![Example {
example: "ulid security-advice",
description: "Display security guidance for ULID usage",
result: None,
}]
}
fn run(
&self,
_plugin: &Self::Plugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
_input: PipelineData,
) -> Result<PipelineData, LabeledError> {
let advice = SecurityWarnings::get_security_advice(call.head);
Ok(PipelineData::Value(advice, None))
}
}
fn generate_single_ulid(
timestamp: Option<i64>,
span: nu_protocol::Span,
) -> Result<PipelineData, LabeledError> {
let ulid = match timestamp {
Some(ts) => UlidEngine::generate_with_timestamp(ts as u64),
None => UlidEngine::generate(),
}
.map_err(|e| LabeledError::new("Generation failed").with_label(e.to_string(), span))?;
Ok(PipelineData::Value(
Value::string(ulid.to_string(), span),
None,
))
}
fn generate_bulk_ulids(
count: i64,
timestamp: Option<i64>,
span: nu_protocol::Span,
) -> Result<PipelineData, LabeledError> {
let count_usize = if count < 0 {
return Err(LabeledError::new("Invalid count").with_label("Count must be positive", span));
} else if count > crate::MAX_BULK_GENERATION as i64 {
return Err(LabeledError::new("Count too large").with_label(
format!("Maximum count is {}", crate::MAX_BULK_GENERATION),
span,
));
} else {
count as usize
};
let ulids = match timestamp {
Some(ts) => {
let mut result = Vec::new();
for _ in 0..count_usize {
let ulid = UlidEngine::generate_with_timestamp(ts as u64).map_err(|e| {
LabeledError::new("Generation failed").with_label(e.to_string(), span)
})?;
result.push(ulid);
}
result
}
None => UlidEngine::generate_bulk(count_usize).map_err(|e| {
LabeledError::new("Bulk generation failed").with_label(e.to_string(), span)
})?,
};
let values: Vec<Value> = ulids
.iter()
.map(|ulid| Value::string(ulid.to_string(), span))
.collect();
Ok(PipelineData::Value(Value::list(values, span), None))
}
#[cfg(test)]
mod tests {
use super::*;
use nu_protocol::{Span, Value};
fn create_test_span() -> Span {
Span::test_data()
}
mod ulid_generate_command {
use super::*;
#[test]
fn test_command_signature() {
let cmd = UlidGenerateCommand;
let signature = cmd.signature();
assert_eq!(signature.name, "ulid generate");
assert!(signature.named.iter().any(|flag| flag.long == "count"));
assert!(signature.named.iter().any(|flag| flag.long == "timestamp"));
assert!(
!signature.named.iter().any(|flag| flag.long == "format"),
"The --format flag should not exist"
);
}
#[test]
fn test_command_name() {
let cmd = UlidGenerateCommand;
assert_eq!(cmd.name(), "ulid generate");
}
#[test]
fn test_command_description() {
let cmd = UlidGenerateCommand;
let desc = cmd.description();
assert!(desc.contains("Generate"));
assert!(desc.contains("ULID"));
}
#[test]
fn test_command_examples() {
let cmd = UlidGenerateCommand;
let examples = cmd.examples();
assert!(!examples.is_empty());
assert!(
examples
.iter()
.any(|ex| ex.example.contains("ulid generate"))
);
}
#[test]
fn test_count_validation_logic() {
let test_cases = vec![
(-1, false, "negative count"),
(0, true, "zero count"),
(1, true, "normal count"),
(5000, true, "medium count"),
(crate::MAX_BULK_GENERATION as i64, true, "max count"),
(
crate::MAX_BULK_GENERATION as i64 + 1,
false,
"over max count",
),
];
for (count, should_be_valid, description) in test_cases {
let is_valid = (0..=crate::MAX_BULK_GENERATION as i64).contains(&count);
assert_eq!(
is_valid, should_be_valid,
"Failed for {}: {}",
count, description
);
}
}
}
mod ulid_validate_command {
use super::*;
#[test]
fn test_command_signature() {
let cmd = UlidValidateCommand;
let signature = cmd.signature();
assert_eq!(signature.name, "ulid validate");
assert_eq!(signature.required_positional.len(), 1);
assert_eq!(signature.required_positional[0].name, "ulid");
assert!(
!signature.named.iter().any(|flag| flag.long == "detailed"),
"The --detailed flag should not exist"
);
assert_eq!(signature.input_output_types.len(), 1);
assert_eq!(signature.input_output_types[0], (Type::Nothing, Type::Bool));
}
#[test]
fn test_command_name() {
let cmd = UlidValidateCommand;
assert_eq!(cmd.name(), "ulid validate");
}
#[test]
fn test_command_description() {
let cmd = UlidValidateCommand;
let desc = cmd.description();
assert!(desc.contains("Validate"));
assert!(desc.contains("ULID"));
}
#[test]
fn test_command_examples() {
let cmd = UlidValidateCommand;
let examples = cmd.examples();
assert_eq!(examples.len(), 2);
assert!(examples[0].example.contains("01AN4Z07BY79KA1307SR9X4MV3"));
assert!(examples[0].result.is_some());
assert!(examples[1].example.contains("invalid-ulid"));
assert!(examples[1].result.is_some());
assert!(
!examples.iter().any(|ex| ex.example.contains("--detailed")),
"No example should reference --detailed"
);
}
#[test]
fn test_validation_logic_integration() {
let test_cases = vec![
("01AN4Z07BY79KA1307SR9X4MV3", true, "standard example ULID"),
("01BX5ZZKBKACTAV9WEVGEMMVRY", true, "another valid ULID"),
("", false, "empty string"),
("too_short", false, "too short"),
("01AN4Z07BY79KA1307SR9X4MV3X", false, "too long"),
("invalid-chars!", false, "invalid characters"),
(
"lowercase123456789012345678",
false,
"lowercase not allowed",
),
];
for (ulid_str, expected_valid, description) in test_cases {
let is_valid = UlidEngine::validate(ulid_str);
assert_eq!(
is_valid, expected_valid,
"Failed for '{}': {}",
ulid_str, description
);
}
}
}
mod ulid_parse_command {
use super::*;
#[test]
fn test_command_signature() {
let cmd = UlidParseCommand;
let signature = cmd.signature();
assert_eq!(signature.name, "ulid parse");
assert_eq!(signature.required_positional.len(), 1);
assert_eq!(signature.required_positional[0].name, "ulid");
}
#[test]
fn test_command_name() {
let cmd = UlidParseCommand;
assert_eq!(cmd.name(), "ulid parse");
}
#[test]
fn test_command_description() {
let cmd = UlidParseCommand;
let desc = cmd.description();
assert!(desc.contains("Parse"));
assert!(desc.contains("ULID"));
assert!(desc.contains("components"));
}
#[test]
fn test_command_examples() {
let cmd = UlidParseCommand;
let examples = cmd.examples();
assert!(!examples.is_empty());
assert!(examples.iter().any(|ex| ex.example.contains("ulid parse")));
}
#[test]
fn test_parsing_logic_integration() {
if let Ok(generated_ulid) = UlidEngine::generate() {
let ulid_str = generated_ulid.to_string();
match UlidEngine::parse(&ulid_str) {
Ok(components) => {
assert_eq!(components.ulid, ulid_str);
assert!(components.valid);
assert!(components.timestamp_ms > 0);
assert!(!components.randomness_hex.is_empty());
}
Err(_) => panic!("Should be able to parse generated ULID"),
}
}
match UlidEngine::parse("invalid-ulid") {
Ok(_) => panic!("Should not be able to parse invalid ULID"),
Err(e) => {
assert!(e.to_string().contains("Invalid") || e.to_string().contains("Error"));
}
}
}
}
mod ulid_security_advice_command {
use super::*;
#[test]
fn test_command_signature() {
let cmd = UlidSecurityAdviceCommand;
let signature = cmd.signature();
assert_eq!(signature.name, "ulid security-advice");
assert_eq!(signature.required_positional.len(), 0);
}
#[test]
fn test_command_name() {
let cmd = UlidSecurityAdviceCommand;
assert_eq!(cmd.name(), "ulid security-advice");
}
#[test]
fn test_command_description() {
let cmd = UlidSecurityAdviceCommand;
let desc = cmd.description();
assert!(desc.contains("security"));
assert!(desc.contains("advice") || desc.contains("guidance"));
}
#[test]
fn test_command_examples() {
let cmd = UlidSecurityAdviceCommand;
let examples = cmd.examples();
assert!(!examples.is_empty());
assert!(
examples
.iter()
.any(|ex| ex.example.contains("ulid security-advice"))
);
}
}
mod input_validation {
#[test]
fn test_count_parameter_bounds() {
let max = crate::MAX_BULK_GENERATION as i64;
let valid_counts = [0, 1, max];
let invalid_counts = [max + 1, -1];
for count in valid_counts {
assert!(
(0..=max).contains(&count),
"Count {} should be valid",
count
);
}
for count in invalid_counts {
assert!(
!(0..=max).contains(&count),
"Count {} should be invalid",
count
);
}
}
#[test]
fn test_timestamp_parameter_validation() {
let valid_timestamps = vec![
0u64, 1640995200000u64, 1000000000000u64, ];
for ts in valid_timestamps {
assert!(ts < u64::MAX, "Timestamp {} should be valid", ts);
}
}
#[test]
fn test_ulid_string_validation_patterns() {
let valid_patterns = vec![
("26 character length", "01AN4Z07BY79KA1307SR9X4MV3"),
("all valid chars", "7ZZZZZZZZZZZZZZZZZZZZZZZZZ"),
("mixed case valid", "01BX5ZZKBKACTAV9WEVGEMMVRY"),
];
for (description, ulid_str) in valid_patterns {
assert_eq!(
ulid_str.len(),
crate::ULID_STRING_LENGTH,
"Length check failed for {}",
description
);
assert!(
ulid_str
.chars()
.all(|c| crate::CROCKFORD_BASE32_CHARSET.contains(c)),
"Character set check failed for {}",
description
);
}
}
}
mod error_handling {
use super::*;
#[test]
fn test_error_message_construction() {
let test_cases = vec![
("Invalid count", "Count must be positive"),
("Count too large", "Maximum count is 10,000"),
("Generation failed", "ULID generation"),
("Parse failed", "parsing"),
];
for (error_type, expected_content) in test_cases {
let error = LabeledError::new(error_type);
assert_eq!(error.msg, error_type);
let error_with_label = error.with_label(expected_content, create_test_span());
assert_eq!(error_with_label.msg, error_type);
}
}
}
mod execution_logic_tests {
use super::*;
#[test]
fn test_ulid_generate_execution() {
let generated_ulid = UlidEngine::generate().expect("Should generate ULID");
let ulid_str = generated_ulid.to_string();
assert_eq!(
ulid_str.len(),
crate::ULID_STRING_LENGTH,
"ULID should be 26 characters"
);
assert!(
UlidEngine::validate(&ulid_str),
"Generated ULID should be valid"
);
let bulk_ulids = UlidEngine::generate_bulk(5).expect("Should generate bulk ULIDs");
assert_eq!(bulk_ulids.len(), 5, "Should generate exactly 5 ULIDs");
let unique_count = bulk_ulids
.iter()
.map(|u| u.to_string())
.collect::<std::collections::HashSet<_>>()
.len();
assert_eq!(unique_count, 5, "All generated ULIDs should be unique");
}
#[test]
fn test_ulid_generate_with_timestamp_execution() {
let custom_timestamp = 1640995200000u64;
let ulid = UlidEngine::generate_with_timestamp(custom_timestamp)
.expect("Should generate ULID with timestamp");
let parsed = UlidEngine::parse(&ulid.to_string()).expect("Should parse generated ULID");
assert_eq!(parsed.timestamp_ms, custom_timestamp);
assert!(parsed.valid);
}
#[test]
fn test_count_validation_execution() {
let test_cases = vec![
(-1, false, "negative count"),
(0, true, "zero count"), (1, true, "single count"),
(crate::MAX_BULK_GENERATION as i64, true, "max count"),
(
crate::MAX_BULK_GENERATION as i64 + 1,
false,
"over max count",
),
];
for (count, should_be_valid, description) in test_cases {
if count < 0 {
assert!(
!should_be_valid,
"Negative count should be invalid: {}",
description
);
} else if count > crate::MAX_BULK_GENERATION as i64 {
let result = UlidEngine::generate_bulk(count as usize);
assert!(
result.is_err(),
"Over-limit count should fail: {}",
description
);
} else {
let result = UlidEngine::generate_bulk(count as usize);
assert!(
result.is_ok(),
"Valid count should succeed: {}",
description
);
assert_eq!(result.unwrap().len(), count as usize);
}
}
}
#[test]
fn test_ulid_validate_execution() {
let valid_ulids = vec!["01AN4Z07BY79KA1307SR9X4MV3", "01BX5ZZKBKACTAV9WEVGEMMVRY"];
let invalid_ulids = vec![
"invalid",
"too_short",
"01AN4Z07BY79KA1307SR9X4MV3X", "", "01AN4Z07BY79KA1307SR9X4MV!", ];
for ulid_str in &valid_ulids {
assert!(
UlidEngine::validate(ulid_str),
"Should validate: {}",
ulid_str
);
}
for ulid_str in &invalid_ulids {
assert!(
!UlidEngine::validate(ulid_str),
"Should not validate: {}",
ulid_str
);
}
}
#[test]
fn test_ulid_parse_execution() {
let test_ulid = UlidEngine::generate().expect("Should generate test ULID");
let ulid_str = test_ulid.to_string();
let components = UlidEngine::parse(&ulid_str).expect("Should parse valid ULID");
assert_eq!(components.ulid, ulid_str);
assert!(components.valid);
assert!(components.timestamp_ms > 0);
assert!(!components.randomness_hex.is_empty());
let span = create_test_span();
let value = UlidEngine::components_to_value(&components, span);
match value {
Value::Record { val, .. } => {
let record = val.into_owned();
assert!(record.contains("ulid"));
assert!(record.contains("timestamp"));
assert!(record.contains("randomness"));
assert!(record.contains("valid"));
}
_ => panic!("Components should convert to Record value"),
}
let invalid_result = UlidEngine::parse("invalid-ulid");
assert!(invalid_result.is_err(), "Should fail to parse invalid ULID");
}
#[test]
fn test_timestamp_boundary_conditions() {
let test_timestamps = vec![
0u64, 1640995200000u64, u64::MAX - 1000, ];
for timestamp in test_timestamps {
let result = UlidEngine::generate_with_timestamp(timestamp);
if timestamp < u64::MAX - 1000 {
assert!(
result.is_ok(),
"Should generate ULID with timestamp {}",
timestamp
);
let ulid = result.unwrap();
let parsed = UlidEngine::parse(&ulid.to_string()).unwrap();
assert_eq!(parsed.timestamp_ms, timestamp);
}
}
}
#[test]
fn test_ulid_uniqueness_and_sorting() {
let mut ulids = Vec::new();
for _ in 0..10 {
let ulid = UlidEngine::generate().expect("Should generate ULID");
ulids.push(ulid.to_string());
}
let unique_count = ulids.iter().collect::<std::collections::HashSet<_>>().len();
assert_eq!(unique_count, 10, "All ULIDs should be unique");
let sorted_ulids = {
let mut sorted = ulids.clone();
sorted.sort();
sorted
};
assert_eq!(sorted_ulids.len(), ulids.len());
}
#[test]
fn test_error_handling_paths() {
let invalid_inputs = vec![
("", "empty string"),
("invalid", "too short"),
("01AN4Z07BY79KA1307SR9X4MV3EXTRA", "too long"),
("01AN4Z07BY79KA1307SR9X4MV!", "invalid character"),
("not-a-ulid-at-all", "completely invalid"),
];
for (input, description) in invalid_inputs {
assert!(
!UlidEngine::validate(input),
"Should reject {}: {}",
input,
description
);
let parse_result = UlidEngine::parse(input);
assert!(
parse_result.is_err(),
"Parsing should fail for {}",
description
);
}
let over_limit_result = UlidEngine::generate_bulk(10_001);
assert!(
over_limit_result.is_err(),
"Should reject over-limit bulk generation"
);
}
#[test]
fn test_output_value_creation() {
let test_ulid = UlidEngine::generate().expect("Should generate test ULID");
let span = create_test_span();
let single_value = Value::string(test_ulid.to_string(), span);
match single_value {
Value::String { val, .. } => {
assert_eq!(val, test_ulid.to_string());
}
_ => panic!("Single ULID should create String value"),
}
let bulk_ulids = [test_ulid];
let list_values: Vec<Value> = bulk_ulids
.iter()
.map(|ulid| Value::string(ulid.to_string(), span))
.collect();
assert_eq!(list_values.len(), 1);
match &list_values[0] {
Value::String { val, .. } => {
assert_eq!(val, &test_ulid.to_string());
}
_ => panic!("Bulk ULID should create String values"),
}
let pipeline_data = PipelineData::Value(Value::list(list_values, span), None);
match pipeline_data {
PipelineData::Value(Value::List { vals, .. }, None) => {
assert_eq!(vals.len(), 1);
}
_ => panic!("Should create proper PipelineData"),
}
}
}
mod generate_single_ulid_tests {
use super::*;
#[test]
fn test_generates_without_timestamp() {
let span = create_test_span();
let result = generate_single_ulid(None, span).unwrap();
match result {
PipelineData::Value(Value::String { val, .. }, _) => {
assert_eq!(val.len(), crate::ULID_STRING_LENGTH);
}
_ => panic!("Expected string pipeline value"),
}
}
#[test]
fn test_generates_with_timestamp() {
let span = create_test_span();
let result = generate_single_ulid(Some(1704067200000), span).unwrap();
match result {
PipelineData::Value(Value::String { val, .. }, _) => {
assert_eq!(val.len(), crate::ULID_STRING_LENGTH);
}
_ => panic!("Expected string pipeline value"),
}
}
}
mod generate_bulk_ulids_tests {
use super::*;
#[test]
fn test_generates_correct_count() {
let span = create_test_span();
let result = generate_bulk_ulids(5, None, span).unwrap();
match result {
PipelineData::Value(Value::List { vals, .. }, _) => {
assert_eq!(vals.len(), 5);
}
_ => panic!("Expected list pipeline value"),
}
}
#[test]
fn test_negative_count_errors() {
let span = create_test_span();
assert!(generate_bulk_ulids(-1, None, span).is_err());
}
#[test]
fn test_over_max_count_errors() {
let span = create_test_span();
assert!(generate_bulk_ulids(10_001, None, span).is_err());
}
#[test]
fn test_with_timestamp() {
let span = create_test_span();
let result = generate_bulk_ulids(3, Some(1704067200000), span).unwrap();
match result {
PipelineData::Value(Value::List { vals, .. }, _) => {
assert_eq!(vals.len(), 3);
}
_ => panic!("Expected list pipeline value"),
}
}
}
}