blob: 691539b38fb18271a1d5a0823d4a57cb4cf8ac43 [file] [log] [blame]
// Copyright 2018 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 crate::error::{Error, Location};
use json5format;
use json5format::{FormatOptions, PathOption};
use maplit::hashmap;
use maplit::hashset;
use serde::ser::Serialize;
use serde_json::ser::{CompactFormatter, PrettyFormatter, Serializer};
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::str::from_utf8;
/// For `.cml` JSON5 files, format the file to match the default style. (The "pretty" option is
/// ignored.) See format_cml() for current style conventions.
///
/// For all other files, assume the input is standard JSON:
/// Read in the json file from the given path, minify it if pretty is false or pretty-ify it if
/// pretty is true, and then write the results to stdout if output is None or to the given path if
/// output is Some.
pub fn format(
file: &PathBuf,
pretty: bool,
cml: bool,
output: Option<PathBuf>,
) -> Result<(), Error> {
let mut buffer = String::new();
fs::File::open(&file)?.read_to_string(&mut buffer)?;
let file_path = file
.clone()
.into_os_string()
.into_string()
.map_err(|e| Error::internal(format!("Unhandled file path: {:?}", e)))?;
let res = if cml || file_path.ends_with(".cml") {
format_cml(buffer, file.as_path())?
} else {
format_cmx(buffer, pretty)?
};
if let Some(output_path) = output {
fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(output_path)?
.write_all(&res)?;
} else {
// Print without a newline because the formatters should have already added a final newline
// (required by most software style guides).
print!("{}", from_utf8(&res)?);
}
Ok(())
}
pub fn format_cmx(buffer: String, pretty: bool) -> Result<Vec<u8>, Error> {
let v: serde_json::Value = serde_json::from_str(&buffer)?;
let mut res = Vec::new();
if pretty {
let mut ser = Serializer::with_formatter(&mut res, PrettyFormatter::with_indent(b" "));
v.serialize(&mut ser)?;
} else {
let mut ser = Serializer::with_formatter(&mut res, CompactFormatter {});
v.serialize(&mut ser)?;
}
Ok(res)
}
pub fn format_cml(buffer: String, file: &Path) -> Result<Vec<u8>, Error> {
let options = FormatOptions {
collapse_containers_of_one: true,
sort_array_items: true, // but use options_by_path to turn this off for program args
options_by_path: hashmap! {
"/*" => hashset! {
PathOption::PropertyNameOrder(vec![
"include",
"program",
"children",
"collections",
"capabilities",
"use",
"offer",
"expose",
"environments",
"facets",
])
},
"/*/program" => hashset! {
PathOption::CollapseContainersOfOne(false),
PathOption::PropertyNameOrder(vec![
"binary",
"args",
])
},
"/*/program/args" => hashset! {
PathOption::SortArrayItems(false),
},
"/*/*/*" => hashset! {
PathOption::PropertyNameOrder(vec![
"name",
"url",
"startup",
"environment",
"durability",
"service",
"protocol",
"directory",
"storage",
"runner",
"resolver",
"event",
"event_stream",
"from",
"as",
"to",
"rights",
"path",
"subdir",
"filter",
"dependency",
"extends",
"runners",
"resolvers",
])
},
},
..Default::default()
};
json5format::format(&buffer, Some(file.to_string_lossy().into_owned()), Some(options)).map_err(
|err| match err {
json5format::Error::Configuration(errstr) => Error::Internal(errstr),
json5format::Error::Parse(location, errstr) => match location {
Some(location) => Error::parse(
errstr,
Some(Location { line: location.line, column: location.col }),
Some(file),
),
None => Error::parse(errstr, None, Some(file)),
},
json5format::Error::Internal(location, errstr) => match location {
Some(location) => Error::Internal(format!("{}: {}", location, errstr)),
None => Error::Internal(errstr),
},
json5format::Error::TestFailure(location, errstr) => match location {
Some(location) => {
Error::Internal(format!("{}: Test failure: {}", location, errstr))
}
None => Error::Internal(format!("Test failure: {}", errstr)),
},
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::TempDir;
fn count_num_newlines(input: &str) -> usize {
let mut ret = 0;
for c in input.chars() {
if c == '\n' {
ret += 1;
}
}
ret
}
#[test]
fn test_format_json() {
let example_json = r#"{ "program": { "binary": "bin/app"
}, "sandbox": {
"dev": [
"class/camera" ], "features": [
"persistent-storage", "vulkan"] } }"#;
let tmp_dir = TempDir::new().unwrap();
let tmp_file_path = tmp_dir.path().join("input.json");
File::create(&tmp_file_path).unwrap().write_all(example_json.as_bytes()).unwrap();
let output_file_path = tmp_dir.path().join("output.json");
// format as not-pretty
let result = format(&tmp_file_path, false, false, Some(output_file_path.clone()));
assert!(result.is_ok());
let mut buffer = String::new();
File::open(&output_file_path).unwrap().read_to_string(&mut buffer).unwrap();
assert_eq!(0, count_num_newlines(&buffer));
// format as pretty (not .cml)
let result = format(&tmp_file_path, true, false, Some(output_file_path.clone()));
assert!(result.is_ok());
let mut buffer = String::new();
File::open(&output_file_path).unwrap().read_to_string(&mut buffer).unwrap();
assert_eq!(13, count_num_newlines(&buffer));
}
#[test]
fn test_format_json5() {
let example_json5 = r##"{
offer: [
{
runner: "elf",
},
{
from: "framework",
to: "#elements",
protocol: "/svc/fuchsia.sys2.Realm",
},
{
to: "#elements",
protocol: [
"/svc/fuchsia.logger.LogSink",
"/svc/fuchsia.cobalt.LoggerFactory",
],
from: "realm",
},
],
collections: [
{
durability: "transient",
name: "elements",
}
],
use: [
{
runner: "elf",
},
{
protocol: "/svc/fuchsia.sys2.Realm",
from: "framework",
},
{
from: "realm",
to: "#elements",
protocol: [
"/svc/fuchsia.logger.LogSink",
"/svc/fuchsia.cobalt.LoggerFactory",
],
},
],
children: [
],
program: {
args: [ "--zarg_first", "zoo_opt", "--arg3", "and_arg3_value" ],
binary: "bin/session_manager",
},
}"##;
let expected = r##"{
program: {
binary: "bin/session_manager",
args: [
"--zarg_first",
"zoo_opt",
"--arg3",
"and_arg3_value",
],
},
children: [],
collections: [
{
name: "elements",
durability: "transient",
},
],
use: [
{ runner: "elf" },
{
protocol: "/svc/fuchsia.sys2.Realm",
from: "framework",
},
{
protocol: [
"/svc/fuchsia.cobalt.LoggerFactory",
"/svc/fuchsia.logger.LogSink",
],
from: "realm",
to: "#elements",
},
],
offer: [
{ runner: "elf" },
{
protocol: "/svc/fuchsia.sys2.Realm",
from: "framework",
to: "#elements",
},
{
protocol: [
"/svc/fuchsia.cobalt.LoggerFactory",
"/svc/fuchsia.logger.LogSink",
],
from: "realm",
to: "#elements",
},
],
}
"##;
let tmp_dir = TempDir::new().unwrap();
let tmp_file_path = tmp_dir.path().join("input.cml");
File::create(&tmp_file_path).unwrap().write_all(example_json5.as_bytes()).unwrap();
let output_file_path = tmp_dir.path().join("output.cml");
// format as json5 with .cml style options
let result = format(&tmp_file_path, false, true, Some(output_file_path.clone()));
assert!(result.is_ok());
let mut buffer = String::new();
File::open(&output_file_path).unwrap().read_to_string(&mut buffer).unwrap();
assert_eq!(buffer, expected);
}
#[test]
fn test_format_invalid_json_fails() {
let example_json = "{\"foo\": 1,}";
let tmp_dir = TempDir::new().unwrap();
let tmp_file_path = tmp_dir.path().join("input.json");
File::create(&tmp_file_path).unwrap().write_all(example_json.as_bytes()).unwrap();
// format as not-pretty (not .cml)
let result = format(&tmp_file_path, false, false, None);
assert!(result.is_err());
}
#[test]
fn test_format_invalid_json_may_be_valid_json5() {
let example_json = "{\"foo\": 1,}";
let tmp_dir = TempDir::new().unwrap();
let tmp_file_path = tmp_dir.path().join("input.json");
File::create(&tmp_file_path).unwrap().write_all(example_json.as_bytes()).unwrap();
// json formatter
let result = format(&tmp_file_path, false, false, None);
assert!(result.is_err());
// json5 formatter
let result = format(&tmp_file_path, false, true, None);
assert!(result.is_ok());
}
#[test]
fn test_format_invalid_json5_fails() {
let example_json5 = "{\"foo\" 1}";
let tmp_dir = TempDir::new().unwrap();
let tmp_file_path = tmp_dir.path().join("input.json");
File::create(&tmp_file_path).unwrap().write_all(example_json5.as_bytes()).unwrap();
// json5 formatter
let result = format(&tmp_file_path, false, true, None);
assert!(result.is_err());
}
}