blob: 77f74fa961906444909a4a1eee82c5d507b46640 [file] [log] [blame]
Zsolt Haraszti034db372016-10-03 22:26:41 -07001#!/usr/bin/env python
2#
3# Copyright 2016 the original author or authors.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
Zsolt Haraszti034db372016-10-03 22:26:41 -070017import sys
18
19from google.protobuf.compiler import plugin_pb2 as plugin
Zsolt Harasztibae12752016-10-10 09:55:30 -070020from google.protobuf.descriptor_pb2 import ServiceDescriptorProto, \
21 MethodOptions
Zsolt Haraszti034db372016-10-03 22:26:41 -070022from jinja2 import Template
23from simplejson import dumps
24
Zsolt Harasztibae12752016-10-10 09:55:30 -070025from chameleon.protos.third_party.google.api import annotations_pb2, http_pb2
26_ = annotations_pb2, http_pb2 # to keep import line from being optimized out
Zsolt Haraszti034db372016-10-03 22:26:41 -070027
28
29template = Template("""
30# Generated file; please do not edit
31
32from simplejson import dumps, load
33from structlog import get_logger
34from protobuf_to_dict import protobuf_to_dict, dict_to_protobuf
35
36{% set package = file_name.replace('.proto', '') %}
37import {{ package + '_pb2' }} as {{ package }}
38
39log = get_logger()
40
41def add_routes(app, grpc_client):
42
43 {% for method in methods %}
44 {% set method_name = method['service'] + '_' + method['method'] %}
45 {% set path = method['path'].replace('{', '<string:').replace('}', '>') %}
46 @app.route('{{ path }}', methods=['{{ method['verb'].upper() }}'])
47 def {{ method_name }}(server, request, **kw):
48 log.debug('{{ method_name }}', request=request, server=server, **kw)
49 {% if method['body'] == '*' %}
50 data = load(request.content)
51 data.update(kw)
52 {% elif method['body'] == '' %}
53 data = kw
54 {% else %}
55 riase NotImplementedError('cannot handle specific body field list')
56 {% endif %}
57 req = dict_to_protobuf({{ method['input_type'] }}, data)
58 res = grpc_client.invoke(
59 {{ '.'.join([package, method['service']]) }}Stub,
60 '{{ method['method'] }}', req)
61 out_data = protobuf_to_dict(res, use_enum_labels=True)
62 request.setHeader('Content-Type', 'application/json')
63 log.debug('{{ method_name }}', **out_data)
64 return dumps(out_data)
65
66 {% endfor %}
67
68""", trim_blocks=True, lstrip_blocks=True)
69
70
71def traverse_methods(proto_file):
72
73 package = proto_file.name
74 for service in proto_file.service:
75 assert isinstance(service, ServiceDescriptorProto)
76
77 for method in service.method:
78 options = method.options
79 assert isinstance(options, MethodOptions)
80 for fd, http in options.ListFields():
81 if fd.full_name == 'google.api.http':
82 assert fd.name == 'http'
83 assert isinstance(http, http_pb2.HttpRule)
84
85 input_type = method.input_type
86 if input_type.startswith('.'):
87 input_type = input_type[1:]
88
89 output_type = method.output_type
90 if output_type.startswith('.'):
91 output_type = output_type[1:]
92
93 if http.delete:
94 verb = 'delete'
95 path = http.delete
96 elif http.get:
97 verb = 'get'
98 path = http.get
99 elif http.patch:
100 verb = 'patch'
101 path = http.patch
102 elif http.post:
103 verb = 'post'
104 path = http.post
105 elif http.put:
106 verb = 'put'
107 path = http.put
108 else:
109 raise AttributeError('No valid verb in method %s' %
110 method.name)
111
112 body = http.body
113
114 data = {
115 'package': package,
116 'filename': proto_file.name,
117 'service': service.name,
118 'method': method.name,
119 'input_type': input_type,
120 'output_type': output_type,
121 'path': path,
122 'verb': verb,
123 'body': body
124 }
125
126 yield data
127
128
129def generate_gw_code(file_name, methods):
130 return template.render(file_name=file_name, methods=methods)
131
132
133def generate_code(request, response):
134
135 assert isinstance(request, plugin.CodeGeneratorRequest)
136 for proto_file in request.proto_file:
137 output = []
138
139 for data in traverse_methods(proto_file):
140 output.append(data)
141
142 # as a nice side-effect, generate a json file capturing the essence
143 # of the RPC method entries
144 f = response.file.add()
145 f.name = proto_file.name + '.json'
146 f.content = dumps(output, indent=4)
147
148 # generate the real Python code file
149 f = response.file.add()
150 assert proto_file.name.endswith('.proto')
151 f.name = proto_file.name.replace('.proto', '_gw.py')
152 f.content = generate_gw_code(proto_file.name, output)
153
154
155if __name__ == '__main__':
156
157 if len(sys.argv) >= 2:
158 # read input from file, to allow troubleshooting
159 with open(sys.argv[1], 'r') as f:
160 data = f.read()
161 else:
162 # read input from stdin
163 data = sys.stdin.read()
164
165 # parse request
166 request = plugin.CodeGeneratorRequest()
167 request.ParseFromString(data)
168
169 # create response object
170 response = plugin.CodeGeneratorResponse()
171
172 # generate the output and the response
173 generate_code(request, response)
174
175 # serialize the response
176 output = response.SerializeToString()
177
178 # write response to stdout
179 sys.stdout.write(output)