Skip to content

Commit

Permalink
Refactor set_path.
Browse files Browse the repository at this point in the history
  • Loading branch information
Nathan Hammond authored and Nathan Hammond committed Jul 13, 2023
1 parent 08ce1a4 commit 6e31073
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 60 deletions.
2 changes: 1 addition & 1 deletion crates/turborepo-lib/src/commands/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crate::ui::CYAN;
use crate::{
cli::LinkTarget,
commands::CommandBase,
rewrite_json::{self, RewriteError},
rewrite_json,
ui::{BOLD, GREY, UNDERLINE},
};

Expand Down
175 changes: 116 additions & 59 deletions crates/turborepo-lib/src/rewrite_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ pub enum RewriteError {
#[error("The JSON document contains no root object.")]
NoRoot,
}

/**
* When generating replacement content it's one of two things.
*/
enum GenerateType {
Object,
Path,
Expand Down Expand Up @@ -95,7 +97,7 @@ pub fn set_path(
let missing_path_segments = path[closest_path.len()..].to_vec();
let computed_object = match generate_type {
GenerateType::Object => generate_object(missing_path_segments, json_value),
GenerateType::Path => generate_path(separator, missing_path_segments, json_value),
GenerateType::Path => generate_path(missing_path_segments, json_value, separator),
};

// Generate a new document!
Expand All @@ -104,6 +106,10 @@ pub fn set_path(
return Ok(output);
}

/**
* get_root returns the document root, or information on the error
* encountered with the input json_document_string.
*/
fn get_root(json_document_string: &str) -> Result<jsonc_parser::ast::Value, RewriteError> {
let parse_result_result = parse_to_ast(
json_document_string,
Expand All @@ -120,37 +126,51 @@ fn get_root(json_document_string: &str) -> Result<jsonc_parser::ast::Value, Rewr
};
}

/**
* get_closest_node traverses the JSON document via recursive tail calls to
* find the last node that exists with in this JSON key path.
*/
fn get_closest_node<'a>(
current_node: &'a jsonc_parser::ast::Value<'a>,
target_path: &Vec<&'a str>,
current_path: Vec<&'a str>,
) -> (Vec<&'a str>, &'a jsonc_parser::ast::Value<'a>) {
// No target_path? We've arrived.
if target_path.len() == 0 {
return (current_path, current_node);
}

match current_node {
// Only objects can have key paths.
jsonc_parser::ast::Value::Object(obj) => {
let object_property = obj.properties.iter().rev().find(|property| {
let current_property_name = property.name.as_str();
return target_path[0] == current_property_name;
});

if object_property.is_some() {
let kv_pair = &object_property.unwrap();
let next_property_name = kv_pair.name.as_str();
let mut next_path = current_path.clone();
next_path.push(next_property_name);
let next_node = &object_property.unwrap().value;
return get_closest_node(next_node, &target_path[1..].to_vec(), next_path);
} else {
return (current_path, current_node);
// See if we found a matching key. If so, recurse.
match object_property {
Some(property) => {
let next_current_node = &property.value;
let next_property_name = property.name.as_str();
let mut next_current_path = current_path.clone();
next_current_path.push(next_property_name);
let next_target_path = &target_path[1..].to_vec();

// Tail call!
get_closest_node(next_current_node, next_target_path, next_current_path)
}
None => (current_path, current_node),
}
}
// All other node types are complete.
_ => (current_path, current_node),
}
}

/**
* Given path segments, generate a JSON object.
*/
fn generate_object(path_segments: Vec<&str>, value: &str) -> String {
let mut output = String::new();
let length = path_segments.len();
Expand All @@ -170,7 +190,11 @@ fn generate_object(path_segments: Vec<&str>, value: &str) -> String {
return output;
}

fn generate_path(separator: &str, path_segments: Vec<&str>, value: &str) -> String {
/**
* Given path segments, generate a JSON object member with an optional
* trailing separator.
*/
fn generate_path(path_segments: Vec<&str>, value: &str, separator: &str) -> String {
let mut output = String::new();
output.push_str("\"");
output.push_str(path_segments[0]);
Expand All @@ -182,89 +206,122 @@ fn generate_path(separator: &str, path_segments: Vec<&str>, value: &str) -> Stri
return output;
}

/**
* Given a JSONC document and an object traversal path, `unset_path` will
* return a minimally-mutated JSONC document with all occurrences of the
* specified path removed.
*/
pub fn unset_path(json_document_string: &str, path: Vec<&str>) -> Result<String, RewriteError> {
let root = get_root(json_document_string)?;

// The key path can appear multiple times. This a vec that contains each time it
// occurs.
let path_ranges = find_all_paths(&root, &path, vec![]);

// We mutate this as we go.
let mut output: String = json_document_string.to_owned();
let mut last_start = None;

// We could either join overlapping ranges, or just carry it over.
// This elects to carry it over.
let mut last_start_position = None;

// We iterate in reverse since we're mutating the string.
path_ranges.iter().rev().for_each(|range| {
let end = if last_start.is_some() {
if range.end > last_start.unwrap() {
last_start.unwrap()
} else {
range.end
let end = match last_start_position {
Some(last_start_position) => {
if range.end > last_start_position {
last_start_position
} else {
range.end
}
}
} else {
range.end
None => range.end,
};

let replacement_char = if end != range.end {
let is_overlapping = end != range.end;
let replacement_char = if is_overlapping {
""
} else {
&range.replacement_char
};

output.replace_range(range.start..end, replacement_char);
last_start = Some(range.start);
last_start_position = Some(range.start);
});

return Ok(output);
}

/**
* find_all_paths returns the list of ranges which define the specified
* token.
*/
fn find_all_paths<'a>(
current_node: &'a jsonc_parser::ast::Value<'a>,
target_path: &Vec<&'a str>,
current_path: Vec<&'a str>,
) -> Vec<Range> {
let mut ranges: Vec<Range> = vec![];

if target_path.len() > 0 {
match current_node {
jsonc_parser::ast::Value::Object(obj) => {
let properties: Vec<_> = obj.properties.iter().collect();
// Early exit when it's impossible to have matching ranges.
if target_path.len() == 0 {
return ranges;
}

for index in 0..properties.len() {
let is_first = index == 0;
let is_last = index == properties.len() - 1;
match current_node {
// We can only find paths on objects.
jsonc_parser::ast::Value::Object(obj) => {
// We need a reference to the previous and next property to identify if we're
// looking at the first or last node which need special handling.
let properties: Vec<_> = obj.properties.iter().collect();

let start = if is_first {
properties[index].range.start
} else {
properties[index - 1].range.end
};
for index in 0..properties.len() {
let property = properties[index];

let end = if is_last {
properties[index].range.end
} else {
properties[index + 1].range.start
};

let replacement_char = if is_first || is_last { "" } else { "," };

let property = properties[index];
let current_property_name = property.name.as_str();
if target_path[0] == current_property_name {
if target_path.len() == 1 {
ranges.push(Range {
start,
end,
replacement_char: replacement_char.to_owned(),
});
let current_property_name = property.name.as_str();
if target_path[0] == current_property_name {
// target_path == 1? We've arrived at a node to remove.
if target_path.len() == 1 {
let is_first = index == 0;
let is_last = index == properties.len() - 1;

// We calculate the range based off the adjacent nodes.
// This is required to ensure that we capture things like commas.
let start = if is_first {
property.range.start
} else {
properties[index - 1].range.end
};

let end = if is_last {
property.range.end
} else {
let mut next_path = current_path.clone();
next_path.push(property.name.as_str());
let next_node = &property.value;
let mut children_ranges =
find_all_paths(&next_node, &target_path[1..].to_vec(), next_path);
ranges.append(&mut children_ranges);
}
properties[index + 1].range.start
};

let replacement_char = if is_first || is_last { "" } else { "," };

ranges.push(Range {
start,
end,
replacement_char: replacement_char.to_owned(),
});
} else {
// We must recurse.
let next_current_node = &property.value;
let next_property_name = property.name.as_str();
let mut next_current_path = current_path.clone();
next_current_path.push(next_property_name);
let next_target_path = &target_path[1..].to_vec();

let mut children_ranges =
find_all_paths(next_current_node, next_target_path, next_current_path);
ranges.append(&mut children_ranges);
}
}
}
_ => {}
}
_ => {}
}

return ranges;
Expand Down

0 comments on commit 6e31073

Please sign in to comment.