use crate::Error;
pub fn extract_field(payload: &serde_json::Value, path: &str) -> Result<String, Error> {
let stripped = path.strip_prefix('.').unwrap_or(path);
if stripped.is_empty() {
return Err(Error::InvalidUrl("field path must not be empty".into()));
}
let mut current = payload;
for segment in stripped.split('.') {
if segment.is_empty() {
return Err(Error::InvalidUrl(format!(
"field path '{path}' contains an empty segment"
)));
}
current = current
.get(segment)
.ok_or_else(|| Error::NotFound(format!("field '{path}' not found")))?;
}
match current {
serde_json::Value::String(s) => Ok(s.clone()),
serde_json::Value::Number(n) => Ok(n.to_string()),
serde_json::Value::Bool(b) => Ok(b.to_string()),
serde_json::Value::Null => Err(Error::InvalidUrl(format!("field '{path}' is null"))),
serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
Err(Error::InvalidUrl(format!("field '{path}' is not a scalar")))
}
}
}
pub fn extract_field_from_str(payload: &str, path: &str) -> Result<String, Error> {
let value: serde_json::Value = serde_json::from_str(payload)
.map_err(|e| Error::InvalidUrl(format!("secret is not JSON; cannot extract field: {e}")))?;
extract_field(&value, path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn top_level_string() {
let v = serde_json::json!({"password": "hunter2"});
assert_eq!(extract_field(&v, "password").unwrap(), "hunter2");
}
#[test]
fn leading_dot_optional() {
let v = serde_json::json!({"password": "hunter2"});
assert_eq!(extract_field(&v, ".password").unwrap(), "hunter2");
}
#[test]
fn nested_dotted_path() {
let v = serde_json::json!({
"credentials": {"api_key": "ak-xyz"}
});
assert_eq!(extract_field(&v, ".credentials.api_key").unwrap(), "ak-xyz");
}
#[test]
fn missing_top_level_is_not_found() {
let v = serde_json::json!({"password": "hunter2"});
let err = extract_field(&v, "username").unwrap_err();
assert!(matches!(err, Error::NotFound(_)));
}
#[test]
fn missing_nested_is_not_found() {
let v = serde_json::json!({"a": {"b": 1}});
let err = extract_field(&v, "a.c").unwrap_err();
assert!(matches!(err, Error::NotFound(_)));
}
#[test]
fn empty_path_is_invalid_url() {
let v = serde_json::json!({});
let err = extract_field(&v, "").unwrap_err();
assert!(matches!(err, Error::InvalidUrl(_)));
}
#[test]
fn empty_segment_is_invalid_url() {
let v = serde_json::json!({"a": {"b": 1}});
let err = extract_field(&v, "a..b").unwrap_err();
assert!(matches!(err, Error::InvalidUrl(_)));
}
#[test]
fn numeric_leaf_stringified() {
let v = serde_json::json!({"port": 5432});
assert_eq!(extract_field(&v, "port").unwrap(), "5432");
}
#[test]
fn bool_leaf_stringified() {
let v = serde_json::json!({"enabled": true});
assert_eq!(extract_field(&v, "enabled").unwrap(), "true");
}
#[test]
fn null_leaf_is_invalid_url() {
let v = serde_json::json!({"x": null});
let err = extract_field(&v, "x").unwrap_err();
assert!(matches!(err, Error::InvalidUrl(_)));
}
#[test]
fn object_leaf_is_invalid_url() {
let v = serde_json::json!({"x": {"y": 1}});
let err = extract_field(&v, "x").unwrap_err();
assert!(matches!(err, Error::InvalidUrl(_)));
}
#[test]
fn array_leaf_is_invalid_url() {
let v = serde_json::json!({"x": [1, 2]});
let err = extract_field(&v, "x").unwrap_err();
assert!(matches!(err, Error::InvalidUrl(_)));
}
#[test]
fn from_str_happy() {
let r = extract_field_from_str(r#"{"k":"v"}"#, "k").unwrap();
assert_eq!(r, "v");
}
#[test]
fn from_str_non_json_is_invalid_url() {
let err = extract_field_from_str("not json", "k").unwrap_err();
assert!(matches!(err, Error::InvalidUrl(_)));
}
#[test]
fn newline_preserved_in_string_leaf() {
let v = serde_json::json!({"k": "v\n"});
assert_eq!(extract_field(&v, "k").unwrap(), "v\n");
}
}