blob: e6dac24c307a10cb6478d00478efc0842fbbcf93 [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#
17import os
18import sys
19
Zsolt Haraszti15044082016-10-05 00:18:57 -070020from google.protobuf import descriptor as _descriptor
Zsolt Haraszti034db372016-10-03 22:26:41 -070021from google.protobuf.compiler import plugin_pb2 as plugin
22from google.protobuf.descriptor import FieldDescriptor
23from google.protobuf.descriptor_pb2 import ServiceDescriptorProto, MethodOptions
24from google.protobuf.message import Message
25from jinja2 import Template
26from simplejson import dumps
27
28# without this import, http method annotations would not be recognized:
Zsolt Haraszti15044082016-10-05 00:18:57 -070029from google.api import annotations_pb2, http_pb2
Zsolt Haraszti034db372016-10-03 22:26:41 -070030
31
32template = Template("""
33# Generated file; please do not edit
34
35from simplejson import dumps, load
36from structlog import get_logger
37from protobuf_to_dict import protobuf_to_dict, dict_to_protobuf
38
39{% set package = file_name.replace('.proto', '') %}
40import {{ package + '_pb2' }} as {{ package }}
41
42log = get_logger()
43
44def add_routes(app, grpc_client):
45
46 {% for method in methods %}
47 {% set method_name = method['service'] + '_' + method['method'] %}
48 {% set path = method['path'].replace('{', '<string:').replace('}', '>') %}
49 @app.route('{{ path }}', methods=['{{ method['verb'].upper() }}'])
50 def {{ method_name }}(server, request, **kw):
51 log.debug('{{ method_name }}', request=request, server=server, **kw)
52 {% if method['body'] == '*' %}
53 data = load(request.content)
54 data.update(kw)
55 {% elif method['body'] == '' %}
56 data = kw
57 {% else %}
58 riase NotImplementedError('cannot handle specific body field list')
59 {% endif %}
60 req = dict_to_protobuf({{ method['input_type'] }}, data)
61 res = grpc_client.invoke(
62 {{ '.'.join([package, method['service']]) }}Stub,
63 '{{ method['method'] }}', req)
64 out_data = protobuf_to_dict(res, use_enum_labels=True)
65 request.setHeader('Content-Type', 'application/json')
66 log.debug('{{ method_name }}', **out_data)
67 return dumps(out_data)
68
69 {% endfor %}
70
71""", trim_blocks=True, lstrip_blocks=True)
72
73
74def traverse_methods(proto_file):
75
76 package = proto_file.name
77 for service in proto_file.service:
78 assert isinstance(service, ServiceDescriptorProto)
79
80 for method in service.method:
81 options = method.options
82 assert isinstance(options, MethodOptions)
83 for fd, http in options.ListFields():
84 if fd.full_name == 'google.api.http':
85 assert fd.name == 'http'
86 assert isinstance(http, http_pb2.HttpRule)
87
88 input_type = method.input_type
89 if input_type.startswith('.'):
90 input_type = input_type[1:]
91
92 output_type = method.output_type
93 if output_type.startswith('.'):
94 output_type = output_type[1:]
95
96 if http.delete:
97 verb = 'delete'
98 path = http.delete
99 elif http.get:
100 verb = 'get'
101 path = http.get
102 elif http.patch:
103 verb = 'patch'
104 path = http.patch
105 elif http.post:
106 verb = 'post'
107 path = http.post
108 elif http.put:
109 verb = 'put'
110 path = http.put
111 else:
112 raise AttributeError('No valid verb in method %s' %
113 method.name)
114
115 body = http.body
116
117 data = {
118 'package': package,
119 'filename': proto_file.name,
120 'service': service.name,
121 'method': method.name,
122 'input_type': input_type,
123 'output_type': output_type,
124 'path': path,
125 'verb': verb,
126 'body': body
127 }
128
129 yield data
130
131
132def generate_gw_code(file_name, methods):
133 return template.render(file_name=file_name, methods=methods)
134
135
136def generate_code(request, response):
137
138 assert isinstance(request, plugin.CodeGeneratorRequest)
139 for proto_file in request.proto_file:
140 output = []
141
142 for data in traverse_methods(proto_file):
143 output.append(data)
144
145 # as a nice side-effect, generate a json file capturing the essence
146 # of the RPC method entries
147 f = response.file.add()
148 f.name = proto_file.name + '.json'
149 f.content = dumps(output, indent=4)
150
151 # generate the real Python code file
152 f = response.file.add()
153 assert proto_file.name.endswith('.proto')
154 f.name = proto_file.name.replace('.proto', '_gw.py')
155 f.content = generate_gw_code(proto_file.name, output)
156
157
158if __name__ == '__main__':
159
160 if len(sys.argv) >= 2:
161 # read input from file, to allow troubleshooting
162 with open(sys.argv[1], 'r') as f:
163 data = f.read()
164 else:
165 # read input from stdin
166 data = sys.stdin.read()
167
168 # parse request
169 request = plugin.CodeGeneratorRequest()
170 request.ParseFromString(data)
171
172 # create response object
173 response = plugin.CodeGeneratorResponse()
174
175 # generate the output and the response
176 generate_code(request, response)
177
178 # serialize the response
179 output = response.SerializeToString()
180
181 # write response to stdout
182 sys.stdout.write(output)