Build API: Support binary serialization.

BUG=chromium:1032573
TEST=run_tests
TEST=manually ran endpoints
TEST=cq

Change-Id: I59c401d228f81a52c28d80e9db9718d41a255015
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/2078962
Reviewed-by: LaMont Jones <lamontjones@chromium.org>
Reviewed-by: Michael Mortensen <mmortensen@google.com>
Commit-Queue: Alex Klein <saklein@chromium.org>
Tested-by: Alex Klein <saklein@chromium.org>
diff --git a/scripts/build_api.py b/scripts/build_api.py
index fadf67c..727c2e3 100644
--- a/scripts/build_api.py
+++ b/scripts/build_api.py
@@ -10,20 +10,17 @@
 import os
 import sys
 
-from google.protobuf import json_format
-
 from chromite.api import api_config as api_config_lib
 from chromite.api import controller
+from chromite.api import message_util
 from chromite.api import router as router_lib
 from chromite.api.gen.chromite.api import build_api_config_pb2
 from chromite.lib import commandline
 from chromite.lib import cros_build_lib
 from chromite.lib import cros_logging as logging
-from chromite.lib import osutils
 from chromite.lib import tee
 from chromite.utils import matching
 
-
 assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
 
 
@@ -34,18 +31,38 @@
   parser.add_argument(
       'service_method',
       help='The "chromite.api.Service/Method" that is being called.')
-  parser.add_argument(
+  # Input arguments.
+  input_args = parser.add_mutually_exclusive_group(required=True)
+  input_args.add_argument(
+      '--input-binary',
+      type='path',
+      help='Path to the protobuf binary serialization of the input message.')
+  input_args.add_argument(
       '--input-json',
       type='path',
       help='Path to the JSON serialized input argument protobuf message.')
+  # Output options.
+  parser.add_argument(
+      '--output-binary',
+      type='path',
+      help='The path to which the protobuf binary serialization of the '
+           'response message should be written.')
   parser.add_argument(
       '--output-json',
       type='path',
-      help='The path to which the result protobuf message should be written.')
-  parser.add_argument(
+      help='The path to which the JSON serialization of the response message '
+           'should be written.')
+  # Config options.
+  config_args = parser.add_mutually_exclusive_group()
+  config_args.add_argument(
+      '--config-binary',
+      type='path',
+      help='The path to the protobuf binary serialization of the Build API '
+           'call configs.')
+  config_args.add_argument(
       '--config-json',
       type='path',
-      help='The path to the Build API call configs.')
+      help='The path to the JSON encoded Build API call configs.')
   # TODO(crbug.com/1040978): Remove after usages removed.
   parser.add_argument(
       '--tee-log',
@@ -86,30 +103,59 @@
   opts.service = parts[0]
   opts.method = parts[1]
 
-  # --input-json and --output-json validation.
-  if not opts.input_json or not opts.output_json:
-    parser.error('--input-json and --output-json are both required.')
+  # Input and output validation.
+  if not opts.output_binary and not opts.output_json:
+    parser.error('At least one output file must be specified.')
 
-  if not os.path.exists(opts.input_json):
+  if not os.path.exists(opts.input_binary or opts.input_json):
     parser.error('Input file does not exist.')
 
   config_msg = build_api_config_pb2.BuildApiConfig()
   if opts.config_json:
+    handler = message_util.get_message_handler(opts.config_json,
+                                               message_util.FORMAT_JSON)
+  else:
+    handler = message_util.get_message_handler(opts.config_binary,
+                                               message_util.FORMAT_BINARY)
+
+  if opts.config_json or opts.config_binary:
+    # We have been given a config, so read it.
     try:
-      json_format.Parse(osutils.ReadFile(opts.config_json), config_msg,
-                        ignore_unknown_fields=True)
-    except IOError as e:
-      parser.error(e)
+      handler.read_into(config_msg)
+    except message_util.Error as e:
+      parser.error(str(e))
 
   opts.config = api_config_lib.build_config_from_proto(config_msg)
+  opts.config_handler = handler
 
   opts.Freeze()
   return opts
 
 
+def _get_io_handlers(opts):
+  """Build the input and output handlers."""
+  if opts.input_binary:
+    input_handler = message_util.get_message_handler(opts.input_binary,
+                                                     message_util.FORMAT_BINARY)
+  else:
+    input_handler = message_util.get_message_handler(opts.input_json,
+                                                     message_util.FORMAT_JSON)
+
+  output_handlers = []
+  if opts.output_binary:
+    handler = message_util.get_message_handler(opts.output_binary,
+                                               message_util.FORMAT_BINARY)
+    output_handlers.append(handler)
+  if opts.output_json:
+    handler = message_util.get_message_handler(opts.output_json,
+                                               message_util.FORMAT_JSON)
+    output_handlers.append(handler)
+
+  return input_handler, output_handlers
+
+
 def main(argv):
   with cros_build_lib.ContextManagerStack() as stack:
-
     router = router_lib.GetRouter()
     opts = _ParseArgs(argv, router)
 
@@ -130,9 +176,11 @@
       # correct return code here.
       return controller.RETURN_CODE_INVALID_INPUT
 
+    input_handler, output_handlers = _get_io_handlers(opts)
+
     try:
-      return router.Route(opts.service, opts.method, opts.input_json,
-                          opts.output_json, opts.config)
+      return router.Route(opts.service, opts.method, opts.config, input_handler,
+                          output_handlers, opts.config_handler)
     except router_lib.Error as e:
       # Handle router_lib.Error derivatives nicely, but let anything else bubble
       # up.