#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum Value {
#[default]
Null,
Str(String),
Array(Vec<Value>),
Object(Vec<(String, Value)>),
}
impl Value {
pub fn get(&self, path: &str) -> Option<&Value> {
let mut current = self;
for segment in path.split('.') {
let Value::Object(entries) = current else {
return None;
};
current = entries.iter().find(|(k, _)| k == segment).map(|(_, v)| v)?;
}
Some(current)
}
pub fn get_str(&self, path: &str) -> Option<&str> {
self.get(path).and_then(Value::as_str)
}
pub fn get_key(&self, key: &str) -> Option<&Value> {
match self {
Value::Object(entries) => entries.iter().find(|(k, _)| k == key).map(|(_, v)| v),
_ => None,
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
Value::Str(s) => Some(s),
_ => None,
}
}
pub fn as_array(&self) -> Option<&[Value]> {
match self {
Value::Array(items) => Some(items),
_ => None,
}
}
pub fn is_null(&self) -> bool {
matches!(self, Value::Null)
}
pub fn insert(&mut self, key: &str, value: Value) {
if let Value::Object(entries) = self {
if let Some(slot) = entries.iter_mut().find(|(k, _)| k == key) {
slot.1 = value;
} else {
entries.push((key.to_string(), value));
}
}
}
}
fn strip_pem(input: &str, label: &str) -> String {
input
.replace(&format!("-----BEGIN {label}-----"), "")
.replace(&format!("-----END {label}-----"), "")
.replace(['\n', '\r', ' ', '\t'], "")
}
pub fn normalize_cert_string(cert: &str) -> String {
strip_pem(cert, "CERTIFICATE")
}
pub fn normalize_pem_string(pem: &str) -> String {
strip_pem(pem, "RSA PRIVATE KEY")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OneOrMany<T> {
One(T),
Many(Vec<T>),
}
impl<T> OneOrMany<T> {
pub fn into_vec(self) -> Vec<T> {
match self {
OneOrMany::One(v) => vec![v],
OneOrMany::Many(v) => v,
}
}
}
pub fn cast_array_opt<T>(value: Option<OneOrMany<T>>) -> Vec<T> {
value.map(OneOrMany::into_vec).unwrap_or_default()
}
pub fn is_non_empty_array<T>(items: &[T]) -> bool {
!items.is_empty()
}
pub fn verify_fields(field: &[String], meta_field: &str) -> bool {
!field.is_empty() && field.iter().all(|f| f == meta_field)
}
pub fn uniq<I, S>(items: I) -> Vec<String>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let mut seen = Vec::new();
for item in items {
let s = item.into();
if !seen.contains(&s) {
seen.push(s);
}
}
seen
}
pub fn last<T>(items: &[T]) -> Option<&T> {
items.last()
}
pub fn camel_case(input: &str) -> String {
let chars: Vec<char> = input.chars().collect();
let mut words: Vec<String> = Vec::new();
let mut cur = String::new();
for i in 0..chars.len() {
let c = chars[i];
if matches!(c, '-' | '_' | ' ' | '.' | '/' | ':') {
if !cur.is_empty() {
words.push(std::mem::take(&mut cur));
}
continue;
}
if !cur.is_empty() {
let prev = chars[i - 1];
let lower_to_upper = prev.is_lowercase() && c.is_uppercase();
let acronym_end = prev.is_uppercase()
&& c.is_uppercase()
&& i + 1 < chars.len()
&& chars[i + 1].is_lowercase();
if lower_to_upper || acronym_end {
words.push(std::mem::take(&mut cur));
}
}
cur.push(c);
}
if !cur.is_empty() {
words.push(cur);
}
let mut out = String::new();
for (i, word) in words.iter().enumerate() {
let lower = word.to_lowercase();
if i == 0 {
out.push_str(&lower);
continue;
}
let mut it = lower.chars();
if let Some(first) = it.next() {
out.extend(first.to_uppercase());
out.push_str(it.as_str());
}
}
out
}
pub fn zip_object(keys: &[String], values: Vec<Value>, skip_duplicated: bool) -> Value {
let mut out: Vec<(String, Value)> = Vec::new();
for (key, value) in keys.iter().cloned().zip(values) {
if skip_duplicated {
if let Some(slot) = out.iter_mut().find(|(k, _)| *k == key) {
slot.1 = value;
} else {
out.push((key, value));
}
continue;
}
if let Some(slot) = out.iter_mut().find(|(k, _)| *k == key) {
match &mut slot.1 {
Value::Array(items) => items.push(value),
existing => {
let prev = std::mem::take(existing);
*existing = Value::Array(vec![prev, value]);
}
}
} else {
out.push((key, value));
}
}
Value::Object(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_cert_strips_header_and_whitespace() {
let pem = "-----BEGIN CERTIFICATE-----\nMIIB AgID\r\n\t-----END CERTIFICATE-----\n";
assert_eq!(normalize_cert_string(pem), "MIIBAgID");
}
#[test]
fn normalize_pem_strips_rsa_label() {
let pem = "-----BEGIN RSA PRIVATE KEY-----\nAAAA\n-----END RSA PRIVATE KEY-----";
assert_eq!(normalize_pem_string(pem), "AAAA");
}
#[test]
fn get_walks_dotted_path_with_default() {
let v = Value::Object(vec![(
"request".into(),
Value::Object(vec![("id".into(), Value::Str("_abc".into()))]),
)]);
assert_eq!(v.get_str("request.id"), Some("_abc"));
assert_eq!(v.get("request.missing"), None);
assert_eq!(v.get_str("nope.id").unwrap_or(""), "");
}
#[test]
fn cast_array_opt_modes() {
assert_eq!(cast_array_opt::<i32>(None), Vec::<i32>::new());
assert_eq!(cast_array_opt(Some(OneOrMany::One(1))), vec![1]);
assert_eq!(
cast_array_opt(Some(OneOrMany::Many(vec![1, 2]))),
vec![1, 2]
);
}
#[test]
fn uniq_and_last() {
assert_eq!(uniq(["a", "b", "a", "c"]), vec!["a", "b", "c"]);
assert_eq!(last(&[1, 2, 3]), Some(&3));
assert_eq!(last::<i32>(&[]), None);
assert!(is_non_empty_array(&[0]));
assert!(!is_non_empty_array::<i32>(&[]));
}
#[test]
fn verify_fields_all_match_non_empty() {
assert!(verify_fields(&["a".to_string()], "a"));
assert!(verify_fields(&["a".to_string(), "a".to_string()], "a"));
assert!(!verify_fields(&["a".to_string(), "b".to_string()], "a"));
assert!(!verify_fields(&[], "a"));
}
#[test]
fn zip_object_skip_vs_aggregate() {
let keys = vec!["a".to_string(), "a".to_string()];
let skip = zip_object(
&keys,
vec![Value::Str("1".into()), Value::Str("2".into())],
true,
);
assert_eq!(skip.get_str("a"), Some("2"));
let agg = zip_object(
&keys,
vec![Value::Str("1".into()), Value::Str("2".into())],
false,
);
assert_eq!(
agg.get("a"),
Some(&Value::Array(vec![
Value::Str("1".into()),
Value::Str("2".into())
]))
);
}
}