use eyre::Result;
pub fn extract(json: &serde_json::Value, path: &str) -> Result<Vec<String>> {
let mut results = Vec::new();
let path = path.trim();
if path.is_empty() || path == "." {
extract_values(json, &mut results);
return Ok(results);
}
let path = path.strip_prefix('.').unwrap_or(path);
extract_recursive(json, path, &mut results);
Ok(results)
}
pub fn extract_auto(json: &serde_json::Value) -> Vec<String> {
let mut results = Vec::new();
match json {
serde_json::Value::String(s) => {
let v = normalize_version(s);
if !v.is_empty() {
results.push(v);
}
}
serde_json::Value::Array(arr) => {
for val in arr {
if let Some(v) = val.as_str() {
let v = normalize_version(v);
if !v.is_empty() {
results.push(v);
}
} else if let Some(obj) = val.as_object() {
for field in ["version", "tag_name", "name", "tag", "v"] {
if let Some(v) = obj.get(field).and_then(|v| v.as_str()) {
let v = normalize_version(v);
if !v.is_empty() {
results.push(v);
break;
}
}
}
}
}
}
serde_json::Value::Object(obj) => {
for field in ["versions", "releases", "tags", "version", "release"] {
if let Some(val) = obj.get(field) {
let extracted = extract_auto(val);
if !extracted.is_empty() {
return extracted;
}
}
}
}
_ => {}
}
results
}
fn extract_recursive(json: &serde_json::Value, path: &str, results: &mut Vec<String>) {
if path.is_empty() {
extract_values(json, results);
return;
}
if path == "[]" {
if let Some(arr) = json.as_array() {
for val in arr {
extract_values(val, results);
}
}
return;
}
if let Some(rest) = path.strip_prefix("[].") {
if let Some(arr) = json.as_array() {
for val in arr {
extract_recursive(val, rest, results);
}
}
return;
}
if let Some(filter_content) = path.strip_prefix("[?") {
if let Some(end_bracket) = filter_content.find(']') {
let filter_expr = &filter_content[..end_bracket];
let rest = &filter_content[end_bracket + 1..];
let rest = rest.strip_prefix('.').unwrap_or(rest);
if let Some((filter_field, filter_value)) = filter_expr.split_once('=')
&& let Some(arr) = json.as_array()
{
for val in arr {
if let Some(obj) = val.as_object()
&& let Some(field_val) = obj.get(filter_field)
&& field_val.as_str() == Some(filter_value)
{
if rest.is_empty() {
extract_values(val, results);
} else {
extract_recursive(val, rest, results);
}
}
}
}
}
return;
}
let (field, rest) = if let Some(idx) = path.find(['.', '[']) {
let (f, r) = path.split_at(idx);
let rest = r.strip_prefix('.').unwrap_or(r);
(f, rest)
} else {
(path, "")
};
if let Some(obj) = json.as_object()
&& let Some(val) = obj.get(field)
{
extract_recursive(val, rest, results);
}
}
fn extract_values(json: &serde_json::Value, results: &mut Vec<String>) {
match json {
serde_json::Value::String(s) => {
let v = normalize_version(s);
if !v.is_empty() {
results.push(v);
}
}
serde_json::Value::Array(arr) => {
for val in arr {
if let Some(s) = val.as_str() {
let v = normalize_version(s);
if !v.is_empty() {
results.push(v);
}
}
}
}
serde_json::Value::Number(n) => {
results.push(n.to_string());
}
serde_json::Value::Object(obj) => {
for field in ["version", "tag_name", "name", "tag", "v"] {
if let Some(v) = obj.get(field).and_then(|v| v.as_str()) {
let v = normalize_version(v);
if !v.is_empty() {
results.push(v);
break;
}
}
}
}
_ => {}
}
}
fn normalize_version(s: &str) -> String {
s.trim().trim_start_matches('v').to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_extract_root_string() {
let data = json!("v2.0.53");
assert_eq!(extract(&data, ".").unwrap(), vec!["2.0.53"]);
}
#[test]
fn test_extract_root_array() {
let data = json!(["1.0.0", "2.0.0"]);
assert_eq!(extract(&data, ".").unwrap(), vec!["1.0.0", "2.0.0"]);
}
#[test]
fn test_extract_array_iterate() {
let data = json!(["v1.0.0", "v2.0.0"]);
assert_eq!(extract(&data, ".[]").unwrap(), vec!["1.0.0", "2.0.0"]);
}
#[test]
fn test_extract_array_field() {
let data = json!([{"version": "1.0.0"}, {"version": "2.0.0"}]);
assert_eq!(
extract(&data, ".[].version").unwrap(),
vec!["1.0.0", "2.0.0"]
);
}
#[test]
fn test_extract_nested_field() {
let data = json!({"data": {"version": "1.0.0"}});
assert_eq!(extract(&data, ".data.version").unwrap(), vec!["1.0.0"]);
}
#[test]
fn test_extract_nested_array() {
let data = json!({"data": {"versions": ["1.0.0", "2.0.0"]}});
assert_eq!(
extract(&data, ".data.versions[]").unwrap(),
vec!["1.0.0", "2.0.0"]
);
}
#[test]
fn test_extract_deeply_nested() {
let data =
json!({"releases": [{"info": {"version": "1.0.0"}}, {"info": {"version": "2.0.0"}}]});
assert_eq!(
extract(&data, ".releases[].info.version").unwrap(),
vec!["1.0.0", "2.0.0"]
);
}
#[test]
fn test_extract_object_field_array() {
let data = json!({"versions": ["1.0.0", "2.0.0"]});
assert_eq!(
extract(&data, ".versions[]").unwrap(),
vec!["1.0.0", "2.0.0"]
);
}
#[test]
fn test_extract_empty_path() {
let data = json!("1.0.0");
assert_eq!(extract(&data, "").unwrap(), vec!["1.0.0"]);
}
#[test]
fn test_extract_missing_field() {
let data = json!({"foo": "bar"});
assert!(extract(&data, ".missing").unwrap().is_empty());
}
#[test]
fn test_extract_auto_string() {
let data = json!("v1.0.0");
assert_eq!(extract_auto(&data), vec!["1.0.0"]);
}
#[test]
fn test_extract_auto_array_strings() {
let data = json!(["v1.0.0", "v2.0.0"]);
assert_eq!(extract_auto(&data), vec!["1.0.0", "2.0.0"]);
}
#[test]
fn test_extract_auto_array_objects() {
let data = json!([{"version": "1.0.0"}, {"tag_name": "v2.0.0"}]);
assert_eq!(extract_auto(&data), vec!["1.0.0", "2.0.0"]);
}
#[test]
fn test_extract_auto_object_versions_field() {
let data = json!({"versions": ["1.0.0", "2.0.0"]});
assert_eq!(extract_auto(&data), vec!["1.0.0", "2.0.0"]);
}
#[test]
fn test_extract_auto_object_releases_field() {
let data = json!({"releases": ["1.0.0", "2.0.0"]});
assert_eq!(extract_auto(&data), vec!["1.0.0", "2.0.0"]);
}
#[test]
fn test_extract_filter_simple() {
let data = json!([
{"version": "1.0.0", "channel": "stable"},
{"version": "2.0.0", "channel": "beta"},
{"version": "3.0.0", "channel": "stable"}
]);
assert_eq!(
extract(&data, ".[?channel=stable].version").unwrap(),
vec!["1.0.0", "3.0.0"]
);
}
#[test]
fn test_extract_filter_nested() {
let data = json!({
"releases": [
{"version": "1.0.0", "channel": "stable"},
{"version": "2.0.0", "channel": "beta"},
{"version": "3.0.0", "channel": "stable"}
]
});
assert_eq!(
extract(&data, ".releases[?channel=stable].version").unwrap(),
vec!["1.0.0", "3.0.0"]
);
}
#[test]
fn test_extract_filter_no_match() {
let data = json!([
{"version": "1.0.0", "channel": "beta"},
{"version": "2.0.0", "channel": "dev"}
]);
assert!(
extract(&data, ".[?channel=stable].version")
.unwrap()
.is_empty()
);
}
#[test]
fn test_extract_filter_whole_object() {
let data = json!([
{"version": "1.0.0", "channel": "stable"},
{"version": "2.0.0", "channel": "beta"}
]);
let result = extract(&data, ".[?channel=stable]").unwrap();
assert_eq!(result, vec!["1.0.0"]);
}
}