defs.bzl 17 KB

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