[json] add tool and action to merge json
CP-80 #comment
CP-85 #comment
TEST: new unit tests, but not sure how to put them on CI
Change-Id: I58fdcae0c1224f7be15ab5b7058bad35805bb62b
diff --git a/packages/json_merge b/packages/json_merge
new file mode 100644
index 0000000..11243e3
--- /dev/null
+++ b/packages/json_merge
@@ -0,0 +1,8 @@
+{
+ "labels": [
+ "//build/tools/json_merge:install"
+ ],
+ "host_tests": [
+ "//build/tools/json_merge:json_merge_test"
+ ]
+}
diff --git a/tools/README.md b/tools/README.md
index 0e56e0b..75338f0 100644
--- a/tools/README.md
+++ b/tools/README.md
@@ -1,3 +1,4 @@
This directory contains a collection of tools used in the GN build:
+- [JSON merge](json_merge/): merges JSON files.
- [JSON validator](json_validator/): verifies that JSON files match a given
schema.
diff --git a/tools/json_merge/BUILD.gn b/tools/json_merge/BUILD.gn
new file mode 100644
index 0000000..0f017be
--- /dev/null
+++ b/tools/json_merge/BUILD.gn
@@ -0,0 +1,48 @@
+# 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.
+
+import("//build/host.gni")
+import("//build/test.gni")
+
+source_set("srcs") {
+ sources = [
+ "json_merge.cc",
+ "json_merge.h",
+ ]
+
+ public_deps = [
+ "//third_party/rapidjson",
+ ]
+}
+
+executable("json_merge") {
+ sources = [
+ "main.cc",
+ ]
+
+ deps = [
+ ":srcs",
+ ]
+}
+
+test("json_merge_test") {
+ sources = [
+ "test.cc",
+ ]
+
+ deps = [
+ ":srcs",
+ "//third_party/googletest:gtest_main",
+ ]
+}
+
+install_host_tools("install") {
+ deps = [
+ ":json_merge",
+ ]
+
+ outputs = [
+ "json_merge",
+ ]
+}
diff --git a/tools/json_merge/example/BUILD.gn b/tools/json_merge/example/BUILD.gn
new file mode 100644
index 0000000..9d2a291
--- /dev/null
+++ b/tools/json_merge/example/BUILD.gn
@@ -0,0 +1,14 @@
+# 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.
+
+import("//build/tools/json_merge/json_merge.gni")
+
+json_merge("merged.json") {
+ sources = [
+ "black_white.json",
+ "rgb.json",
+ ]
+
+ minify = true
+}
diff --git a/tools/json_merge/example/black_white.json b/tools/json_merge/example/black_white.json
new file mode 100644
index 0000000..db5d907
--- /dev/null
+++ b/tools/json_merge/example/black_white.json
@@ -0,0 +1,14 @@
+{
+ "black": {
+ "code": {
+ "rgba": [0,0,0,1],
+ "hex": "#000"
+ }
+ },
+ "white": {
+ "code": {
+ "rgba": [0,0,0,1],
+ "hex": "#FFF"
+ }
+ }
+}
diff --git a/tools/json_merge/example/rgb.json b/tools/json_merge/example/rgb.json
new file mode 100644
index 0000000..5949376
--- /dev/null
+++ b/tools/json_merge/example/rgb.json
@@ -0,0 +1,20 @@
+{
+ "red": {
+ "code": {
+ "rgba": [255,0,0,1],
+ "hex": "#F00"
+ }
+ },
+ "green": {
+ "code": {
+ "rgba": [0,255,0,1],
+ "hex": "#0F0"
+ }
+ },
+ "blue": {
+ "code": {
+ "rgba": [0,0,255,1],
+ "hex": "#00F"
+ }
+ }
+}
diff --git a/tools/json_merge/json_merge.cc b/tools/json_merge/json_merge.cc
new file mode 100644
index 0000000..e523486
--- /dev/null
+++ b/tools/json_merge/json_merge.cc
@@ -0,0 +1,59 @@
+// 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.
+
+#include "build/tools/json_merge/json_merge.h"
+
+#include "third_party/rapidjson/rapidjson/document.h"
+#include "third_party/rapidjson/rapidjson/error/en.h"
+#include "third_party/rapidjson/rapidjson/filewritestream.h"
+#include "third_party/rapidjson/rapidjson/istreamwrapper.h"
+#include "third_party/rapidjson/rapidjson/ostreamwrapper.h"
+#include "third_party/rapidjson/rapidjson/prettywriter.h"
+#include "third_party/rapidjson/rapidjson/writer.h"
+
+int JSONMerge(const std::vector<struct input_file>& inputs,
+ std::ostream& output, std::ostream& errors, bool minify) {
+ rapidjson::Document merged;
+ merged.SetObject();
+ auto& allocator = merged.GetAllocator();
+
+ for (auto input_it = inputs.begin(); input_it != inputs.end(); ++input_it) {
+ rapidjson::IStreamWrapper isw(*input_it->contents.get());
+ rapidjson::Document input_doc;
+ rapidjson::ParseResult parse_result = input_doc.ParseStream(isw);
+ if (!parse_result) {
+ errors << "Failed to parse " << input_it->name << "!\n";
+ errors << rapidjson::GetParseError_En(parse_result.Code()) << " (offset "
+ << parse_result.Offset() << ")\n";
+ return 1;
+ }
+ if (!input_doc.IsObject()) {
+ errors << input_it->name << " is not a JSON object!\n";
+ return 1;
+ }
+
+ for (auto value_it = input_doc.MemberBegin();
+ value_it != input_doc.MemberEnd(); ++value_it) {
+ if (merged.HasMember(value_it->name)) {
+ errors << input_it->name << " has a conflicting value for key \""
+ << value_it->name.GetString() << "\"!\n";
+ return 1;
+ }
+ merged.AddMember(value_it->name,
+ rapidjson::Value(value_it->value, allocator).Move(),
+ allocator);
+ }
+ }
+
+ rapidjson::OStreamWrapper osw(output);
+ if (minify) {
+ rapidjson::Writer<rapidjson::OStreamWrapper> writer(osw);
+ merged.Accept(writer);
+ } else {
+ rapidjson::PrettyWriter<rapidjson::OStreamWrapper> writer(osw);
+ merged.Accept(writer);
+ }
+
+ return 0;
+}
diff --git a/tools/json_merge/json_merge.gni b/tools/json_merge/json_merge.gni
new file mode 100644
index 0000000..763d919
--- /dev/null
+++ b/tools/json_merge/json_merge.gni
@@ -0,0 +1,67 @@
+# 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.
+
+import("//build/compiled_action.gni")
+
+# Merge one or more json files.
+#
+# If any input is not a valid JSON, the merge operation will fail. Consequently
+# you can "merge" one JSON file to perform validation.
+# If any two inputs overlap in key space, the merge operation will fail.
+# Optionally the merged output can be minified. Consequently you can "merge"
+# one JSON file to perform minification.
+#
+# Parameters
+#
+# sources (required)
+# [files list] One or more JSON files to merge.
+#
+# minify (optional)
+# [boolean] Whether to minify the result.
+# Minified JSON is functionally equivalent but less readable to humans.
+#
+# testonly (optional)
+# visibility (optional)
+# Standard GN meaning.
+#
+# Example of usage:
+#
+# json_merge("merged.json") {
+# sources = [ "one.json", "two.json" ]
+# minify = true
+# }
+template("json_merge") {
+ compiled_action(target_name) {
+ forward_variables_from(invoker,
+ [
+ "deps",
+ "sources",
+ "testonly",
+ "visibility",
+ "minify",
+ ])
+
+ tool = "//build/tools/json_merge"
+
+ merged_output = "$target_out_dir/$target_name"
+ outputs = [
+ merged_output,
+ ]
+
+ args = []
+ foreach(source, sources) {
+ args += [
+ "--input",
+ rebase_path(source, root_build_dir),
+ ]
+ }
+ args += [
+ "--output",
+ rebase_path(merged_output, root_build_dir),
+ ]
+ if (minify) {
+ args += [ "--minify" ]
+ }
+ }
+}
diff --git a/tools/json_merge/json_merge.h b/tools/json_merge/json_merge.h
new file mode 100644
index 0000000..f42d14e
--- /dev/null
+++ b/tools/json_merge/json_merge.h
@@ -0,0 +1,24 @@
+// 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.
+
+#ifndef BUILD_TOOLS_JSON_MERGE_JSON_MERGE_H_
+#define BUILD_TOOLS_JSON_MERGE_JSON_MERGE_H_
+
+#include <iostream>
+#include <memory>
+#include <vector>
+
+struct input_file {
+ std::string name;
+ std::unique_ptr<std::istream> contents;
+};
+
+// Merge one or more JSON documents.
+// Returns zero if successful.
+// On non-zero return value, writes human-readable errors to |errors|.
+// If |minify| then output merged JSON will be minified.
+int JSONMerge(const std::vector<struct input_file>& inputs,
+ std::ostream& output, std::ostream& errors, bool minify);
+
+#endif // BUILD_TOOLS_JSON_MERGE_JSON_MERGE_H_
diff --git a/tools/json_merge/main.cc b/tools/json_merge/main.cc
new file mode 100644
index 0000000..d163271
--- /dev/null
+++ b/tools/json_merge/main.cc
@@ -0,0 +1,83 @@
+// 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.
+
+#include <getopt.h>
+#include <cstdio>
+#include <fstream>
+#include <iostream>
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "build/tools/json_merge/json_merge.h"
+
+static void usage(const char* exe_name) {
+ fprintf(
+ stderr,
+ "Usage: %s --input [infile] [--output outfile] [--minify]\n"
+ "\n"
+ "Merge one or more JSON files to a single JSON file.\n"
+ "If any input is not a valid JSON, the merge operation will fail.\n"
+ "Consequently you can \"merge\" one JSON file to perform validation.\n"
+ "If any two inputs overlap in the top-level key space, the merge "
+ "operation will fail.\n"
+ "Optionally the merged output can be minified.\n"
+ "Consequently you can \"merge\" one JSON file to perform "
+ "minification.\n"
+ "\n"
+ "Example usages:\n"
+ "%s --input in1.json --input in2.json # merges to STDOUT\n"
+ "%s --input in1.json --minify --output out.json # minifies to out.json\n"
+ "%s --help # prints this message\n",
+ exe_name, exe_name, exe_name, exe_name);
+}
+
+int main(int argc, char** argv) {
+ std::vector<input_file_t> inputs;
+ std::ofstream output_file;
+ auto output_buf = std::cout.rdbuf();
+ bool minify = false;
+
+ static struct option long_options[] = {
+ {"input", required_argument, 0, 'i'},
+ {"output", required_argument, 0, 'o'},
+ {"minify", no_argument, 0, 'm'},
+ {"help", no_argument, 0, 'h'},
+ };
+
+ int opt;
+ while ((opt = getopt_long(argc, argv, "iomh", long_options, nullptr)) != -1) {
+ switch (opt) {
+ case 'i': {
+ auto input = std::make_unique<std::ifstream>(optarg);
+ if (!input->is_open()) {
+ printf(stderr, "Could not read from input file %s\n", optarg);
+ return 1;
+ }
+ inputs.push_back({.name = optarg, .contents = std::move(input)});
+ break;
+ }
+
+ case 'o':
+ output_file.open(optarg);
+ if (!output_file.is_open()) {
+ printf(stderr, "Could not write to output file %s\n", optarg);
+ return 1;
+ }
+ output_buf = output_file.rdbuf();
+ break;
+
+ case 'm':
+ minify = true;
+ break;
+
+ case 'h':
+ usage(argv[0]);
+ return 0;
+ }
+ }
+
+ std::ostream output(output_buf);
+ return JSONMerge(inputs, output, std::cerr, minify);
+}
diff --git a/tools/json_merge/test.cc b/tools/json_merge/test.cc
new file mode 100644
index 0000000..5c412a2
--- /dev/null
+++ b/tools/json_merge/test.cc
@@ -0,0 +1,113 @@
+// 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.
+
+#include <sstream>
+
+#include "build/tools/json_merge/json_merge.h"
+#include "gtest/gtest.h"
+
+namespace {
+
+class JsonMerge : public ::testing::Test {
+ protected:
+ void AddInput(const std::string& filename, const std::string& input) {
+ inputs.push_back({.name = filename,
+ .contents = std::make_unique<std::istringstream>(input)});
+ }
+
+ int Merge(bool minify) { return JSONMerge(inputs, output, errors, minify); }
+
+ std::string Output() { return output.str(); }
+
+ std::string Errors() { return errors.str(); }
+
+ void ExpectNoErrors() { EXPECT_TRUE(Errors().empty()); }
+
+ void ExpectError(const std::string& expected_error) {
+ EXPECT_EQ(Errors(), expected_error);
+ }
+
+ private:
+ std::vector<input_file> inputs;
+ std::ostringstream output;
+ std::ostringstream errors;
+};
+
+TEST_F(JsonMerge, MergeOne) {
+ const std::string input = R"JSON({
+ "key1": {
+ "key2": [
+ "value1",
+ "value2",
+ "value3"
+ ],
+ "key3": "value4"
+ }
+})JSON";
+ AddInput("file1.json", input);
+
+ EXPECT_EQ(Merge(false), 0);
+ EXPECT_EQ(Output(), input);
+ ExpectNoErrors();
+}
+
+TEST_F(JsonMerge, MergeOneAndMinify) {
+ const std::string input = R"JSON({
+ "key1": {
+ "key2": [
+ "value1",
+ "value2",
+ "value3"
+ ],
+ "key3": "value4"
+ }
+})JSON";
+ AddInput("file1.json", input);
+
+ EXPECT_EQ(Merge(true), 0);
+ const std::string output =
+ R"JSON({"key1":{"key2":["value1","value2","value3"],"key3":"value4"}})JSON";
+ EXPECT_EQ(Output(), output);
+ ExpectNoErrors();
+}
+
+TEST_F(JsonMerge, MergeThree) {
+ const std::string input1 = R"JSON({
+ "key1": "value1"
+})JSON";
+ AddInput("file1.json", input1);
+ const std::string input2 = R"JSON({
+ "key2": "value2"
+})JSON";
+ AddInput("file2.json", input2);
+ const std::string input3 = R"JSON({
+ "key3": "value3"
+})JSON";
+ AddInput("file3.json", input3);
+
+ EXPECT_EQ(Merge(false), 0);
+ const std::string output = R"JSON({
+ "key1": "value1",
+ "key2": "value2",
+ "key3": "value3"
+})JSON";
+ EXPECT_EQ(Output(), output);
+ ExpectNoErrors();
+}
+
+TEST_F(JsonMerge, MergeConflict) {
+ const std::string input1 = R"JSON({
+ "key1": "value1"
+})JSON";
+ AddInput("file1.json", input1);
+ const std::string input2 = R"JSON({
+ "key1": "value2"
+})JSON";
+ AddInput("file2.json", input2);
+
+ EXPECT_NE(Merge(false), 0);
+ ExpectError("file2.json has a conflicting value for key \"key1\"!\n");
+}
+
+} // namespace