|
| 1 | +abstract type AbstractChunkReader end |
| 2 | +abstract type AbstractHTTPLibError end |
| 3 | +const HTTPLibResponse = Union{HTTP.Response, Downloads.Response} |
| 4 | +const HTTPLibError = Union{Downloads.RequestError, AbstractHTTPLibError} |
| 5 | + |
| 6 | +# methods to get exception messages out of errors which could be surfaced either as request or response errors |
| 7 | +get_message(::HTTPLibError) = "" |
| 8 | +get_message(::HTTPLibResponse) = "" |
| 9 | +get_response(::HTTPLibError) = nothing |
| 10 | +get_status(::HTTPLibError) = 0 |
| 11 | + |
| 12 | +# collection formats (OpenAPI v2) |
| 13 | +# TODO: OpenAPI v3 has style and explode options instead of collection formats, which are yet to be supported |
| 14 | +# TODO: Examine whether multi is now supported |
| 15 | +const COLL_MULTI = "multi" # (legacy) aliased to CSV, as multi is not supported by Requests.jl (https://github.com/JuliaWeb/Requests.jl/issues/140) |
| 16 | +const COLL_PIPES = "pipes" |
| 17 | +const COLL_SSV = "ssv" |
| 18 | +const COLL_TSV = "tsv" |
| 19 | +const COLL_CSV = "csv" |
| 20 | +const COLL_DLM = Dict{String,String}([COLL_PIPES=>"|", COLL_SSV=>" ", COLL_TSV=>"\t", COLL_CSV=>",", COLL_MULTI=>","]) |
| 21 | + |
| 22 | +const DEFAULT_TIMEOUT_SECS = 5*60 |
| 23 | +const DEFAULT_LONGPOLL_TIMEOUT_SECS = 15*60 |
| 24 | + |
| 25 | +const HTTPLib = ( |
| 26 | + HTTP = :http, |
| 27 | + Downloads = :downloads |
| 28 | +) |
| 29 | + |
| 30 | +struct ApiException <: Exception |
| 31 | + status::Int |
| 32 | + reason::String |
| 33 | + resp::Union{Nothing, HTTPLibResponse} |
| 34 | + error::Union{Nothing, HTTPLibError} |
| 35 | + |
| 36 | + function ApiException(error::HTTPLibError; reason::String="") |
| 37 | + isempty(reason) && (reason = get_message(error)) |
| 38 | + resp = get_response(error) |
| 39 | + status = get_status(error) |
| 40 | + new(status, reason, resp, error) |
| 41 | + end |
| 42 | +end |
| 43 | + |
| 44 | +""" |
| 45 | + ApiResponse |
| 46 | +
|
| 47 | +Represents the HTTP API response from the server. This is returned as the second return value from all API calls. |
| 48 | +
|
| 49 | +Properties available: |
| 50 | +- `status`: the HTTP status code |
| 51 | +- `message`: the HTTP status message |
| 52 | +- `headers`: the HTTP headers |
| 53 | +- `raw`: the raw response from the HTTP library used |
| 54 | +""" |
| 55 | +struct ApiResponse |
| 56 | + raw::HTTPLibResponse |
| 57 | +end |
| 58 | + |
| 59 | +get_response_property(raw::HTTPLibResponse, name::Symbol) = getproperty(raw, name) |
| 60 | +function Base.getproperty(resp::ApiResponse, name::Symbol) |
| 61 | + raw = getfield(resp, :raw) |
| 62 | + if name in (:status, :message, :headers) |
| 63 | + return get_response_property(raw, name) |
| 64 | + else |
| 65 | + return getfield(resp, name) |
| 66 | + end |
| 67 | +end |
| 68 | + |
| 69 | + |
| 70 | +function get_api_return_type(return_types::Dict{Regex,Type}, ::Nothing, response_data::String) |
| 71 | + # this is the async case, where we do not have the response code yet |
| 72 | + # in such cases we look for the 200 response code |
| 73 | + return get_api_return_type(return_types, 200, response_data) |
| 74 | +end |
| 75 | +function get_api_return_type(return_types::Dict{Regex,Type}, response_code::Integer, response_data::String) |
| 76 | + default_response_code = 0 |
| 77 | + for code in string.([response_code, default_response_code]) |
| 78 | + for (re, rt) in return_types |
| 79 | + if match(re, code) !== nothing |
| 80 | + return rt |
| 81 | + end |
| 82 | + end |
| 83 | + end |
| 84 | + # if no specific return type was defined, we assume that: |
| 85 | + # - if response code is 2xx, then we make the method call return nothing |
| 86 | + # - otherwise we make it throw an ApiException |
| 87 | + return (200 <= response_code <=206) ? Nothing : nothing # first(return_types)[2] |
| 88 | +end |
| 89 | + |
| 90 | +function default_debug_hook(type, message) |
| 91 | + @info("OpenAPI HTTP transport", type, message) |
| 92 | +end |
| 93 | + |
| 94 | +""" |
| 95 | + Client(root::String; |
| 96 | + headers::Dict{String,String}=Dict{String,String}(), |
| 97 | + get_return_type::Function=get_api_return_type, |
| 98 | + long_polling_timeout::Int=DEFAULT_LONGPOLL_TIMEOUT_SECS, |
| 99 | + timeout::Int=DEFAULT_TIMEOUT_SECS, |
| 100 | + pre_request_hook::Function=noop_pre_request_hook, |
| 101 | + escape_path_params::Union{Nothing,Bool}=nothing, |
| 102 | + chunk_reader_type::Union{Nothing,Type{<:AbstractChunkReader}}=nothing, |
| 103 | + verbose::Union{Bool,Function}=false, |
| 104 | + httplib::Symbol=HTTPLib.Downloads, |
| 105 | + ) |
| 106 | +
|
| 107 | +Create a new OpenAPI client context. |
| 108 | +
|
| 109 | +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. |
| 110 | +The client context needs to be passed as the first parameter of all API calls. |
| 111 | +
|
| 112 | +Parameters: |
| 113 | +- `root`: The root URL of the server. This is the base URL that will be used for all API calls. |
| 114 | +
|
| 115 | +Keyword parameters: |
| 116 | +- `headers`: A dictionary of HTTP headers to be sent with all API calls. |
| 117 | +- `get_return_type`: A function that is called to determine the return type of an API call. This function is called with the following parameters: |
| 118 | + - `return_types`: A dictionary of regular expressions and their corresponding return types. The regular expressions are matched against the HTTP status code of the response. |
| 119 | + - `response_code`: The HTTP status code of the response. |
| 120 | + - `response_data`: The response data as a string. |
| 121 | + The function should return the return type to be used for the API call. |
| 122 | +- `long_polling_timeout`: The timeout in seconds for long polling requests. This is the time after which the request will be aborted if no data is received from the server. |
| 123 | +- `timeout`: The timeout in seconds for all other requests. This is the time after which the request will be aborted if no data is received from the server. |
| 124 | +- `pre_request_hook`: A function that is called before every API call. This function must provide two methods: |
| 125 | + - `pre_request_hook(ctx::Ctx)`: This method is called before every API call. It is passed the context object that will be used for the API call. The function should return the context object to be used for the API call. |
| 126 | + - `pre_request_hook(resource_path::AbstractString, body::Any, headers::Dict{String,String})`: This method is called before every API call. It is passed the resource path, request body and request headers that will be used for the API call. The function should return those after making any modifications to them. |
| 127 | +- `escape_path_params`: Whether the path parameters should be escaped before being used in the URL. This is useful if the path parameters contain characters that are not allowed in URLs or contain path separators themselves. |
| 128 | +- `chunk_reader_type`: The type of chunk reader to be used for streaming responses. This can be one of `LineChunkReader`, `JSONChunkReader` or `RFC7464ChunkReader`. If not specified, then the type is automatically determined based on the return type of the API call. |
| 129 | +- `verbose`: Can be set either to a boolean or a function (function support depends on the HTTP library). |
| 130 | + - If set to true, then the client will log all HTTP requests and responses. |
| 131 | + - If set to a function (only supported with Downloads.jl backend), then that function will be called with the following parameters: |
| 132 | + - `type`: The type of message. |
| 133 | + - `message`: The message to be logged. |
| 134 | + - Note: When using HTTP.jl backend (`httplib=OpenAPI.HTTPLib.HTTP`), the `verbose` parameter must be a boolean. |
| 135 | +- `httplib`: The HTTP client library to use for making requests. Can be `OpenAPI.HTTPLib.Downloads` (default) for Downloads.jl or `OpenAPI.HTTPLib.HTTP` for HTTP.jl. |
| 136 | +
|
| 137 | +""" |
| 138 | +struct Client |
| 139 | + root::String |
| 140 | + headers::Dict{String,String} |
| 141 | + get_return_type::Function # user provided hook to get return type from response data |
| 142 | + clntoptions::Dict{Symbol,Any} |
| 143 | + downloader::Union{Nothing,Downloader} |
| 144 | + timeout::Ref{Int} |
| 145 | + pre_request_hook::Function # user provided hook to modify the request before it is sent |
| 146 | + escape_path_params::Union{Nothing,Bool} |
| 147 | + chunk_reader_type::Union{Nothing,Type{<:AbstractChunkReader}} |
| 148 | + long_polling_timeout::Int |
| 149 | + request_interrupt_supported::Bool |
| 150 | + httplib::Symbol # which http implementation to use |
| 151 | + |
| 152 | + function Client(root::String; |
| 153 | + headers::Dict{String,String}=Dict{String,String}(), |
| 154 | + get_return_type::Function=get_api_return_type, |
| 155 | + long_polling_timeout::Int=DEFAULT_LONGPOLL_TIMEOUT_SECS, |
| 156 | + timeout::Int=DEFAULT_TIMEOUT_SECS, |
| 157 | + pre_request_hook::Function=noop_pre_request_hook, |
| 158 | + escape_path_params::Union{Nothing,Bool}=nothing, |
| 159 | + chunk_reader_type::Union{Nothing,Type{<:AbstractChunkReader}}=nothing, |
| 160 | + verbose::Union{Bool,Function}=false, |
| 161 | + httplib::Symbol=:http, |
| 162 | + ) |
| 163 | + # Validate library choice |
| 164 | + if httplib ∉ values(HTTPLib) |
| 165 | + throw(ArgumentError("Invalid httplib: $httplib")) |
| 166 | + end |
| 167 | + |
| 168 | + clntoptions = Dict{Symbol,Any}(:throw=>false) |
| 169 | + if isa(verbose, Bool) |
| 170 | + clntoptions[:verbose] = verbose |
| 171 | + elseif isa(verbose, Function) |
| 172 | + if httplib === HTTPLib.HTTP |
| 173 | + throw(ArgumentError("With HTTP.jl, `verbose` can only be a boolean")) |
| 174 | + end |
| 175 | + clntoptions[:debug] = verbose |
| 176 | + end |
| 177 | + |
| 178 | + if httplib === HTTPLib.HTTP |
| 179 | + downloader = nothing |
| 180 | + interruptable = false |
| 181 | + else |
| 182 | + downloader = Downloads.Downloader() |
| 183 | + downloader.easy_hook = (easy, opts) -> begin |
| 184 | + Downloads.Curl.setopt(easy, LibCURL.CURLOPT_LOW_SPEED_TIME, long_polling_timeout) |
| 185 | + # disable ALPN to support servers that enable both HTTP/2 and HTTP/1.1 on same port |
| 186 | + Downloads.Curl.setopt(easy, LibCURL.CURLOPT_SSL_ENABLE_ALPN, 0) |
| 187 | + end |
| 188 | + |
| 189 | + interruptable = request_supports_interrupt() |
| 190 | + end |
| 191 | + new(root, headers, get_return_type, clntoptions, downloader, Ref{Int}(timeout), pre_request_hook, escape_path_params, chunk_reader_type, long_polling_timeout, interruptable, httplib) |
| 192 | + end |
| 193 | +end |
| 194 | + |
| 195 | +struct Ctx |
| 196 | + client::Client |
| 197 | + method::String |
| 198 | + return_types::Dict{Regex,Type} |
| 199 | + resource::String |
| 200 | + auth::Vector{String} |
| 201 | + |
| 202 | + path::Dict{String,String} |
| 203 | + query::Dict{String,String} |
| 204 | + header::Dict{String,String} |
| 205 | + form::Dict{String,String} |
| 206 | + file::Dict{String,String} |
| 207 | + body::Any |
| 208 | + timeout::Int |
| 209 | + curl_mime_upload::Ref{Any} |
| 210 | + pre_request_hook::Function |
| 211 | + escape_path_params::Bool |
| 212 | + chunk_reader_type::Union{Nothing,Type{<:AbstractChunkReader}} |
| 213 | + |
| 214 | + function Ctx(client::Client, method::String, return_types::Dict{Regex,Type}, resource::String, auth, body=nothing; |
| 215 | + timeout::Int=client.timeout[], |
| 216 | + pre_request_hook::Function=client.pre_request_hook, |
| 217 | + escape_path_params::Bool=something(client.escape_path_params, true), |
| 218 | + chunk_reader_type::Union{Nothing,Type{<:AbstractChunkReader}}=client.chunk_reader_type, |
| 219 | + ) |
| 220 | + resource = client.root * resource |
| 221 | + headers = copy(client.headers) |
| 222 | + new(client, method, return_types, resource, auth, Dict{String,String}(), Dict{String,String}(), headers, Dict{String,String}(), Dict{String,String}(), body, timeout, Ref{Any}(nothing), pre_request_hook, escape_path_params, chunk_reader_type) |
| 223 | + end |
| 224 | +end |
0 commit comments