From 7f97f4937bb8dbc610fe99d8ee40abf8e9291bd8 Mon Sep 17 00:00:00 2001 From: mrmeku Date: Mon, 21 Oct 2019 11:05:51 -0600 Subject: [PATCH] Initial release of openapi_tools_generator_bazel --- .gitignore | 1 + BUILD.bazel | 0 README.md | 41 +++++++++ WORKSPACE | 5 ++ defs.bzl | 9 ++ internal/BUILD.bazel | 0 internal/openapi_generator.bzl | 147 +++++++++++++++++++++++++++++++++ internal/test/BUILD.bazel | 41 +++++++++ internal/test/petstore.yaml | 101 ++++++++++++++++++++++ 9 files changed, 345 insertions(+) create mode 100644 .gitignore create mode 100644 BUILD.bazel create mode 100644 README.md create mode 100644 WORKSPACE create mode 100644 defs.bzl create mode 100644 internal/BUILD.bazel create mode 100644 internal/openapi_generator.bzl create mode 100644 internal/test/BUILD.bazel create mode 100644 internal/test/petstore.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac51a05 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bazel-* diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4e1dd8 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# OpenAPI Generator Bazel + +This repo was created to integrate the OpenAPI code generation CLI with Bazel. + +## Quickstart + +To use the Bazel bindings provided by this repo within a Bazel workspace, +you must do the following steps: + +1. Add the following code to your WORKSPACE file at the root of your repository + + ``` + load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + + openapi_tools_generator_bazel_release = "0.1.0" + http_archive( + name = "openapi_tools_generator_bazel", + sha256 = "", # Determine the SHA for the specific release you're targeting + url = "https://github.com/OpenAPITools/openapi-generator-bazel/releases/download/RELEASE/rules_nodejs-RELEASE.tar.gz".format(RELEASE = openapi_tools_generator_bazel_release), + ) + + load("@openapi_tools_generator_bazel//:defs.bzl", "openapi_tools_generator_bazel_repositories") + + # You can provide any version of the CLI that has been uploaded to Maven + openapi_tools_generator_bazel_repositories( + openapi_generator_cli_version = "4.1.0" + ) + ``` + +2. Create a BUILD.bazel file next to the .yaml file you wish to generate code from. + The below example generates a go library within a generated directory named `petstore_go` + +``` +load("@openapi_tools_generator_bazel//:defs.bzl", "openapi_generator") + +openapi_generator( + name = "petstore_go", + generator = "go", + spec = "petstore.yaml", +) +``` diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 0000000..8abc8c1 --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,5 @@ +workspace(name = "openapi_tools_generator_bazel") + +load("//:defs.bzl", "openapi_tools_generator_bazel_repositories") + +openapi_tools_generator_bazel_repositories() diff --git a/defs.bzl b/defs.bzl new file mode 100644 index 0000000..8439975 --- /dev/null +++ b/defs.bzl @@ -0,0 +1,9 @@ +load( + "//internal:openapi_generator.bzl", + _openapi_generator = "openapi_generator", + _openapi_tools_generator_bazel_repositories = "openapi_tools_generator_bazel_repositories", +) + +openapi_generator = _openapi_generator + +openapi_tools_generator_bazel_repositories = _openapi_tools_generator_bazel_repositories diff --git a/internal/BUILD.bazel b/internal/BUILD.bazel new file mode 100644 index 0000000..e69de29 diff --git a/internal/openapi_generator.bzl b/internal/openapi_generator.bzl new file mode 100644 index 0000000..597d579 --- /dev/null +++ b/internal/openapi_generator.bzl @@ -0,0 +1,147 @@ +def openapi_tools_generator_bazel_repositories(openapi_generator_cli_version = "4.1.0", prefix = "openapi_tools_generator_bazel"): + native.maven_jar( + name = prefix + "_cli", + artifact = "org.openapitools:openapi-generator-cli:" + openapi_generator_cli_version, + ) + native.bind( + name = prefix + "/dependency/openapi-generator-cli", + actual = "@" + prefix + "_cli//jar", + ) + +def _comma_separated_pairs(pairs): + return ",".join([ + "{}={}".format(k, v) + for k, v in pairs.items() + ]) + +def _new_generator_command(ctx, declared_dir, rjars): + java_path = ctx.attr._jdk[java_common.JavaRuntimeInfo].java_executable_exec_path + gen_cmd = str(java_path) + + gen_cmd += " -cp {cli_jar}:{jars} org.openapitools.codegen.OpenAPIGenerator generate -i {spec} -g {generator} -o {output}".format( + java = java_path, + cli_jar = ctx.file.openapi_generator_cli.path, + jars = ":".join([j.path for j in rjars.to_list()]), + spec = ctx.file.spec.path, + generator = ctx.attr.generator, + output = declared_dir.path, + ) + + gen_cmd += ' -p "{properties}"'.format( + properties = _comma_separated_pairs(ctx.attr.system_properties), + ) + + additional_properties = dict(ctx.attr.additional_properties) + + # This is needed to ensure reproducible Java output + if ctx.attr.generator == "java" and \ + "hideGenerationTimestamp" not in ctx.attr.additional_properties: + additional_properties["hideGenerationTimestamp"] = "true" + + gen_cmd += ' --additional-properties "{properties}"'.format( + properties = _comma_separated_pairs(additional_properties), + ) + + gen_cmd += ' --type-mappings "{mappings}"'.format( + mappings = _comma_separated_pairs(ctx.attr.type_mappings), + ) + + if ctx.attr.api_package: + gen_cmd += " --api-package {package}".format( + package = ctx.attr.api_package, + ) + if ctx.attr.invoker_package: + gen_cmd += " --invoker-package {package}".format( + package = ctx.attr.invoker_package, + ) + if ctx.attr.model_package: + gen_cmd += " --model-package {package}".format( + package = ctx.attr.model_package, + ) + + # fixme: by default, openapi-generator is rather verbose. this helps with that but can also mask useful error messages + # when it fails. look into log configuration options. it's a java app so perhaps just a log4j.properties or something + gen_cmd += " 1>/dev/null" + return gen_cmd + +def _impl(ctx): + jars = _collect_jars(ctx.attr.deps) + (cjars, rjars) = (jars.compiletime, jars.runtime) + + declared_dir = ctx.actions.declare_directory("%s" % (ctx.attr.name)) + + inputs = [ + ctx.file.openapi_generator_cli, + ctx.file.spec, + ] + cjars.to_list() + rjars.to_list() + + # TODO: Convert to run + ctx.actions.run_shell( + inputs = inputs, + command = "mkdir -p {gen_dir}".format( + gen_dir = declared_dir.path, + ) + " && " + _new_generator_command(ctx, declared_dir, rjars), + outputs = [declared_dir], + tools = ctx.files._jdk, + ) + + srcs = declared_dir.path + + return DefaultInfo(files = depset([ + declared_dir, + ])) + +def _collect_jars(targets): + """Compute the runtime and compile-time dependencies from the given targets""" # noqa + compile_jars = depset() + runtime_jars = depset() + for target in targets: + found = False + if hasattr(target, "scala"): + if hasattr(target.scala.outputs, "ijar"): + compile_jars = depset(transitive = [compile_jars, [target.scala.outputs.ijar]]) + compile_jars = depset(transitive = [compile_jars, target.scala.transitive_compile_exports]) + runtime_jars = depset(transitive = [runtime_jars, target.scala.transitive_runtime_deps]) + runtime_jars = depset(transitive = [runtime_jars, target.scala.transitive_runtime_exports]) + found = True + if hasattr(target, "java"): + compile_jars = depset(transitive = [compile_jars, target.java.transitive_deps]) + runtime_jars = depset(transitive = [runtime_jars, target.java.transitive_runtime_deps]) + found = True + if not found: + runtime_jars = depset(transitive = [runtime_jars, target.files]) + compile_jars = depset(transitive = [compile_jars, target.files]) + + return struct(compiletime = compile_jars, runtime = runtime_jars) + +openapi_generator = rule( + attrs = { + # downstream dependencies + "deps": attr.label_list(), + # openapi spec file + "spec": attr.label( + mandatory = True, + allow_single_file = [ + ".json", + ".yaml", + ], + ), + "generator": attr.string(mandatory = True), + "api_package": attr.string(), + "invoker_package": attr.string(), + "model_package": attr.string(), + "additional_properties": attr.string_dict(), + "system_properties": attr.string_dict(), + "type_mappings": attr.string_dict(), + "_jdk": attr.label( + default = Label("@bazel_tools//tools/jdk:current_java_runtime"), + providers = [java_common.JavaRuntimeInfo], + ), + "openapi_generator_cli": attr.label( + cfg = "host", + default = Label("//external:openapi_tools_generator_bazel/dependency/openapi-generator-cli"), + allow_single_file = True, + ), + }, + implementation = _impl, +) diff --git a/internal/test/BUILD.bazel b/internal/test/BUILD.bazel new file mode 100644 index 0000000..cd3b825 --- /dev/null +++ b/internal/test/BUILD.bazel @@ -0,0 +1,41 @@ +load("@openapi_tools_generator_bazel//:defs.bzl", "openapi_generator") + +openapi_generator( + name = "petstore", + generator = "go", + spec = "petstore.yaml", +) + +openapi_generator( + name = "petstore_java", + generator = "java", + spec = "petstore.yaml", +) + +openapi_generator( + name = "petstore_java_feign", + additional_properties = { + "library": "feign", + }, + generator = "java", + spec = "petstore.yaml", +) + +openapi_generator( + name = "petstore_java_no_tests", + generator = "java", + spec = "petstore.yaml", + system_properties = { + "apiTests": "false", + "modelTests": "false", + }, +) + +openapi_generator( + name = "petstore_java_bigdecimal", + generator = "java", + spec = "petstore.yaml", + type_mappings = { + "Integer": "java.math.BigDecimal", + }, +) diff --git a/internal/test/petstore.yaml b/internal/test/petstore.yaml new file mode 100644 index 0000000..8d9872b --- /dev/null +++ b/internal/test/petstore.yaml @@ -0,0 +1,101 @@ +swagger: "2.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +host: petstore.swagger.io +basePath: /v1 +schemes: + - http +consumes: + - application/json +produces: + - application/json +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + type: integer + format: int32 + responses: + "200": + description: An paged array of pets + headers: + x-next: + type: string + description: A link to the next page of responses + schema: + $ref: "#/definitions/Pets" + default: + description: unexpected error + schema: + $ref: "#/definitions/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + "201": + description: Null response + default: + description: unexpected error + schema: + $ref: "#/definitions/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + type: string + responses: + "200": + description: Expected response to a valid request + schema: + $ref: "#/definitions/Pets" + default: + description: unexpected error + schema: + $ref: "#/definitions/Error" +definitions: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/definitions/Pet" + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string