1"""Generated an open-api spec for a grpc api spec.
2
3Reads the the api spec in protobuf format and generate an open-api spec.
4Optionally applies settings from the grpc-service configuration.
5"""
6
7load("@rules_proto//proto:defs.bzl", "ProtoInfo")
8
9# TODO(yannic): Replace with |proto_common.direct_source_infos| when
10# https://github.com/bazelbuild/rules_proto/pull/22 lands.
11def _direct_source_infos(proto_info, provided_sources = []):
12 """Returns sequence of `ProtoFileInfo` for `proto_info`'s direct sources.
13
14 Files that are both in `proto_info`'s direct sources and in
15 `provided_sources` are skipped. This is useful, e.g., for well-known
16 protos that are already provided by the Protobuf runtime.
17
18 Args:
19 proto_info: An instance of `ProtoInfo`.
20 provided_sources: Optional. A sequence of files to ignore.
21 Usually, these files are already provided by the
22 Protocol Buffer runtime (e.g. Well-Known protos).
23
24 Returns: A sequence of `ProtoFileInfo` containing information about
25 `proto_info`'s direct sources.
26 """
27
28 source_root = proto_info.proto_source_root
29 if "." == source_root:
30 return [struct(file = src, import_path = src.path) for src in proto_info.check_deps_sources.to_list()]
31
32 offset = len(source_root) + 1 # + '/'.
33
34 infos = []
35 for src in proto_info.check_deps_sources.to_list():
36 # TODO(yannic): Remove this hack when we drop support for Bazel < 1.0.
37 local_offset = offset
38 if src.root.path and not source_root.startswith(src.root.path):
39 # Before Bazel 1.0, `proto_source_root` wasn't guaranteed to be a
40 # prefix of `src.path`. This could happend, e.g., if `file` was
41 # generated (https://github.com/bazelbuild/bazel/issues/9215).
42 local_offset += len(src.root.path) + 1 # + '/'.
43 infos.append(struct(file = src, import_path = src.path[local_offset:]))
44
45 return infos
46
47def _run_proto_gen_openapi(
48 actions,
49 proto_info,
50 target_name,
51 transitive_proto_srcs,
52 protoc,
53 protoc_gen_openapiv2,
54 single_output,
55 allow_delete_body,
56 grpc_api_configuration,
57 json_names_for_fields,
58 repeated_path_param_separator,
59 include_package_in_tags,
60 fqn_for_openapi_name,
61 openapi_naming_strategy,
62 use_go_templates,
63 go_template_args,
64 ignore_comments,
65 remove_internal_comments,
66 disable_default_errors,
67 disable_service_tags,
68 enums_as_ints,
69 omit_enum_default_value,
70 output_format,
71 simple_operation_ids,
72 proto3_optional_nullable,
73 openapi_configuration,
74 generate_unbound_methods,
75 visibility_restriction_selectors,
76 use_allof_for_refs):
77 args = actions.args()
78
79 args.add("--plugin", "protoc-gen-openapiv2=%s" % protoc_gen_openapiv2.path)
80
81 extra_inputs = []
82 if grpc_api_configuration:
83 extra_inputs.append(grpc_api_configuration)
84 args.add("--openapiv2_opt", "grpc_api_configuration=%s" % grpc_api_configuration.path)
85
86 if openapi_configuration:
87 extra_inputs.append(openapi_configuration)
88 args.add("--openapiv2_opt", "openapi_configuration=%s" % openapi_configuration.path)
89
90 if not json_names_for_fields:
91 args.add("--openapiv2_opt", "json_names_for_fields=false")
92
93 if fqn_for_openapi_name:
94 args.add("--openapiv2_opt", "fqn_for_openapi_name=true")
95
96 if openapi_naming_strategy:
97 args.add("--openapiv2_opt", "openapi_naming_strategy=%s" % openapi_naming_strategy)
98
99 if generate_unbound_methods:
100 args.add("--openapiv2_opt", "generate_unbound_methods=true")
101
102 if simple_operation_ids:
103 args.add("--openapiv2_opt", "simple_operation_ids=true")
104
105 if allow_delete_body:
106 args.add("--openapiv2_opt", "allow_delete_body=true")
107
108 if include_package_in_tags:
109 args.add("--openapiv2_opt", "include_package_in_tags=true")
110
111 if use_go_templates:
112 args.add("--openapiv2_opt", "use_go_templates=true")
113
114 for go_template_arg in go_template_args:
115 args.add("--openapiv2_opt", "go_template_args=%s" % go_template_arg)
116
117 if ignore_comments:
118 args.add("--openapiv2_opt", "ignore_comments=true")
119
120 if remove_internal_comments:
121 args.add("--openapiv2_opt", "remove_internal_comments=true")
122
123 if disable_default_errors:
124 args.add("--openapiv2_opt", "disable_default_errors=true")
125
126 if disable_service_tags:
127 args.add("--openapiv2_opt", "disable_service_tags=true")
128
129 if enums_as_ints:
130 args.add("--openapiv2_opt", "enums_as_ints=true")
131
132 if omit_enum_default_value:
133 args.add("--openapiv2_opt", "omit_enum_default_value=true")
134
135 if output_format:
136 args.add("--openapiv2_opt", "output_format=%s" % output_format)
137
138 if proto3_optional_nullable:
139 args.add("--openapiv2_opt", "proto3_optional_nullable=true")
140
141 for visibility_restriction_selector in visibility_restriction_selectors:
142 args.add("--openapiv2_opt", "visibility_restriction_selectors=%s" % visibility_restriction_selector)
143
144 if use_allof_for_refs:
145 args.add("--openapiv2_opt", "use_allof_for_refs=true")
146
147 args.add("--openapiv2_opt", "repeated_path_param_separator=%s" % repeated_path_param_separator)
148
149 proto_file_infos = _direct_source_infos(proto_info)
150
151 # TODO(yannic): Use |proto_info.transitive_descriptor_sets| when
152 # https://github.com/bazelbuild/bazel/issues/9337 is fixed.
153 args.add_all(proto_info.transitive_proto_path, format_each = "--proto_path=%s")
154
155 if single_output:
156 args.add("--openapiv2_opt", "allow_merge=true")
157 args.add("--openapiv2_opt", "merge_file_name=%s" % target_name)
158
159 openapi_file = actions.declare_file("%s.swagger.json" % target_name)
160 args.add("--openapiv2_out", openapi_file.dirname)
161
162 args.add_all([f.import_path for f in proto_file_infos])
163
164 actions.run(
165 executable = protoc,
166 tools = [protoc_gen_openapiv2],
167 inputs = depset(
168 direct = extra_inputs,
169 transitive = [transitive_proto_srcs],
170 ),
171 outputs = [openapi_file],
172 arguments = [args],
173 )
174
175 return [openapi_file]
176
177 # TODO(yannic): We may be able to generate all files in a single action,
178 # but that will change at least the semantics of `use_go_template.proto`.
179 openapi_files = []
180 for proto_file_info in proto_file_infos:
181 # TODO(yannic): This probably doesn't work as expected: we only add this
182 # option after we have seen it, so `.proto` sources that happen to be
183 # in the list of `.proto` files before `use_go_template.proto` will be
184 # compiled without this option, and all sources that get compiled after
185 # `use_go_template.proto` will have this option on.
186 if proto_file_info.file.basename == "use_go_template.proto":
187 args.add("--openapiv2_opt", "use_go_templates=true")
188
189 file_name = "%s.swagger.json" % proto_file_info.import_path[:-len(".proto")]
190 openapi_file = actions.declare_file(
191 "_virtual_imports/%s/%s" % (target_name, file_name),
192 )
193
194 file_args = actions.args()
195
196 offset = len(file_name) + 1 # + '/'.
197 file_args.add("--openapiv2_out", openapi_file.path[:-offset])
198
199 file_args.add(proto_file_info.import_path)
200
201 actions.run(
202 executable = protoc,
203 tools = [protoc_gen_openapiv2],
204 inputs = depset(
205 direct = extra_inputs,
206 transitive = [transitive_proto_srcs],
207 ),
208 outputs = [openapi_file],
209 arguments = [args, file_args],
210 )
211 openapi_files.append(openapi_file)
212
213 return openapi_files
214
215def _proto_gen_openapi_impl(ctx):
216 proto = ctx.attr.proto[ProtoInfo]
217 return [
218 DefaultInfo(
219 files = depset(
220 _run_proto_gen_openapi(
221 actions = ctx.actions,
222 proto_info = proto,
223 target_name = ctx.attr.name,
224 transitive_proto_srcs = depset(
225 direct = ctx.files._well_known_protos,
226 transitive = [proto.transitive_sources],
227 ),
228 protoc = ctx.executable._protoc,
229 protoc_gen_openapiv2 = ctx.executable._protoc_gen_openapi,
230 single_output = ctx.attr.single_output,
231 allow_delete_body = ctx.attr.allow_delete_body,
232 grpc_api_configuration = ctx.file.grpc_api_configuration,
233 json_names_for_fields = ctx.attr.json_names_for_fields,
234 repeated_path_param_separator = ctx.attr.repeated_path_param_separator,
235 include_package_in_tags = ctx.attr.include_package_in_tags,
236 fqn_for_openapi_name = ctx.attr.fqn_for_openapi_name,
237 openapi_naming_strategy = ctx.attr.openapi_naming_strategy,
238 use_go_templates = ctx.attr.use_go_templates,
239 go_template_args = ctx.attr.go_template_args,
240 ignore_comments = ctx.attr.ignore_comments,
241 remove_internal_comments = ctx.attr.remove_internal_comments,
242 disable_default_errors = ctx.attr.disable_default_errors,
243 disable_service_tags = ctx.attr.disable_service_tags,
244 enums_as_ints = ctx.attr.enums_as_ints,
245 omit_enum_default_value = ctx.attr.omit_enum_default_value,
246 output_format = ctx.attr.output_format,
247 simple_operation_ids = ctx.attr.simple_operation_ids,
248 proto3_optional_nullable = ctx.attr.proto3_optional_nullable,
249 openapi_configuration = ctx.file.openapi_configuration,
250 generate_unbound_methods = ctx.attr.generate_unbound_methods,
251 visibility_restriction_selectors = ctx.attr.visibility_restriction_selectors,
252 use_allof_for_refs = ctx.attr.use_allof_for_refs,
253 ),
254 ),
255 ),
256 ]
257
258protoc_gen_openapiv2 = rule(
259 attrs = {
260 "proto": attr.label(
261 mandatory = True,
262 providers = [ProtoInfo],
263 ),
264 "single_output": attr.bool(
265 default = False,
266 mandatory = False,
267 doc = "if set, the rule will generate a single OpenAPI file",
268 ),
269 "allow_delete_body": attr.bool(
270 default = False,
271 mandatory = False,
272 doc = "unless set, HTTP DELETE methods may not have a body",
273 ),
274 "grpc_api_configuration": attr.label(
275 allow_single_file = True,
276 mandatory = False,
277 doc = "path to file which describes the gRPC API Configuration in YAML format",
278 ),
279 "json_names_for_fields": attr.bool(
280 default = True,
281 mandatory = False,
282 doc = "if disabled, the original proto name will be used for generating OpenAPI definitions",
283 ),
284 "repeated_path_param_separator": attr.string(
285 default = "csv",
286 mandatory = False,
287 values = ["csv", "pipes", "ssv", "tsv"],
288 doc = "configures how repeated fields should be split." +
289 " Allowed values are `csv`, `pipes`, `ssv` and `tsv`",
290 ),
291 "include_package_in_tags": attr.bool(
292 default = False,
293 mandatory = False,
294 doc = "if unset, the gRPC service name is added to the `Tags`" +
295 " field of each operation. If set and the `package` directive" +
296 " is shown in the proto file, the package name will be " +
297 " prepended to the service name",
298 ),
299 "fqn_for_openapi_name": attr.bool(
300 default = False,
301 mandatory = False,
302 doc = "if set, the object's OpenAPI names will use the fully" +
303 " qualified names from the proto definition" +
304 " (ie my.package.MyMessage.MyInnerMessage",
305 ),
306 "openapi_naming_strategy": attr.string(
307 default = "",
308 mandatory = False,
309 values = ["", "simple", "legacy", "fqn"],
310 doc = "configures how OpenAPI names are determined." +
311 " Allowed values are `` (empty), `simple`, `legacy` and `fqn`." +
312 " If unset, either `legacy` or `fqn` are selected, depending" +
313 " on the value of the `fqn_for_openapi_name` setting",
314 ),
315 "use_go_templates": attr.bool(
316 default = False,
317 mandatory = False,
318 doc = "if set, you can use Go templates in protofile comments",
319 ),
320 "go_template_args": attr.string_list(
321 mandatory = False,
322 doc = "specify a key value pair as inputs to the Go template of the protofile" +
323 " comments. Repeat this option to specify multiple template arguments." +
324 " Requires the `use_go_templates` option to be set.",
325 ),
326 "ignore_comments": attr.bool(
327 default = False,
328 mandatory = False,
329 doc = "if set, all protofile comments are excluded from output",
330 ),
331 "remove_internal_comments": attr.bool(
332 default = False,
333 mandatory = False,
334 doc = "if set, removes all substrings in comments that start with " +
335 "`(--` and end with `--)` as specified in " +
336 "https://google.aip.dev/192#internal-comments",
337 ),
338 "disable_default_errors": attr.bool(
339 default = False,
340 mandatory = False,
341 doc = "if set, disables generation of default errors." +
342 " This is useful if you have defined custom error handling",
343 ),
344 "disable_service_tags": attr.bool(
345 default = False,
346 mandatory = False,
347 doc = "if set, disables generation of service tags." +
348 " This is useful if you do not want to expose the names of your backend grpc services.",
349 ),
350 "enums_as_ints": attr.bool(
351 default = False,
352 mandatory = False,
353 doc = "whether to render enum values as integers, as opposed to string values",
354 ),
355 "omit_enum_default_value": attr.bool(
356 default = False,
357 mandatory = False,
358 doc = "if set, omit default enum value",
359 ),
360 "output_format": attr.string(
361 default = "json",
362 mandatory = False,
363 values = ["json", "yaml"],
364 doc = "output content format. Allowed values are: `json`, `yaml`",
365 ),
366 "simple_operation_ids": attr.bool(
367 default = False,
368 mandatory = False,
369 doc = "whether to remove the service prefix in the operationID" +
370 " generation. Can introduce duplicate operationIDs, use with caution.",
371 ),
372 "proto3_optional_nullable": attr.bool(
373 default = False,
374 mandatory = False,
375 doc = "whether Proto3 Optional fields should be marked as x-nullable",
376 ),
377 "openapi_configuration": attr.label(
378 allow_single_file = True,
379 mandatory = False,
380 doc = "path to file which describes the OpenAPI Configuration in YAML format",
381 ),
382 "generate_unbound_methods": attr.bool(
383 default = False,
384 mandatory = False,
385 doc = "generate swagger metadata even for RPC methods that have" +
386 " no HttpRule annotation",
387 ),
388 "visibility_restriction_selectors": attr.string_list(
389 mandatory = False,
390 doc = "list of `google.api.VisibilityRule` visibility labels to include" +
391 " in the generated output when a visibility annotation is defined." +
392 " Repeat this option to supply multiple values. Elements without" +
393 " visibility annotations are unaffected by this setting.",
394 ),
395 "use_allof_for_refs": attr.bool(
396 default = False,
397 mandatory = False,
398 doc = "if set, will use allOf as container for $ref to preserve" +
399 " same-level properties.",
400 ),
401 "_protoc": attr.label(
402 default = "@com_google_protobuf//:protoc",
403 executable = True,
404 cfg = "exec",
405 ),
406 "_well_known_protos": attr.label(
407 default = "@com_google_protobuf//:well_known_type_protos",
408 allow_files = True,
409 ),
410 "_protoc_gen_openapi": attr.label(
411 default = Label("//protoc-gen-openapiv2:protoc-gen-openapiv2"),
412 executable = True,
413 cfg = "exec",
414 ),
415 },
416 implementation = _proto_gen_openapi_impl,
417)
View as plain text