use rhai::{Array, Dynamic, Engine, Map};
use std::cell::RefCell;
#[derive(Debug, Clone)]
pub enum CapturedMessage {
Stdout(String),
Stderr(String),
}
thread_local! {
static CAPTURED_PRINTS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
static CAPTURED_EPRINTS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
static CAPTURED_MESSAGES: RefCell<Vec<CapturedMessage>> = const { RefCell::new(Vec::new()) };
static PARALLEL_MODE: RefCell<bool> = const { RefCell::new(false) };
static SUPPRESS_SIDE_EFFECTS: RefCell<bool> = const { RefCell::new(false) };
}
pub fn capture_print(message: String) {
CAPTURED_PRINTS.with(|prints| {
prints.borrow_mut().push(message);
});
}
pub fn capture_eprint(message: String) {
CAPTURED_EPRINTS.with(|eprints| {
eprints.borrow_mut().push(message);
});
}
pub fn take_captured_prints() -> Vec<String> {
CAPTURED_PRINTS.with(|prints| std::mem::take(&mut *prints.borrow_mut()))
}
pub fn take_captured_eprints() -> Vec<String> {
CAPTURED_EPRINTS.with(|eprints| std::mem::take(&mut *eprints.borrow_mut()))
}
pub fn capture_message(message: CapturedMessage) {
CAPTURED_MESSAGES.with(|messages| {
messages.borrow_mut().push(message);
});
}
pub fn capture_stdout(message: String) {
capture_message(CapturedMessage::Stdout(message));
}
pub fn capture_stderr(message: String) {
capture_message(CapturedMessage::Stderr(message));
}
pub fn take_captured_messages() -> Vec<CapturedMessage> {
CAPTURED_MESSAGES.with(|messages| std::mem::take(&mut *messages.borrow_mut()))
}
pub fn clear_captured_prints() {
CAPTURED_PRINTS.with(|prints| {
prints.borrow_mut().clear();
});
}
pub fn clear_captured_eprints() {
CAPTURED_EPRINTS.with(|eprints| {
eprints.borrow_mut().clear();
});
}
pub fn set_parallel_mode(enabled: bool) {
PARALLEL_MODE.with(|mode| {
*mode.borrow_mut() = enabled;
});
}
pub fn is_parallel_mode() -> bool {
PARALLEL_MODE.with(|mode| *mode.borrow())
}
pub fn set_suppress_side_effects(suppress: bool) {
SUPPRESS_SIDE_EFFECTS.with(|flag| {
*flag.borrow_mut() = suppress;
});
}
pub fn is_suppress_side_effects() -> bool {
SUPPRESS_SIDE_EFFECTS.with(|flag| *flag.borrow())
}
fn mask_ip_impl(ip: &str, octets_to_mask: usize) -> String {
let parts: Vec<&str> = ip.split('.').collect();
if parts.len() != 4 {
return ip.to_string(); }
for part in &parts {
if part.parse::<u8>().is_err() {
return ip.to_string(); }
}
let mut result = parts.clone();
let mask_count = octets_to_mask.clamp(1, 4);
for item in result.iter_mut().skip(4 - mask_count) {
*item = "X";
}
result.join(".")
}
fn is_private_ip_impl(ip: &str) -> bool {
let parts: Vec<&str> = ip.split('.').collect();
if parts.len() != 4 {
return false; }
let octets: Result<Vec<u8>, _> = parts.iter().map(|s| s.parse::<u8>()).collect();
let octets = match octets {
Ok(o) => o,
Err(_) => return false,
};
match octets[0] {
10 => true, 172 => octets[1] >= 16 && octets[1] <= 31, 192 => octets[1] == 168, 127 => true, _ => false,
}
}
fn parse_kv_impl(text: &str, sep: Option<&str>, kv_sep: &str) -> rhai::Map {
let mut map = rhai::Map::new();
let pairs: Vec<&str> = if let Some(separator) = sep {
text.split(separator).collect()
} else {
text.split_whitespace().collect()
};
for pair in pairs {
let pair = pair.trim();
if pair.is_empty() {
continue;
}
if let Some(kv_pos) = pair.find(kv_sep) {
let key = pair[..kv_pos].trim();
let value = pair[kv_pos + kv_sep.len()..].trim();
if !key.is_empty() {
map.insert(key.into(), rhai::Dynamic::from(value.to_string()));
}
}
else if !pair.is_empty() {
map.insert(pair.into(), rhai::Dynamic::from(String::new()));
}
}
map
}
pub fn register_functions(engine: &mut Engine) {
engine.register_fn("eprint", |message: rhai::Dynamic| {
if is_suppress_side_effects() {
return;
}
let msg = message.to_string();
if is_parallel_mode() {
capture_eprint(msg.clone());
capture_stderr(msg);
} else {
eprintln!("{}", msg);
}
});
engine.register_fn("contains", |text: &str, pattern: &str| {
text.contains(pattern)
});
engine.register_fn("matches", |text: &str, pattern: &str| {
regex::Regex::new(pattern)
.map(|re| re.is_match(text))
.unwrap_or(false)
});
engine.register_fn("to_int", |text: &str| -> rhai::Dynamic {
text.parse::<i64>()
.map(Dynamic::from)
.unwrap_or(Dynamic::from(0i64))
});
engine.register_fn("to_float", |text: &str| -> rhai::Dynamic {
text.parse::<f64>()
.map(Dynamic::from)
.unwrap_or(Dynamic::UNIT)
});
engine.register_fn("slice", |s: &str, spec: &str| -> String {
let chars: Vec<char> = s.chars().collect();
let len = chars.len() as i32;
if len == 0 {
return String::new();
}
let parts: Vec<&str> = spec.split(':').collect();
let step = if parts.len() > 2 && !parts[2].trim().is_empty() {
parts[2].trim().parse::<i32>().unwrap_or(1)
} else {
1
};
if step == 0 {
return String::new();
}
let (default_start, default_end) = if step > 0 { (0, len) } else { (len - 1, -1) };
let start = if !parts.is_empty() && !parts[0].trim().is_empty() {
let mut s = parts[0].trim().parse::<i32>().unwrap_or(default_start);
if s < 0 {
s += len;
}
if step > 0 {
s.clamp(0, len)
} else {
s.clamp(0, len - 1)
}
} else {
default_start
};
let end = if parts.len() > 1 && !parts[1].trim().is_empty() {
let mut e = parts[1].trim().parse::<i32>().unwrap_or(default_end);
if e < 0 {
e += len;
}
if step > 0 {
e.clamp(0, len)
} else {
e.clamp(-1, len - 1)
}
} else {
default_end
};
let mut result = String::new();
let mut i = start;
if step > 0 {
while i < end {
if i >= 0 && i < len {
result.push(chars[i as usize]);
}
i += step;
}
} else {
while i > end {
if i >= 0 && i < len {
result.push(chars[i as usize]);
}
i += step;
}
}
result
});
engine.register_fn("after", |text: &str, substring: &str| -> String {
if let Some(pos) = text.find(substring) {
text[pos + substring.len()..].to_string()
} else {
String::new()
}
});
engine.register_fn("before", |text: &str, substring: &str| -> String {
if let Some(pos) = text.find(substring) {
text[..pos].to_string()
} else {
String::new()
}
});
engine.register_fn(
"between",
|text: &str, start_substring: &str, end_substring: &str| -> String {
if let Some(start_pos) = text.find(start_substring) {
let start_idx = start_pos + start_substring.len();
let remainder = &text[start_idx..];
if end_substring.is_empty() {
remainder.to_string()
} else if let Some(end_pos) = remainder.find(end_substring) {
remainder[..end_pos].to_string()
} else {
String::new()
}
} else {
String::new()
}
},
);
engine.register_fn("starting_with", |text: &str, prefix: &str| -> String {
if text.starts_with(prefix) {
text.to_string()
} else {
String::new()
}
});
engine.register_fn("ending_with", |text: &str, suffix: &str| -> String {
if text.ends_with(suffix) {
text.to_string()
} else {
String::new()
}
});
engine.register_fn("parse_kv", |text: &str| -> rhai::Map {
parse_kv_impl(text, None, "=")
});
engine.register_fn("parse_kv", |text: &str, sep: &str| -> rhai::Map {
parse_kv_impl(text, Some(sep), "=")
});
engine.register_fn(
"parse_kv",
|text: &str, sep: &str, kv_sep: &str| -> rhai::Map {
parse_kv_impl(text, Some(sep), kv_sep)
},
);
engine.register_fn(
"parse_kv",
|text: &str, _sep: (), kv_sep: &str| -> rhai::Map { parse_kv_impl(text, None, kv_sep) },
);
engine.register_fn("lower", |text: &str| -> String { text.to_lowercase() });
engine.register_fn("upper", |text: &str| -> String { text.to_uppercase() });
engine.register_fn("is_digit", |text: &str| -> bool {
!text.is_empty() && text.chars().all(|c| c.is_ascii_digit())
});
engine.register_fn("count", |text: &str, pattern: &str| -> i64 {
if pattern.is_empty() {
return 0;
}
text.matches(pattern).count() as i64
});
engine.register_fn("strip", |text: &str| -> String { text.trim().to_string() });
engine.register_fn("strip", |text: &str, chars: &str| -> String {
let chars_to_remove: std::collections::HashSet<char> = chars.chars().collect();
text.trim_matches(|c: char| chars_to_remove.contains(&c))
.to_string()
});
engine.register_fn("join", |separator: &str, items: rhai::Array| -> String {
items
.into_iter()
.filter_map(|item| item.into_string().ok())
.collect::<Vec<String>>()
.join(separator)
});
engine.register_fn("join", |items: rhai::Array, separator: &str| -> String {
items
.into_iter()
.filter_map(|item| item.into_string().ok())
.collect::<Vec<String>>()
.join(separator)
});
engine.register_fn("extract_re", |text: &str, pattern: &str| -> String {
match regex::Regex::new(pattern) {
Ok(re) => {
if let Some(captures) = re.captures(text) {
if captures.len() > 1 {
captures
.get(1)
.map(|m| m.as_str())
.unwrap_or("")
.to_string()
} else {
captures
.get(0)
.map(|m| m.as_str())
.unwrap_or("")
.to_string()
}
} else {
String::new()
}
}
Err(_) => String::new(), }
});
engine.register_fn(
"extract_re",
|text: &str, pattern: &str, group: i64| -> String {
match regex::Regex::new(pattern) {
Ok(re) => {
if let Some(captures) = re.captures(text) {
let group_idx = if group < 0 {
0
} else {
group as usize
};
captures
.get(group_idx)
.map(|m| m.as_str())
.unwrap_or("")
.to_string()
} else {
String::new()
}
}
Err(_) => String::new(), }
},
);
engine.register_fn(
"extract_all_re",
|text: &str, pattern: &str| -> rhai::Array {
match regex::Regex::new(pattern) {
Ok(re) => {
let mut results = rhai::Array::new();
for captures in re.captures_iter(text) {
if captures.len() > 1 {
let groups: rhai::Array = captures
.iter()
.skip(1) .filter_map(|m| {
m.map(|match_| Dynamic::from(match_.as_str().to_string()))
})
.collect();
results.push(Dynamic::from(groups));
} else {
if let Some(full_match) = captures.get(0) {
results.push(Dynamic::from(full_match.as_str().to_string()));
}
}
}
results
}
Err(_) => rhai::Array::new(), }
},
);
engine.register_fn(
"extract_all_re",
|text: &str, pattern: &str, group: i64| -> rhai::Array {
match regex::Regex::new(pattern) {
Ok(re) => {
let mut results = rhai::Array::new();
let group_idx = if group < 0 {
0
} else {
group as usize
};
for captures in re.captures_iter(text) {
if let Some(group_match) = captures.get(group_idx) {
results.push(Dynamic::from(group_match.as_str().to_string()));
}
}
results
}
Err(_) => rhai::Array::new(), }
},
);
engine.register_fn("split_re", |text: &str, pattern: &str| -> rhai::Array {
match regex::Regex::new(pattern) {
Ok(re) => re
.split(text)
.map(|s| Dynamic::from(s.to_string()))
.collect(),
Err(_) => vec![Dynamic::from(text.to_string())], }
});
engine.register_fn(
"replace_re",
|text: &str, pattern: &str, replacement: &str| -> String {
match regex::Regex::new(pattern) {
Ok(re) => re.replace_all(text, replacement).to_string(),
Err(_) => text.to_string(), }
},
);
engine.register_fn("extract_ip", |text: &str| -> String {
let ip_pattern = r"\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b";
match regex::Regex::new(ip_pattern) {
Ok(re) => {
re.find(text)
.map(|m| m.as_str().to_string())
.unwrap_or_else(String::new)
}
Err(_) => String::new(),
}
});
engine.register_fn("extract_ips", |text: &str| -> rhai::Array {
let ip_pattern = r"\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b";
match regex::Regex::new(ip_pattern) {
Ok(re) => re
.find_iter(text)
.map(|m| Dynamic::from(m.as_str().to_string()))
.collect(),
Err(_) => rhai::Array::new(),
}
});
engine.register_fn("mask_ip", |ip: &str| -> String {
mask_ip_impl(ip, 1) });
engine.register_fn("mask_ip", |ip: &str, octets: i64| -> String {
mask_ip_impl(ip, octets.clamp(1, 4) as usize) });
engine.register_fn("is_private_ip", |ip: &str| -> bool {
is_private_ip_impl(ip)
});
engine.register_fn("extract_url", |text: &str| -> String {
let url_pattern = r##"https?://[^\s<>"]+[^\s<>".,;!?]"##;
match regex::Regex::new(url_pattern) {
Ok(re) => re
.find(text)
.map(|m| m.as_str().to_string())
.unwrap_or_else(String::new),
Err(_) => String::new(),
}
});
engine.register_fn("extract_domain", |text: &str| -> String {
let url_pattern = r##"https?://([^/\s<>"]+)"##;
let email_pattern = r##"[a-zA-Z0-9._%+-]+@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})"##;
if let Ok(re) = regex::Regex::new(url_pattern) {
if let Some(caps) = re.captures(text) {
if let Some(domain) = caps.get(1) {
return domain.as_str().to_string();
}
}
}
if let Ok(re) = regex::Regex::new(email_pattern) {
if let Some(caps) = re.captures(text) {
if let Some(domain) = caps.get(1) {
return domain.as_str().to_string();
}
}
}
String::new()
});
engine.register_fn("unflatten", |map: rhai::Map| -> rhai::Map {
unflatten_map(map, "_")
});
engine.register_fn(
"unflatten",
|map: rhai::Map, separator: &str| -> rhai::Map { unflatten_map(map, separator) },
);
}
fn unflatten_map(flat_map: Map, separator: &str) -> Map {
let mut result = Map::new();
let mut key_analysis = std::collections::HashMap::new();
for flat_key in flat_map.keys() {
let parts: Vec<&str> = flat_key.split(separator).collect();
analyze_key_path(&parts, &mut key_analysis, separator);
}
for (flat_key, value) in flat_map {
let parts: Vec<&str> = flat_key.split(separator).collect();
if !parts.is_empty() {
set_nested_value(&mut result, &parts, value, &key_analysis, separator);
}
}
result
}
fn analyze_key_path(
parts: &[&str],
analysis: &mut std::collections::HashMap<String, ContainerType>,
separator: &str,
) {
let mut current_path = String::new();
for (i, part) in parts.iter().enumerate() {
if i > 0 {
current_path.push_str(separator);
}
current_path.push_str(part);
if i + 1 < parts.len() {
let next_part = parts[i + 1];
let container_type = if is_array_index(next_part) {
ContainerType::Array
} else {
ContainerType::Object
};
match analysis.get(¤t_path) {
Some(existing_type) => {
if *existing_type != container_type {
analysis.insert(current_path.clone(), ContainerType::Object);
}
}
None => {
analysis.insert(current_path.clone(), container_type);
}
}
}
}
}
fn is_array_index(s: &str) -> bool {
s.parse::<usize>().is_ok()
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ContainerType {
Array,
Object,
}
fn set_nested_value(
container: &mut Map,
parts: &[&str],
value: Dynamic,
analysis: &std::collections::HashMap<String, ContainerType>,
separator: &str,
) {
set_nested_value_with_path(container, parts, value, analysis, separator, &[]);
}
fn set_nested_value_with_path(
container: &mut Map,
parts: &[&str],
value: Dynamic,
analysis: &std::collections::HashMap<String, ContainerType>,
separator: &str,
parent_path: &[&str],
) {
if parts.is_empty() {
return;
}
if parts.len() == 1 {
container.insert(parts[0].into(), value);
return;
}
let current_key = parts[0];
let remaining_parts = &parts[1..];
let mut full_path = parent_path.to_vec();
full_path.push(current_key);
let lookup_key = full_path.join(separator);
let container_type = analysis
.get(&lookup_key)
.copied()
.unwrap_or(ContainerType::Object);
match container_type {
ContainerType::Object => {
let nested_map = container
.entry(current_key.into())
.or_insert_with(|| Dynamic::from(Map::new()));
if let Some(mut map) = nested_map.clone().try_cast::<Map>() {
let mut new_path = parent_path.to_vec();
new_path.push(current_key);
set_nested_value_with_path(
&mut map,
remaining_parts,
value,
analysis,
separator,
&new_path,
);
*nested_map = Dynamic::from(map);
}
}
ContainerType::Array => {
let nested_array = container
.entry(current_key.into())
.or_insert_with(|| Dynamic::from(Array::new()));
if let Some(mut array) = nested_array.clone().try_cast::<Array>() {
let mut new_path = parent_path.to_vec();
new_path.push(current_key);
set_array_value_with_path(
&mut array,
remaining_parts,
value,
analysis,
separator,
&new_path,
);
*nested_array = Dynamic::from(array);
}
}
}
}
fn set_array_value_with_path(
array: &mut Array,
parts: &[&str],
value: Dynamic,
analysis: &std::collections::HashMap<String, ContainerType>,
separator: &str,
parent_path: &[&str],
) {
if parts.is_empty() {
return;
}
if parts.len() == 1 {
if let Ok(index) = parts[0].parse::<usize>() {
while array.len() <= index {
array.push(Dynamic::UNIT);
}
array[index] = value;
}
return;
}
let current_index_str = parts[0];
let remaining_parts = &parts[1..];
if let Ok(index) = current_index_str.parse::<usize>() {
while array.len() <= index {
array.push(Dynamic::UNIT);
}
let mut full_path = parent_path.to_vec();
full_path.push(current_index_str);
let lookup_key = full_path.join(separator);
let container_type = analysis
.get(&lookup_key)
.copied()
.unwrap_or(ContainerType::Object);
match container_type {
ContainerType::Object => {
if array[index].is_unit() {
array[index] = Dynamic::from(Map::new());
}
if let Some(mut map) = array[index].clone().try_cast::<Map>() {
let mut new_path = parent_path.to_vec();
new_path.push(current_index_str);
set_nested_value_with_path(
&mut map,
remaining_parts,
value,
analysis,
separator,
&new_path,
);
array[index] = Dynamic::from(map);
}
}
ContainerType::Array => {
if array[index].is_unit() {
array[index] = Dynamic::from(Array::new());
}
if let Some(mut nested_array) = array[index].clone().try_cast::<Array>() {
let mut new_path = parent_path.to_vec();
new_path.push(current_index_str);
set_array_value_with_path(
&mut nested_array,
remaining_parts,
value,
analysis,
separator,
&new_path,
);
array[index] = Dynamic::from(nested_array);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rhai::Scope;
#[test]
fn test_after_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "hello world test");
let result: String = engine
.eval_with_scope(&mut scope, r#"text.after("world")"#)
.unwrap();
assert_eq!(result, " test");
let result: String = engine
.eval_with_scope(&mut scope, r#"text.after("missing")"#)
.unwrap();
assert_eq!(result, "");
}
#[test]
fn test_before_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "hello world test");
let result: String = engine
.eval_with_scope(&mut scope, r#"text.before("world")"#)
.unwrap();
assert_eq!(result, "hello ");
let result: String = engine
.eval_with_scope(&mut scope, r#"text.before("missing")"#)
.unwrap();
assert_eq!(result, "");
}
#[test]
fn test_between_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "start[content]end");
let result: String = engine
.eval_with_scope(&mut scope, r#"text.between("[", "]")"#)
.unwrap();
assert_eq!(result, "content");
let result: String = engine
.eval_with_scope(&mut scope, r#"text.between("missing", "]")"#)
.unwrap();
assert_eq!(result, "");
let result: String = engine
.eval_with_scope(&mut scope, r#"text.between("[", "missing")"#)
.unwrap();
assert_eq!(result, "");
let result: String = engine
.eval_with_scope(&mut scope, r#"text.between("[", "")"#)
.unwrap();
assert_eq!(result, "content]end");
scope.push("log", "ERROR: connection failed");
let result: String = engine
.eval_with_scope(&mut scope, r#"log.between("ERROR: ", "")"#)
.unwrap();
assert_eq!(result, "connection failed");
}
#[test]
fn test_starting_with_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "hello world");
let result: String = engine
.eval_with_scope(&mut scope, r#"text.starting_with("hello")"#)
.unwrap();
assert_eq!(result, "hello world");
let result: String = engine
.eval_with_scope(&mut scope, r#"text.starting_with("world")"#)
.unwrap();
assert_eq!(result, "");
}
#[test]
fn test_ending_with_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "hello world");
let result: String = engine
.eval_with_scope(&mut scope, r#"text.ending_with("world")"#)
.unwrap();
assert_eq!(result, "hello world");
let result: String = engine
.eval_with_scope(&mut scope, r#"text.ending_with("hello")"#)
.unwrap();
assert_eq!(result, "");
}
#[test]
fn test_parse_kv_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "key1=value1 key2=value2");
let result: rhai::Map = engine
.eval_with_scope(&mut scope, r#"parse_kv(text)"#)
.unwrap();
assert_eq!(
result.get("key1").unwrap().clone().into_string().unwrap(),
"value1"
);
assert_eq!(
result.get("key2").unwrap().clone().into_string().unwrap(),
"value2"
);
scope.push("text2", "key1=value1,key2=value2");
let result: rhai::Map = engine
.eval_with_scope(&mut scope, r#"parse_kv(text2, ",")"#)
.unwrap();
assert_eq!(
result.get("key1").unwrap().clone().into_string().unwrap(),
"value1"
);
assert_eq!(
result.get("key2").unwrap().clone().into_string().unwrap(),
"value2"
);
scope.push("text3", "key1:value1 key2:value2");
let result: rhai::Map = engine
.eval_with_scope(&mut scope, r#"parse_kv(text3, (), ":")"#)
.unwrap();
assert_eq!(
result.get("key1").unwrap().clone().into_string().unwrap(),
"value1"
);
assert_eq!(
result.get("key2").unwrap().clone().into_string().unwrap(),
"value2"
);
scope.push("text4", r#"key1="quoted" key2=simple"#);
let result: rhai::Map = engine
.eval_with_scope(&mut scope, r#"parse_kv(text4)"#)
.unwrap();
assert_eq!(
result.get("key1").unwrap().clone().into_string().unwrap(),
"\"quoted\""
);
assert_eq!(
result.get("key2").unwrap().clone().into_string().unwrap(),
"simple"
);
scope.push("text5", "key1=value1 standalone key2=value2");
let result: rhai::Map = engine
.eval_with_scope(&mut scope, r#"parse_kv(text5)"#)
.unwrap();
assert_eq!(
result.get("key1").unwrap().clone().into_string().unwrap(),
"value1"
);
assert_eq!(
result
.get("standalone")
.unwrap()
.clone()
.into_string()
.unwrap(),
""
);
assert_eq!(
result.get("key2").unwrap().clone().into_string().unwrap(),
"value2"
);
scope.push("empty", "");
let result: rhai::Map = engine
.eval_with_scope(&mut scope, r#"parse_kv(empty)"#)
.unwrap();
assert!(result.is_empty());
scope.push("spaces", " key1=value1 key2=value2 ");
let result: rhai::Map = engine
.eval_with_scope(&mut scope, r#"parse_kv(spaces)"#)
.unwrap();
assert_eq!(
result.get("key1").unwrap().clone().into_string().unwrap(),
"value1"
);
assert_eq!(
result.get("key2").unwrap().clone().into_string().unwrap(),
"value2"
);
scope.push("empty_vals", "key1= key2=value2");
let result: rhai::Map = engine
.eval_with_scope(&mut scope, r#"parse_kv(empty_vals)"#)
.unwrap();
assert_eq!(
result.get("key1").unwrap().clone().into_string().unwrap(),
""
);
assert_eq!(
result.get("key2").unwrap().clone().into_string().unwrap(),
"value2"
);
}
#[test]
fn test_lower_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "Hello World");
let result: String = engine
.eval_with_scope(&mut scope, r#"text.lower()"#)
.unwrap();
assert_eq!(result, "hello world");
scope.push("mixed", "MiXeD cAsE");
let result: String = engine
.eval_with_scope(&mut scope, r#"mixed.lower()"#)
.unwrap();
assert_eq!(result, "mixed case");
}
#[test]
fn test_upper_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "Hello World");
let result: String = engine
.eval_with_scope(&mut scope, r#"text.upper()"#)
.unwrap();
assert_eq!(result, "HELLO WORLD");
scope.push("mixed", "MiXeD cAsE");
let result: String = engine
.eval_with_scope(&mut scope, r#"mixed.upper()"#)
.unwrap();
assert_eq!(result, "MIXED CASE");
}
#[test]
fn test_is_digit_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("digits", "12345");
scope.push("mixed", "123abc");
scope.push("empty", "");
scope.push("letters", "abcde");
let result: bool = engine
.eval_with_scope(&mut scope, r#"digits.is_digit()"#)
.unwrap();
assert!(result);
let result: bool = engine
.eval_with_scope(&mut scope, r#"mixed.is_digit()"#)
.unwrap();
assert!(!result);
let result: bool = engine
.eval_with_scope(&mut scope, r#"empty.is_digit()"#)
.unwrap();
assert!(!result);
let result: bool = engine
.eval_with_scope(&mut scope, r#"letters.is_digit()"#)
.unwrap();
assert!(!result);
}
#[test]
fn test_count_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "hello world hello");
scope.push("empty", "");
let result: i64 = engine
.eval_with_scope(&mut scope, r#"text.count("hello")"#)
.unwrap();
assert_eq!(result, 2);
let result: i64 = engine
.eval_with_scope(&mut scope, r#"text.count("l")"#)
.unwrap();
assert_eq!(result, 5);
let result: i64 = engine
.eval_with_scope(&mut scope, r#"text.count("missing")"#)
.unwrap();
assert_eq!(result, 0);
let result: i64 = engine
.eval_with_scope(&mut scope, r#"empty.count("x")"#)
.unwrap();
assert_eq!(result, 0);
let result: i64 = engine
.eval_with_scope(&mut scope, r#"text.count("")"#)
.unwrap();
assert_eq!(result, 0);
}
#[test]
fn test_strip_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", " hello world ");
scope.push("custom", "###hello world###");
let result: String = engine
.eval_with_scope(&mut scope, r#"text.strip()"#)
.unwrap();
assert_eq!(result, "hello world");
let result: String = engine
.eval_with_scope(&mut scope, r##"custom.strip("#")"##)
.unwrap();
assert_eq!(result, "hello world");
scope.push("mixed", " ##hello world## ");
let result: String = engine
.eval_with_scope(&mut scope, r##"mixed.strip(" #")"##)
.unwrap();
assert_eq!(result, "hello world");
}
#[test]
fn test_join_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
let result: String = engine
.eval_with_scope(&mut scope, r#"",".join(["a", "b", "c"])"#)
.unwrap();
assert_eq!(result, "a,b,c");
let result: String = engine
.eval_with_scope(&mut scope, r#"" ".join(["hello", "world"])"#)
.unwrap();
assert_eq!(result, "hello world");
let result: String = engine
.eval_with_scope(&mut scope, r#""-".join(["one"])"#)
.unwrap();
assert_eq!(result, "one");
let result: String = engine
.eval_with_scope(&mut scope, r#"",".join([])"#)
.unwrap();
assert_eq!(result, "");
let result: String = engine
.eval_with_scope(&mut scope, r#"",".join(["a", 123, "b"])"#)
.unwrap();
assert_eq!(result, "a,b");
let result: String = engine
.eval_with_scope(&mut scope, r#"["a", "b", "c"].join(",")"#)
.unwrap();
assert_eq!(result, "a,b,c");
let result: String = engine
.eval_with_scope(&mut scope, r#"["hello", "world"].join(" ")"#)
.unwrap();
assert_eq!(result, "hello world");
let result: String = engine
.eval_with_scope(&mut scope, r#"["one"].join("-")"#)
.unwrap();
assert_eq!(result, "one");
let result: String = engine
.eval_with_scope(&mut scope, r#"[].join(",")"#)
.unwrap();
assert_eq!(result, "");
let result: String = engine
.eval_with_scope(&mut scope, r#"["a", 123, "b"].join(",")"#)
.unwrap();
assert_eq!(result, "a,b");
}
#[test]
fn test_extract_re_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "user=alice status=200");
let result: String = engine
.eval_with_scope(&mut scope, r##"text.extract_re("user=(\\w+)")"##)
.unwrap();
assert_eq!(result, "alice");
let result: String = engine
.eval_with_scope(&mut scope, r##"text.extract_re("\\d+")"##)
.unwrap();
assert_eq!(result, "200");
let result: String = engine
.eval_with_scope(&mut scope, r##"text.extract_re("missing")"##)
.unwrap();
assert_eq!(result, "");
let result: String = engine
.eval_with_scope(&mut scope, r##"text.extract_re("[")"##)
.unwrap();
assert_eq!(result, "");
}
#[test]
fn test_extract_re_with_group_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "user=alice status=200 level=info");
let result: String = engine
.eval_with_scope(
&mut scope,
r##"text.extract_re("user=(\\w+).*status=(\\d+)", 0)"##,
)
.unwrap();
assert_eq!(result, "user=alice status=200");
let result: String = engine
.eval_with_scope(
&mut scope,
r##"text.extract_re("user=(\\w+).*status=(\\d+)", 1)"##,
)
.unwrap();
assert_eq!(result, "alice");
let result: String = engine
.eval_with_scope(
&mut scope,
r##"text.extract_re("user=(\\w+).*status=(\\d+)", 2)"##,
)
.unwrap();
assert_eq!(result, "200");
let result: String = engine
.eval_with_scope(&mut scope, r##"text.extract_re("user=(\\w+)", 5)"##)
.unwrap();
assert_eq!(result, "");
let result: String = engine
.eval_with_scope(&mut scope, r##"text.extract_re("user=(\\w+)", -1)"##)
.unwrap();
assert_eq!(result, "user=alice");
}
#[test]
fn test_extract_all_re_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "a=1 b=2 c=3");
let result: rhai::Array = engine
.eval_with_scope(&mut scope, r##"text.extract_all_re("(\\w+)=(\\d+)")"##)
.unwrap();
assert_eq!(result.len(), 3);
let first_match = result[0].clone().into_array().unwrap();
assert_eq!(first_match[0].clone().into_string().unwrap(), "a");
assert_eq!(first_match[1].clone().into_string().unwrap(), "1");
scope.push("numbers", "10 20 30 40");
let result: rhai::Array = engine
.eval_with_scope(&mut scope, r##"numbers.extract_all_re("\\d+")"##)
.unwrap();
assert_eq!(result.len(), 4);
assert_eq!(result[0].clone().into_string().unwrap(), "10");
assert_eq!(result[3].clone().into_string().unwrap(), "40");
let result: rhai::Array = engine
.eval_with_scope(&mut scope, r##"text.extract_all_re("missing")"##)
.unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_extract_all_re_with_group_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push(
"text",
"user=alice status=200 user=bob status=404 user=charlie status=500",
);
let result: rhai::Array = engine
.eval_with_scope(
&mut scope,
r##"text.extract_all_re("user=(\\w+).*?status=(\\d+)", 1)"##,
)
.unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result[0].clone().into_string().unwrap(), "alice");
assert_eq!(result[1].clone().into_string().unwrap(), "bob");
assert_eq!(result[2].clone().into_string().unwrap(), "charlie");
let result: rhai::Array = engine
.eval_with_scope(
&mut scope,
r##"text.extract_all_re("user=(\\w+).*?status=(\\d+)", 2)"##,
)
.unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result[0].clone().into_string().unwrap(), "200");
assert_eq!(result[1].clone().into_string().unwrap(), "404");
assert_eq!(result[2].clone().into_string().unwrap(), "500");
let result: rhai::Array = engine
.eval_with_scope(&mut scope, r##"text.extract_all_re("user=(\\w+)", 0)"##)
.unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result[0].clone().into_string().unwrap(), "user=alice");
assert_eq!(result[1].clone().into_string().unwrap(), "user=bob");
assert_eq!(result[2].clone().into_string().unwrap(), "user=charlie");
let result: rhai::Array = engine
.eval_with_scope(&mut scope, r##"text.extract_all_re("user=(\\w+)", 5)"##)
.unwrap();
assert_eq!(result.len(), 0);
}
#[test]
fn test_split_re_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "one,two;three:four");
let result: rhai::Array = engine
.eval_with_scope(&mut scope, r##"text.split_re("[,;:]")"##)
.unwrap();
assert_eq!(result.len(), 4);
assert_eq!(result[0].clone().into_string().unwrap(), "one");
assert_eq!(result[1].clone().into_string().unwrap(), "two");
assert_eq!(result[2].clone().into_string().unwrap(), "three");
assert_eq!(result[3].clone().into_string().unwrap(), "four");
scope.push("spaced", "hello world\ttab\nnewline");
let result: rhai::Array = engine
.eval_with_scope(&mut scope, r##"spaced.split_re("\\s+")"##)
.unwrap();
assert_eq!(result.len(), 4);
assert_eq!(result[0].clone().into_string().unwrap(), "hello");
assert_eq!(result[1].clone().into_string().unwrap(), "world");
let result: rhai::Array = engine
.eval_with_scope(&mut scope, r##"text.split_re("[")"##)
.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(
result[0].clone().into_string().unwrap(),
"one,two;three:four"
);
}
#[test]
fn test_replace_re_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "The year 2023 and 2024 are here");
let result: String = engine
.eval_with_scope(&mut scope, r##"text.replace_re("\\d{4}", "YEAR")"##)
.unwrap();
assert_eq!(result, "The year YEAR and YEAR are here");
scope.push("emails", "Contact alice@example.com or bob@test.org");
let result: String = engine
.eval_with_scope(
&mut scope,
r##"emails.replace_re("(\\w+)@(\\w+\\.\\w+)", "[$1 at $2]")"##,
)
.unwrap();
assert_eq!(
result,
"Contact [alice at example.com] or [bob at test.org]"
);
let result: String = engine
.eval_with_scope(&mut scope, r##"text.replace_re("nomatch", "replacement")"##)
.unwrap();
assert_eq!(result, "The year 2023 and 2024 are here");
let result: String = engine
.eval_with_scope(&mut scope, r##"text.replace_re("[", "replacement")"##)
.unwrap();
assert_eq!(result, "The year 2023 and 2024 are here");
}
#[test]
fn test_extract_ip_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "Server 192.168.1.100 responded");
let result: String = engine
.eval_with_scope(&mut scope, r##"text.extract_ip()"##)
.unwrap();
assert_eq!(result, "192.168.1.100");
scope.push("no_ip", "No IP address here");
let result: String = engine
.eval_with_scope(&mut scope, r##"no_ip.extract_ip()"##)
.unwrap();
assert_eq!(result, "");
scope.push("multi", "From 10.0.0.1 to 172.16.0.1");
let result: String = engine
.eval_with_scope(&mut scope, r##"multi.extract_ip()"##)
.unwrap();
assert_eq!(result, "10.0.0.1");
}
#[test]
fn test_extract_ips_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "From 10.0.0.1 to 172.16.0.1 via 192.168.1.1");
let result: rhai::Array = engine
.eval_with_scope(&mut scope, r##"text.extract_ips()"##)
.unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result[0].clone().into_string().unwrap(), "10.0.0.1");
assert_eq!(result[1].clone().into_string().unwrap(), "172.16.0.1");
assert_eq!(result[2].clone().into_string().unwrap(), "192.168.1.1");
scope.push("no_ips", "No IP addresses here");
let result: rhai::Array = engine
.eval_with_scope(&mut scope, r##"no_ips.extract_ips()"##)
.unwrap();
assert_eq!(result.len(), 0);
scope.push("invalid", "300.400.500.600 and 192.168.1.1");
let result: rhai::Array = engine
.eval_with_scope(&mut scope, r##"invalid.extract_ips()"##)
.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].clone().into_string().unwrap(), "192.168.1.1");
}
#[test]
fn test_mask_ip_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("ip", "192.168.1.100");
let result: String = engine
.eval_with_scope(&mut scope, r##"ip.mask_ip()"##)
.unwrap();
assert_eq!(result, "192.168.1.X");
let result: String = engine
.eval_with_scope(&mut scope, r##"ip.mask_ip(2)"##)
.unwrap();
assert_eq!(result, "192.168.X.X");
let result: String = engine
.eval_with_scope(&mut scope, r##"ip.mask_ip(3)"##)
.unwrap();
assert_eq!(result, "192.X.X.X");
let result: String = engine
.eval_with_scope(&mut scope, r##"ip.mask_ip(4)"##)
.unwrap();
assert_eq!(result, "X.X.X.X");
scope.push("invalid", "not.an.ip.address");
let result: String = engine
.eval_with_scope(&mut scope, r##"invalid.mask_ip()"##)
.unwrap();
assert_eq!(result, "not.an.ip.address");
let result: String = engine
.eval_with_scope(&mut scope, r##"ip.mask_ip(0)"##)
.unwrap();
assert_eq!(result, "192.168.1.X");
let result: String = engine
.eval_with_scope(&mut scope, r##"ip.mask_ip(10)"##)
.unwrap();
assert_eq!(result, "X.X.X.X"); }
#[test]
fn test_is_private_ip_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("private1", "10.0.0.1");
let result: bool = engine
.eval_with_scope(&mut scope, r##"private1.is_private_ip()"##)
.unwrap();
assert!(result);
scope.push("private2", "172.16.0.1");
let result: bool = engine
.eval_with_scope(&mut scope, r##"private2.is_private_ip()"##)
.unwrap();
assert!(result);
scope.push("private3", "192.168.1.1");
let result: bool = engine
.eval_with_scope(&mut scope, r##"private3.is_private_ip()"##)
.unwrap();
assert!(result);
scope.push("loopback", "127.0.0.1");
let result: bool = engine
.eval_with_scope(&mut scope, r##"loopback.is_private_ip()"##)
.unwrap();
assert!(result);
scope.push("public1", "8.8.8.8");
let result: bool = engine
.eval_with_scope(&mut scope, r##"public1.is_private_ip()"##)
.unwrap();
assert!(!result);
scope.push("public2", "1.1.1.1");
let result: bool = engine
.eval_with_scope(&mut scope, r##"public2.is_private_ip()"##)
.unwrap();
assert!(!result);
scope.push("edge1", "172.15.0.1");
let result: bool = engine
.eval_with_scope(&mut scope, r##"edge1.is_private_ip()"##)
.unwrap();
assert!(!result);
scope.push("edge2", "172.32.0.1");
let result: bool = engine
.eval_with_scope(&mut scope, r##"edge2.is_private_ip()"##)
.unwrap();
assert!(!result);
scope.push("invalid", "not.an.ip");
let result: bool = engine
.eval_with_scope(&mut scope, r##"invalid.is_private_ip()"##)
.unwrap();
assert!(!result);
}
#[test]
fn test_extract_url_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "Visit https://example.com/path for more info");
let result: String = engine
.eval_with_scope(&mut scope, r##"text.extract_url()"##)
.unwrap();
assert_eq!(result, "https://example.com/path");
scope.push("http", "Go to http://test.org/page.html");
let result: String = engine
.eval_with_scope(&mut scope, r##"http.extract_url()"##)
.unwrap();
assert_eq!(result, "http://test.org/page.html");
scope.push("no_url", "No URL in this text");
let result: String = engine
.eval_with_scope(&mut scope, r##"no_url.extract_url()"##)
.unwrap();
assert_eq!(result, "");
scope.push(
"complex",
"API endpoint: https://api.example.com/v1/users?page=2&limit=10",
);
let result: String = engine
.eval_with_scope(&mut scope, r##"complex.extract_url()"##)
.unwrap();
assert_eq!(result, "https://api.example.com/v1/users?page=2&limit=10");
scope.push("multi", "Visit https://first.com or https://second.com");
let result: String = engine
.eval_with_scope(&mut scope, r##"multi.extract_url()"##)
.unwrap();
assert_eq!(result, "https://first.com");
}
#[test]
fn test_extract_domain_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
scope.push("text", "Visit https://example.com/path for more info");
let result: String = engine
.eval_with_scope(&mut scope, r##"text.extract_domain()"##)
.unwrap();
assert_eq!(result, "example.com");
scope.push("email", "Contact us at support@test.org");
let result: String = engine
.eval_with_scope(&mut scope, r##"email.extract_domain()"##)
.unwrap();
assert_eq!(result, "test.org");
scope.push("both", "Visit https://example.com or email admin@test.org");
let result: String = engine
.eval_with_scope(&mut scope, r##"both.extract_domain()"##)
.unwrap();
assert_eq!(result, "example.com");
scope.push("no_domain", "No domain in this text");
let result: String = engine
.eval_with_scope(&mut scope, r##"no_domain.extract_domain()"##)
.unwrap();
assert_eq!(result, "");
scope.push("subdomain", "API: https://api.v2.example.com/endpoint");
let result: String = engine
.eval_with_scope(&mut scope, r##"subdomain.extract_domain()"##)
.unwrap();
assert_eq!(result, "api.v2.example.com");
scope.push("port", "Connect to http://localhost:8080/api");
let result: String = engine
.eval_with_scope(&mut scope, r##"port.extract_domain()"##)
.unwrap();
assert_eq!(result, "localhost:8080");
}
#[test]
fn test_unflatten_function() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
let result: rhai::Map = engine
.eval_with_scope(
&mut scope,
r##"
let flat = #{
"user_name": "alice",
"user_age": "30",
"user_settings_theme": "dark"
};
flat.unflatten()
"##,
)
.unwrap();
let user_map = result
.get("user")
.unwrap()
.clone()
.try_cast::<rhai::Map>()
.unwrap();
assert_eq!(
user_map.get("name").unwrap().clone().into_string().unwrap(),
"alice"
);
assert_eq!(
user_map.get("age").unwrap().clone().into_string().unwrap(),
"30"
);
let settings_map = user_map
.get("settings")
.unwrap()
.clone()
.try_cast::<rhai::Map>()
.unwrap();
assert_eq!(
settings_map
.get("theme")
.unwrap()
.clone()
.into_string()
.unwrap(),
"dark"
);
let result: rhai::Map = engine
.eval_with_scope(
&mut scope,
r##"
let flat = #{
"items_0_name": "first",
"items_1_name": "second",
"items_2_name": "third"
};
flat.unflatten()
"##,
)
.unwrap();
let items_array = result
.get("items")
.unwrap()
.clone()
.try_cast::<rhai::Array>()
.unwrap();
assert_eq!(items_array.len(), 3);
let first_item = items_array[0].clone().try_cast::<rhai::Map>().unwrap();
assert_eq!(
first_item
.get("name")
.unwrap()
.clone()
.into_string()
.unwrap(),
"first"
);
let second_item = items_array[1].clone().try_cast::<rhai::Map>().unwrap();
assert_eq!(
second_item
.get("name")
.unwrap()
.clone()
.into_string()
.unwrap(),
"second"
);
let result: rhai::Map = engine
.eval_with_scope(
&mut scope,
r##"
let flat = #{
"users_0_name": "alice",
"users_0_roles_0": "admin",
"users_0_roles_1": "user",
"users_1_name": "bob",
"users_1_roles_0": "user"
};
flat.unflatten()
"##,
)
.unwrap();
let users_array = result
.get("users")
.unwrap()
.clone()
.try_cast::<rhai::Array>()
.unwrap();
assert_eq!(users_array.len(), 2);
let alice = users_array[0].clone().try_cast::<rhai::Map>().unwrap();
assert_eq!(
alice.get("name").unwrap().clone().into_string().unwrap(),
"alice"
);
let alice_roles = alice
.get("roles")
.unwrap()
.clone()
.try_cast::<rhai::Array>()
.unwrap();
assert_eq!(alice_roles.len(), 2);
assert_eq!(alice_roles[0].clone().into_string().unwrap(), "admin");
assert_eq!(alice_roles[1].clone().into_string().unwrap(), "user");
let result: rhai::Map = engine
.eval_with_scope(
&mut scope,
r##"
let flat = #{
"user.name": "alice",
"user.settings.theme": "dark"
};
flat.unflatten(".")
"##,
)
.unwrap();
let user_map = result
.get("user")
.unwrap()
.clone()
.try_cast::<rhai::Map>()
.unwrap();
assert_eq!(
user_map.get("name").unwrap().clone().into_string().unwrap(),
"alice"
);
let settings_map = user_map
.get("settings")
.unwrap()
.clone()
.try_cast::<rhai::Map>()
.unwrap();
assert_eq!(
settings_map
.get("theme")
.unwrap()
.clone()
.into_string()
.unwrap(),
"dark"
);
let result: rhai::Map = engine
.eval_with_scope(
&mut scope,
r##"
let flat = #{};
flat.unflatten()
"##,
)
.unwrap();
assert!(result.is_empty());
let result: rhai::Map = engine
.eval_with_scope(
&mut scope,
r##"
let flat = #{
"name": "alice",
"age": "30"
};
flat.unflatten()
"##,
)
.unwrap();
assert_eq!(
result.get("name").unwrap().clone().into_string().unwrap(),
"alice"
);
assert_eq!(
result.get("age").unwrap().clone().into_string().unwrap(),
"30"
);
}
#[test]
fn test_unflatten_array_edge_cases() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
let result: rhai::Map = engine
.eval_with_scope(
&mut scope,
r##"
let flat = #{
"items_0": "first",
"items_2": "third",
"items_5": "sixth"
};
flat.unflatten()
"##,
)
.unwrap();
let items_array = result
.get("items")
.unwrap()
.clone()
.try_cast::<rhai::Array>()
.unwrap();
assert_eq!(items_array.len(), 6); assert_eq!(items_array[0].clone().into_string().unwrap(), "first");
assert!(items_array[1].is_unit()); assert_eq!(items_array[2].clone().into_string().unwrap(), "third");
assert!(items_array[3].is_unit()); assert!(items_array[4].is_unit()); assert_eq!(items_array[5].clone().into_string().unwrap(), "sixth");
let result: rhai::Map = engine
.eval_with_scope(
&mut scope,
r##"
let flat = #{
"mixed_0": "zero",
"mixed_name": "alice",
"mixed_1": "one"
};
flat.unflatten()
"##,
)
.unwrap();
let mixed_map = result
.get("mixed")
.unwrap()
.clone()
.try_cast::<rhai::Map>()
.unwrap();
assert_eq!(
mixed_map.get("0").unwrap().clone().into_string().unwrap(),
"zero"
);
assert_eq!(
mixed_map
.get("name")
.unwrap()
.clone()
.into_string()
.unwrap(),
"alice"
);
assert_eq!(
mixed_map.get("1").unwrap().clone().into_string().unwrap(),
"one"
);
}
#[test]
fn test_unflatten_deep_nesting() {
let mut engine = rhai::Engine::new();
register_functions(&mut engine);
let mut scope = Scope::new();
let result: rhai::Map = engine
.eval_with_scope(
&mut scope,
r##"
let flat = #{
"app_config_database_host": "localhost",
"app_config_database_port": "5432",
"app_config_cache_redis_url": "redis://localhost",
"app_config_cache_ttl": "3600",
"app_features_0_name": "auth",
"app_features_0_enabled": "true",
"app_features_1_name": "logging",
"app_features_1_enabled": "false"
};
flat.unflatten()
"##,
)
.unwrap();
let app_map = result
.get("app")
.unwrap()
.clone()
.try_cast::<rhai::Map>()
.unwrap();
let config_map = app_map
.get("config")
.unwrap()
.clone()
.try_cast::<rhai::Map>()
.unwrap();
let db_map = config_map
.get("database")
.unwrap()
.clone()
.try_cast::<rhai::Map>()
.unwrap();
assert_eq!(
db_map.get("host").unwrap().clone().into_string().unwrap(),
"localhost"
);
assert_eq!(
db_map.get("port").unwrap().clone().into_string().unwrap(),
"5432"
);
let cache_map = config_map
.get("cache")
.unwrap()
.clone()
.try_cast::<rhai::Map>()
.unwrap();
assert_eq!(
cache_map.get("ttl").unwrap().clone().into_string().unwrap(),
"3600"
);
let redis_map = cache_map
.get("redis")
.unwrap()
.clone()
.try_cast::<rhai::Map>()
.unwrap();
assert_eq!(
redis_map.get("url").unwrap().clone().into_string().unwrap(),
"redis://localhost"
);
let features_array = app_map
.get("features")
.unwrap()
.clone()
.try_cast::<rhai::Array>()
.unwrap();
assert_eq!(features_array.len(), 2);
let auth_feature = features_array[0].clone().try_cast::<rhai::Map>().unwrap();
assert_eq!(
auth_feature
.get("name")
.unwrap()
.clone()
.into_string()
.unwrap(),
"auth"
);
assert_eq!(
auth_feature
.get("enabled")
.unwrap()
.clone()
.into_string()
.unwrap(),
"true"
);
}
}