0
votes

The problem I want to solve is:

Given the recursively nested data structure, eg. a JSON tree, and a path pointing to (possibly non-existent) element inside it, return the mutable reference of the element, that's the closest to given path.

Example: if we have JSON document in form { a: { b: { c: "foo" } } } and a path a.b.d, we want to have a mutable pointer to value stored under key "b".

This is a code snippet, what I've got so far:

use std::collections::HashMap;

enum Json {
    Number(i64),
    Bool(bool),
    String(String),
    Array(Vec<Json>),
    Object(HashMap<String, Json>)
}

struct Pointer<'a, 'b> {
    value: &'a mut Json,
    path: Vec<&'b str>,
    position: usize
}

/// Return a mutable pointer to JSON element having shared 
/// the nearest common path with provided JSON. 
fn nearest_mut<'a,'b>(obj: &'a mut Json, path: Vec<&'b str>) -> Pointer<'a,'b> {
    let mut i = 0;
    let mut current = obj;
    for &key in path.iter() {
        match current {
            Json::Array(array) => {
                match key.parse::<usize>() {
                    Ok(index) => {
                        match array.get_mut(index) {
                            Some(inner) => current = inner,
                            None => break,
                        }
                    },
                    _ => break,
                }
            } ,
            Json::Object(map) => {
                match map.get_mut(key) {
                    Some(inner) => current = inner,
                    None => break
                }
            },
            _ => break,
        };
        i += 1;
    }
    Pointer { path, position: i, value: current }
}

The problem is that this doesn't pass through Rust's borrow checker, as current is borrowed as mutable reference twice, once inside match statement and once at the end of the function, when constructing the pointer method.

I've tried a different approaches, but not figured out how to achieve the goal (maybe going the unsafe path).

1
How about using an RcAkiner Alkan
Take an additional closure parameter, and return a Result?edwardw

1 Answers

1
votes

I completely misread your question and I owe you an apology.

You cannot do it in one pass - you're going to need to do a read-only pass to find the nearest path (or exact path), and then a read-write pass to actually extract the reference, or pass a mutator function in the form of a closure.

I've implemented the two-pass method for you. Do note that it is still pretty performant:

fn nearest_mut<'a, 'b>(obj: &'a mut Json, path: Vec<&'b str>) -> Pointer<'a, 'b> {
    let valid_path = nearest_path(obj, path);
    exact_mut(obj, valid_path).unwrap()
}
fn exact_mut<'a, 'b>(obj: &'a mut Json, path: Vec<&'b str>) -> Option<Pointer<'a, 'b>> {
    let mut i = 0;
    let mut target = obj;
    for token in path.iter() {
        i += 1;
        // borrow checker gets confused about `target` being mutably borrowed too many times because of the loop
        // this once-per-loop binding makes the scope clearer and circumvents the error
        let target_once = target;
        let target_opt = match *target_once {
            Json::Object(ref mut map) => map.get_mut(*token),
            Json::Array(ref mut list) => match token.parse::<usize>() {
                Ok(t) => list.get_mut(t),
                Err(_) => None,
            },
            _ => None,
        };
        if let Some(t) = target_opt {
            target = t;
        } else {
            return None;
        }
    }
    Some(Pointer {
        path,
        position: i,
        value: target,
    })
}
/// Return a mutable pointer to JSON element having shared
/// the nearest common path with provided JSON.
fn nearest_path<'a, 'b>(obj: &'a Json, path: Vec<&'b str>) -> Vec<&'b str> {
    let mut i = 0;
    let mut target = obj;
    let mut valid_paths = vec![];
    for token in path.iter() {
        // borrow checker gets confused about `target` being mutably borrowed too many times because of the loop
        // this once-per-loop binding makes the scope clearer and circumvents the error
        let target_opt = match *target {
            Json::Object(ref map) => map.get(*token),
            Json::Array(ref list) => match token.parse::<usize>() {
                Ok(t) => list.get(t),
                Err(_) => None,
            },
            _ => None,
        };
        if let Some(t) = target_opt {
            target = t;
            valid_paths.push(*token)
        } else {
            return valid_paths;
        }
    }
    return valid_paths
}

The principle is simple - I reused the method I wrote in my initial question in order to get the nearest valid path (or exact path).

From there, I feed that straight into the function that I had in my original answer, and since I am certain the path is valid (from the prior function call) I can safely unwrap() :-)