[Extensions Bindings] Add a native ValidateType method

Add a utility method exposed to JS to allow bindings to validate a type
against an expected schema. This is used to e.g. match expected
instances used in the declarative APIs.

This eliminates one of the remaining reliances on the utils/schemaUtils
JS files for native bindings.

Bug: 810487

Change-Id: Id87d38f8196f56b285e9a69221a34f643957d237
Reviewed-on: https://chromium-review.googlesource.com/905422
Commit-Queue: Devlin <rdevlin.cronin@chromium.org>
Reviewed-by: Jeremy Roman <jbroman@chromium.org>
Reviewed-by: Istiaque Ahmed <lazyboy@chromium.org>
Cr-Commit-Position: refs/heads/master@{#536374}
diff --git a/extensions/renderer/bindings/api_binding_js_util.cc b/extensions/renderer/bindings/api_binding_js_util.cc
index 6d75855c..3f3962b 100644
--- a/extensions/renderer/bindings/api_binding_js_util.cc
+++ b/extensions/renderer/bindings/api_binding_js_util.cc
@@ -10,6 +10,7 @@
 #include "extensions/renderer/bindings/api_request_handler.h"
 #include "extensions/renderer/bindings/api_signature.h"
 #include "extensions/renderer/bindings/api_type_reference_map.h"
+#include "extensions/renderer/bindings/argument_spec.h"
 #include "extensions/renderer/bindings/declarative_event.h"
 #include "extensions/renderer/bindings/exception_handler.h"
 #include "extensions/renderer/bindings/js_runner.h"
@@ -49,7 +50,8 @@
       .SetMethod("runCallbackWithLastError",
                  &APIBindingJSUtil::RunCallbackWithLastError)
       .SetMethod("handleException", &APIBindingJSUtil::HandleException)
-      .SetMethod("setExceptionHandler", &APIBindingJSUtil::SetExceptionHandler);
+      .SetMethod("setExceptionHandler", &APIBindingJSUtil::SetExceptionHandler)
+      .SetMethod("validateType", &APIBindingJSUtil::ValidateType);
 }
 
 void APIBindingJSUtil::SendRequest(
@@ -245,4 +247,26 @@
       arguments->GetHolderCreationContext(), handler);
 }
 
+void APIBindingJSUtil::ValidateType(gin::Arguments* arguments,
+                                    const std::string& type_name,
+                                    v8::Local<v8::Value> value) {
+  v8::Isolate* isolate = arguments->isolate();
+  v8::HandleScope handle_scope(isolate);
+  v8::Local<v8::Context> context = arguments->GetHolderCreationContext();
+
+  const ArgumentSpec* spec = type_refs_->GetSpec(type_name);
+  if (!spec) {
+    // We shouldn't be asked to validate unknown specs, but since this comes
+    // from JS, assume nothing.
+    NOTREACHED();
+    return;
+  }
+
+  std::string error;
+  if (!spec->ParseArgument(context, value, *type_refs_, nullptr, nullptr,
+                           &error)) {
+    arguments->ThrowTypeError(error);
+  }
+}
+
 }  // namespace extensions
diff --git a/extensions/renderer/bindings/api_binding_js_util.h b/extensions/renderer/bindings/api_binding_js_util.h
index e2d3012..cfb7c16 100644
--- a/extensions/renderer/bindings/api_binding_js_util.h
+++ b/extensions/renderer/bindings/api_binding_js_util.h
@@ -100,6 +100,13 @@
   void SetExceptionHandler(gin::Arguments* arguments,
                            v8::Local<v8::Function> handler);
 
+  // Validates a given |value| against the specification for the type with
+  // |type_name|. Throws an error if the validation fails; otherwise returns
+  // undefined.
+  void ValidateType(gin::Arguments* arguments,
+                    const std::string& type_name,
+                    v8::Local<v8::Value> value);
+
   // Type references. Guaranteed to outlive this object.
   APITypeReferenceMap* const type_refs_;
 
diff --git a/extensions/renderer/bindings/api_binding_js_util_unittest.cc b/extensions/renderer/bindings/api_binding_js_util_unittest.cc
index c3cb077..c9d754f 100644
--- a/extensions/renderer/bindings/api_binding_js_util_unittest.cc
+++ b/extensions/renderer/bindings/api_binding_js_util_unittest.cc
@@ -5,9 +5,11 @@
 #include "extensions/renderer/bindings/api_binding_js_util.h"
 
 #include "base/bind.h"
+#include "base/optional.h"
 #include "extensions/renderer/bindings/api_binding_test_util.h"
 #include "extensions/renderer/bindings/api_bindings_system.h"
 #include "extensions/renderer/bindings/api_bindings_system_unittest.h"
+#include "extensions/renderer/bindings/api_invocation_errors.h"
 #include "gin/arguments.h"
 #include "gin/handle.h"
 #include "testing/gmock/include/gmock/gmock.h"
@@ -287,4 +289,44 @@
   EXPECT_EQ("\"some error\"", error_info.exception_message);
 }
 
+TEST_F(APIBindingJSUtilUnittest, TestValidateType) {
+  v8::HandleScope handle_scope(isolate());
+  v8::Local<v8::Context> context = MainContext();
+
+  gin::Handle<APIBindingJSUtil> util = CreateUtil();
+  v8::Local<v8::Object> v8_util = util.ToV8().As<v8::Object>();
+
+  auto call_validate_type = [context, v8_util](
+                                const char* function,
+                                base::Optional<std::string> expected_error) {
+    v8::Local<v8::Function> v8_function = FunctionFromString(context, function);
+    v8::Local<v8::Value> args[] = {v8_util};
+    if (expected_error) {
+      RunFunctionAndExpectError(v8_function, context, arraysize(args), args,
+                                *expected_error);
+    } else {
+      RunFunction(v8_function, context, arraysize(args), args);
+    }
+  };
+
+  // Test a case that should succeed (a valid value).
+  call_validate_type(
+      R"((function(util) {
+           util.validateType('alpha.objRef', {prop1: 'hello'});
+         }))",
+      base::nullopt);
+
+  // Test a failing case (prop1 is supposed to be a string).
+  std::string expected_error =
+      "Uncaught TypeError: " +
+      api_errors::PropertyError(
+          "prop1", api_errors::InvalidType(api_errors::kTypeString,
+                                           api_errors::kTypeInteger));
+  call_validate_type(
+      R"((function(util) {
+           util.validateType('alpha.objRef', {prop1: 2});
+         }))",
+      expected_error);
+}
+
 }  // namespace extensions
diff --git a/extensions/renderer/resources/declarative_webrequest_custom_bindings.js b/extensions/renderer/resources/declarative_webrequest_custom_bindings.js
index b905bbd3..2727bc3 100644
--- a/extensions/renderer/resources/declarative_webrequest_custom_bindings.js
+++ b/extensions/renderer/resources/declarative_webrequest_custom_bindings.js
@@ -7,19 +7,21 @@
 var binding =
     apiBridge || require('binding').Binding.create('declarativeWebRequest');
 
-var utils = require('utils');
-var validate = require('schemaUtils').validate;
+var utils = bindingUtil ? undefined : require('utils');
+var validate = bindingUtil ? undefined : require('schemaUtils').validate;
+
+function validateType(schemaTypes, typeName, value) {
+  if (bindingUtil) {
+    bindingUtil.validateType(typeName, value);
+  } else {
+    var schema = utils.lookup(schemaTypes, 'id', typeName);
+    validate([value], [schema]);
+  }
+}
 
 binding.registerCustomHook(function(api) {
   var declarativeWebRequest = api.compiledApi;
 
-  // Returns the schema definition of type |typeId| defined in |namespace|.
-  function getSchema(typeId) {
-    return utils.lookup(api.schema.types,
-                        'id',
-                        'declarativeWebRequest.' + typeId);
-  }
-
   // Helper function for the constructor of concrete datatypes of the
   // declarative webRequest API.
   // Makes sure that |this| contains the union of parameters and
@@ -31,15 +33,11 @@
         instance[key] = parameters[key];
       }
     }
-    instance.instanceType = 'declarativeWebRequest.' + typeId;
-    if (!apiBridge) {
-      var schema = getSchema(typeId);
-      // TODO(devlin): This won't work with native bindings, but it's lower
-      // priority. declarativeWebRequest never shipped, and validation will
-      // fail later when trying to use the created object. Still, it'd be
-      // potentially nice to fix.
-      validate([instance], [schema]);
-    }
+
+    var qualifiedType = 'declarativeWebRequest.' + typeId;
+    instance.instanceType = qualifiedType;
+    validateType(bindingUtil ? undefined : api.schema.types, qualifiedType,
+                 instance);
   }
 
   // Setup all data types for the declarative webRequest API.
diff --git a/extensions/renderer/resources/guest_view/web_view/web_view_request_custom_bindings.js b/extensions/renderer/resources/guest_view/web_view/web_view_request_custom_bindings.js
index 3fd27b58..09cfa8c 100644
--- a/extensions/renderer/resources/guest_view/web_view/web_view_request_custom_bindings.js
+++ b/extensions/renderer/resources/guest_view/web_view/web_view_request_custom_bindings.js
@@ -8,20 +8,22 @@
 
 var declarativeWebRequestSchema =
     requireNative('schema_registry').GetSchema('declarativeWebRequest');
-var utils = require('utils');
-var validate = require('schemaUtils').validate;
+
+var utils = bindingUtil ? undefined : require('utils');
+var validate = bindingUtil ? undefined : require('schemaUtils').validate;
+
+function validateType(schemaTypes, typeName, value) {
+  if (bindingUtil) {
+    bindingUtil.validateType(typeName, value);
+  } else {
+    var schema = utils.lookup(schemaTypes, 'id', typeName);
+    validate([value], [schema]);
+  }
+}
 
 binding.registerCustomHook(function(api) {
   var webViewRequest = api.compiledApi;
 
-  // Returns the schema definition of type |typeId| defined in
-  // |declarativeWebRequestScheme.types|.
-  function getSchema(typeId) {
-    return utils.lookup(declarativeWebRequestSchema.types,
-                        'id',
-                        'declarativeWebRequest.' + typeId);
-  }
-
   // Helper function for the constructor of concrete datatypes of the
   // declarative webRequest API.
   // Makes sure that |this| contains the union of parameters and
@@ -34,9 +36,10 @@
       }
     }
 
-    instance.instanceType = 'declarativeWebRequest.' + typeId;
-    var schema = getSchema(typeId);
-    validate([instance], [schema]);
+    var qualifiedType = 'declarativeWebRequest.' + typeId;
+    instance.instanceType = qualifiedType;
+    validateType(bindingUtil ? undefined : declarativeWebRequestSchema.types,
+                 qualifiedType, instance);
   }
 
   // Setup all data types for the declarative webRequest API from the schema.