|
1 |
| -const SwaggerImage = (UI="swaggerapi/swagger-ui", Editor="swaggerapi/swagger-editor") |
| 1 | +const SwaggerImage = ( |
| 2 | + UI="swaggerapi/swagger-ui", |
| 3 | + Editor="swaggerapi/swagger-editor", |
| 4 | +) |
| 5 | +const OpenAPIImage = ( |
| 6 | + GeneratorOnline="openapitools/openapi-generator-online", |
| 7 | + GeneratorCLI="openapitools/openapi-generator-cli", |
| 8 | +) |
| 9 | + |
| 10 | +const GeneratorHost = ( |
| 11 | + OpenAPIGeneratorTech = ( |
| 12 | + Stable = "https://api.openapi-generator.tech", |
| 13 | + Master = "https://api-latest-master.openapi-generator.tech", |
| 14 | + ), |
| 15 | + Local="http://localhost:8080", |
| 16 | +) |
| 17 | + |
| 18 | +const GeneratorHeaders = [ |
| 19 | + "Content-Type" => "application/json", |
| 20 | + "Accept" => "application/json", |
| 21 | +] |
2 | 22 |
|
3 | 23 | docker_cmd(; use_sudo::Bool=false) = use_sudo ? `sudo docker` : `docker`
|
4 | 24 |
|
| 25 | +function _start_docker(cmd, port) |
| 26 | + run(cmd) |
| 27 | + return "http://localhost:$port" |
| 28 | +end |
| 29 | + |
| 30 | +function _stop_docker(image_name::AbstractString, image_type::AbstractString; use_sudo::Bool=false) |
| 31 | + docker = docker_cmd(; use_sudo=use_sudo) |
| 32 | + find_cmd = `$docker ps -a -q -f ancestor=$image_name` |
| 33 | + container_id = strip(String(read(find_cmd))) |
| 34 | + |
| 35 | + if !isempty(container_id) |
| 36 | + stop_cmd = `$docker stop $container_id` |
| 37 | + stop_res = strip(String(read(stop_cmd))) |
| 38 | + |
| 39 | + if stop_res == container_id |
| 40 | + @debug("Stopped $(image_type) container") |
| 41 | + elseif isempty(stop_res) |
| 42 | + @debug("$(image_type) container not running") |
| 43 | + else |
| 44 | + @error("Failed to stop $(image_type) container: $stop_res") |
| 45 | + return false |
| 46 | + end |
| 47 | + |
| 48 | + container_id = strip(String(read(find_cmd))) |
| 49 | + if !isempty(container_id) |
| 50 | + rm_cmd = `$docker rm $container_id` |
| 51 | + rm_res = strip(String(read(rm_cmd))) |
| 52 | + |
| 53 | + if rm_res == container_id |
| 54 | + @debug("Removed $(image_type) container") |
| 55 | + elseif isempty(rm_res) |
| 56 | + @debug("$(image_type) container not found") |
| 57 | + else |
| 58 | + @error("Failed to remove $(image_type) container: $rm_res") |
| 59 | + return false |
| 60 | + end |
| 61 | + end |
| 62 | + |
| 63 | + return true |
| 64 | + else |
| 65 | + @debug("$(image_type) container not found") |
| 66 | + end |
| 67 | + |
| 68 | + return false |
| 69 | +end |
| 70 | + |
| 71 | +""" |
| 72 | + stop_openapi_generator(; use_sudo=false) |
| 73 | +
|
| 74 | +Stop and remove the OpenAPI Generator container, if it is running. |
| 75 | +Returns true if the container was stopped and removed, false otherwise. |
| 76 | +""" |
| 77 | +stop_openapi_generator(; use_sudo::Bool=false) = _stop_docker(OpenAPIImage.GeneratorOnline, "OpenAPI Generator"; use_sudo=use_sudo) |
| 78 | + |
5 | 79 | """
|
6 | 80 | stop_swagger_ui(; use_sudo=false)
|
7 | 81 |
|
@@ -30,50 +104,120 @@ function stop_swagger(; use_sudo::Bool=false)
|
30 | 104 | return stopped
|
31 | 105 | end
|
32 | 106 |
|
33 |
| -function _stop_swagger(image_name::AbstractString; use_sudo::Bool=false) |
| 107 | +_stop_swagger(image_name::AbstractString; use_sudo::Bool=false) = _stop_docker(image_name, "Swagger", use_sudo=use_sudo) |
| 108 | +_start_swagger(cmd, port) = _start_docker(cmd, port) |
| 109 | + |
| 110 | +""" |
| 111 | + openapi_generator(; port=8080, use_sudo=false) |
| 112 | +
|
| 113 | +Start an OpenAPI Generator Online container. Returns the URL of the OpenAPI Generator. |
| 114 | +
|
| 115 | +Optional arguments: |
| 116 | +- `port`: The port to use for the OpenAPI Generator. Defaults to 8080. |
| 117 | +- `use_sudo`: Whether to use `sudo` to run Docker commands. Defaults to false. |
| 118 | +""" |
| 119 | +function openapi_generator(; port::Int=8080, use_sudo::Bool=false) |
34 | 120 | docker = docker_cmd(; use_sudo=use_sudo)
|
35 |
| - find_cmd = `$docker ps -a -q -f ancestor=$image_name` |
36 |
| - container_id = strip(String(read(find_cmd))) |
37 |
| - |
38 |
| - if !isempty(container_id) |
39 |
| - stop_cmd = `$docker stop $container_id` |
40 |
| - stop_res = strip(String(read(stop_cmd))) |
| 121 | + cmd = `$docker run -d --rm -p $port:8080 $(OpenAPIImage.GeneratorOnline)` |
| 122 | + return _start_docker(cmd, port) |
| 123 | +end |
41 | 124 |
|
42 |
| - if stop_res == container_id |
43 |
| - @debug("Stopped Swagger container") |
44 |
| - elseif isempty(stop_res) |
45 |
| - @debug("Swagger container not running") |
46 |
| - else |
47 |
| - @error("Failed to stop Swagger container: $stop_res") |
48 |
| - return false |
49 |
| - end |
| 125 | +function _strip_trailing_pathsep(path::AbstractString) |
| 126 | + if endswith(path, '/') |
| 127 | + return path[1:end-1] |
| 128 | + end |
| 129 | + return path |
| 130 | +end |
50 | 131 |
|
51 |
| - container_id = strip(String(read(find_cmd))) |
52 |
| - if !isempty(container_id) |
53 |
| - rm_cmd = `$docker rm $container_id` |
54 |
| - rm_res = strip(String(read(rm_cmd))) |
| 132 | +""" |
| 133 | + generate( |
| 134 | + spec::Dict{String,Any}; |
| 135 | + type::Symbol=:client, |
| 136 | + package_name::AbstractString="APIClient", |
| 137 | + export_models::Bool=false, |
| 138 | + export_operations::Bool=false, |
| 139 | + output_dir::AbstractString="", |
| 140 | + generator_host::AbstractString=GeneratorHost.Local |
| 141 | + ) |
55 | 142 |
|
56 |
| - if rm_res == container_id |
57 |
| - @debug("Removed Swagger container") |
58 |
| - elseif isempty(rm_res) |
59 |
| - @debug("Swagger container not found") |
60 |
| - else |
61 |
| - @error("Failed to remove Swagger container: $rm_res") |
62 |
| - return false |
63 |
| - end |
64 |
| - end |
| 143 | +Generate client or server code from an OpenAPI spec using the OpenAPI Generator. |
| 144 | +The OpenAPI Generator must be running at the specified `generator_host`. |
65 | 145 |
|
66 |
| - return true |
| 146 | +Returns the path to the generated code. |
| 147 | +
|
| 148 | +Optional arguments: |
| 149 | +- `type`: The type of code to generate. Must be `:client` or `:server`. Defaults to `:client`. |
| 150 | +- `package_name`: The name of the package to generate. Defaults to "APIClient". |
| 151 | +- `export_models`: Whether to export models. Defaults to false. |
| 152 | +- `export_operations`: Whether to export operations. Defaults to false. |
| 153 | +- `output_dir`: The directory to save the generated code. Defaults to a temporary directory. Directory will be created if it does not exist. |
| 154 | +- `generator_host`: The host of the OpenAPI Generator. Defaults to `GeneratorHost.Local`. |
| 155 | + Other possible values are `GeneratorHost.OpenAPIGeneratorTech.Stable` or `GeneratorHost.OpenAPIGeneratorTech.Master`, which point to |
| 156 | + the service hosted by OpenAPI org. It can also be any other URL where the OpenAPI Generator is running. |
| 157 | +
|
| 158 | +A locally hosted generator service is preferred by default for privacy reasons. |
| 159 | +Use `openapi_generator` to start a local container. |
| 160 | +Use `stop_openapi_generator` to stop the local generator service after use. |
| 161 | +""" |
| 162 | +function generate( |
| 163 | + spec::Dict{String,Any}; |
| 164 | + type::Symbol=:client, |
| 165 | + package_name::AbstractString="APIClient", |
| 166 | + export_models::Bool=false, |
| 167 | + export_operations::Bool=false, |
| 168 | + output_dir::AbstractString="", |
| 169 | + generator_host::AbstractString=GeneratorHost.Local, |
| 170 | +) |
| 171 | + if type === :client |
| 172 | + generator_path = "clients/julia-client" |
| 173 | + elseif type === :server |
| 174 | + generator_path = "servers/julia-server" |
67 | 175 | else
|
68 |
| - @debug("Swagger container not found") |
| 176 | + throw(ArgumentError("Invalid generator type: $type. Must be :client or :server")) |
69 | 177 | end
|
70 | 178 |
|
71 |
| - return false |
72 |
| -end |
| 179 | + if isempty(output_dir) |
| 180 | + output_dir = mktempdir() |
| 181 | + end |
73 | 182 |
|
74 |
| -function _start_swagger(cmd, port) |
75 |
| - run(cmd) |
76 |
| - return "http://localhost:$port" |
| 183 | + url = _strip_trailing_pathsep(generator_host) * "/api/gen/" * generator_path |
| 184 | + post_json = Dict{String,Any}( |
| 185 | + "spec" => spec, |
| 186 | + "options" => Dict{String,Any}( |
| 187 | + "packageName" => package_name, |
| 188 | + "exportModels" => string(export_models), |
| 189 | + "exportOperations" => string(export_operations), |
| 190 | + ) |
| 191 | + ) |
| 192 | + |
| 193 | + out = PipeBuffer() |
| 194 | + inp = PipeBuffer() |
| 195 | + JSON.print(inp, post_json, 4) |
| 196 | + closewrite(inp) |
| 197 | + Downloads.request(url; method="POST", headers=GeneratorHeaders, input=inp, output=out, throw=true) |
| 198 | + res = JSON.parse(out) |
| 199 | + |
| 200 | + url = res["link"] |
| 201 | + mktempdir() do extracted_dir |
| 202 | + mktempdir() do download_dir |
| 203 | + output_file = joinpath(download_dir, "generated.zip") |
| 204 | + open(output_file, "w") do out |
| 205 | + Downloads.request(url; method="GET", output=out) |
| 206 | + end |
| 207 | + |
| 208 | + p7zip = p7zip_jll.p7zip() |
| 209 | + run(`$p7zip x -o$extracted_dir $output_file`) |
| 210 | + |
| 211 | + # we expect a single containing root directory in the extrated zip, the contents of which we move to the output directory |
| 212 | + root_dir = only(readdir(extracted_dir)) |
| 213 | + mkpath(output_dir) |
| 214 | + for entry in readdir(joinpath(extracted_dir, root_dir)) |
| 215 | + mv(joinpath(extracted_dir, root_dir, entry), joinpath(output_dir, entry); force=true) |
| 216 | + end |
| 217 | + end |
| 218 | + end |
| 219 | + |
| 220 | + return output_dir |
77 | 221 | end
|
78 | 222 |
|
79 | 223 | """
|
|
0 commit comments