146d4c2bc3267f3f028f39e7e311b0f89aba2e4fdTorne (Richard Coles)// Copyright 2014 The Chromium Authors. All rights reserved.
25821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Use of this source code is governed by a BSD-style license that can be
35821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// found in the LICENSE file.
45821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
55821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Routines used to validate and normalize arguments.
6868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)// TODO(benwells): unit test this file.
75821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
8868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)var JSONSchemaValidator = require('json_schema').JSONSchemaValidator;
95821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
10868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)var schemaValidator = new JSONSchemaValidator();
115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
12868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)// Validate arguments.
13868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)function validate(args, parameterSchemas) {
14868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)  if (args.length > parameterSchemas.length)
15868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)    throw new Error("Too many arguments.");
16868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)  for (var i = 0; i < parameterSchemas.length; i++) {
17868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)    if (i in args && args[i] !== null && args[i] !== undefined) {
18868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)      schemaValidator.resetErrors();
19868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)      schemaValidator.validate(args[i], parameterSchemas[i]);
20868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)      if (schemaValidator.errors.length == 0)
21868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)        continue;
22868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)      var message = "Invalid value for argument " + (i + 1) + ". ";
23868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)      for (var i = 0, err;
24868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)          err = schemaValidator.errors[i]; i++) {
25868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)        if (err.path) {
26868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)          message += "Property '" + err.path + "': ";
275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        }
28868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)        message += err.message;
29868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)        message = message.substring(0, message.length - 1);
30868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)        message += ", ";
315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      }
32868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)      message = message.substring(0, message.length - 2);
33868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)      message += ".";
34868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)      throw new Error(message);
35868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)    } else if (!parameterSchemas[i].optional) {
36868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)      throw new Error("Parameter " + (i + 1) + " (" +
37868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)          parameterSchemas[i].name + ") is required.");
385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
39868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)  }
405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)}
415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Generate all possible signatures for a given API function.
435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)function getSignatures(parameterSchemas) {
445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if (parameterSchemas.length === 0)
455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return [[]];
465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var signatures = [];
47eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch  var remaining = getSignatures($Array.slice(parameterSchemas, 1));
485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  for (var i = 0; i < remaining.length; i++)
49eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    $Array.push(signatures, $Array.concat([parameterSchemas[0]], remaining[i]))
505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if (parameterSchemas[0].optional)
51eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch    return $Array.concat(signatures, remaining);
525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return signatures;
535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)};
545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Return true if arguments match a given signature's schema.
565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)function argumentsMatchSignature(args, candidateSignature) {
575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if (args.length != candidateSignature.length)
585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return false;
595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  for (var i = 0; i < candidateSignature.length; i++) {
60868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)    var argType =  JSONSchemaValidator.getType(args[i]);
615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if (!schemaValidator.isValidSchemaType(argType,
625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        candidateSignature[i]))
635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      return false;
645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  }
655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return true;
665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)};
675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Finds the function signature for the given arguments.
695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)function resolveSignature(args, definedSignature) {
705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var candidateSignatures = getSignatures(definedSignature);
715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  for (var i = 0; i < candidateSignatures.length; i++) {
725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if (argumentsMatchSignature(args, candidateSignatures[i]))
735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      return candidateSignatures[i];
745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  }
755821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return null;
765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)};
775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Returns a string representing the defined signature of the API function.
795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Example return value for chrome.windows.getCurrent:
805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// "windows.getCurrent(optional object populate, function callback)"
815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)function getParameterSignatureString(name, definedSignature) {
825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var getSchemaTypeString = function(schema) {
835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    var schemaTypes = schemaValidator.getAllTypesForSchema(schema);
845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    var typeName = schemaTypes.join(" or ") + " " + schema.name;
855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if (schema.optional)
865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      return "optional " + typeName;
875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return typeName;
885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  };
895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var typeNames = definedSignature.map(getSchemaTypeString);
905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return name + "(" + typeNames.join(", ") + ")";
915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)};
925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Returns a string representing a call to an API function.
945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Example return value for call: chrome.windows.get(1, callback) is:
955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// "windows.get(int, function)"
965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)function getArgumentSignatureString(name, args) {
97868fa2fe829687343ffae624259930155e16dbd8Torne (Richard Coles)  var typeNames = args.map(JSONSchemaValidator.getType);
985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return name + "(" + typeNames.join(", ") + ")";
995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)};
1005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Finds the correct signature for the given arguments, then validates the
1025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// arguments against that signature. Returns a 'normalized' arguments list
1035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// where nulls are inserted where optional parameters were omitted.
1045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// |args| is expected to be an array.
1055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)function normalizeArgumentsAndValidate(args, funDef) {
1065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if (funDef.allowAmbiguousOptionalArguments) {
1075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    validate(args, funDef.definition.parameters);
1085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return args;
1095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  }
1105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var definedSignature = funDef.definition.parameters;
1115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var resolvedSignature = resolveSignature(args, definedSignature);
1125821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if (!resolvedSignature)
1135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    throw new Error("Invocation of form " +
1145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        getArgumentSignatureString(funDef.name, args) +
1155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        " doesn't match definition " +
1165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        getParameterSignatureString(funDef.name, definedSignature));
1175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  validate(args, resolvedSignature);
1185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var normalizedArgs = [];
1195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var ai = 0;
1205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  for (var si = 0; si < definedSignature.length; si++) {
1215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if (definedSignature[si] === resolvedSignature[ai])
122eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch      $Array.push(normalizedArgs, args[ai++]);
1235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    else
124eb525c5499e34cc9c4b825d6d9e75bb07cc06aceBen Murdoch      $Array.push(normalizedArgs, null);
1255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  }
1265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return normalizedArgs;
1275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)};
1285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)// Validates that a given schema for an API function is not ambiguous.
1305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)function isFunctionSignatureAmbiguous(functionDef) {
1315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  if (functionDef.allowAmbiguousOptionalArguments)
1325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return false;
1335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var signaturesAmbiguous = function(signature1, signature2) {
1345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    if (signature1.length != signature2.length)
1355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      return false;
1365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    for (var i = 0; i < signature1.length; i++) {
1375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (!schemaValidator.checkSchemaOverlap(
1385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)          signature1[i], signature2[i]))
1395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return false;
1405821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
1415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return true;
1425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  };
1435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  var candidateSignatures = getSignatures(functionDef.parameters);
1445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  for (var i = 0; i < candidateSignatures.length; i++) {
1455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    for (var j = i + 1; j < candidateSignatures.length; j++) {
1465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)      if (signaturesAmbiguous(candidateSignatures[i], candidateSignatures[j]))
1475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        return true;
1485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    }
1495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  }
1505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)  return false;
1515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)};
1525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)exports.isFunctionSignatureAmbiguous = isFunctionSignatureAmbiguous;
1545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)exports.normalizeArgumentsAndValidate = normalizeArgumentsAndValidate;
1555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)exports.schemaValidator = schemaValidator;
1565821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)exports.validate = validate;
157