use crate::runtime::Runtime;
use crate::script::support::into_option_string;
use mlua::{Lua, Table, Value};
use uuid::Uuid;
pub fn init_module(lua: &Lua, _runtime: &Runtime) -> crate::Result<Table> {
let table = lua.create_table()?;
table.set("new", lua.create_function(lua_new_v4)?)?;
table.set("new_v4", lua.create_function(lua_new_v4)?)?;
table.set("new_v7", lua.create_function(lua_new_v7)?)?;
table.set("new_v4_b64", lua.create_function(lua_new_v4_b64)?)?;
table.set("new_v4_b64u", lua.create_function(lua_new_v4_b64url_nopad)?)?;
table.set("new_v4_b58", lua.create_function(lua_new_v4_b58)?)?;
table.set("new_v7_b64", lua.create_function(lua_new_v7_b64)?)?;
table.set("new_v7_b64u", lua.create_function(lua_new_v7_b64url_nopad)?)?;
table.set("new_v7_b58", lua.create_function(lua_new_v7_b58)?)?;
table.set("to_time_epoch_ms", lua.create_function(lua_to_time_epoch_ms)?)?;
Ok(table)
}
fn lua_new_v4(_lua: &Lua, (): ()) -> mlua::Result<String> {
Ok(uuid_extra::new_v4().to_string())
}
fn lua_new_v7(_lua: &Lua, (): ()) -> mlua::Result<String> {
Ok(uuid_extra::new_v7().to_string())
}
fn lua_new_v4_b64(_lua: &Lua, (): ()) -> mlua::Result<String> {
Ok(uuid_extra::new_v4_b64())
}
fn lua_new_v4_b64url_nopad(_lua: &Lua, (): ()) -> mlua::Result<String> {
Ok(uuid_extra::new_v4_b64url_nopad())
}
fn lua_new_v4_b58(_lua: &Lua, (): ()) -> mlua::Result<String> {
Ok(uuid_extra::new_v4_b58())
}
fn lua_new_v7_b64(_lua: &Lua, (): ()) -> mlua::Result<String> {
Ok(uuid_extra::new_v7_b64())
}
fn lua_new_v7_b64url_nopad(_lua: &Lua, (): ()) -> mlua::Result<String> {
Ok(uuid_extra::new_v7_b64url_nopad())
}
fn lua_new_v7_b58(_lua: &Lua, (): ()) -> mlua::Result<String> {
Ok(uuid_extra::new_v7_b58())
}
fn lua_to_time_epoch_ms(_lua: &Lua, value: Value) -> mlua::Result<Value> {
let Some(uuid_str) = into_option_string(value, "aip.uuid.to_time_epoch_ms")? else {
return Ok(Value::Nil);
};
match Uuid::parse_str(&uuid_str) {
Ok(uuid) => {
if let Some(timestamp) = uuid.get_timestamp() {
let (secs, nanos) = timestamp.to_unix();
let millis = (secs as i64 * 1000) + (nanos as i64 / 1_000_000);
Ok(Value::Integer(millis))
} else {
Ok(Value::Nil)
}
}
Err(_) => {
Ok(Value::Nil)
}
}
}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use super::*;
use crate::_test_support::{eval_lua, setup_lua};
use uuid::Uuid;
fn is_base64_url_chars(s: &str) -> bool {
s.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}
fn is_base64_standard_chars(s: &str) -> bool {
s.chars().all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=')
}
fn is_base58_chars(s: &str) -> bool {
const BASE58_CHARS: &str = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
s.chars().all(|c| BASE58_CHARS.contains(c))
}
#[tokio::test]
async fn test_lua_aip_uuid_new_v4_simple() -> Result<()> {
let lua = setup_lua(init_module, "uuid").await?;
let res_new = eval_lua(&lua, "return aip.uuid.new()")?;
let res_new_v4 = eval_lua(&lua, "return aip.uuid.new_v4()")?;
let uuid_str_new = res_new.as_str().ok_or("aip.uuid.new() should return a string")?;
let parsed_uuid_new = Uuid::parse_str(uuid_str_new)?;
assert_eq!(
parsed_uuid_new.get_version_num(),
4,
"UUID from new() should be version 4"
);
let uuid_str_new_v4 = res_new_v4.as_str().ok_or("aip.uuid.new_v4() should return a string")?;
let parsed_uuid_new_v4 = Uuid::parse_str(uuid_str_new_v4)?;
assert_eq!(
parsed_uuid_new_v4.get_version_num(),
4,
"UUID from new_v4() should be version 4"
);
Ok(())
}
#[tokio::test]
async fn test_lua_aip_uuid_new_v7_simple() -> Result<()> {
let lua = setup_lua(init_module, "uuid").await?;
let res = eval_lua(&lua, "return aip.uuid.new_v7()")?;
let uuid_str = res.as_str().ok_or("Result should be a string")?;
let parsed_uuid = Uuid::parse_str(uuid_str)?;
assert_eq!(parsed_uuid.get_version_num(), 7, "UUID should be version 7");
Ok(())
}
#[tokio::test]
async fn test_lua_aip_uuid_new_v4_b64_simple() -> Result<()> {
let lua = setup_lua(init_module, "uuid").await?;
let res = eval_lua(&lua, "return aip.uuid.new_v4_b64()")?;
let b64_str = res.as_str().ok_or("Result should be a string")?;
assert!(
is_base64_standard_chars(b64_str),
"String should contain only Base64 standard characters (possibly with padding)"
);
assert!(
(22..=24).contains(&b64_str.len()),
"Base64 string length for UUIDv4 should be between 22 and 24. Got: {b64_str_len}",
b64_str_len = b64_str.len()
);
Ok(())
}
#[tokio::test]
async fn test_lua_aip_uuid_new_v4_b64u_simple() -> Result<()> {
let lua = setup_lua(init_module, "uuid").await?;
let res = eval_lua(&lua, "return aip.uuid.new_v4_b64u()")?;
let b64u_str = res.as_str().ok_or("Result should be a string")?;
assert!(
is_base64_url_chars(b64u_str),
"String should contain only Base64 URL safe characters"
);
assert_eq!(
b64u_str.len(),
22,
"Base64 URL (no pad) string length for UUID should be 22"
);
assert!(!b64u_str.contains('='), "String should not contain padding");
Ok(())
}
#[tokio::test]
async fn test_lua_aip_uuid_new_v4_b58_simple() -> Result<()> {
let lua = setup_lua(init_module, "uuid").await?;
let res = eval_lua(&lua, "return aip.uuid.new_v4_b58()")?;
let b58_str = res.as_str().ok_or("Result should be a string")?;
assert!(is_base58_chars(b58_str), "String should contain only Base58 characters");
assert!(
(21..=23).contains(&b58_str.len()),
"Base58 string length for UUIDv4 should be around 21-23. Got: {b58_str_len}",
b58_str_len = b58_str.len()
);
Ok(())
}
#[tokio::test]
async fn test_lua_aip_uuid_new_v7_b64_simple() -> Result<()> {
let lua = setup_lua(init_module, "uuid").await?;
let res = eval_lua(&lua, "return aip.uuid.new_v7_b64()")?;
let b64_str = res.as_str().ok_or("Result should be a string")?;
assert!(
is_base64_standard_chars(b64_str),
"String should contain only Base64 standard characters"
);
assert!(
(22..=24).contains(&b64_str.len()),
"Base64 string length for UUIDv7 should be between 22 and 24. Got: {b64_str_len}",
b64_str_len = b64_str.len()
);
Ok(())
}
#[tokio::test]
async fn test_lua_aip_uuid_new_v7_b64u_simple() -> Result<()> {
let lua = setup_lua(init_module, "uuid").await?;
let res = eval_lua(&lua, "return aip.uuid.new_v7_b64u()")?;
let b64u_str = res.as_str().ok_or("Result should be a string")?;
assert!(
is_base64_url_chars(b64u_str),
"String should contain only Base64 URL safe characters"
);
assert_eq!(
b64u_str.len(),
22,
"Base64 URL (no pad) string length for UUIDv7 should be 22"
);
assert!(!b64u_str.contains('='), "String should not contain padding");
Ok(())
}
#[tokio::test]
async fn test_lua_aip_uuid_new_v7_b58_simple() -> Result<()> {
let lua = setup_lua(init_module, "uuid").await?;
let res = eval_lua(&lua, "return aip.uuid.new_v7_b58()")?;
let b58_str = res.as_str().ok_or("Result should be a string")?;
assert!(is_base58_chars(b58_str), "String should contain only Base58 characters");
assert!(
(21..=23).contains(&b58_str.len()),
"Base58 string length for UUIDv7 should be around 21-23. Got: {b58_str_len}",
b58_str_len = b58_str.len()
);
Ok(())
}
#[tokio::test]
async fn test_lua_aip_uuid_to_time_epoch_ms_nil_input() -> Result<()> {
let lua = setup_lua(init_module, "uuid").await?;
let script = "return aip.uuid.to_time_epoch_ms(nil)";
let result_val = eval_lua(&lua, script)?;
assert!(result_val.is_null(), "Expected nil for nil input");
Ok(())
}
#[tokio::test]
async fn test_lua_aip_uuid_to_time_epoch_ms_invalid_uuid_string() -> Result<()> {
let lua = setup_lua(init_module, "uuid").await?;
let script = "return aip.uuid.to_time_epoch_ms('not-a-valid-uuid')";
let result_val = eval_lua(&lua, script)?;
assert!(result_val.is_null(), "Expected nil for invalid UUID string");
Ok(())
}
#[tokio::test]
async fn test_lua_aip_uuid_to_time_epoch_ms_v4_uuid() -> Result<()> {
let lua = setup_lua(init_module, "uuid").await?;
let v4_uuid_str = uuid_extra::new_v4().to_string();
let script = format!("return aip.uuid.to_time_epoch_ms('{v4_uuid_str}')");
let result_val = eval_lua(&lua, &script)?;
assert!(
result_val.is_null(),
"Expected nil for V4 UUID as it has no extractable timestamp"
);
Ok(())
}
#[tokio::test]
async fn test_lua_aip_uuid_to_time_epoch_ms_v7_uuid() -> Result<()> {
let lua = setup_lua(init_module, "uuid").await?;
let v7_uuid = uuid_extra::new_v7(); let v7_uuid_str = v7_uuid.to_string();
let ts_opt = v7_uuid.get_timestamp();
let expected_millis = ts_opt
.map(|ts| {
let (secs, nanos) = ts.to_unix();
(secs as i64 * 1000) + (nanos as i64 / 1_000_000)
})
.ok_or("Failed to get timestamp from generated V7 UUID")?;
let script = format!("return aip.uuid.to_time_epoch_ms('{v7_uuid_str}')");
let result_val = eval_lua(&lua, &script)?;
let actual_millis = result_val.as_i64().ok_or("Result should be an integer (milliseconds)")?;
assert_eq!(actual_millis, expected_millis, "Timestamp mismatch for V7 UUID");
Ok(())
}
}