Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add tool to generate code from spec #75

Merged
merged 1 commit into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading