forked from userver-framework/userver
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgenerator.py
executable file
·185 lines (147 loc) · 5.24 KB
/
generator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# pylint: disable=invalid-name
"""
This script is a Protobuf protoc plugin that generates userver asynchronous
wrappers for gRPC clients.
For each `path/X.proto` file that contains gRPC services, we generate
`path/X_{client,handler}.usrv.pb.{hpp,cpp}` using
`{client,handler}.usrv.pb.{hpp,cpp}.jinja` templates.
"""
import enum
import itertools
import os
import sys
from typing import Iterable
from typing import Optional
from typing import Tuple
from google.protobuf.compiler import plugin_pb2 as plugin
import jinja2
_AUTOGEN_EMPTY_HEADER = '\n'.join(
[
'// THIS FILE IS AUTOGENERATED, DO NOT EDIT!',
'',
'// This file is empty intentionally',
'',
],
)
class Mode(enum.Enum):
Service = enum.auto()
Client = enum.auto()
Both = enum.auto()
def is_service(self) -> bool:
return self == self.Service
def is_client(self) -> bool:
return self == self.Client
def is_both(self) -> bool:
return self == self.Both
def _grpc_to_cpp_name(in_str):
return in_str.replace('.', '::')
def _to_package_prefix(package):
return f'{package}.' if package else ''
class _CodeGenerator:
def __init__(
self,
proto_file,
response: plugin.CodeGeneratorResponse,
jinja_env: jinja2.Environment,
mode: Mode,
skip_files_wo_service: bool,
) -> None:
self.proto_file = proto_file
self.response = response
self.jinja_env = jinja_env
self.mode = mode
self.skip_files_wo_service = skip_files_wo_service
def run(self) -> None:
if self.proto_file.service:
self._generate_code_with_service()
elif not self.skip_files_wo_service:
self._generate_code_empty()
def _generate_code_with_service(self) -> None:
data = {
'source_file': self.proto_file.name,
'source_file_without_ext': self._proto_file_stem(),
'package_prefix': _to_package_prefix(self.proto_file.package),
'namespace': _grpc_to_cpp_name(self.proto_file.package),
'services': list(self.proto_file.service),
}
for file_type, file_ext in self._iter_src_files():
template_name = f'{file_type}.usrv.{file_ext}.jinja'
template = self.jinja_env.get_template(template_name)
file = self.response.file.add()
file.name = self._proto_file_dest(
file_type=file_type, file_ext=file_ext,
)
file.content = template.render(proto=data)
def _generate_code_empty(self) -> None:
for file_type, file_ext in self._iter_src_files():
file = self.response.file.add()
file.name = self._proto_file_dest(
file_type=file_type, file_ext=file_ext,
)
file.content = _AUTOGEN_EMPTY_HEADER
def _iter_src_files(self) -> Iterable[Tuple[str, str]]:
if self.mode.is_service():
src_files = ['service']
elif self.mode.is_client():
src_files = ['client']
elif self.mode.is_both():
src_files = ['client', 'service']
else:
raise Exception(f'Unknown mode {self.mode}')
return itertools.product(src_files, ['hpp', 'cpp'])
def _proto_file_stem(self) -> str:
return self.proto_file.name.replace('.proto', '')
def _proto_file_dest(self, file_type: str, file_ext: str) -> str:
return f'{self._proto_file_stem()}_{file_type}.usrv.pb.{file_ext}'
def generate(
loader: jinja2.BaseLoader, mode: Mode, skip_files_wo_service: bool,
) -> None:
data = sys.stdin.buffer.read()
request = plugin.CodeGeneratorRequest()
request.ParseFromString(data)
response = plugin.CodeGeneratorResponse()
if hasattr(response, 'FEATURE_PROTO3_OPTIONAL'):
setattr(
response,
'supported_features',
getattr(response, 'FEATURE_PROTO3_OPTIONAL'),
)
jinja_env = jinja2.Environment(
loader=loader,
trim_blocks=True,
lstrip_blocks=True,
# We do not render HTML pages with Jinja. However the gRPC data could
# be shown on web. Assuming that the generation should not deal with
# HTML special characters, it is safer to turn on autoescaping.
autoescape=True,
)
jinja_env.filters['grpc_to_cpp_name'] = _grpc_to_cpp_name
# pylint: disable=no-member
for proto_file in request.proto_file:
_CodeGenerator(
jinja_env=jinja_env,
proto_file=proto_file,
response=response,
mode=mode,
skip_files_wo_service=skip_files_wo_service,
).run()
output = response.SerializeToString()
sys.stdout.buffer.write(output)
def main(
loader: Optional[jinja2.BaseLoader] = None,
mode: Mode = Mode.Both,
skip_files_wo_service: bool = True,
) -> None:
if loader is None:
loader = jinja2.FileSystemLoader(
os.path.join(
os.path.dirname(os.path.abspath(__file__)), 'templates',
),
)
generate(
loader=loader, mode=mode, skip_files_wo_service=skip_files_wo_service,
)
if __name__ == '__main__':
main()