Skip to content

Commit

Permalink
Refactor the requests API (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
DilumAluthge authored Aug 3, 2020
1 parent 4595cdf commit 9b9f259
Show file tree
Hide file tree
Showing 14 changed files with 311 additions and 68 deletions.
4 changes: 3 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ julia = "1.3"
[extras]
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
PrettyPrint = "8162dcfd-2161-5ef2-ae6c-7681170c5f98"
StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["DataFrames", "Documenter", "PrettyPrint", "StructArrays", "Tables", "Test"]
test = ["DataFrames", "Documenter", "HTTP", "JSON3", "PrettyPrint", "StructArrays", "Tables", "Test"]
14 changes: 7 additions & 7 deletions docs/src/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@ CurrentModule = FHIRClient
```julia
julia> using FHIRClient

julia> using DataFrames, StructArrays, Tables
julia> using DataFrames, PrettyPrint, StructArrays, Tables

julia> endpoint = FHIRClient.Endpoint("https://hapi.fhir.org/baseR4")
FHIRClient.Endpoint(HTTP.URI("https://hapi.fhir.org/baseR4"))
julia> base_url = FHIRClient.BaseURL("https://hapi.fhir.org/baseR4")
FHIRClient.BaseURL(HTTP.URI("https://hapi.fhir.org/baseR4"))

julia> fhir_version = FHIRClient.R4()
FHIRClient.R4()

julia> auth = FHIRClient.AnonymousAuth()
FHIRClient.AnonymousAuth()

julia> client = FHIRClient.Client(fhir_version, endpoint, auth)
FHIRClient.Client{FHIRClient.R4,FHIRClient.AnonymousAuth}(FHIRClient.R4(), FHIRClient.Endpoint(HTTP.URI("https://hapi.fhir.org/baseR4")), FHIRClient.AnonymousAuth())
julia> client = FHIRClient.Client(fhir_version, base_url, auth)
FHIRClient.Client{FHIRClient.R4,FHIRClient.AnonymousAuth}(FHIRClient.R4(), FHIRClient.BaseURL(HTTP.URI("https://hapi.fhir.org/baseR4")), FHIRClient.AnonymousAuth())

julia> patient1 = FHIRClient.fhir_get_struct(client, "/Patient/22692", FHIRClient.FHIRPatient)
julia> patient1 = FHIRClient.request(FHIRClient.FHIRPatient, client, "GET", "/Patient/22692")
FHIRClient.FHIRPatient("Patient", "22692", FHIRClient.FHIRMeta("2", "2019-12-13T19:29:48.520+00:00", "#iJwIftqIuKvSfrIv"), FHIRClient.FHIRText("generated", "<div xmlns=\"http://www.w3.org/1999/xhtml\"><div class=\"hapiHeaderText\">Jason <b>ARGONAUT </b></div><table class=\"hapiPropertyTable\"><tbody><tr><td>Identifier</td><td>E3826</td></tr><tr><td>Address</td><td><span>1979 Milky Way Dr. </span><br/><span>Verona </span><span>WI </span><span>US </span></td></tr><tr><td>Date of birth</td><td><span>01 August 1985</span></td></tr></tbody></table></div>"), FHIRClient.FHIRExtension[FHIRClient.FHIRExtension("http://hl7.org/fhir/StructureDefinition/us-core-race", FHIRClient.FHIRCodeableConcept(FHIRClient.FHIRCoding[FHIRClient.FHIRCoding("2.16.840.1.113883.5.104", "2028-9", "Asian")], "Asian")), FHIRClient.FHIRExtension("http://hl7.org/fhir/StructureDefinition/us-core-ethnicity", FHIRClient.FHIRCodeableConcept(FHIRClient.FHIRCoding[FHIRClient.FHIRCoding("2.16.840.1.113883.5.50", "2186-5", "Not Hispanic or Latino")], "Not Hispanic or Latino")), FHIRClient.FHIRExtension("http://hl7.org/fhir/StructureDefinition/us-core-birth-sex", FHIRClient.FHIRCodeableConcept(FHIRClient.FHIRCoding[FHIRClient.FHIRCoding("http://hl7.org/fhir/v3/AdministrativeGender", "M", "Male")], "Male"))], FHIRClient.FHIRIdentifier[FHIRClient.FHIRIdentifier("usual", "urn:oid:1.2.840.114350.1.13.327.1.7.5.737384.0", "E3826"), FHIRClient.FHIRIdentifier("usual", "urn:oid:1.2.3.4", "203579")], true, FHIRClient.FHIRName[FHIRClient.FHIRName("usual", "Jason Argonaut", "Argonaut", ["Jason"])], FHIRClient.FHIRCodeableConcept[FHIRClient.FHIRCodeableConcept(missing, missing), FHIRClient.FHIRCodeableConcept(missing, missing), FHIRClient.FHIRCodeableConcept(missing, missing), FHIRClient.FHIRCodeableConcept(missing, missing), FHIRClient.FHIRCodeableConcept(missing, missing), FHIRClient.FHIRCodeableConcept(missing, missing)], "male", "1985-08-01", false, FHIRClient.FHIRAddress[FHIRClient.FHIRAddress("home", ["1979 Milky Way Dr."], "Verona", "WI", "53593", "US"), FHIRClient.FHIRAddress("temp", ["5301 Tokay Blvd"], "MADISON", "WI", "53711", "US")], FHIRClient.FHIRCodeableConcept(FHIRClient.FHIRCoding[FHIRClient.FHIRCoding("http://hl7.org/fhir/ValueSet/marital-status", "S", "Never Married")], "Single"), FHIRClient.FHIRCommunication[FHIRClient.FHIRCommunication(missing, true)], FHIRClient.FHIRGeneralPractitioner[FHIRClient.FHIRGeneralPractitioner("Practitioner/338866", "Physician Family Medicine")])

julia> typeof(patient1)
Expand All @@ -34,7 +34,7 @@ julia> patient1.name
julia> patient1.birthDate
"1985-08-01"

julia> patient2 = FHIRClient.fhir_get_struct(client, "/Patient/1227306", FHIRClient.FHIRPatient)
julia> patient2 = FHIRClient.request(FHIRClient.FHIRPatient, client, "GET", "/Patient/1227306")
FHIRClient.FHIRPatient("Patient", "1227306", FHIRClient.FHIRMeta("1", "2020-06-19T13:55:09.498+00:00", "#RmgibCwFUwNrHlw1"), FHIRClient.FHIRText("generated", "<div xmlns=\"http://www.w3.org/1999/xhtml\"><div class=\"hapiHeaderText\">Jason <b>GLOVER </b></div><table class=\"hapiPropertyTable\"><tbody><tr><td>Identifier</td><td>CT10681</td></tr></tbody></table></div>"), missing, FHIRClient.FHIRIdentifier[FHIRClient.FHIRIdentifier(missing, missing, "CT10681"), FHIRClient.FHIRIdentifier(missing, missing, "9800005998")], missing, FHIRClient.FHIRName[FHIRClient.FHIRName(missing, missing, "Glover", ["Jason"])], missing, "male", missing, missing, missing, missing, missing, missing)

julia> typeof(patient2)
Expand Down
6 changes: 3 additions & 3 deletions src/FHIRClient.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ import StructTypes

export FHIRType
export FHIRVersion
export fhir_get_struct

include("types.jl")
include("version.jl")

include("authentication.jl")
include("authentication-headers.jl")
include("client.jl")
include("endpoint.jl")
include("base-url.jl")
include("headers.jl")
include("json-struct-mapping/json-struct-mapping.jl")
include("omop-common-data-model/omop-common-data-model.jl")
include("requests.jl")
include("utils.jl")
include("uri.jl")
include("version-utils.jl")

end # end module FHIRClient
6 changes: 6 additions & 0 deletions src/base-url.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
Construct a `BaseURL` object given the base URL.
The base URL is also called the "Service Root URL"
"""
BaseURL(base_url::AbstractString) = BaseURL(HTTP.URI(base_url))
2 changes: 1 addition & 1 deletion src/client.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
get_auth(client::Client) = client.auth
get_endpoint(client::Client) = client.endpoint
get_base_url(client::Client) = client.base_url
get_fhir_version(client::Client) = client.fhir_version
6 changes: 0 additions & 6 deletions src/endpoint.jl

This file was deleted.

277 changes: 238 additions & 39 deletions src/requests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,242 @@ import DocStringExtensions
import HTTP
import JSON3

function _fhir_get_raw(client::Client,
query::AbstractString;
headers::AbstractDict = Dict{String, String}())::String
endpoint = get_endpoint(client)
base_url = get_base_url(endpoint)
full_url = string(base_url, query)
new_headers = Dict{String, String}()
json_headers!(new_headers)
authentication_headers!(new_headers, client)
merge!(new_headers, headers)
r = HTTP.request("GET", full_url, new_headers)
empty!(new_headers)
body = String(r.body)
return body
end

# `JSON3.read(body)`` is slower than `JSON3.read(body, T)`.
# Therefore, you should always use `fhir_get_struct` instead of `_fhir_get_json`.
# `_fhir_get_json` is really only useful for debugging.
function _fhir_get_json(client::Client,
query::AbstractString;
headers::AbstractDict = Dict{String, String}())
body::String = _fhir_get_raw(client, query; headers = headers)::String
json = JSON3.read(body)
return json
end

"""
$(DocStringExtensions.FUNCTIONNAME)(client::FHIRClient.Client, query::String, T)
Make a query and store the result in a struct of type `T`.
"""
function fhir_get_struct(client::Client,
query::AbstractString,
::Type{T};
headers::AbstractDict = Dict{String, String}())::T where T
body::String = _fhir_get_raw(client, query; headers = headers)::String
object::T = JSON3.read(body, T)::T
return object
function _assert_is_valid_http_verb(verb::AbstractString)::Nothing
if !( _is_valid_http_verb(verb) )
throw(ArgumentError("Not a valid HTTP verb: \"$(verb)\""))
end
return nothing
end

function _is_valid_http_verb(verb::AbstractString)::Bool
if verb == "DELETE"
return true
elseif verb == "GET"
return true
elseif verb == "HEAD"
return true
elseif verb == "OPTIONS"
return true
elseif verb == "PATCH"
return true
elseif verb == "POST"
return true
elseif verb == "PUT"
return true
elseif verb == "TRACE"
return true
end
return false
end

function _request_http(verb::AbstractString,
full_url::HTTP.URI,
headers::AbstractDict,
query::Nothing,
body::Nothing)
response = HTTP.request(
verb,
full_url,
headers,
)
return response
end

function _request_http(verb::AbstractString,
full_url::HTTP.URI,
headers::AbstractDict,
query::Nothing,
body::AbstractString)
response = HTTP.request(
verb,
full_url,
headers,
body,
)
return response
end

function _request_http(verb::AbstractString,
full_url::HTTP.URI,
headers::AbstractDict,
query::AbstractDict,
body::Nothing)
response = HTTP.request(
verb,
full_url,
headers;
query = query,
)
return response
end

function _request_http(verb::AbstractString,
full_url::HTTP.URI,
headers::AbstractDict,
query::AbstractDict,
body::AbstractString)
response = HTTP.request(
verb,
full_url,
headers,
body;
query = query,
)
return response
end

function _add_trailing_slash(url::HTTP.URI)::HTTP.URI
_url_string = _get_http_uri_string(url)
if endswith(_url_string, "/")
return url
end
return HTTP.URI(string(_url_string, "/"))
end

function _generate_full_url(client::Client,
path::AbstractString)::HTTP.URI
base_url = get_base_url(client)
result = _generate_full_url(base_url, path)
return result
end

function _generate_full_url(base_url::BaseURL,
path::AbstractString)::HTTP.URI
base_url_uri = _get_http_uri(base_url)
result = _generate_full_url(base_url_uri, path)
return result
end

function _generate_full_url(base_url_uri::HTTP.URI,
path::AbstractString)::HTTP.URI
base_url_uri_with_trailing_slash = _add_trailing_slash(base_url_uri)
base_url_uri_string = _get_http_uri_string(base_url_uri_with_trailing_slash)
full_url_uri_string = string(base_url_uri_string, path)
full_url_uri = HTTP.URI(full_url_uri_string)
return full_url_uri
end

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
_assert_is_valid_http_verb(verb)
full_url = _generate_full_url(client,
path)
_new_headers = Dict{String, String}()
json_headers!(_new_headers)
authentication_headers!(_new_headers, client)
merge!(_new_headers, headers)
response = _request_http(verb,
full_url,
_new_headers,
query,
body)
empty!(_new_headers)
response_body_string::String = String(response.body)::String
return response_body_string
end

function _write_json_request_body(body::Nothing)::Nothing
return nothing
end

function _write_json_request_body(body::JSON3.Object)::String
body_string::String = JSON3.write(body)::String
return body_string
end

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)
_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
response_json = JSON3.read(response_body)
return response_json
end

function _write_struct_request_body(body::Nothing)::Nothing
return nothing
end

function _write_struct_request_body(body)::String
body_string::String = JSON3.write(body)::String
return body_string
end

function _request(::Type{T},
client::Client,
verb::AbstractString,
path::AbstractString;
body = nothing,
headers::AbstractDict = Dict{String, String}(),
query::Union{AbstractDict, Nothing} = nothing,
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
response_object::T = JSON3.read(response_body,
T;
kwargs...)::T
return response_object
end

function _depagination_placeholder(x)
return x
end

function _depagination_placeholder(::Type{T}, x) where T
return x
end

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,
kwargs...)
response_json = _request_json(client,
verb,
path;
body = body,
headers = headers,
query = query,
kwargs...)
depaginated_response_json = _depagination_placeholder(response_json)
return depaginated_response_json
end

function request(::Type{T},
client::Client,
verb::AbstractString,
path::AbstractString;
body = nothing,
headers::AbstractDict = Dict{String, String}(),
query::Union{AbstractDict, Nothing} = nothing,
kwargs...)::T where T
response_object::T = _request(T,
client,
verb,
path;
body = body,
headers = headers,
query = query,
kwargs...)::T
depaginated_response_object::T = _depagination_placeholder(T, response_object)::T
return depaginated_response_object
end
10 changes: 6 additions & 4 deletions src/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,20 @@ A struct corresponding to a version of the FHIR specification.
abstract type FHIRVersion end

"""
A FHIR endpoint.
The base URL for a FHIR server.
The base URL is also called the "Service Root URL"
"""
struct Endpoint
base_url::HTTP.URI
struct BaseURL
uri::HTTP.URI
end

"""
A FHIR client.
"""
struct Client{V <: FHIRVersion, A <: Authentication} <: HealthBase.AbstractFHIRClient
fhir_version::V
endpoint::Endpoint
base_url::BaseURL
auth::A
end

Expand Down
Loading

2 comments on commit 9b9f259

@DilumAluthge
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register branch=master

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request updated: JuliaRegistries/General/18754

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.1.0 -m "<description of version>" 9b9f25991acf10948eccfba6cc40ae759f4e7168
git push origin v0.1.0

Please sign in to comment.