blob: 9161f9edade0edc4aaf139a45774e721f950675f [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow;
use json5format::*;
use std::mem::discriminant;
/// A recursive function that takes in two references to `json5format::Value`s,
/// and modifies the second one to incorporate comments contained in the first.
/// The function does a depth-first search of the JSON5 tree structure,
/// transferring comments for a value X in `json5_val` to a value Y in
/// `jq_output_val` if both a) X and Y have the same path from the root of the
/// tree. b) X and Y are the same `json5format::Value` variant. In particular, a
/// comment attached to X does NOT transfer over to anything if X's path from the
/// root does not exist in `jq_output_val`, or if the path points to a different
/// `Value` variant. For example, comments do not transfer from an object to an
/// array, even if they have the same path from root. Specific examples are
/// provided in tests.
///
/// `json5_val` is the original value containing the comments to be transferred.
/// `jq_output_val` is presumed to reference a comment-less value generated from
/// stdout from a jq call. While it may actually contain comments, it is
/// presumed that it will not. If it does, many will likely be deleted,
/// especially if it is similar in structure to `json5_val`.
fn fill_comments_helper(json5_val: &Value, jq_output_val: &mut Value) -> Result<(), anyhow::Error> {
match json5_val {
Value::Object { val, comments } => {
match jq_output_val {
Value::Object { val: out_val, comments: out_comments } => {
*out_comments = comments.clone();
*out_val.trailing_comments_mut() = val.trailing_comments().clone();
let mut out_properties : Vec<_> = out_val.properties_mut().collect();
for property in val.properties() {
let index = out_properties
.iter()
.position(|p_output| p_output.name() == property.name());
if let Some(i) = index {
//Using two nested conditionals due to compiler bug when using if-let syntax
if discriminant(&*property.value())
== discriminant(&*out_properties[i].value())
{
fill_comments_helper(
&property.value(),
&mut out_properties[i].value_mut(),
)?;
}
}
}
}
_ => {
return Err(anyhow::anyhow!(
"fill_comments_helper was called on mismatched Value variants.\njson5_val was: {:?}\njq_output_val was: {:?}",
json5_val,
jq_output_val
))
}
}
}
Value::Array { val, comments } => {
match jq_output_val {
Value::Array { val: out_val, comments: out_comments } => {
*out_comments = comments.clone();
*out_val.trailing_comments_mut() = val.trailing_comments().clone();
for (sub_val, mut out_sub_val) in val.items().zip(out_val.items_mut()) {
if discriminant(&(*sub_val)) == discriminant(&(*out_sub_val))
{
fill_comments_helper(
&sub_val,
&mut out_sub_val,
)?;
}
}
}
_ => {
return Err(anyhow::anyhow!(
"fill_comments_helper was called on mismatched Value variants.\njson5_val was: {:?}\njq_output_val was: {:?}",
json5_val,
jq_output_val
))
}
}
}
Value::Primitive { comments, .. } => match jq_output_val {
Value::Primitive { comments: out_comments, .. } => {
*out_comments = comments.clone();
}
_ => {
return Err(anyhow::anyhow!(
"fill_comments_helper was called on mismatched Value variants.\njson5_val was: {:?}\njq_output_val was: {:?}",
json5_val,
jq_output_val
))
}
},
};
Ok(())
}
/// Transfers the comments at the bottom of the document below the json5 object
/// and calls `fill_comments_helper` to transfer the rest of the comments.
#[inline]
pub(crate) fn fill_comments(
json5_content: &Array,
jq_output_content: &mut Array,
) -> Result<(), anyhow::Error> {
*jq_output_content.trailing_comments_mut() = json5_content.trailing_comments().clone();
fill_comments_helper(
&json5_content.items().next().unwrap(),
&mut jq_output_content.items_mut().next().unwrap(),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fill_comments_handles_changed_field_type() {
let json5_original = String::from(
r##"{
// Foo
"offer": [
{
// Bar
"directory": "input",
"from": "self",
// Baz
"to": [ "#pwrbtn-monitor" ]
},
{
"protocol": "fuchsia.hardware.power.statecontrol.Admin",
"from": "self",
"to": [ "#pwrbtn-monitor" ]
}
]
}"##,
);
let json5_target = String::from(
r##"{
"offer": [
{
"directory": "input",
"from": "self",
"to": "#pwrbtn-monitor"
},
{
"protocol": "fuchsia.hardware.power.statecontrol.Admin",
"from": "self",
"to": "#pwrbtn-monitor"
}
]
}"##,
);
let expected_outcome = String::from(
r##"{
// Foo
"offer": [
{
// Bar
"directory": "input",
"from": "self",
"to": "#pwrbtn-monitor"
},
{
"protocol": "fuchsia.hardware.power.statecontrol.Admin",
"from": "self",
"to": "#pwrbtn-monitor"
}
]
}"##,
);
let parsed_json5_original =
json5format::ParsedDocument::from_str(&json5_original[..], None).unwrap();
let mut parsed_json5_target =
json5format::ParsedDocument::from_str(&json5_target[..], None).unwrap();
let parsed_expected_outcome =
json5format::ParsedDocument::from_str(&expected_outcome[..], None).unwrap();
fill_comments(&parsed_json5_original.content, &mut parsed_json5_target.content).unwrap();
let format = json5format::Json5Format::new().unwrap();
assert_eq!(
format.to_string(&parsed_expected_outcome).unwrap(),
format.to_string(&parsed_json5_target).unwrap()
);
}
#[test]
fn simple_transfer_object1() {
let json5_original = String::from(
r##"
//Comment at
//Beginning of
//Document
{
// Foo
hello: 'world',
// Bar
yoinks: "everybody", //Baz
//Contained Comment (at end of object)
}
"##,
);
let json5_target = String::from(
r##"
{
hello: 'world',
yoinks: "everybody",
}
"##,
);
let parsed_json5_original =
json5format::ParsedDocument::from_str(&json5_original[..], None).unwrap();
let mut parsed_json5_target =
json5format::ParsedDocument::from_str(&json5_target[..], None).unwrap();
fill_comments(&parsed_json5_original.content, &mut parsed_json5_target.content).unwrap();
let format = json5format::Json5Format::new().unwrap();
assert_eq!(
format.to_string(&parsed_json5_original).unwrap(),
format.to_string(&parsed_json5_target).unwrap()
);
}
#[test]
fn simple_transfer_object2() {
let json5_original = String::from(
r##"
//Comment at
//Beginning of
//Document
{
// Foo
hello: 'world',
// Bar
yoinks: "everybody", //Baz
//Contained Comment (at end of object)
}
"##,
);
let json5_target = String::from(
r##"
//All
{
//these
hello: 'world', //comments
//will
yoinks: "everybody", //be
//deleted
//by fill_comments
//since they are attached to nodes that correspond to a path in the original json5.
}
"##,
);
let parsed_json5_original =
json5format::ParsedDocument::from_str(&json5_original[..], None).unwrap();
let mut parsed_json5_target =
json5format::ParsedDocument::from_str(&json5_target[..], None).unwrap();
fill_comments(&parsed_json5_original.content, &mut parsed_json5_target.content).unwrap();
let format = json5format::Json5Format::new().unwrap();
assert_eq!(
format.to_string(&parsed_json5_original).unwrap(),
format.to_string(&parsed_json5_target).unwrap()
);
}
#[test]
fn simple_transfer_object3() {
let json5_original = String::from(
r##"
//Comment at
//Beginning of
//Document
{
// Foo
hello: 'world',
// Bar
yoinks: 'everybody', //Baz
//This comment will be deleted since the field does not appear in the target
removed_in_target: "Away I must go!",
//Contained Comment (at end of object)
}
"##,
);
let json5_target = String::from(
r##"
{
hello: 'world',
yoinks: 'everybody',
//This comment will stay, since there is no node in the original that overrides this node.
addition_in_target: 'Nice to meet you!',
}
"##,
);
let parsed_json5_original =
json5format::ParsedDocument::from_str(&json5_original[..], None).unwrap();
let mut parsed_json5_target =
json5format::ParsedDocument::from_str(&json5_target[..], None).unwrap();
fill_comments(&parsed_json5_original.content, &mut parsed_json5_target.content).unwrap();
let format = json5format::Json5Format::new().unwrap();
let expected_outcome = String::from(
r##"
//Comment at
//Beginning of
//Document
{
// Foo
hello: 'world',
// Bar
yoinks: 'everybody', //Baz
//This comment will stay, since there is no node in the original that overrides this node.
addition_in_target: 'Nice to meet you!',
//Contained Comment (at end of object)
}
"##,
);
assert_eq!(
format.to_string(&parsed_json5_target).unwrap(),
format
.to_string(
&json5format::ParsedDocument::from_str(&expected_outcome[..], None).unwrap()
)
.unwrap()
);
}
#[test]
fn simple_transfer_array() {
let json5_original = String::from(
r##"
{
"array": [
//First Comment.
1,
//Second Comment.
2
//Contained Comment (end of array).
]
}
"##,
);
let json5_target = String::from(
r##"
{
"array": [
1,
2,
3
]
}
"##,
);
let parsed_json5_original =
json5format::ParsedDocument::from_str(&json5_original[..], None).unwrap();
let mut parsed_json5_target =
json5format::ParsedDocument::from_str(&json5_target[..], None).unwrap();
fill_comments(&parsed_json5_original.content, &mut parsed_json5_target.content).unwrap();
let format = json5format::Json5Format::new().unwrap();
let expected_outcome = String::from(
r##"
{
"array": [
//First Comment.
1,
//Second Comment.
2,
3
//Contained Comment (end of array).
]
}
"##,
);
assert_eq!(
format.to_string(&parsed_json5_target).unwrap(),
format
.to_string(
&json5format::ParsedDocument::from_str(&expected_outcome[..], None).unwrap()
)
.unwrap()
);
}
#[test]
#[ignore]
/*
This test fails with the current implementation, and is included to show an
example of what fill_comments does *not* do. When transferring comments
between arrays, a deleted entry shifts the entries that come after it. This
presents certain issues.
For example, if a comment is attached to a value at index 1 and the value at
index 0 is deleted in the target array, fill_comments does *not* transfer
the comment to index 0, as one might desire. Instead, it transfers it to
the value at index 1 (or doesn't transfer it at all if index 1 does
not exist in the target).
*/
fn transfer_array_deleted_value() {
let json5_original = String::from(
r##"
{
"array": [
//First Comment.
1,
//Second Comment.
2
]
}
"##,
);
let json5_target = String::from(
r##"
{
"array": [
2,
]
}
"##,
);
let parsed_json5_original =
json5format::ParsedDocument::from_str(&json5_original[..], None).unwrap();
let mut parsed_json5_target =
json5format::ParsedDocument::from_str(&json5_target[..], None).unwrap();
fill_comments(&parsed_json5_original.content, &mut parsed_json5_target.content).unwrap();
let format = json5format::Json5Format::new().unwrap();
let expected_outcome = String::from(
r##"
{
"array": [
//Second Comment.
2,
]
}
"##,
);
assert_eq!(
format.to_string(&parsed_json5_target).unwrap(),
format
.to_string(
&json5format::ParsedDocument::from_str(&expected_outcome[..], None).unwrap()
)
.unwrap()
);
}
#[test]
fn recursive_transfer() {
let json5_original = String::from(
r##"
//Comment at beginning.
{
//Foo1
"Foo1": "Bar1",
//Array
"arr": [
//Primitive in array. Even though the actual value changes, this comment
//gets carried over since the same path from root exists in the target.
"Baz",
//Object in array
{
//Foo2
"Foo2": "Bar2",
//Field entry not present in target. This comment will not carry over.
"deleted_field2": "bye, again!"
//Comment at end of second object.
}
//Comment at end of array.
],
//Field entry not present in target. This comment will not carry over.
"deleted_field1": "bye, world!"
//Comment at end of first object.
}
// Comment at end of document.
"##,
);
let json5_target = String::from(
r##"
//This comment will be overriden since its path exists in the original.
{
//This comment will also be overriden.
"Foo1": "Bar1",
//This comment will also be overriden.
"arr": [
//This comment will also be overriden (even though the value changed).
"BazBaz",
//This comment will also be overriden.
{
//This comment will also be overriden.
"Foo2": "Bar2",
//Comment on new_field2 (will stay).
"new_field2": "hello, again!"
},
//Comment on new array entry, 0 (will stay).
0
],
//Comment on new_field1 (will stay).
"new_field1": "hello, world!"
}
"##,
);
let parsed_json5_original =
json5format::ParsedDocument::from_str(&json5_original[..], None).unwrap();
let mut parsed_json5_target =
json5format::ParsedDocument::from_str(&json5_target[..], None).unwrap();
fill_comments(&parsed_json5_original.content, &mut parsed_json5_target.content).unwrap();
let format = json5format::Json5Format::new().unwrap();
let expected_outcome = String::from(
r##"
//Comment at beginning.
{
//Foo1
"Foo1": "Bar1",
//Array
"arr": [
//Primitive in array. Even though the actual value changes, this comment
//gets carried over since the same path from root exists in the target.
"BazBaz",
//Object in array
{
//Foo2
"Foo2": "Bar2",
//Comment on new_field2 (will stay).
"new_field2": "hello, again!"
//Comment at end of second object.
},
//Comment on new array entry, 0 (will stay).
0
//Comment at end of array.
],
//Comment on new_field1 (will stay).
"new_field1": "hello, world!"
//Comment at end of first object.
}
// Comment at end of document.
"##,
);
assert_eq!(
format.to_string(&parsed_json5_target).unwrap(),
format
.to_string(
&json5format::ParsedDocument::from_str(&expected_outcome[..], None).unwrap()
)
.unwrap()
);
}
}