Skip to content

Commit

Permalink
feat: add tool to generate code from spec (#75)
Browse files Browse the repository at this point in the history
Add a few methods that help invoke the code generator providing a spec file and generating Julia code.

The [OpenAPI Generator Docker image](https://hub.docker.com/r/openapitools/openapi-generator-cli) is a code generator that can generate client libraries, server stubs, and API documentation from an OpenAPI Specification. It can also be hosted as a service. OpenAPI.jl will now make use of that to provide a way to generate code. Methos `OpenAPI.generate` will generate code from an OpenAPI specification. It can be pointed at a server hosted on the local machine or a remote server. The OpenAPI Generator must be running at the specified `generator_host`. Returns the folder containing generated code.

```julia
OpenAPI.generate(
    spec::Dict{String,Any};
    type::Symbol=:client,
    package_name::AbstractString="APIClient",
    export_models::Bool=false,
    export_operations::Bool=false,
    output_dir::AbstractString="",
    generator_host::AbstractString=GeneratorHost.Local
)
```

Arguments:
- `spec`: The OpenAPI specification as a Dict. It can be obtained by parsing a JSON or YAML file using `JSON.parse` or `YAML.load`.

Optional arguments:
- `type`: The type of code to generate. Must be `:client` or `:server`. Defaults to `:client`.
- `package_name`: The name of the package to generate. Defaults to "APIClient".
- `export_models`: Whether to export models. Defaults to false.
- `export_operations`: Whether to export operations. Defaults to false.
- `output_dir`: The directory to save the generated code. Defaults to a temporary directory. Directory will be created if it does not exist.
- `generator_host`: The host of the OpenAPI Generator. Defaults to `GeneratorHost.Local` (which points to `http://localhost:8080`).

The `generator_host` can be pointed to any other URL where the OpenAPI Generator is running, e.g. `https://openapigen.myorg.com`. Other possible pre-defined values of `generator_host`, which point to the public service hosted by OpenAPI org are:
- `OpenAPI.GeneratorHost.OpenAPIGeneratorTech.Stable`: Runs a stable version of the OpenAPI Generator at <https://api.openapi-generator.tech>.
- `OpenAPI.GeneratorHost.OpenAPIGeneratorTech.Master`: Runs the latest version of the OpenAPI Generator at <https://api-latest-master.openapi-generator.tech>.

A locally hosted generator service is preferred by default for privacy reasons. One can be started on the local machine using `OpenAPI.openapi_generator`. It uses the `openapitools/openapi-generator-online` docker image and requires docker engine to be installed. Use `OpenAPI.stop_openapi_generator` to stop the local generator service after use.

```julia
OpenAPI.openapi_generator(;
    port::Int=8080,         # port to use
    use_sudo::Bool=false    # whether to use sudo while invoking docker
)

OpenAPI.stop_openapi_generator(;
    use_sudo::Bool=false    # whether to use sudo while invoking docker
)
```
  • Loading branch information
tanmaykm authored May 18, 2024
1 parent 512cb75 commit 35834a6
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 40 deletions.
6 changes: 4 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ keywords = ["Swagger", "OpenAPI", "REST"]
license = "MIT"
desc = "OpenAPI server and client helper for Julia"
authors = ["JuliaHub Inc."]
version = "0.1.22"
version = "0.1.23"

[deps]
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
Expand All @@ -17,17 +17,19 @@ MIMEs = "6c6e2e6c-3030-632d-7369-2d6c69616d65"
MbedTLS = "739be429-bea8-5141-9913-cc70e7f3736d"
TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53"
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
p7zip_jll = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"

[compat]
Downloads = "1"
HTTP = "1"
JSON = "0.20, 0.21"
LibCURL = "0.6"
MIMEs = "0.1"
MbedTLS = "0.6.8, 0.7, 1"
TimeZones = "1"
URIs = "1.3"
julia = "1.6"
MIMEs = "0.1"
p7zip_jll = "17"

[extras]
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Expand Down
3 changes: 3 additions & 0 deletions docs/src/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ Refer to the User Guide section for mode details of the API that is generated.
## Tools

```@docs
openapi_generator
stop_openapi_generator
generate
swagger_ui
stop_swagger_ui
swagger_editor
Expand Down
50 changes: 48 additions & 2 deletions docs/src/tools.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,66 @@
# Tools

## Code Generator

The [OpenAPI Generator Docker image](https://hub.docker.com/r/openapitools/openapi-generator-cli) is a code generator that can generate client libraries, server stubs, and API documentation from an OpenAPI Specification. OpenAPI.jl includes convenience methods to use the OpenAPI Generator from Julia.

Use `OpenAPI.generate` to generate code from an OpenAPI specification. It can be pointed at a server hosted on the local machine or a remote server. The OpenAPI Generator must be running at the specified `generator_host`. Returns the folder containing generated code.

```julia
OpenAPI.generate(
spec::Dict{String,Any};
type::Symbol=:client,
package_name::AbstractString="APIClient",
export_models::Bool=false,
export_operations::Bool=false,
output_dir::AbstractString="",
generator_host::AbstractString=GeneratorHost.Local
)
```

Arguments:
- `spec`: The OpenAPI specification as a Dict. It can be obtained by parsing a JSON or YAML file using `JSON.parse` or `YAML.load`.

Optional arguments:
- `type`: The type of code to generate. Must be `:client` or `:server`. Defaults to `:client`.
- `package_name`: The name of the package to generate. Defaults to "APIClient".
- `export_models`: Whether to export models. Defaults to false.
- `export_operations`: Whether to export operations. Defaults to false.
- `output_dir`: The directory to save the generated code. Defaults to a temporary directory. Directory will be created if it does not exist.
- `generator_host`: The host of the OpenAPI Generator. Defaults to `GeneratorHost.Local` (which points to `http://localhost:8080`).

The `generator_host` can be pointed to any other URL where the OpenAPI Generator is running, e.g. `https://openapigen.myorg.com`. Other possible pre-defined values of `generator_host`, which point to the public service hosted by OpenAPI org are:
- `OpenAPI.GeneratorHost.OpenAPIGeneratorTech.Stable`: Runs a stable version of the OpenAPI Generator at <https://api.openapi-generator.tech>.
- `OpenAPI.GeneratorHost.OpenAPIGeneratorTech.Master`: Runs the latest version of the OpenAPI Generator at <https://api-latest-master.openapi-generator.tech>.

A locally hosted generator service is preferred by default for privacy reasons. One can be started on the local machine using `OpenAPI.openapi_generator`. It uses the `openapitools/openapi-generator-online` docker image and requires docker engine to be installed. Use `OpenAPI.stop_openapi_generator` to stop the local generator service after use.

```julia
OpenAPI.openapi_generator(;
port::Int=8080, # port to use
use_sudo::Bool=false # whether to use sudo while invoking docker
)

OpenAPI.stop_openapi_generator(;
use_sudo::Bool=false # whether to use sudo while invoking docker
)
```

## Swagger UI

[Swagger UI](https://swagger.io/tools/swagger-ui/) allows visualization and interaction with the API’s resources without having any of the implementation logic in place. OpenAPI.jl includes convenience methods to launch Swagger UI from Julia.

Use `OpenAPI.swagger_ui` to open Swagger UI. It uses the standard `swaggerapi/swagger-ui` docker image and requires docker engine to be installed.

```julia
# specify a specification file to start with
# provide a specification file to start with
OpenAPI.swagger_ui(
spec::AbstractString; # the OpenAPI specification to use
port::Int=8080, # port to use
use_sudo::Bool=false # whether to use sudo while invoking docker
)

# specify a folder and specification file name to start with
# provide a folder and specification file name to start with
OpenAPI.swagger_ui(
spec_dir::AbstractString; # folder containing the specification file
spec_file::AbstractString; # the specification file
Expand Down
2 changes: 2 additions & 0 deletions src/OpenAPI.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module OpenAPI

using HTTP, JSON, URIs, Dates, TimeZones, Base64
using Downloads
using p7zip_jll

import Base: getindex, keys, length, iterate, hasproperty
import JSON: lower
Expand Down
216 changes: 180 additions & 36 deletions src/tools.jl
Original file line number Diff line number Diff line change
@@ -1,7 +1,81 @@
const SwaggerImage = (UI="swaggerapi/swagger-ui", Editor="swaggerapi/swagger-editor")
const SwaggerImage = (
UI="swaggerapi/swagger-ui",
Editor="swaggerapi/swagger-editor",
)
const OpenAPIImage = (
GeneratorOnline="openapitools/openapi-generator-online",
GeneratorCLI="openapitools/openapi-generator-cli",
)

const GeneratorHost = (
OpenAPIGeneratorTech = (
Stable = "https://api.openapi-generator.tech",
Master = "https://api-latest-master.openapi-generator.tech",
),
Local="http://localhost:8080",
)

const GeneratorHeaders = [
"Content-Type" => "application/json",
"Accept" => "application/json",
]

docker_cmd(; use_sudo::Bool=false) = use_sudo ? `sudo docker` : `docker`

function _start_docker(cmd, port)
run(cmd)
return "http://localhost:$port"
end

function _stop_docker(image_name::AbstractString, image_type::AbstractString; use_sudo::Bool=false)
docker = docker_cmd(; use_sudo=use_sudo)
find_cmd = `$docker ps -a -q -f ancestor=$image_name`
container_id = strip(String(read(find_cmd)))

if !isempty(container_id)
stop_cmd = `$docker stop $container_id`
stop_res = strip(String(read(stop_cmd)))

if stop_res == container_id
@debug("Stopped $(image_type) container")
elseif isempty(stop_res)
@debug("$(image_type) container not running")
else
@error("Failed to stop $(image_type) container: $stop_res")
return false
end

container_id = strip(String(read(find_cmd)))
if !isempty(container_id)
rm_cmd = `$docker rm $container_id`
rm_res = strip(String(read(rm_cmd)))

if rm_res == container_id
@debug("Removed $(image_type) container")
elseif isempty(rm_res)
@debug("$(image_type) container not found")
else
@error("Failed to remove $(image_type) container: $rm_res")
return false
end
end

return true
else
@debug("$(image_type) container not found")
end

return false
end

"""
stop_openapi_generator(; use_sudo=false)
Stop and remove the OpenAPI Generator container, if it is running.
Returns true if the container was stopped and removed, false otherwise.
"""
stop_openapi_generator(; use_sudo::Bool=false) = _stop_docker(OpenAPIImage.GeneratorOnline, "OpenAPI Generator"; use_sudo=use_sudo)

"""
stop_swagger_ui(; use_sudo=false)
Expand Down Expand Up @@ -30,50 +104,120 @@ function stop_swagger(; use_sudo::Bool=false)
return stopped
end

function _stop_swagger(image_name::AbstractString; use_sudo::Bool=false)
_stop_swagger(image_name::AbstractString; use_sudo::Bool=false) = _stop_docker(image_name, "Swagger", use_sudo=use_sudo)
_start_swagger(cmd, port) = _start_docker(cmd, port)

"""
openapi_generator(; port=8080, use_sudo=false)
Start an OpenAPI Generator Online container. Returns the URL of the OpenAPI Generator.
Optional arguments:
- `port`: The port to use for the OpenAPI Generator. Defaults to 8080.
- `use_sudo`: Whether to use `sudo` to run Docker commands. Defaults to false.
"""
function openapi_generator(; port::Int=8080, use_sudo::Bool=false)
docker = docker_cmd(; use_sudo=use_sudo)
find_cmd = `$docker ps -a -q -f ancestor=$image_name`
container_id = strip(String(read(find_cmd)))

if !isempty(container_id)
stop_cmd = `$docker stop $container_id`
stop_res = strip(String(read(stop_cmd)))
cmd = `$docker run -d --rm -p $port:8080 $(OpenAPIImage.GeneratorOnline)`
return _start_docker(cmd, port)
end

if stop_res == container_id
@debug("Stopped Swagger container")
elseif isempty(stop_res)
@debug("Swagger container not running")
else
@error("Failed to stop Swagger container: $stop_res")
return false
end
function _strip_trailing_pathsep(path::AbstractString)
if endswith(path, '/')
return path[1:end-1]
end
return path
end

container_id = strip(String(read(find_cmd)))
if !isempty(container_id)
rm_cmd = `$docker rm $container_id`
rm_res = strip(String(read(rm_cmd)))
"""
generate(
spec::Dict{String,Any};
type::Symbol=:client,
package_name::AbstractString="APIClient",
export_models::Bool=false,
export_operations::Bool=false,
output_dir::AbstractString="",
generator_host::AbstractString=GeneratorHost.Local
)
if rm_res == container_id
@debug("Removed Swagger container")
elseif isempty(rm_res)
@debug("Swagger container not found")
else
@error("Failed to remove Swagger container: $rm_res")
return false
end
end
Generate client or server code from an OpenAPI spec using the OpenAPI Generator.
The OpenAPI Generator must be running at the specified `generator_host`.
return true
Returns the path to the generated code.
Optional arguments:
- `type`: The type of code to generate. Must be `:client` or `:server`. Defaults to `:client`.
- `package_name`: The name of the package to generate. Defaults to "APIClient".
- `export_models`: Whether to export models. Defaults to false.
- `export_operations`: Whether to export operations. Defaults to false.
- `output_dir`: The directory to save the generated code. Defaults to a temporary directory. Directory will be created if it does not exist.
- `generator_host`: The host of the OpenAPI Generator. Defaults to `GeneratorHost.Local`.
Other possible values are `GeneratorHost.OpenAPIGeneratorTech.Stable` or `GeneratorHost.OpenAPIGeneratorTech.Master`, which point to
the service hosted by OpenAPI org. It can also be any other URL where the OpenAPI Generator is running.
A locally hosted generator service is preferred by default for privacy reasons.
Use `openapi_generator` to start a local container.
Use `stop_openapi_generator` to stop the local generator service after use.
"""
function generate(
spec::Dict{String,Any};
type::Symbol=:client,
package_name::AbstractString="APIClient",
export_models::Bool=false,
export_operations::Bool=false,
output_dir::AbstractString="",
generator_host::AbstractString=GeneratorHost.Local,
)
if type === :client
generator_path = "clients/julia-client"
elseif type === :server
generator_path = "servers/julia-server"
else
@debug("Swagger container not found")
throw(ArgumentError("Invalid generator type: $type. Must be :client or :server"))
end

return false
end
if isempty(output_dir)
output_dir = mktempdir()
end

function _start_swagger(cmd, port)
run(cmd)
return "http://localhost:$port"
url = _strip_trailing_pathsep(generator_host) * "/api/gen/" * generator_path
post_json = Dict{String,Any}(
"spec" => spec,
"options" => Dict{String,Any}(
"packageName" => package_name,
"exportModels" => string(export_models),
"exportOperations" => string(export_operations),
)
)

out = PipeBuffer()
inp = PipeBuffer()
JSON.print(inp, post_json, 4)
closewrite(inp)
Downloads.request(url; method="POST", headers=GeneratorHeaders, input=inp, output=out, throw=true)
res = JSON.parse(out)

url = res["link"]
mktempdir() do extracted_dir
mktempdir() do download_dir
output_file = joinpath(download_dir, "generated.zip")
open(output_file, "w") do out
Downloads.request(url; method="GET", output=out)
end

p7zip = p7zip_jll.p7zip()
run(`$p7zip x -o$extracted_dir $output_file`)

# we expect a single containing root directory in the extrated zip, the contents of which we move to the output directory
root_dir = only(readdir(extracted_dir))
mkpath(output_dir)
for entry in readdir(joinpath(extracted_dir, root_dir))
mv(joinpath(extracted_dir, root_dir, entry), joinpath(output_dir, entry); force=true)
end
end
end

return output_dir
end

"""
Expand Down

2 comments on commit 35834a6

@tanmaykm
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/107120

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.1.23 -m "<description of version>" 35834a699b1170edf7d3840dfbfac424d4f056bf
git push origin v0.1.23

Please sign in to comment.