Support for JSON input conversion

This adds support for reading JSON files in GN and converting them
to equivalent GN values.

Change-Id: I5bca3abb4668da852774d69c98d4dd6575a42957
Reviewed-on: https://chromium-review.googlesource.com/894857
Commit-Queue: Petr Hosek <phosek@chromium.org>
Reviewed-by: Dirk Pranke <dpranke@chromium.org>
Reviewed-by: Brett Wilson <brettw@chromium.org>
Cr-Original-Commit-Position: refs/heads/master@{#535078}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: c986c354e49a7360b947d3406e8a05ac360b5b1c
diff --git a/tools/gn/docs/reference.md b/tools/gn/docs/reference.md
index a462d5d..b6857e8 100644
--- a/tools/gn/docs/reference.md
+++ b/tools/gn/docs/reference.md
@@ -5872,6 +5872,20 @@
       Note that if the input is empty, the result will be a null value which
       will produce an error if assigned to a variable.
 
+  "json"
+      Parse the input as a JSON and convert it to equivalent GN rvalue. The data
+      type mapping is:
+        a string in JSON maps to string in GN
+        an integer in JSON maps to integer in GN
+        a float in JSON is unsupported and will result in an error
+        an object in JSON maps to scope in GN
+        an array in JSON maps to list in GN
+        a boolean in JSON maps to boolean in GN
+        a null in JSON is unsupported and will result in an error
+
+      Nota that the dictionary keys have to be valid GN identifiers otherwise
+      they will produce an error.
+
   "trim ..."
       Prefixing any of the other transformations with the word "trim" will
       result in whitespace being trimmed from the beginning and end of the
diff --git a/tools/gn/input_conversion.cc b/tools/gn/input_conversion.cc
index 7cce107..a7abcf4 100644
--- a/tools/gn/input_conversion.cc
+++ b/tools/gn/input_conversion.cc
@@ -7,9 +7,11 @@
 #include <memory>
 #include <utility>
 
+#include "base/json/json_reader.h"
 #include "base/macros.h"
 #include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
+#include "base/values.h"
 #include "tools/gn/build_settings.h"
 #include "tools/gn/err.h"
 #include "tools/gn/input_file.h"
@@ -106,6 +108,98 @@
   return ret;
 }
 
+bool IsIdentifier(const base::StringPiece& buffer) {
+  DCHECK(buffer.size() > 0);
+  if (!Tokenizer::IsIdentifierFirstChar(buffer[0]))
+    return false;
+  for (size_t i = 1; i < buffer.size(); i++)
+    if (!Tokenizer::IsIdentifierContinuingChar(buffer[i]))
+      return false;
+  return true;
+}
+
+Value ParseJSONValue(const Settings* settings,
+                     const base::Value& value,
+                     const ParseNode* origin,
+                     InputFile* input_file,
+                     Err* err) {
+  switch (value.type()) {
+    case base::Value::Type::NONE:
+      *err = Err(origin, "Null values are not supported.");
+      return Value();
+    case base::Value::Type::BOOLEAN:
+      return Value(origin, value.GetBool());
+    case base::Value::Type::INTEGER:
+      return Value(origin, static_cast<int64_t>(value.GetInt()));
+    case base::Value::Type::DOUBLE:
+      *err = Err(origin, "Floating point values are not supported.");
+      return Value();
+    case base::Value::Type::STRING:
+      return Value(origin, value.GetString());
+    case base::Value::Type::BINARY:
+      *err = Err(origin, "Binary values are not supported.");
+      return Value();
+    case base::Value::Type::DICTIONARY: {
+      std::unique_ptr<Scope> scope = std::make_unique<Scope>(settings);
+      for (const auto& it : value.DictItems()) {
+        Value value =
+            ParseJSONValue(settings, it.second, origin, input_file, err);
+        if (!IsIdentifier(it.first)) {
+          *err = Err(origin, "Invalid identifier \"" + it.first + "\".");
+          return Value();
+        }
+        // Search for the key in the input file. We know it's present because
+        // it was parsed by the JSON reader, but we need its location to
+        // construct a StringPiece that can be used as key in the Scope.
+        size_t off = input_file->contents().find("\"" + it.first + "\"");
+        if (off == std::string::npos) {
+          *err = Err(origin, "Invalid encoding \"" + it.first + "\".");
+          return Value();
+        }
+        base::StringPiece key(&input_file->contents()[off + 1],
+                              it.first.size());
+        scope->SetValue(key, std::move(value), origin);
+      }
+      return Value(origin, std::move(scope));
+    }
+    case base::Value::Type::LIST: {
+      Value result(origin, Value::LIST);
+      result.list_value().reserve(value.GetList().size());
+      for (const auto& val : value.GetList()) {
+        Value value = ParseJSONValue(settings, val, origin, input_file, err);
+        result.list_value().push_back(value);
+      }
+      return result;
+    }
+  }
+  return Value();
+}
+
+// Parses the JSON string and converts it to GN value.
+Value ParseJSON(const Settings* settings,
+                const std::string& input,
+                const ParseNode* origin,
+                Err* err) {
+  InputFile* input_file;
+  std::vector<Token>* tokens;
+  std::unique_ptr<ParseNode>* parse_root_ptr;
+  g_scheduler->input_file_manager()->AddDynamicInput(SourceFile(), &input_file,
+                                                     &tokens, &parse_root_ptr);
+  input_file->SetContents(input);
+
+  int error_code_out;
+  std::string error_msg_out;
+  std::unique_ptr<base::Value> value = base::JSONReader::ReadAndReturnError(
+      input, base::JSONParserOptions::JSON_PARSE_RFC, &error_code_out,
+      &error_msg_out);
+  if (!value) {
+    *err = Err(origin, "Input is not a valid JSON: " + error_msg_out);
+    return Value();
+  }
+
+  return ParseJSONValue(settings, *value, origin, input_file, err);
+}
+
 // Backend for ConvertInputToValue, this takes the extracted string for the
 // input conversion so we can recursively call ourselves to handle the optional
 // "trim" prefix. This original value is also kept for the purposes of throwing
@@ -139,6 +233,8 @@
     return ParseList(input, origin, err);
   if (input_conversion == "scope")
     return ParseValueOrScope(settings, input, PARSE_SCOPE, origin, err);
+  if (input_conversion == "json")
+    return ParseJSON(settings, input, origin, err);
 
   *err = Err(original_input_conversion, "Not a valid input_conversion.",
              "Run gn help input_conversion to see your options.");
@@ -189,6 +285,20 @@
       Note that if the input is empty, the result will be a null value which
       will produce an error if assigned to a variable.
 
+  "json"
+      Parse the input as a JSON and convert it to equivalent GN rvalue. The data
+      type mapping is:
+        a string in JSON maps to string in GN
+        an integer in JSON maps to integer in GN
+        a float in JSON is unsupported and will result in an error
+        an object in JSON maps to scope in GN
+        an array in JSON maps to list in GN
+        a boolean in JSON maps to boolean in GN
+        a null in JSON is unsupported and will result in an error
+
+      Nota that the dictionary keys have to be valid GN identifiers otherwise
+      they will produce an error.
+
   "trim ..."
       Prefixing any of the other transformations with the word "trim" will
       result in whitespace being trimmed from the beginning and end of the
diff --git a/tools/gn/input_conversion_unittest.cc b/tools/gn/input_conversion_unittest.cc
index 9568daf..dfc82c9 100644
--- a/tools/gn/input_conversion_unittest.cc
+++ b/tools/gn/input_conversion_unittest.cc
@@ -132,6 +132,98 @@
   EXPECT_EQ(input, a_file->contents());
 }
 
+TEST_F(InputConversionTest, ValueJSON) {
+  Err err;
+  std::string input(R"*({
+  "a": 5,
+  "b": "foo",
+  "c": {
+    "d": true,
+    "e": [
+      {
+        "f": "bar"
+      }
+    ]
+  }
+})*");
+  Value result = ConvertInputToValue(settings(), input, nullptr,
+                                     Value(nullptr, "json"), &err);
+  EXPECT_FALSE(err.has_error());
+  ASSERT_EQ(Value::SCOPE, result.type());
+
+  const Value* a_value = result.scope_value()->GetValue("a");
+  ASSERT_TRUE(a_value);
+  EXPECT_EQ(5, a_value->int_value());
+
+  const Value* b_value = result.scope_value()->GetValue("b");
+  ASSERT_TRUE(b_value);
+  EXPECT_EQ("foo", b_value->string_value());
+
+  const Value* c_value = result.scope_value()->GetValue("c");
+  ASSERT_TRUE(c_value);
+  ASSERT_EQ(Value::SCOPE, c_value->type());
+
+  const Value* d_value = c_value->scope_value()->GetValue("d");
+  ASSERT_TRUE(d_value);
+  EXPECT_EQ(true, d_value->boolean_value());
+
+  const Value* e_value = c_value->scope_value()->GetValue("e");
+  ASSERT_TRUE(e_value);
+  ASSERT_EQ(Value::LIST, e_value->type());
+
+  EXPECT_EQ(1u, e_value->list_value().size());
+  ASSERT_EQ(Value::SCOPE, e_value->list_value()[0].type());
+  const Value* f_value = e_value->list_value()[0].scope_value()->GetValue("f");
+  ASSERT_TRUE(f_value);
+  EXPECT_EQ("bar", f_value->string_value());
+}
+
+TEST_F(InputConversionTest, ValueJSONInvalidInput) {
+  Err err;
+  std::string input(R"*({
+  "a": 5,
+  "b":
+})*");
+  Value result = ConvertInputToValue(settings(), input, nullptr,
+                                     Value(nullptr, "json"), &err);
+  EXPECT_TRUE(err.has_error());
+  EXPECT_EQ("Input is not a valid JSON: Line: 4, column: 2, Unexpected token.",
+            err.message());
+}
+
+TEST_F(InputConversionTest, ValueJSONUnsupportedValue) {
+  Err err;
+  std::string input(R"*({
+  "a": null
+})*");
+  Value result = ConvertInputToValue(settings(), input, nullptr,
+                                     Value(nullptr, "json"), &err);
+  EXPECT_TRUE(err.has_error());
+  EXPECT_EQ("Null values are not supported.", err.message());
+}
+
+TEST_F(InputConversionTest, ValueJSONInvalidVariable) {
+  Err err;
+  std::string input(R"*({
+  "a\\x0001b": 5
+})*");
+  Value result = ConvertInputToValue(settings(), input, nullptr,
+                                     Value(nullptr, "json"), &err);
+  EXPECT_TRUE(err.has_error());
+  EXPECT_EQ("Invalid identifier \"a\\x0001b\".", err.message());
+}
+
+TEST_F(InputConversionTest, ValueJSONUnsupported) {
+  Err err;
+  std::string input(R"*({
+  "d": 0.0
+})*");
+  Value result = ConvertInputToValue(settings(), input, nullptr,
+                                     Value(nullptr, "json"), &err);
+  EXPECT_TRUE(err.has_error());
+  EXPECT_EQ("Floating point values are not supported.", err.message());
+}
+
 TEST_F(InputConversionTest, ValueEmpty) {
   Err err;
   Value result = ConvertInputToValue(settings(), "", nullptr,