๐Ÿฆ€ Functional Rust

957: JSON Query by Path

Difficulty: Intermediate Category: Serialization / Pattern Matching Concept: Recursive path traversal returning `Option` โ€” drilling into nested JSON Key Insight: OCaml returns `Some json` (a copy); Rust returns `Option<&'a JsonValue>` โ€” a borrowed reference into the original data, requiring lifetime annotation `'a` to express that the result lives as long as the input
// 957: JSON Query by Path
// get(["users", "0", "name"], json) โ†’ Option<&JsonValue>
// Rust uses lifetime-annotated references; OCaml returns values directly

#[derive(Debug, Clone, PartialEq)]
pub enum JsonValue {
    Null,
    Bool(bool),
    Number(f64),
    Str(String),
    Array(Vec<JsonValue>),
    Object(Vec<(String, JsonValue)>),
}

// Approach 1: Path query returning Option<&JsonValue> (borrows from source)
pub fn get<'a>(path: &[&str], json: &'a JsonValue) -> Option<&'a JsonValue> {
    match path {
        [] => Some(json),
        [key, rest @ ..] => match json {
            JsonValue::Object(pairs) => {
                let found = pairs.iter().find(|(k, _)| k == key);
                found.and_then(|(_, v)| get(rest, v))
            }
            JsonValue::Array(items) => {
                let idx: usize = key.parse().ok()?;
                items.get(idx).and_then(|v| get(rest, v))
            }
            _ => None,
        },
    }
}

// Approach 2: Typed extractors (return borrowed inner values)
pub fn get_string<'a>(path: &[&str], json: &'a JsonValue) -> Option<&'a str> {
    match get(path, json) {
        Some(JsonValue::Str(s)) => Some(s.as_str()),
        _ => None,
    }
}

pub fn get_number(path: &[&str], json: &JsonValue) -> Option<f64> {
    match get(path, json) {
        Some(JsonValue::Number(n)) => Some(*n),
        _ => None,
    }
}

pub fn get_bool(path: &[&str], json: &JsonValue) -> Option<bool> {
    match get(path, json) {
        Some(JsonValue::Bool(b)) => Some(*b),
        _ => None,
    }
}

pub fn get_array<'a>(path: &[&str], json: &'a JsonValue) -> Option<&'a Vec<JsonValue>> {
    match get(path, json) {
        Some(JsonValue::Array(items)) => Some(items),
        _ => None,
    }
}

// Approach 3: Query with default (clones for ownership)
pub fn get_or(default: JsonValue, path: &[&str], json: &JsonValue) -> JsonValue {
    match get(path, json) {
        Some(v) => v.clone(),
        None => default,
    }
}

fn main() {
    let json = JsonValue::Object(vec![
        (
            "users".to_string(),
            JsonValue::Array(vec![
                JsonValue::Object(vec![
                    ("name".to_string(), JsonValue::Str("Alice".to_string())),
                    ("age".to_string(), JsonValue::Number(30.0)),
                    ("active".to_string(), JsonValue::Bool(true)),
                ]),
                JsonValue::Object(vec![
                    ("name".to_string(), JsonValue::Str("Bob".to_string())),
                    ("age".to_string(), JsonValue::Number(25.0)),
                    ("active".to_string(), JsonValue::Bool(false)),
                ]),
            ]),
        ),
        ("count".to_string(), JsonValue::Number(2.0)),
        (
            "meta".to_string(),
            JsonValue::Object(vec![
                ("version".to_string(), JsonValue::Str("1.0".to_string())),
                ("tag".to_string(), JsonValue::Null),
            ]),
        ),
    ]);

    println!("count: {:?}", get(&["count"], &json));
    println!("users[0].name: {:?}", get_string(&["users", "0", "name"], &json));
    println!("users[1].age: {:?}", get_number(&["users", "1", "age"], &json));
    println!("missing: {:?}", get(&["missing"], &json));
}

#[cfg(test)]
mod tests {
    use super::*;

    fn make_json() -> JsonValue {
        JsonValue::Object(vec![
            (
                "users".to_string(),
                JsonValue::Array(vec![
                    JsonValue::Object(vec![
                        ("name".to_string(), JsonValue::Str("Alice".to_string())),
                        ("age".to_string(), JsonValue::Number(30.0)),
                        ("active".to_string(), JsonValue::Bool(true)),
                    ]),
                    JsonValue::Object(vec![
                        ("name".to_string(), JsonValue::Str("Bob".to_string())),
                        ("age".to_string(), JsonValue::Number(25.0)),
                        ("active".to_string(), JsonValue::Bool(false)),
                    ]),
                ]),
            ),
            ("count".to_string(), JsonValue::Number(2.0)),
            (
                "meta".to_string(),
                JsonValue::Object(vec![
                    ("version".to_string(), JsonValue::Str("1.0".to_string())),
                    ("tag".to_string(), JsonValue::Null),
                ]),
            ),
        ])
    }

    #[test]
    fn test_basic_queries() {
        let json = make_json();
        assert_eq!(get(&["count"], &json), Some(&JsonValue::Number(2.0)));
        assert_eq!(
            get(&["users", "0", "name"], &json),
            Some(&JsonValue::Str("Alice".to_string()))
        );
        assert_eq!(
            get(&["users", "1", "name"], &json),
            Some(&JsonValue::Str("Bob".to_string()))
        );
        assert_eq!(get(&["meta", "tag"], &json), Some(&JsonValue::Null));
    }

    #[test]
    fn test_missing_paths() {
        let json = make_json();
        assert_eq!(get(&["missing"], &json), None);
        assert_eq!(get(&["users", "5", "name"], &json), None);
        assert_eq!(get(&["users", "0", "missing"], &json), None);
    }

    #[test]
    fn test_typed_extractors() {
        let json = make_json();
        assert_eq!(get_string(&["users", "0", "name"], &json), Some("Alice"));
        assert_eq!(get_number(&["count"], &json), Some(2.0));
        assert_eq!(get_bool(&["users", "0", "active"], &json), Some(true));
        assert_eq!(get_bool(&["users", "1", "active"], &json), Some(false));
    }

    #[test]
    fn test_empty_path_returns_root() {
        let json = make_json();
        assert_eq!(get(&[], &json), Some(&json));
    }

    #[test]
    fn test_get_or_default() {
        let json = make_json();
        let result = get_or(JsonValue::Str("default".into()), &["missing"], &json);
        assert_eq!(result, JsonValue::Str("default".to_string()));
        let result2 = get_or(JsonValue::Null, &["count"], &json);
        assert_eq!(result2, JsonValue::Number(2.0));
    }
}
(* 957: JSON Query by Path *)

type json =
  | Null
  | Bool of bool
  | Number of float
  | Str of string
  | Array of json list
  | Object of (string * json) list

(* Approach 1: Path query returning Option *)

let rec get path json =
  match path, json with
  | [], j -> Some j
  | key :: rest, Object pairs ->
    (match List.assoc_opt key pairs with
     | Some v -> get rest v
     | None -> None)
  | idx :: rest, Array items ->
    (match int_of_string_opt idx with
     | Some i when i >= 0 && i < List.length items ->
       get rest (List.nth items i)
     | _ -> None)
  | _ -> None

(* Approach 2: Extract typed values *)

let get_string path json =
  match get path json with
  | Some (Str s) -> Some s
  | _ -> None

let get_number path json =
  match get path json with
  | Some (Number n) -> Some n
  | _ -> None

let get_bool path json =
  match get path json with
  | Some (Bool b) -> Some b
  | _ -> None

let get_array path json =
  match get path json with
  | Some (Array items) -> Some items
  | _ -> None

(* Approach 3: Query with default *)

let get_or default path json =
  match get path json with
  | Some v -> v
  | None -> default

let () =
  let json = Object [
    ("users", Array [
      Object [("name", Str "Alice"); ("age", Number 30.0); ("active", Bool true)];
      Object [("name", Str "Bob");   ("age", Number 25.0); ("active", Bool false)];
    ]);
    ("count", Number 2.0);
    ("meta", Object [("version", Str "1.0"); ("tag", Null)]);
  ] in

  (* Basic path queries *)
  assert (get ["count"] json = Some (Number 2.0));
  assert (get ["users"; "0"; "name"] json = Some (Str "Alice"));
  assert (get ["users"; "1"; "name"] json = Some (Str "Bob"));
  assert (get ["users"; "0"; "active"] json = Some (Bool true));
  assert (get ["users"; "1"; "active"] json = Some (Bool false));
  assert (get ["meta"; "version"] json = Some (Str "1.0"));
  assert (get ["meta"; "tag"] json = Some Null);

  (* Missing paths return None *)
  assert (get ["missing"] json = None);
  assert (get ["users"; "5"; "name"] json = None);
  assert (get ["users"; "0"; "missing"] json = None);

  (* Typed extractors *)
  assert (get_string ["users"; "0"; "name"] json = Some "Alice");
  assert (get_number ["count"] json = Some 2.0);
  assert (get_bool ["users"; "0"; "active"] json = Some true);

  (* Empty path returns whole document *)
  assert (get [] json = Some json);

  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

JSON Query by Path โ€” Comparison

Core Insight

Recursive path traversal is the same algorithm in both languages. The critical difference: OCaml's GC makes returning values trivial (`Some v`), while Rust must track where the returned data lives using lifetime annotations (`Option<&'a JsonValue>`). The borrow is more efficient (no copy) but requires explicit lifetime reasoning.

OCaml Approach

  • `List.assoc_opt key pairs` finds a key in association list, returning `Option`
  • `List.nth items i` indexes into a list (O(n) โ€” fine for small arrays)
  • `int_of_string_opt` safely parses array indices
  • Recursive `match path, json with` cleanly handles all combinations
  • Returns `Some j` โ€” a GC-managed copy (or shared immutable value)

Rust Approach

  • `pairs.iter().find(|(k, _)| k == key)` searches Vec of pairs
  • `items.get(idx)` bounds-checked index returning `Option<&T>`
  • `key.parse::<usize>().ok()` for index parsing
  • Slice pattern `[key, rest @ ..]` for head/tail deconstruction
  • Returns `Option<&'a JsonValue>` โ€” a borrowed reference, zero-copy
  • `'a` lifetime links output reference to input reference

Comparison Table

AspectOCamlRust
Return type`json option``Option<&'a JsonValue>`
Memory modelGC, shared immutableBorrow, zero-copy, explicit lifetime
Assoc list lookup`List.assoc_opt key pairs``pairs.iter().find(\(k,_)\k==key)`
Array index`List.nth items i``items.get(idx)`
Index parsing`int_of_string_opt``key.parse::<usize>().ok()`
Path deconstruction`key :: rest``[key, rest @ ..]` slice pattern
Chaining options`match ... with \Some v -> get rest v``.and_then(\v\get(rest, v))`