Skip to content

Commit 35834a6

Browse files
authored
feat: add tool to generate code from spec (#75)
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 ) ```
1 parent 512cb75 commit 35834a6

File tree

5 files changed

+237
-40
lines changed

5 files changed

+237
-40
lines changed

Project.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ keywords = ["Swagger", "OpenAPI", "REST"]
44
license = "MIT"
55
desc = "OpenAPI server and client helper for Julia"
66
authors = ["JuliaHub Inc."]
7-
version = "0.1.22"
7+
version = "0.1.23"
88

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

2122
[compat]
2223
Downloads = "1"
2324
HTTP = "1"
2425
JSON = "0.20, 0.21"
2526
LibCURL = "0.6"
27+
MIMEs = "0.1"
2628
MbedTLS = "0.6.8, 0.7, 1"
2729
TimeZones = "1"
2830
URIs = "1.3"
2931
julia = "1.6"
30-
MIMEs = "0.1"
32+
p7zip_jll = "17"
3133

3234
[extras]
3335
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"

docs/src/reference.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ Refer to the User Guide section for mode details of the API that is generated.
5757
## Tools
5858

5959
```@docs
60+
openapi_generator
61+
stop_openapi_generator
62+
generate
6063
swagger_ui
6164
stop_swagger_ui
6265
swagger_editor

docs/src/tools.md

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,66 @@
11
# Tools
22

3+
## Code Generator
4+
5+
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.
6+
7+
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.
8+
9+
```julia
10+
OpenAPI.generate(
11+
spec::Dict{String,Any};
12+
type::Symbol=:client,
13+
package_name::AbstractString="APIClient",
14+
export_models::Bool=false,
15+
export_operations::Bool=false,
16+
output_dir::AbstractString="",
17+
generator_host::AbstractString=GeneratorHost.Local
18+
)
19+
```
20+
21+
Arguments:
22+
- `spec`: The OpenAPI specification as a Dict. It can be obtained by parsing a JSON or YAML file using `JSON.parse` or `YAML.load`.
23+
24+
Optional arguments:
25+
- `type`: The type of code to generate. Must be `:client` or `:server`. Defaults to `:client`.
26+
- `package_name`: The name of the package to generate. Defaults to "APIClient".
27+
- `export_models`: Whether to export models. Defaults to false.
28+
- `export_operations`: Whether to export operations. Defaults to false.
29+
- `output_dir`: The directory to save the generated code. Defaults to a temporary directory. Directory will be created if it does not exist.
30+
- `generator_host`: The host of the OpenAPI Generator. Defaults to `GeneratorHost.Local` (which points to `http://localhost:8080`).
31+
32+
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:
33+
- `OpenAPI.GeneratorHost.OpenAPIGeneratorTech.Stable`: Runs a stable version of the OpenAPI Generator at <https://api.openapi-generator.tech>.
34+
- `OpenAPI.GeneratorHost.OpenAPIGeneratorTech.Master`: Runs the latest version of the OpenAPI Generator at <https://api-latest-master.openapi-generator.tech>.
35+
36+
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.
37+
38+
```julia
39+
OpenAPI.openapi_generator(;
40+
port::Int=8080, # port to use
41+
use_sudo::Bool=false # whether to use sudo while invoking docker
42+
)
43+
44+
OpenAPI.stop_openapi_generator(;
45+
use_sudo::Bool=false # whether to use sudo while invoking docker
46+
)
47+
```
48+
349
## Swagger UI
450

551
[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.
652

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

955
```julia
10-
# specify a specification file to start with
56+
# provide a specification file to start with
1157
OpenAPI.swagger_ui(
1258
spec::AbstractString; # the OpenAPI specification to use
1359
port::Int=8080, # port to use
1460
use_sudo::Bool=false # whether to use sudo while invoking docker
1561
)
1662

17-
# specify a folder and specification file name to start with
63+
# provide a folder and specification file name to start with
1864
OpenAPI.swagger_ui(
1965
spec_dir::AbstractString; # folder containing the specification file
2066
spec_file::AbstractString; # the specification file

src/OpenAPI.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module OpenAPI
22

33
using HTTP, JSON, URIs, Dates, TimeZones, Base64
4+
using Downloads
5+
using p7zip_jll
46

57
import Base: getindex, keys, length, iterate, hasproperty
68
import JSON: lower

src/tools.jl

Lines changed: 180 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,81 @@
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+
]
222

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

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+
579
"""
680
stop_swagger_ui(; use_sudo=false)
781
@@ -30,50 +104,120 @@ function stop_swagger(; use_sudo::Bool=false)
30104
return stopped
31105
end
32106

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)
34120
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
41124

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
50131

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+
)
55142
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`.
65145
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"
67175
else
68-
@debug("Swagger container not found")
176+
throw(ArgumentError("Invalid generator type: $type. Must be :client or :server"))
69177
end
70178

71-
return false
72-
end
179+
if isempty(output_dir)
180+
output_dir = mktempdir()
181+
end
73182

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
77221
end
78222

79223
"""

0 commit comments

Comments
 (0)