Skip to content

Commit

Permalink
Support chunked transfer encoding
Browse files Browse the repository at this point in the history
Handle APIs that return chunked transfer encoded data. E.g. Kubernetes `watch` APIs.
Fixes #41
  • Loading branch information
tanmaykm committed Oct 12, 2021
1 parent 5c85a77 commit bb07bcc
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 24 deletions.
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ First, you need to build the Swagger Java libraries. Ensure you have Java and ma
plugin/build.sh
```

A single jar file (julia-swagger-codegen-0.0.3.jar) will be produced in `plugin/target`.
A single jar file (julia-swagger-codegen-0.0.4.jar) will be produced in `plugin/target`.

You can now use that for codegen.

Expand All @@ -32,7 +32,7 @@ Note: problems have been reported while building with JDK 9 on MacOS likely beca

Use the supplied script `plugin/generate.sh` and point it to the specification file and a configuration file. E.g.:

```
```bash
${SWAGGERDIR}/plugin/generate.sh -i ${SPECDIR}/${SPECFILE} -o ${GENDIR} -c config.json
```
_where_
Expand All @@ -51,16 +51,26 @@ The configuration file (`config.json`) can have the following options:

### APIs

Each API set is generated into a file named `api_<apiname>.jl`. It is represented as a `struct` and the APIs under it are generated as methods. An API set can be constructed by providing the
swagger client instance that it can use for communication.
Each API set is generated into a file named `api_<apiname>.jl`. It is represented as a `struct` and the APIs under it are generated as methods. An API set can be constructed by providing the swagger client instance that it can use for communication.

The required API parameters are generated as regular function arguments. Optional parameters are generated as keyword arguments. Method documentation is generated with description, parameter information and return value. Two variants of the API are generated. The first variant is suitable for calling synchronously and returns a single instance of the result struct.

```julia
# example synchronous API that returns an Order instance
getOrderById(api::StoreApi, orderId::Int64)
```

The second variant is suitable for asynchronous calls to methods that return chunked transfer encoded responses, where in the API streams the response objects into an output channel.

The required API parameters are generated as regular function arguments. Optional parameters are generated as keyword arguments. Method
documentation is generated with description, parameter information and return value.
```julia
# example asynchronous API that streams matching Pet instances into response_stream
findPetsByStatus(api::PetApi, response_stream::Channel, status::Vector{String})
```

A client context holds common information to be used across APIs. It also holds a connection to the server and uses that across API calls.
The client context needs to be passed as the first parameter of all API calls. It can be created as:

```
```julia
Client(root::String;
headers::Dict{String,String}=Dict{String,String}(),
get_return_type::Function=(default,data)->default,
Expand Down Expand Up @@ -108,7 +118,7 @@ In addition to these standard Julia methods, these convenience methods are also

E.g:

```
```julia
# access o.field.subfield1.subfield2
if haspropertyat(o, "field", "subfield1", "subfield2")
getpropertyat(o, "field", "subfield1", "subfield2")
Expand Down
2 changes: 1 addition & 1 deletion plugin/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ cd ${DIR}/../plugin
echo "Building Julia plugin..."
mvn package
mvn dependency:resolve dependency:build-classpath -Dmdep.outputFile=classpath.tmp
echo "`cat classpath.tmp`:$DIR/target/julia-swagger-codegen-0.0.3.jar" > classpath
echo "`cat classpath.tmp`:$DIR/target/julia-swagger-codegen-0.0.4.jar" > classpath
rm -f ./classpath.tmp
echo "Build successful"
echo "---------------------------------------"
Expand Down
2 changes: 1 addition & 1 deletion plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<groupId>com.juliacomputing.swagger.codegen</groupId>
<artifactId>julia-swagger-codegen</artifactId>
<packaging>jar</packaging>
<version>0.0.3</version>
<version>0.0.4</version>
<name>julia-swagger-codegen</name>
<url>http://maven.apache.org</url>
<dependencies>
Expand Down
12 changes: 11 additions & 1 deletion plugin/src/main/resources/julia/api.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Param: {{paramName}}::{{dataType}}{{#required}} (required){{/required}}
{{/allParams}}
Return: {{#returnType}}{{returnType}}{{/returnType}}{{^returnType}}Nothing{{/returnType}}
"""
function {{operationId}}(_api::{{classname}}{{#allParams}}{{#required}}, {{paramName}}{{^isBodyParam}}::{{dataType}}{{/isBodyParam}}{{/required}}{{/allParams}};{{#allParams}}{{^required}} {{paramName}}=nothing,{{/required}}{{/allParams}} _mediaType=nothing)
function _swaggerinternal_{{operationId}}(_api::{{classname}}{{#allParams}}{{#required}}, {{paramName}}{{^isBodyParam}}::{{dataType}}{{/isBodyParam}}{{/required}}{{/allParams}};{{#allParams}}{{^required}} {{paramName}}=nothing,{{/required}}{{/allParams}} _mediaType=nothing)
{{#allParams}}
{{#hasValidation}}
{{#maxLength}}
Expand Down Expand Up @@ -57,9 +57,19 @@ function {{operationId}}(_api::{{classname}}{{#allParams}}{{#required}}, {{param
{{/formParams}}
Swagger.set_header_accept(_ctx, [{{#produces}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/produces}}])
Swagger.set_header_content_type(_ctx, (_mediaType === nothing) ? [{{#consumes}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/consumes}}] : [_mediaType])
return _ctx
end

function {{operationId}}(_api::{{classname}}{{#allParams}}{{#required}}, {{paramName}}{{^isBodyParam}}::{{dataType}}{{/isBodyParam}}{{/required}}{{/allParams}};{{#allParams}}{{^required}} {{paramName}}=nothing,{{/required}}{{/allParams}} _mediaType=nothing)
_ctx = _swaggerinternal_{{operationId}}(_api{{#allParams}}{{#required}}, {{paramName}}{{/required}}{{/allParams}};{{#allParams}}{{^required}} {{paramName}}={{paramName}},{{/required}}{{/allParams}} _mediaType=_mediaType)
Swagger.exec(_ctx)
end

function {{operationId}}(_api::{{classname}}, response_stream::Channel{{#allParams}}{{#required}}, {{paramName}}{{^isBodyParam}}::{{dataType}}{{/isBodyParam}}{{/required}}{{/allParams}};{{#allParams}}{{^required}} {{paramName}}=nothing,{{/required}}{{/allParams}} _mediaType=nothing)
_ctx = _swaggerinternal_{{operationId}}(_api{{#allParams}}{{#required}}, {{paramName}}{{/required}}{{/allParams}};{{#allParams}}{{^required}} {{paramName}}={{paramName}},{{/required}}{{/allParams}} _mediaType=_mediaType)
Swagger.exec(_ctx, response_stream)
end

{{/operation}}
export {{#operation}}{{operationId}}{{#hasMore}}, {{/hasMore}}{{/operation}}
{{/operations}}
92 changes: 79 additions & 13 deletions src/client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ struct Client
end
end

set_user_agent(client::Client, ua::String) = set_header("User-Agent", ua)
set_cookie(client::Client, ck::String) = set_header("Cookie", ck)
set_user_agent(client::Client, ua::String) = set_header(client, "User-Agent", ua)
set_cookie(client::Client, ck::String) = set_header(client, "Cookie", ck)
set_header(client::Client, name::String, value::String) = (client.headers[name] = value)

struct Ctx
Expand Down Expand Up @@ -180,13 +180,13 @@ function prep_args(ctx::Ctx)
return body, kwargs
end

response(::Type{Nothing}, resp::HTTP.Response) = nothing::Nothing
response(::Type{T}, resp::HTTP.Response) where {T <: Real} = response(T, resp.body)::T
response(::Type{T}, resp::HTTP.Response) where {T <: String} = response(T, resp.body)::T
function response(::Type{T}, resp::HTTP.Response) where {T}
response(::Type{Nothing}, resp::HTTP.Response, body=resp.body) = nothing::Nothing
response(::Type{T}, resp::HTTP.Response, body=resp.body) where {T <: Real} = response(T, body)::T
response(::Type{T}, resp::HTTP.Response, body=resp.body) where {T <: String} = response(T, body)::T
function response(::Type{T}, resp::HTTP.Response, body=resp.body) where {T}
ctype = HTTP.header(resp, "Content-Type", "application/json")
(length(resp.body) == 0) && return T()
v = response(T, is_json_mime(ctype) ? JSON.parse(String(resp.body)) : resp.body)
(length(body) == 0) && return T()
v = response(T, is_json_mime(ctype) ? JSON.parse(String(body)) : body)
v::T
end
response(::Type{T}, data::Vector{UInt8}) where {T<:Real} = parse(T, String(data))
Expand All @@ -197,22 +197,88 @@ response(::Type{T}, data::Dict{String,Any}) where {T} = from_json(T, data)::T
response(::Type{T}, data::Dict{String,Any}) where {T<:Dict} = convert(T, data)
response(::Type{Vector{T}}, data::Vector{V}) where {T,V} = [response(T, v) for v in data]

function exec(ctx::Ctx)
struct ChunkReader
http
buffered_input::Base.BufferStream
buffer_task::Task

function ChunkReader(http)
buffered_input = Base.BufferStream()
buffer_task = @async try
write(buffered_input, http)
catch ex
if !isa(ex, EOFError)
@error("exception reading http stream", exception=(ex, catch_backtrace()))
rethrow(ex)
end
finally
close(buffered_input)
end
new(http, buffered_input, buffer_task)
end
end

function Base.iterate(iter::ChunkReader, _state=nothing)
if eof(iter.buffered_input)
return nothing
else
out = IOBuffer()
while !eof(iter.buffered_input)
byte = read(iter.buffered_input, UInt8)
(byte == codepoint('\n')) && break
write(out, byte)
end
return (take!(out), iter)
end
end

function do_request(ctx::Ctx, stream::Bool=false; stream_to::Union{Channel,Nothing}=nothing)
resource_path = replace(ctx.resource, "{format}"=>"json")
for (k,v) in ctx.path
resource_path = replace(resource_path, "{$k}"=>v)
end

# TODO: use auth_settings for authentication
body, kwargs = prep_args(ctx)
if body !== nothing
resp = HTTP.request(uppercase(ctx.method), HTTP.URIs.URI(resource_path), ctx.header, body; kwargs...)
if stream
@assert stream_to !== nothing
end

resp = nothing

function process_stream(http)
if body !== nothing
write(http, body)
end
resp = startread(http)
for chunk in ChunkReader(http)
return_type = ctx.client.get_return_type(ctx.return_type, String(copy(chunk)))
data = response(return_type, resp, chunk)
put!(stream_to, data)
end
end

if stream
HTTP.open(process_stream, uppercase(ctx.method), HTTP.URIs.URI(resource_path), ctx.header; kwargs...)
close(stream_to)
else
resp = HTTP.request(uppercase(ctx.method), HTTP.URIs.URI(resource_path), ctx.header; kwargs...)
if body !== nothing
resp = HTTP.request(uppercase(ctx.method), HTTP.URIs.URI(resource_path), ctx.header, body; kwargs...)
else
resp = HTTP.request(uppercase(ctx.method), HTTP.URIs.URI(resource_path), ctx.header; kwargs...)
end
end

return resp
end

function exec(ctx::Ctx, stream_to::Union{Channel,Nothing}=nothing)
stream = stream_to !== nothing
resp = do_request(ctx, stream; stream_to=stream_to)

(200 <= resp.status <= 206) || throw(ApiException(resp))

response(ctx.client.get_return_type(ctx.return_type, resp), resp)
return stream ? resp : response(ctx.client.get_return_type(ctx.return_type, resp), resp)
end

property_type(::Type{T}, name::Symbol) where {T<:SwaggerModel} = error("invalid type $T")
Expand Down
15 changes: 15 additions & 0 deletions test/petstore/test_StoreApi.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@ function test(uri)
@test isa(order, Order)
@test order.id == 10

println(" - getOrderById (async)")
response_channel = Channel{Order}(1)
@test_throws Swagger.ValidationException getOrderById(api, response_channel, 0)
@sync begin
@async begin
resp = getOrderById(api, response_channel, 10)
@test (200 <= resp.status <= 206)
end
@async begin
order = take!(response_channel)
@test isa(order, Order)
@test order.id == 10
end
end

println(" - deleteOrder")
@test deleteOrder(api, 10) == nothing

Expand Down

0 comments on commit bb07bcc

Please sign in to comment.