Chameleon self boot-strapping API
diff --git a/protoc_plugins/__init__.py b/protoc_plugins/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/protoc_plugins/__init__.py
diff --git a/protoc_plugins/gw_gen.py b/protoc_plugins/gw_gen.py
new file mode 100755
index 0000000..aa2a3ce
--- /dev/null
+++ b/protoc_plugins/gw_gen.py
@@ -0,0 +1,181 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import os
+import sys
+
+from google.protobuf.compiler import plugin_pb2 as plugin
+from google.protobuf.descriptor import FieldDescriptor
+from google.protobuf.descriptor_pb2 import ServiceDescriptorProto, MethodOptions
+from google.protobuf.message import Message
+from jinja2 import Template
+from simplejson import dumps
+
+# without this import, http method annotations would not be recognized:
+from google.api import annotations_pb2 as _, http_pb2
+
+
+template = Template("""
+# Generated file; please do not edit
+
+from simplejson import dumps, load
+from structlog import get_logger
+from protobuf_to_dict import protobuf_to_dict, dict_to_protobuf
+
+{% set package = file_name.replace('.proto', '') %}
+import {{ package + '_pb2' }} as {{ package }}
+
+log = get_logger()
+
+def add_routes(app, grpc_client):
+
+    {% for method in methods %}
+    {% set method_name = method['service'] + '_' + method['method'] %}
+    {% set path = method['path'].replace('{', '<string:').replace('}', '>') %}
+    @app.route('{{ path }}', methods=['{{ method['verb'].upper() }}'])
+    def {{ method_name }}(server, request, **kw):
+        log.debug('{{ method_name }}', request=request, server=server, **kw)
+        {% if method['body'] == '*' %}
+        data = load(request.content)
+        data.update(kw)
+        {% elif method['body'] == '' %}
+        data = kw
+        {% else %}
+        riase NotImplementedError('cannot handle specific body field list')
+        {% endif %}
+        req = dict_to_protobuf({{ method['input_type'] }}, data)
+        res = grpc_client.invoke(
+            {{ '.'.join([package, method['service']]) }}Stub,
+            '{{ method['method'] }}', req)
+        out_data = protobuf_to_dict(res, use_enum_labels=True)
+        request.setHeader('Content-Type', 'application/json')
+        log.debug('{{ method_name }}', **out_data)
+        return dumps(out_data)
+
+    {% endfor %}
+
+""", trim_blocks=True, lstrip_blocks=True)
+
+
+def traverse_methods(proto_file):
+
+    package = proto_file.name
+    for service in proto_file.service:
+        assert isinstance(service, ServiceDescriptorProto)
+
+        for method in service.method:
+            options = method.options
+            assert isinstance(options, MethodOptions)
+            for fd, http in options.ListFields():
+                if fd.full_name == 'google.api.http':
+                    assert fd.name == 'http'
+                    assert isinstance(http, http_pb2.HttpRule)
+
+                    input_type = method.input_type
+                    if input_type.startswith('.'):
+                        input_type = input_type[1:]
+
+                    output_type = method.output_type
+                    if output_type.startswith('.'):
+                        output_type = output_type[1:]
+
+                    if http.delete:
+                        verb = 'delete'
+                        path = http.delete
+                    elif http.get:
+                        verb = 'get'
+                        path = http.get
+                    elif http.patch:
+                        verb = 'patch'
+                        path = http.patch
+                    elif http.post:
+                        verb = 'post'
+                        path = http.post
+                    elif http.put:
+                        verb = 'put'
+                        path = http.put
+                    else:
+                        raise AttributeError('No valid verb in method %s' %
+                                             method.name)
+
+                    body = http.body
+
+                    data = {
+                        'package': package,
+                        'filename': proto_file.name,
+                        'service': service.name,
+                        'method': method.name,
+                        'input_type': input_type,
+                        'output_type': output_type,
+                        'path': path,
+                        'verb': verb,
+                        'body': body
+                    }
+
+                    yield data
+
+
+def generate_gw_code(file_name, methods):
+    return template.render(file_name=file_name, methods=methods)
+
+
+def generate_code(request, response):
+
+    assert isinstance(request, plugin.CodeGeneratorRequest)
+    for proto_file in request.proto_file:
+        output = []
+
+        for data in traverse_methods(proto_file):
+            output.append(data)
+
+        # as a nice side-effect, generate a json file capturing the essence
+        # of the RPC method entries
+        f = response.file.add()
+        f.name = proto_file.name + '.json'
+        f.content = dumps(output, indent=4)
+
+        # generate the real Python code file
+        f = response.file.add()
+        assert proto_file.name.endswith('.proto')
+        f.name = proto_file.name.replace('.proto', '_gw.py')
+        f.content = generate_gw_code(proto_file.name, output)
+
+
+if __name__ == '__main__':
+
+    if len(sys.argv) >= 2:
+        # read input from file, to allow troubleshooting
+        with open(sys.argv[1], 'r') as f:
+            data = f.read()
+    else:
+        # read input from stdin
+        data = sys.stdin.read()
+
+    # parse request
+    request = plugin.CodeGeneratorRequest()
+    request.ParseFromString(data)
+
+    # create response object
+    response = plugin.CodeGeneratorResponse()
+
+    # generate the output and the response
+    generate_code(request, response)
+
+    # serialize the response
+    output = response.SerializeToString()
+
+    # write response to stdout
+    sys.stdout.write(output)