Skip to content

Commit

Permalink
Support absolute uri in request_json (#127)
Browse files Browse the repository at this point in the history
* Add compat entries for stdlibs

* Support absolute URLs in `request_json`

* Check scheme, host, and path of the request

* Add tests for `_generate_full_url`

* Update keyword arguments and add tests

* Clean Project.toml

* Apply suggestions from code review

Co-authored-by: Dilum Aluthge <[email protected]>

---------

Co-authored-by: Dilum Aluthge <[email protected]>
  • Loading branch information
devmotion and DilumAluthge authored Nov 17, 2023
1 parent aef5379 commit be2ce2c
Show file tree
Hide file tree
Showing 8 changed files with 278 additions and 62 deletions.
8 changes: 4 additions & 4 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,24 @@ Mmap = "a63ad114-7e13-5084-954f-fe012c677804"
SaferIntegers = "88634af6-177f-5301-88b8-7819386cfa38"
StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4"
TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53"
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

[compat]
Base64 = "1"
Dates = "1"
HTTP = "0.8.19, 0.9, 1"
HTTP = "0.9.8, 1"
JSON3 = "1.5.1"
Mmap = "1"
SaferIntegers = "2.5.1, 3"
StructTypes = "1.2.3"
TimeZones = "1.5.2"
URIs = "1.3"
UUIDs = "1"
julia = "1.4"

[extras]
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Dates", "HTTP", "Test"]
test = ["Test"]
1 change: 1 addition & 0 deletions src/FHIRClient.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import JSON3
import SaferIntegers
import StructTypes
import TimeZones
import URIs

include("types.jl")

Expand Down
155 changes: 125 additions & 30 deletions src/requests.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@inline function _request_http(verb::AbstractString,
full_url::HTTP.URI,
full_url::URIs.URI,
headers::AbstractDict,
query::Nothing,
body::Nothing)
Expand All @@ -12,7 +12,7 @@
end

@inline function _request_http(verb::AbstractString,
full_url::HTTP.URI,
full_url::URIs.URI,
headers::AbstractDict,
query::Nothing,
body::AbstractString)
Expand All @@ -26,7 +26,7 @@ end
end

@inline function _request_http(verb::AbstractString,
full_url::HTTP.URI,
full_url::URIs.URI,
headers::AbstractDict,
query::AbstractDict,
body::Nothing)
Expand All @@ -40,7 +40,7 @@ end
end

@inline function _request_http(verb::AbstractString,
full_url::HTTP.URI,
full_url::URIs.URI,
headers::AbstractDict,
query::AbstractDict,
body::AbstractString)
Expand All @@ -62,27 +62,62 @@ end
return HTTP.URI(string(_url_string, "/"))
end

@inline function _generate_full_url(client::Client, path::AbstractString)::HTTP.URI
base_url = get_base_url(client)
base_url_uri_with_trailing_slash = _add_trailing_slash(base_url.uri)
full_url_uri_string = string(base_url_uri_with_trailing_slash, path)
full_url_uri = HTTP.URI(full_url_uri_string)
return full_url_uri
@inline function _generate_full_url(base::HTTP.URI, path::AbstractString)::HTTP.URI
# Add trailing slash to base URL
# This is important since
# URIs.resolvereference("http://foo/bar", "./baz") = URIs.URI("http://foo/baz")
# whereas
# URIs.resolvereference("http://foo/bar/", "./baz") = URIs.URI("http://foo/bar/baz")
# (compliant with RFC 3986 Section 5.2)
base_url = _add_trailing_slash(base)

# Treat all paths without scheme as relative
# Adding the dot is important since
# URIs.resolvereference("http://foo/bar/", "/baz") = URIs.URI("http://foo/baz")
# whereas
# URIs.resolvereference("http://foo/bar/", "./baz") = URIs.URI("http://foo/bar/baz")
# (compliant with RFC 3986 Section 5.2)
_path = startswith(path, "/") ? "." * path : path

return URIs.resolvereference(base_url, _path)
end

"""
request_raw(client, verb, path; query, body, headers)
request_raw(client, verb, path; query, headers)
request_raw(client, verb, path; body, headers)
request_raw(client, verb, path; headers)
request_raw(
client::Client, verb::AbstractString, path::AbstractString;
<keyword arguments>
)
Perform a request with target `path` and method `verb` (such as `"GET"` or `"POST"`)
for the FHIR `client`, and return the body of the response as `String`.
# Arguments
- `body::Union{AbstractString, Nothing} = nothing`: body of the request.
- `headers::AbstractDict = Dict{String, String}()`: headers of the request.
- `query::Union{AbstractDict, Nothing} = nothing`: query parameters.
- `require_base_url::Symbol = :strict`: to what extent the requested URL has to match the base URL of the `client`.
Possible values are `:strict` (requested URL has to start with the base URL),
`:host` (host and scheme of the requested URL and base URL have to be equal),
`:scheme` (scheme of the requested URL and base URL have to be equal),
and `:no` (requested URL does not have to match the base URL).
See also [`request_json`](@ref) and [`request`](@ref).
"""
@inline function request_raw(client::Client,
verb::AbstractString,
path::AbstractString;
body::Union{AbstractString, Nothing} = nothing,
headers::AbstractDict = Dict{String, String}(),
query::Union{AbstractDict, Nothing} = nothing)::String
response = _request_raw_response(client, verb, path; body=body, headers=headers, query=query)
query::Union{AbstractDict, Nothing} = nothing,
require_base_url::Symbol = :strict)::String
response = _request_raw_response(client,
verb,
path;
body = body,
headers = headers,
query = query,
require_base_url = require_base_url)
response_body_string::String = String(response.body)::String
return response_body_string
end
Expand All @@ -92,9 +127,32 @@ end
path::AbstractString;
body::Union{AbstractString, Nothing} = nothing,
headers::AbstractDict = Dict{String, String}(),
query::Union{AbstractDict, Nothing} = nothing)
full_url = _generate_full_url(client,
path)
query::Union{AbstractDict, Nothing} = nothing,
require_base_url::Symbol = :strict)
# Check that `require_base_url` is valid
if require_base_url !== :strict && require_base_url !== :host && require_base_url !== :scheme && require_base_url !== :no
throw(ArgumentError("The provided keyword argument `require_base_url = $(require_base_url)` is invalid: `require_base_url` must be `:strict`, `:host`, `:scheme`, or `:no`."))
end

# Construct and check the validity of the target URL
base_url = get_base_url(client).uri
full_url = _generate_full_url(base_url, path)
if require_base_url !== :no
if lowercase(full_url.scheme) != lowercase(base_url.scheme)
throw(ArgumentError("The scheme of the requested URL ($full_url) and the base URL ($base_url) are not equal: If the requested URL is correct, set `require_base_url = :no`."))
end

if require_base_url !== :scheme
if lowercase(full_url.host) !== lowercase(base_url.host)
throw(ArgumentError("The host of the requested URL ($full_url) and the base URL ($base_url) are not equal: If the requested URL is correct, set `require_base_url = :scheme`."))
end

if require_base_url !== :host && !startswith(full_url.path, base_url.path)
throw(ArgumentError("The requested URL ($full_url) does not start with the base URL ($base_url): If the requested URL is correct, set `require_base_url = :host`."))
end
end
end

_new_headers = Dict{String, String}()
json_headers!(_new_headers)
authentication_headers!(_new_headers, client)
Expand All @@ -118,24 +176,42 @@ end
end

"""
request_json(client, verb, path; query, body, headers)
request_json(client, verb, path; query, headers)
request_json(client, verb, path; body, headers)
request_json(client, verb, path; headers)
request_json(
client::Client, verb::AbstractString, path::AbstractString;
<keyword arguments>
)
Perform a request with target `path` and method `verb` (such as `"GET"` or `"POST"`)
for the FHIR `client`, and parse the JSON response with JSON3.
# Arguments
- `body::Union{JSON3.Object, Nothing} = nothing`: JSON body of the request.
- `headers::AbstractDict = Dict{String, String}()`: headers of the request.
- `query::Union{AbstractDict, Nothing} = nothing`: query parameters.
- `require_base_url::Symbol = :strict`: to what extent the requested URL has to match the base URL of the `client`.
Possible values are `:strict` (requested URL has to start with the base URL),
`:host` (host and scheme of the requested URL and base URL have to be equal),
`:scheme` (scheme of the requested URL and base URL have to be equal),
and `:no` (requested URL does not have to match the base URL).
See also [`request`](@ref) and [`request_raw`](@ref).
"""
@inline function request_json(client::Client,
verb::AbstractString,
path::AbstractString;
body::Union{JSON3.Object, Nothing} = nothing,
headers::AbstractDict = Dict{String, String}(),
query::Union{AbstractDict, Nothing} = nothing)
query::Union{AbstractDict, Nothing} = nothing,
require_base_url::Symbol = :strict)
_new_request_body = _write_json_request_body(body)
response_body::String = request_raw(client,
verb,
path;
body = _new_request_body,
headers = headers,
query = query)::String
query = query,
require_base_url = require_base_url)::String
response_json = JSON3.read(response_body)
return response_json
end
Expand All @@ -150,10 +226,27 @@ end
end

"""
request(T, client, verb, path; query, body, headers, kwargs...)
request(T, client, verb, path; query, headers, kwargs...)
request(T, client, verb, path; body, headers, kwargs...)
request(T, client, verb, path; headers, kwargs...)
request(
T, client::Client, verb::AbstractString, path::AbstractString;
<keyword arguments>
)
Perform a request with target `path` and method `verb` (such as `"GET"` or `"POST"`)
for the FHIR `client`, and parse the JSON response with JSON3 as an object of type `T`.
# Arguments
- `body = nothing`: JSON body of the request.
- `headers::AbstractDict = Dict{String, String}()`: headers of the request.
- `query::Union{AbstractDict, Nothing} = nothing`: query parameters.
- `require_base_url::Symbol = :strict`: to what extent the requested URL has to match the base URL of the `client`.
Possible values are `:strict` (requested URL has to start with the base URL),
`:host` (host and scheme of the requested URL and base URL have to be equal),
`:scheme` (scheme of the requested URL and base URL have to be equal),
and `:no` (requested URL does not have to match the base URL).
- `kwargs...`: remaining keyword arguments that are forwarded to `JSON3.read` for parsing the JSON response.
See also [`request_json`](@ref) and [`request_raw`](@ref).
"""
@inline function request(::Type{T},
client::Client,
Expand All @@ -162,14 +255,16 @@ end
body = nothing,
headers::AbstractDict = Dict{String, String}(),
query::Union{AbstractDict, Nothing} = nothing,
require_base_url::Symbol = :strict,
kwargs...)::T where T
_new_request_body = _write_struct_request_body(body)
response_body::String = request_raw(client,
verb,
path;
body = _new_request_body,
headers = headers,
query = query)::String
query = query,
require_base_url = require_base_url)::String
response_object::T = JSON3.read(response_body,
T;
kwargs...)::T
Expand Down
8 changes: 4 additions & 4 deletions src/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ The base URL is also called the "Service Root URL"
struct BaseURL <: Any
## Fields
- uri :: HTTP.URIs.URI
- uri :: URIs.URI
"""
struct BaseURL
uri::HTTP.URI
uri::URIs.URI

"""
BaseURL(base_url::Union{URIs.URI, AbstractString})
Expand All @@ -38,8 +38,8 @@ struct BaseURL
The base URL is also called the "Service Root URL".
"""
function BaseURL(uri::Union{HTTP.URI, AbstractString}; require_https::Bool = true)
_uri = uri isa HTTP.URI ? uri : HTTP.URI(uri)
function BaseURL(uri::Union{URIs.URI, AbstractString}; require_https::Bool = true)
_uri = uri isa URIs.URI ? uri : URIs.URI(uri)
if lowercase(_uri.scheme) != "https"
msg = "The following FHIR Base URL does not use HTTPS: $(uri)"
if require_https
Expand Down
Loading

0 comments on commit be2ce2c

Please sign in to comment.