Skip to content

Commit 092463f

Browse files
authored
Merge pull request #108 from fhir-crucible/headers_update
Update Headers
2 parents 772e090 + 535a5da commit 092463f

File tree

16 files changed

+357
-119
lines changed

16 files changed

+357
-119
lines changed

lib/fhir_client.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
require_relative 'fhir_client/client'
1414
require_relative 'fhir_client/resource_address'
1515
require_relative 'fhir_client/resource_format'
16+
require_relative 'fhir_client/return_preferences'
1617
require_relative 'fhir_client/patch_format'
1718
require_relative 'fhir_client/client_exception'
1819
require_relative 'fhir_client/version'

lib/fhir_client/client.rb

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ class Client
2626
attr_accessor :proxy
2727
attr_accessor :exception_class
2828

29+
attr_accessor :use_accept_header
30+
attr_accessor :use_accept_charset
31+
attr_accessor :use_return_preference
32+
2933
# Call method to initialize FHIR client. This method must be invoked
3034
# with a valid base server URL prior to using the client.
3135
#
@@ -37,10 +41,15 @@ def initialize(base_service_url, default_format: FHIR::Formats::ResourceFormat::
3741
@base_service_url = base_service_url
3842
FHIR.logger.info "Initializing client with #{@base_service_url}"
3943
@use_format_param = false
40-
@exception_class = ClientException
44+
@use_accept_header = true
45+
@use_accept_charset = true
4146
@default_format = default_format
4247
@fhir_version = :stu3
48+
@use_return_preference = false
49+
@return_preference = FHIR::Formats::ReturnPreferences::REPRESENTATION
50+
@exception_class = ClientException
4351
@proxy = proxy
52+
4453
set_no_auth
4554
end
4655

@@ -78,6 +87,20 @@ def use_dstu2
7887
end
7988
end
8089

90+
#
91+
# Instructs the client to specify the minimal Prefer Header where applicable
92+
def use_minimal_preference
93+
@use_return_preference = true
94+
@return_preference = FHIR::Formats::ReturnPreferences::MINIMAL
95+
end
96+
97+
#
98+
# Instructs the client to specify the representation Prefer Header where applicable
99+
def use_representation_preference
100+
@use_return_preference = true
101+
@return_preference = FHIR::Formats::ReturnPreferences::REPRESENTATION
102+
end
103+
81104
def versioned_resource_class(klass)
82105
if @fhir_version == :stu3
83106
FHIR.const_get(klass)
@@ -259,7 +282,7 @@ def try_conformance_formats(default_format)
259282
@default_format = nil
260283

261284
formats.each do |frmt|
262-
reply = get 'metadata', fhir_headers(format: frmt)
285+
reply = get 'metadata', fhir_headers({accept: "#{frmt}"})
263286
next unless reply.code == 200
264287
begin
265288
@cached_capability_statement = parse_reply(FHIR::CapabilityStatement, frmt, reply)
@@ -283,17 +306,15 @@ def try_conformance_formats(default_format)
283306
end
284307

285308
def resource_url(options)
286-
FHIR::ResourceAddress.new.resource_url(options, @use_format_param)
309+
FHIR::ResourceAddress.resource_url(options, @use_format_param)
287310
end
288311

289312
def full_resource_url(options)
290313
@base_service_url + resource_url(options)
291314
end
292315

293316
def fhir_headers(options = {})
294-
options.merge!(additional_headers) unless additional_headers.nil?
295-
296-
FHIR::ResourceAddress.new.fhir_headers(options, @use_format_param)
317+
FHIR::ResourceAddress.fhir_headers(options, additional_headers, @default_format, @use_accept_header, @use_accept_charset)
297318
end
298319

299320
def parse_reply(klass, format, response)
@@ -359,8 +380,10 @@ def base_path(path)
359380
# Extract the request payload in the specified format, defaults to XML
360381
def request_payload(resource, headers)
361382
if headers
362-
format_specified = headers[:format] || headers['format']
363-
if format_specified.downcase.include?('xml')
383+
format_specified = headers['Content-Type']
384+
if format_specified.nil?
385+
resource.to_xml
386+
elsif format_specified.downcase.include?('xml')
364387
resource.to_xml
365388
elsif format_specified.downcase.include?('json')
366389
resource.to_json
@@ -394,7 +417,7 @@ def request_patch_payload(patchset, format)
394417

395418
def clean_headers(headers)
396419
headers.delete_if { |k, v| (k.nil? || v.nil?) }
397-
headers.each_with_object({}) { |(k, v), h| h[k.to_s] = v.to_s; h }
420+
FHIR::ResourceAddress.convert_symbol_headers(headers)
398421
end
399422

400423
def scrubbed_response_headers(result)
@@ -404,10 +427,10 @@ def scrubbed_response_headers(result)
404427
end
405428
end
406429

407-
def get(path, headers)
430+
def get(path, headers = {})
408431
url = Addressable::URI.parse(build_url(path)).to_s
409432
FHIR.logger.info "GETTING: #{url}"
410-
headers = clean_headers(headers)
433+
headers = clean_headers(headers) unless headers.empty?
411434
if @use_oauth2_auth
412435
# @client.refresh!
413436
begin
@@ -583,7 +606,7 @@ def patch(path, patchset, headers)
583606
url = URI(build_url(path)).to_s
584607
FHIR.logger.info "PATCHING: #{url}"
585608
headers = clean_headers(headers)
586-
payload = request_patch_payload(patchset, headers['format'])
609+
payload = request_patch_payload(patchset, headers['Content-Type'])
587610
if @use_oauth2_auth
588611
# @client.refresh!
589612
begin

lib/fhir_client/resource_address.rb

Lines changed: 70 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,46 +6,83 @@ class ResourceAddress
66
format: 'application/fhir+xml'
77
}.freeze
88

9-
DEFAULT_CHARSET = 'UTF-8'.freeze
10-
11-
def fhir_headers(options, use_format_param = false)
12-
options = DEFAULTS.merge(options)
13-
14-
format = options[:format] || FHIR::Formats::ResourceFormat::RESOURCE_XML
15-
fhir_headers = {
16-
'User-Agent' => 'Ruby FHIR Client',
17-
'Content-Type' => format + ';charset=' + DEFAULT_CHARSET,
18-
'Accept-Charset' => DEFAULT_CHARSET
19-
}
20-
# remove the content-type header if the format is 'xml' or 'json' because
21-
# even those are valid _format parameter options, they are not valid MimeTypes.
22-
fhir_headers.delete('Content-Type') if %w(xml json).include?(format.downcase)
23-
24-
if options[:category]
25-
# options[:category] should be an Array of FHIR::Tag objects
26-
tags = {
27-
'Category' => options[:category].collect(&:to_header).join(',')
28-
}
29-
fhir_headers.merge!(tags)
30-
options.delete(:category)
9+
DEFAULT_CHARSET = 'utf-8'.freeze
10+
DEFAULT_CONTENT_TYPE = 'application/fhir+xml'.freeze # time to default to json?
11+
12+
#
13+
# Normalize submitted header key value pairs
14+
#
15+
# 'content-type', 'Content-Type', and :content_type would all represent the content-type header
16+
#
17+
# Assumes symbols like :content_type to be "content-type"
18+
# if for some odd reason the Header string representation contains underscores it would need to be specified
19+
# as a string (i.e. options {"Underscore_Header" => 'why not hyphens'})
20+
# Note that servers like apache or nginx consider that invalid anyways and drop them
21+
# http://httpd.apache.org/docs/trunk/new_features_2_4.html
22+
# http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
23+
def self.normalize_headers(to_be_normalized, to_symbol = true, capitalized = false)
24+
to_be_normalized.inject({}) do |result, (key, value)|
25+
key = key.to_s.downcase.split(/-|_/)
26+
key.map!(&:capitalize) if capitalized
27+
key = to_symbol ? key.join('_').to_sym : key.join('-')
28+
result[key] = value.to_s
29+
result
3130
end
31+
end
3232

33-
if use_format_param
34-
fhir_headers.delete('Accept')
35-
options.delete('Accept')
36-
options.delete(:accept)
37-
else
38-
fhir_headers['Accept'] = format
33+
def self.convert_symbol_headers headers
34+
headers.inject({}) do |result, (key, value)|
35+
if key.is_a? Symbol
36+
key = key.to_s.split(/_/).map(&:capitalize).join('-')
37+
end
38+
result[key] = value.to_s
39+
result
3940
end
41+
end
4042

41-
fhir_headers.merge!(options) unless options.empty?
42-
fhir_headers[:operation] = options[:operation][:name] if options[:operation] && options[:operation][:name]
43-
fhir_headers.delete('id')
44-
fhir_headers.delete('resource')
43+
# Returns normalized HTTP Headers
44+
# header key value pairs can be supplied with keys specified as symbols or strings
45+
# keys will be normalized to symbols.
46+
# e.g. the keys :accept, "accept", and "Accept" all represent the Accept HTTP Header
47+
# @param [Hash] options key value pairs for the http headerx
48+
# @return [Hash] The normalized FHIR Headers
49+
def self.fhir_headers(headers = {}, additional_headers = {}, format = DEFAULT_CONTENT_TYPE, use_accept_header = true, use_accept_charset = true)
50+
# normalizes header names to be case-insensitive
51+
# See relevant HTTP RFCs:
52+
# https://tools.ietf.org/html/rfc2616#section-4.2
53+
# https://tools.ietf.org/html/rfc7230#section-3.2
54+
#
55+
# https://tools.ietf.org/html/rfc7231#section-5.3.2
56+
# optional white space before and
57+
# https://tools.ietf.org/html/rfc2616#section-3.4
58+
# utf-8 is case insensitive
59+
#
60+
headers ||= {}
61+
additional_headers ||= {}
62+
63+
fhir_headers = {user_agent: 'Ruby FHIR Client'}
64+
65+
fhir_headers[:accept_charset] = DEFAULT_CHARSET if use_accept_charset
66+
67+
# https://www.hl7.org/fhir/DSTU2/http.html#mime-type
68+
# could add option for ;charset=#{DEFAULT_CHARSET} in accept header
69+
fhir_headers[:accept] = "#{format}" if use_accept_header
70+
71+
# maybe in a future update normalize everything to symbols
72+
# Headers should be case insensitive anyways...
73+
#headers = normalize_headers(headers) unless headers.empty?
74+
#
75+
fhir_headers = convert_symbol_headers(fhir_headers)
76+
headers = convert_symbol_headers(headers)
77+
78+
# supplied headers will always be used, e.g. if @use_accept_header is false
79+
# ,but an accept header is explicitly supplied then it will be used (or override the existing)
80+
fhir_headers.merge!(headers) unless headers.empty?
81+
fhir_headers.merge!(additional_headers)
4582
fhir_headers
4683
end
4784

48-
def resource_url(options, use_format_param = false)
85+
def self.resource_url(options, use_format_param = false)
4986
options = DEFAULTS.merge(options)
5087

5188
params = {}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module FHIR
2+
module Formats
3+
module ReturnPreferences
4+
MINIMAL = 'return=minimal'.freeze
5+
REPRESENTATION = 'return=representation'.freeze
6+
end
7+
end
8+
end

0 commit comments

Comments
 (0)