serde_json_path 0.7.2

Query serde_json Values using JSONPath
Documentation
use std::cmp::Ordering;

use serde_json::{json, Value};
use serde_json_path::{JsonPath, JsonPathExt};
use serde_json_path_core::spec::functions::{NodesType, ValueType};
#[cfg(feature = "trace")]
use test_log::test;

#[test]
fn test_length() {
    let value = json!([
        "a short string",
        "a slightly longer string",
        "a string that is even longer!",
    ]);
    let query = JsonPath::parse("$[?length(@) > 14]").unwrap();
    let nodes = query.query(&value);
    println!("{nodes:#?}");
    assert_eq!(nodes.len(), 2);
}

#[test]
fn test_length_invalid_args() {
    let error = JsonPath::parse("$[? length(@.foo.*)]").unwrap_err();
    println!("{error:#?}");
}

#[test]
fn test_length_incorrect_use() {
    let error = JsonPath::parse("$[?length(@.author)]").unwrap_err();
    assert!(
        error
            .to_string()
            .contains("in long-hand segment, function with incorrect return type used"),
        "error did not contain the expected message"
    );
}

#[test]
fn test_count() {
    let value = json!([
        {"foo": [1]},
        {"foo": [1, 2]},
    ]);
    let path = JsonPath::parse("$[?count(@.foo.*) > 1]").unwrap();
    let q = path.query(&value);
    assert_eq!(1, q.len());
}

#[test]
fn test_count_singular_query() {
    let value = json!([
        {"foo": "bar"},
        {"foo": "baz"},
    ]);
    let path = JsonPath::parse("$[? count(@.foo) == 1]").unwrap();
    let q = path.query(&value);
    println!("{q:#?}");
    assert_eq!(q.len(), 2);
}

fn simpsons_characters() -> Value {
    json!([
        { "name": "Homer Simpson" },
        { "name": "Marge Simpson" },
        { "name": "Bart Simpson" },
        { "name": "Lisa Simpson" },
        { "name": "Maggie Simpson" },
        { "name": "Ned Flanders" },
        { "name": "Rod Flanders" },
        { "name": "Todd Flanders" },
    ])
}

#[test]
fn test_match() {
    let value = simpsons_characters();
    let path = JsonPath::parse("$[? match(@.name, 'M[A-Za-z ]*Simpson')].name").unwrap();
    let nodes = path.query(&value);
    println!("{nodes:#?}");
    assert_eq!(2, nodes.len());
    assert_eq!("Marge Simpson", nodes.first().unwrap().as_str().unwrap(),);
}

#[test]
fn test_search() {
    let value = simpsons_characters();
    let path = JsonPath::parse("$[? search(@.name, 'Flanders')]").unwrap();
    let nodes = path.query(&value);
    println!("{nodes:#?}");
    assert_eq!(3, nodes.len());
}

fn get_some_books() -> Value {
    json!([
        {
            "books": [
                {
                    "author": "Alexandre Dumas",
                    "title": "The Three Musketeers",
                    "price": 14.99
                },
                {
                    "author": "Leo Tolstoy",
                    "title": "War and Peace",
                    "price": 19.99
                }
            ]
        },
        {
            "books": [
                {
                    "author": "Charles Dickens",
                    "title": "Great Expectations",
                    "price": 13.99
                },
                {
                    "author": "Fyodor Dostoevsky",
                    "title": "The Brothers Karamazov",
                    "price": 12.99
                }
            ]
        }
    ])
}

#[serde_json_path::function]
fn first(nodes: NodesType) -> ValueType {
    match nodes.first() {
        Some(v) => ValueType::Node(v),
        None => ValueType::Nothing,
    }
}

#[serde_json_path::function]
fn sort<'a>(nodes: NodesType<'a>, on: ValueType<'a>) -> NodesType<'a> {
    if let Some(Ok(path)) = on.as_value().and_then(Value::as_str).map(JsonPath::parse) {
        let mut nl = nodes.all();
        nl.sort_by(|a, b| {
            if let (Some(a), Some(b)) = (
                a.json_path(&path).exactly_one().ok(),
                b.json_path(&path).exactly_one().ok(),
            ) {
                match (a, b) {
                    (Value::Bool(b1), Value::Bool(b2)) => b1.cmp(b2),
                    (Value::Number(n1), Value::Number(n2)) => {
                        if let (Some(f1), Some(f2)) = (n1.as_f64(), n2.as_f64()) {
                            f1.partial_cmp(&f2).unwrap_or(Ordering::Equal)
                        } else if let (Some(u1), Some(u2)) = (n1.as_u64(), n2.as_u64()) {
                            u1.cmp(&u2)
                        } else if let (Some(i1), Some(i2)) = (n1.as_i64(), n2.as_i64()) {
                            i1.cmp(&i2)
                        } else {
                            Ordering::Equal
                        }
                    }
                    (Value::String(s1), Value::String(s2)) => s1.cmp(s2),
                    _ => Ordering::Equal,
                }
            } else {
                Ordering::Equal
            }
        });
        nl.into()
    } else {
        nodes
    }
}

#[serde_json_path::function]
fn get<'a>(node: ValueType<'a>, path: ValueType<'a>) -> ValueType<'a> {
    if let Some(Ok(path)) = path.as_value().and_then(Value::as_str).map(JsonPath::parse) {
        match node {
            ValueType::Node(v) => v
                .json_path(&path)
                .exactly_one()
                .map(ValueType::Node)
                .unwrap_or_default(),
            // This is awkward, because we can't return a reference to a value owned by `node`
            // This means that `get` can only be used when dealing with nodes within the JSON object
            // being queried.
            _ => ValueType::Nothing,
        }
    } else {
        ValueType::Nothing
    }
}

#[test]
fn custom_first_function() {
    let value = get_some_books();
    let path = JsonPath::parse("$[? first(@.books.*.author) == 'Alexandre Dumas']").unwrap();
    let node = path.query(&value).exactly_one().unwrap();
    println!("{node:#?}");
    assert_eq!(
        "War and Peace",
        node.pointer("/books/1/title").unwrap().as_str().unwrap(),
    );
}

#[test]
fn function_as_argument() {
    let value = get_some_books();
    let path = JsonPath::parse("$[? length(first(@.books.*.title)) == 18 ]").unwrap();
    let node = path.query(&value).exactly_one().unwrap();
    println!("{node:#?}");
    assert_eq!(
        "The Brothers Karamazov",
        node.pointer("/books/1/title").unwrap().as_str().unwrap(),
    )
}

#[test]
fn combine_custom_functions() {
    let value = get_some_books();
    let path = JsonPath::parse(
        // `sort` the books in each node by price, take the `first` one, and check for specific
        // title by using `get`:
        "$[? get(first(sort(@.books.*, '$.price')), '$.title') == 'The Brothers Karamazov']",
    )
    .unwrap();
    let node = path.query(&value).exactly_one().unwrap();
    println!("{node:#?}");
    assert_eq!(
        "The Brothers Karamazov",
        node.pointer("/books/1/title").unwrap().as_str().unwrap(),
    );
}