fhir_client 3.0.7 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e672a83cc2c6d38d9b7b907e593fcee05ecead0a75320f79a8a65627deff0df7
4
- data.tar.gz: 79219638d996eaa08b9ec7b38957da73eb1e6e5c5d26b428480178ee2e939fc4
3
+ metadata.gz: 0652e0b08ba87a20e874e73ea62fca47611c6d3631681f6d9b6316e1a3e08528
4
+ data.tar.gz: 3980e902188d8fa627f8c9a8c72fb010ecffbc3c8fb056bd5a3a78cc50aebe05
5
5
  SHA512:
6
- metadata.gz: 904a33c45ee559acfbc0372435c9ee31e76412ef840d3616ae6f8e584aa9381d8a43cb81d937a3818891a82fe26ca5acbcaa74778602bbbe42c01c78d2e7ea35
7
- data.tar.gz: ad3ec868c7c139105890e4091e1780efffb30649df4b7e63a5fbc8ed599dcfa249cfac2d7b84f12236712a32ec4d69b4a58c5d8fb1891330a6fc30907dfddae2
6
+ metadata.gz: b2f783cbbd33c2a437d6c45ea01ce3ba7aa60ca277d18b2dcb7dd0e09fcd037e854f1b6fd1be3d6487c296ef129280d3bbca2cbe2aa85fa5e0ac6d7e86bdfbaf
7
+ data.tar.gz: eb13cd8355a0116d1b30043231881a7748fa164c39a5b2075f2d5517f99f353934d4b14121e056b0e0117077e3c070a28b5edf3f256792993e5614499c824956
data/.travis.yml CHANGED
@@ -1,9 +1,9 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.2
4
3
  - 2.3
5
4
  - 2.4
6
5
  - 2.5
6
+ - 2.6
7
7
  before_install:
8
8
  - gem update --system
9
9
  - gem install bundler
data/README.md CHANGED
@@ -84,6 +84,15 @@ patient = FHIR::DSTU2::Patient.read('example')
84
84
  patient = client.read(FHIR::DSTU2::Patient, 'example').resource
85
85
  ```
86
86
 
87
+ ### Configuration
88
+
89
+ You can specify additional properties for the `client`:
90
+
91
+ ```ruby
92
+ client.additional_headers = {Prefer: 'return=representation'}
93
+ client.proxy = 'https://your-proxy.com/'
94
+ ```
95
+
87
96
  ### CRUD Examples
88
97
  ```ruby
89
98
  # read an existing patient with id "example"
data/fhir_client.gemspec CHANGED
@@ -30,7 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.add_dependency 'rest-client', '~> 2.0'
31
31
  spec.add_dependency 'tilt', '>= 1.1'
32
32
 
33
- spec.add_development_dependency 'bundler', '~> 1.13'
33
+ spec.add_development_dependency 'bundler', '~> 2.0'
34
34
  spec.add_development_dependency 'rake', '~> 10.0'
35
35
  spec.add_development_dependency 'pry'
36
36
  spec.add_development_dependency 'webmock'
data/lib/fhir_client.rb CHANGED
@@ -13,6 +13,7 @@ end
13
13
  require_relative 'fhir_client/client'
14
14
  require_relative 'fhir_client/resource_address'
15
15
  require_relative 'fhir_client/resource_format'
16
+ require_relative 'fhir_client/return_preferences'
16
17
  require_relative 'fhir_client/patch_format'
17
18
  require_relative 'fhir_client/client_exception'
18
19
  require_relative 'fhir_client/version'
@@ -23,6 +23,12 @@ module FHIR
23
23
  attr_accessor :fhir_version
24
24
  attr_accessor :cached_capability_statement
25
25
  attr_accessor :additional_headers
26
+ attr_accessor :proxy
27
+ attr_accessor :exception_class
28
+
29
+ attr_accessor :use_accept_header
30
+ attr_accessor :use_accept_charset
31
+ attr_accessor :use_return_preference
26
32
 
27
33
  # Call method to initialize FHIR client. This method must be invoked
28
34
  # with a valid base server URL prior to using the client.
@@ -31,12 +37,19 @@ module FHIR
31
37
  # @param default_format Default Format Mime type
32
38
  # @return
33
39
  #
34
- def initialize(base_service_url, default_format: FHIR::Formats::ResourceFormat::RESOURCE_XML)
40
+ def initialize(base_service_url, default_format: FHIR::Formats::ResourceFormat::RESOURCE_XML, proxy: nil)
35
41
  @base_service_url = base_service_url
36
42
  FHIR.logger.info "Initializing client with #{@base_service_url}"
37
43
  @use_format_param = false
44
+ @use_accept_header = true
45
+ @use_accept_charset = true
38
46
  @default_format = default_format
39
47
  @fhir_version = :stu3
48
+ @use_return_preference = false
49
+ @return_preference = FHIR::Formats::ReturnPreferences::REPRESENTATION
50
+ @exception_class = ClientException
51
+ @proxy = proxy
52
+
40
53
  set_no_auth
41
54
  end
42
55
 
@@ -74,6 +87,20 @@ module FHIR
74
87
  end
75
88
  end
76
89
 
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
+
77
104
  def versioned_resource_class(klass)
78
105
  if @fhir_version == :stu3
79
106
  FHIR.const_get(klass)
@@ -102,6 +129,8 @@ module FHIR
102
129
  @use_basic_auth = false
103
130
  @security_headers = {}
104
131
  @client = RestClient
132
+ @client.proxy = proxy unless proxy.nil?
133
+ @client
105
134
  end
106
135
 
107
136
  # Set the client to use HTTP Basic Authentication
@@ -113,6 +142,8 @@ module FHIR
113
142
  @use_oauth2_auth = false
114
143
  @use_basic_auth = true
115
144
  @client = RestClient
145
+ @client.proxy = proxy unless proxy.nil?
146
+ @client
116
147
  end
117
148
 
118
149
  # Set the client to use Bearer Token Authentication
@@ -123,6 +154,8 @@ module FHIR
123
154
  @use_oauth2_auth = false
124
155
  @use_basic_auth = true
125
156
  @client = RestClient
157
+ @client.proxy = proxy unless proxy.nil?
158
+ @client
126
159
  end
127
160
 
128
161
  # Set the client to use OpenID Connect OAuth2 Authentication
@@ -142,6 +175,7 @@ module FHIR
142
175
  raise_errors: true
143
176
  }
144
177
  client = OAuth2::Client.new(client, secret, options)
178
+ client.connection.proxy(proxy) unless proxy.nil?
145
179
  @client = client.client_credentials.get_token
146
180
  end
147
181
 
@@ -248,7 +282,7 @@ module FHIR
248
282
  @default_format = nil
249
283
 
250
284
  formats.each do |frmt|
251
- reply = get 'metadata', fhir_headers(format: frmt)
285
+ reply = get 'metadata', fhir_headers({accept: "#{frmt}"})
252
286
  next unless reply.code == 200
253
287
  begin
254
288
  @cached_capability_statement = parse_reply(FHIR::CapabilityStatement, frmt, reply)
@@ -272,7 +306,7 @@ module FHIR
272
306
  end
273
307
 
274
308
  def resource_url(options)
275
- FHIR::ResourceAddress.new.resource_url(options, @use_format_param)
309
+ FHIR::ResourceAddress.resource_url(options, @use_format_param)
276
310
  end
277
311
 
278
312
  def full_resource_url(options)
@@ -280,9 +314,7 @@ module FHIR
280
314
  end
281
315
 
282
316
  def fhir_headers(options = {})
283
- options.merge!(additional_headers) unless additional_headers.nil?
284
-
285
- 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)
286
318
  end
287
319
 
288
320
  def parse_reply(klass, format, response)
@@ -348,8 +380,10 @@ module FHIR
348
380
  # Extract the request payload in the specified format, defaults to XML
349
381
  def request_payload(resource, headers)
350
382
  if headers
351
- format_specified = headers[:format] || headers['format']
352
- 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')
353
387
  resource.to_xml
354
388
  elsif format_specified.downcase.include?('json')
355
389
  resource.to_json
@@ -383,7 +417,7 @@ module FHIR
383
417
 
384
418
  def clean_headers(headers)
385
419
  headers.delete_if { |k, v| (k.nil? || v.nil?) }
386
- headers.each_with_object({}) { |(k, v), h| h[k.to_s] = v.to_s; h }
420
+ FHIR::ResourceAddress.convert_symbol_headers(headers)
387
421
  end
388
422
 
389
423
  def scrubbed_response_headers(result)
@@ -393,10 +427,10 @@ module FHIR
393
427
  end
394
428
  end
395
429
 
396
- def get(path, headers)
430
+ def get(path, headers = {})
397
431
  url = Addressable::URI.parse(build_url(path)).to_s
398
432
  FHIR.logger.info "GETTING: #{url}"
399
- headers = clean_headers(headers)
433
+ headers = clean_headers(headers) unless headers.empty?
400
434
  if @use_oauth2_auth
401
435
  # @client.refresh!
402
436
  begin
@@ -572,7 +606,7 @@ module FHIR
572
606
  url = URI(build_url(path)).to_s
573
607
  FHIR.logger.info "PATCHING: #{url}"
574
608
  headers = clean_headers(headers)
575
- payload = request_patch_payload(patchset, headers['format'])
609
+ payload = request_patch_payload(patchset, headers['Content-Type'])
576
610
  if @use_oauth2_auth
577
611
  # @client.refresh!
578
612
  begin
@@ -50,7 +50,7 @@ module FHIR
50
50
 
51
51
  private
52
52
  def handle_response(response)
53
- raise ClientException.new "Server returned #{response.code}.", response if response.code.between?(400, 599)
53
+ raise client.exception_class.new "Server returned #{response.code}.", response if response.code.between?(400, 599)
54
54
  response.resource
55
55
  end
56
56
  end
@@ -66,59 +66,59 @@ module FHIR
66
66
  end
67
67
 
68
68
  def read(id, client = self.client)
69
- handle_response client.read(self, id)
69
+ handle_response client.exception_class, client.read(self, id)
70
70
  end
71
71
 
72
72
  def read_with_summary(id, summary, client = self.client)
73
- handle_response client.read(self, id, client.default_format, summary)
73
+ handle_response client.exception_class, client.read(self, id, client.default_format, summary)
74
74
  end
75
75
 
76
76
  def vread(id, version_id, client = self.client)
77
- handle_response client.vread(self, id, version_id)
77
+ handle_response client.exception_class, client.vread(self, id, version_id)
78
78
  end
79
79
 
80
80
  def resource_history(client = self.client)
81
- handle_response client.resource_history(self)
81
+ handle_response client.exception_class, client.resource_history(self)
82
82
  end
83
83
 
84
84
  def resource_history_as_of(last_update, client = self.client)
85
- handle_response client.resource_history_as_of(self, last_update)
85
+ handle_response client.exception_class, client.resource_history_as_of(self, last_update)
86
86
  end
87
87
 
88
88
  def resource_instance_history(id, client = self.client)
89
- handle_response client.resource_instance_history(self, id)
89
+ handle_response client.exception_class, client.resource_instance_history(self, id)
90
90
  end
91
91
 
92
92
  def resource_instance_history_as_of(id, last_update, client = self.client)
93
- handle_response client.resource_instance_history_as_of(self, id, last_update)
93
+ handle_response client.exception_class, client.resource_instance_history_as_of(self, id, last_update)
94
94
  end
95
95
 
96
96
  def search(params = {}, client = self.client)
97
- handle_response client.search(self, search: { parameters: params })
97
+ handle_response client.exception_class, client.search(self, search: { parameters: params })
98
98
  end
99
99
 
100
100
  def create(model, client = self.client)
101
101
  model = new(model) unless model.is_a?(self)
102
- handle_response client.create(model)
102
+ handle_response client.exception_class, client.create(model)
103
103
  end
104
104
 
105
105
  def conditional_create(model, params, client = self.client)
106
106
  model = new(model) unless model.is_a?(self)
107
- handle_response client.conditional_create(model, params)
107
+ handle_response client.exception_class, client.conditional_create(model, params)
108
108
  end
109
109
 
110
110
  def partial_update(id, patchset, options = {})
111
- handle_response client.partial_update(self, id, patchset, options)
111
+ handle_response client.exception_class, client.partial_update(self, id, patchset, options)
112
112
  end
113
113
 
114
114
  def all(client = self.client)
115
- handle_response client.read_feed(self)
115
+ handle_response client.exception_class, client.read_feed(self)
116
116
  end
117
117
 
118
118
  private
119
119
 
120
- def handle_response(response)
121
- raise ClientException.new "Server returned #{response.code}.", response if response.code.between?(400, 599)
120
+ def handle_response(exception_class, response)
121
+ raise exception_class.new "Server returned #{response.code}.", response if response.code.between?(400, 599)
122
122
  response.resource
123
123
  end
124
124
  end
@@ -59,7 +59,7 @@ module FHIR
59
59
  if relative? || reference == client.full_resource_url(resource: resource_class, id: reference_id)
60
60
  read_client = client
61
61
  else
62
- read_client = FHIR::Client.new base_uri, default_format: client.default_format
62
+ read_client = FHIR::Client.new base_uri, default_format: client.default_format, proxy: client.proxy
63
63
  end
64
64
  resource_class.read(reference_id, read_client)
65
65
  end
@@ -69,7 +69,7 @@ module FHIR
69
69
  if relative? || reference == client.full_resource_url(resource: resource_class, id: reference_id)
70
70
  read_client = client
71
71
  else
72
- read_client = FHIR::Client.new base_uri, default_format: client.default_format
72
+ read_client = FHIR::Client.new base_uri, default_format: client.default_format, proxy: client.proxy
73
73
  end
74
74
  resource_class.vread(reference_id, version_id, read_client)
75
75
  end
@@ -6,46 +6,83 @@ module FHIR
6
6
  format: 'application/fhir+xml'
7
7
  }.freeze
8
8
 
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
31
30
  end
31
+ end
32
32
 
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
39
40
  end
41
+ end
40
42
 
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)
45
82
  fhir_headers
46
83
  end
47
84
 
48
- def resource_url(options, use_format_param = false)
85
+ def self.resource_url(options, use_format_param = false)
49
86
  options = DEFAULTS.merge(options)
50
87
 
51
88
  params = {}
@@ -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
@@ -4,11 +4,47 @@ module FHIR
4
4
  #
5
5
  # Read the current state of a resource.
6
6
  #
7
- def read(klass, id, format = @default_format, summary = nil, options = {})
8
- options = { resource: klass, id: id, format: format }.merge(options)
7
+ def read(klass, id, format = nil, summary = nil, options = {})
8
+ options = { resource: klass, id: id, format: format || @default_format}.merge(options)
9
9
  options[:summary] = summary if summary
10
- reply = get resource_url(options), fhir_headers(options)
11
- reply.resource = parse_reply(klass, format, reply)
10
+ headers = { accept: "#{format}" } if format
11
+ reply = get resource_url(options), fhir_headers(headers)
12
+ reply.resource = parse_reply(klass, format || @default_format, reply)
13
+ reply.resource_class = klass
14
+ reply
15
+ end
16
+
17
+ #
18
+ # Conditionally Read the resource if it has been modified since the supplied date
19
+ #
20
+ # See If-Modified-Since RRC 7232 https://tools.ietf.org/html/rfc7232#section-3.3 and
21
+ # See HTTP-date https://tools.ietf.org/html/rfc7231#section-7.1.1.1
22
+ #
23
+ # @param klass the FHIR Resource class
24
+ # @param id the resource id
25
+ # @param since_date the date
26
+ def conditional_read_since(klass, id, since_date, options = {})
27
+
28
+ options = { resource: klass, id: id}.merge(options)
29
+ headers = { if_modified_since: since_date }
30
+ reply = get resource_url(options), fhir_headers(headers)
31
+ reply.resource = parse_reply(klass, @default_format, reply)
32
+ reply.resource_class = klass
33
+ reply
34
+ end
35
+
36
+ #
37
+ # Conditionally Read the resource based on the provided ETag
38
+ #
39
+ # @param klass the FHIR Resource class
40
+ # @param id the resource id
41
+ # @param version_id the version_id used for the ETag
42
+ def conditional_read_version(klass, id, version_id, options = {})
43
+
44
+ options = { resource: klass, id: id}.merge(options)
45
+ headers = { if_none_match: "W/#{version_id}" }
46
+ reply = get resource_url(options), fhir_headers(headers)
47
+ reply.resource = parse_reply(klass, @default_format, reply)
12
48
  reply.resource_class = klass
13
49
  reply
14
50
  end
@@ -16,9 +52,12 @@ module FHIR
16
52
  #
17
53
  # Read a resource bundle (an XML ATOM feed)
18
54
  #
19
- def read_feed(klass, format = @default_format)
20
- options = { resource: klass, format: format }
21
- reply = get resource_url(options), fhir_headers(options)
55
+ #
56
+ def read_feed(klass, format = nil)
57
+ headers = { accept: "#{format}" } if format
58
+ format ||= @default_format
59
+ options = { resource: klass, format: format}
60
+ reply = get resource_url(options), fhir_headers(headers)
22
61
  reply.resource = parse_reply(klass, format, reply)
23
62
  reply.resource_class = klass
24
63
  reply
@@ -27,33 +66,36 @@ module FHIR
27
66
  #
28
67
  # Read the state of a specific version of the resource
29
68
  #
30
- def vread(klass, id, version_id, format = @default_format)
69
+ def vread(klass, id, version_id, format = nil)
70
+ headers = { accept: "#{format}" } if format
71
+ format ||= @default_format
31
72
  options = { resource: klass, id: id, format: format, history: { id: version_id } }
32
- reply = get resource_url(options), fhir_headers(options)
73
+ reply = get resource_url(options), fhir_headers(headers)
33
74
  reply.resource = parse_reply(klass, format, reply)
34
75
  reply.resource_class = klass
35
76
  reply
36
77
  end
37
78
 
38
79
  def raw_read(options)
39
- get resource_url(options), fhir_headers(options)
80
+ get resource_url(options), fhir_headers
40
81
  end
41
82
 
42
- def raw_read_url(url, format = @default_format)
43
- get url, fhir_headers(format: format)
83
+ def raw_read_url(url, format = nil)
84
+ headers = { accept: "#{format}" } if format
85
+ get url, fhir_headers(headers)
44
86
  end
45
87
 
46
88
  #
47
89
  # Update an existing resource by its id or create it if it is a new resource, not present on the server
48
90
  #
49
- def update(resource, id, format = @default_format)
91
+ def update(resource, id, format = nil)
50
92
  base_update(resource, id, nil, format)
51
93
  end
52
94
 
53
95
  #
54
96
  # Update an existing resource by its id or create it if it is a new resource, not present on the server
55
97
  #
56
- def conditional_update(resource, id, search_params, format = @default_format)
98
+ def conditional_update(resource, id, search_params, format = nil)
57
99
  options = {
58
100
  search: {
59
101
  flag: false,
@@ -67,15 +109,29 @@ module FHIR
67
109
  base_update(resource, id, options, format)
68
110
  end
69
111
 
112
+ #
113
+ # Version Aware Update using version_id
114
+ # prevents Lost Update Problem https://www.w3.org/1999/04/Editing/
115
+ # @param resource the FHIR resource object
116
+ # @param id the resource id
117
+ def version_aware_update(resource, id, version_id, format = nil, options = {})
118
+ base_update(resource, id, options, format, {if_match: "W/#{version_id}"})
119
+ end
120
+
70
121
  #
71
122
  # Update an existing resource by its id or create it if it is a new resource, not present on the server
72
123
  #
73
- def base_update(resource, id, options, format)
124
+ def base_update(resource, id, options, format = nil, headers = nil)
125
+ headers ||= {}
126
+ headers[:accept] = "#{format}" if format
127
+ format ||= @default_format
128
+ headers[:content_type] = "#{format}"
129
+ headers[:prefer] = @return_preference if @use_return_preference
74
130
  options = {} if options.nil?
75
131
  options[:resource] = resource.class
76
132
  options[:format] = format
77
133
  options[:id] = id
78
- reply = put resource_url(options), resource, fhir_headers(options)
134
+ reply = put resource_url(options), resource, fhir_headers(headers)
79
135
  reply.resource = parse_reply(resource.class, format, reply) if reply.body.present?
80
136
  reply.resource_class = resource.class
81
137
  reply
@@ -84,18 +140,20 @@ module FHIR
84
140
  #
85
141
  # Partial update using a patchset (PATCH)
86
142
  #
87
- def partial_update(klass, id, patchset, options = {}, format = @default_format)
88
- options = { resource: klass, id: id, format: format }.merge options
89
-
143
+ def partial_update(klass, id, patchset, options = {}, format = nil)
144
+ headers = {}
145
+ headers[:accept] = "#{format}" if format
146
+ format ||= @default_format
147
+ options = { resource: klass, id: id, format: format}.merge options
90
148
  if [FHIR::Formats::ResourceFormat::RESOURCE_XML, FHIR::Formats::ResourceFormat::RESOURCE_XML_DSTU2].include?(format)
91
149
  options[:format] = FHIR::Formats::PatchFormat::PATCH_XML
92
- options[:Accept] = format
150
+ headers[:content_type] = "#{FHIR::Formats::PatchFormat::PATCH_XML}"
93
151
  elsif [FHIR::Formats::ResourceFormat::RESOURCE_JSON, FHIR::Formats::ResourceFormat::RESOURCE_JSON_DSTU2].include?(format)
94
152
  options[:format] = FHIR::Formats::PatchFormat::PATCH_JSON
95
- options[:Accept] = format
153
+ headers[:content_type] = "#{FHIR::Formats::PatchFormat::PATCH_JSON}"
96
154
  end
97
-
98
- reply = patch resource_url(options), patchset, fhir_headers(options)
155
+ headers[:prefer] = @return_preference if @use_return_preference
156
+ reply = patch resource_url(options), patchset, fhir_headers(headers)
99
157
  reply.resource = parse_reply(klass, format, reply)
100
158
  reply.resource_class = klass
101
159
  reply
@@ -106,9 +164,7 @@ module FHIR
106
164
  #
107
165
  def destroy(klass, id = nil, options = {})
108
166
  options = { resource: klass, id: id, format: @default_format }.merge options
109
- headers = fhir_headers(options)
110
- headers.delete('Content-Type')
111
- reply = delete resource_url(options), headers
167
+ reply = delete resource_url(options), fhir_headers
112
168
  reply.resource_class = klass
113
169
  reply
114
170
  end
@@ -117,33 +173,38 @@ module FHIR
117
173
  # Create a new resource with a server assigned id. Return the newly created
118
174
  # resource with the id the server assigned.
119
175
  #
120
- def create(resource, options = {}, format = @default_format)
176
+ def create(resource, options = {}, format = nil)
121
177
  base_create(resource, options, format)
122
178
  end
123
179
 
124
180
  #
125
181
  # Conditionally create a new resource with a server assigned id.
126
182
  #
127
- def conditional_create(resource, if_none_exist_parameters, format = @default_format)
183
+ def conditional_create(resource, if_none_exist_parameters, format = nil)
128
184
  query = ''
129
185
  if_none_exist_parameters.each do |key, value|
130
186
  query += "#{key}=#{value}&"
131
187
  end
132
188
  query = query[0..-2] # strip off the trailing ampersand
133
- options = {}
134
- options['If-None-Exist'] = query
135
- base_create(resource, options, format)
189
+ header = {if_none_exist: query}
190
+ base_create(resource, options, format, header)
136
191
  end
137
192
 
138
193
  #
139
194
  # Create a new resource with a server assigned id. Return the newly created
140
195
  # resource with the id the server assigned.
141
196
  #
142
- def base_create(resource, options, format)
197
+ def base_create(resource, options, format = nil, additional_header = {})
198
+ headers = {}
199
+ headers[:accept] = "#{format}" if format
200
+ format ||= @default_format
201
+ headers = {content_type: "#{format}"}
202
+ headers[:prefer] = @return_preference if @use_return_preference
203
+ headers.merge!(additional_header)
143
204
  options = {} if options.nil?
144
205
  options[:resource] = resource.class
145
- options[:format] = format
146
- reply = post resource_url(options), resource, fhir_headers(options)
206
+ options[:format] = format || @default_format
207
+ reply = post resource_url(options), resource, fhir_headers(headers)
147
208
  if [200, 201].include? reply.code
148
209
  type = reply.response[:headers].detect{|x, _y| x.downcase=='content-type'}.try(:last)
149
210
  if !type.nil?
@@ -10,7 +10,7 @@ module FHIR
10
10
  bundle = current.resource
11
11
  link = bundle.method(page).call
12
12
  return nil unless link
13
- reply = get strip_base(link.url), fhir_headers(format: @default_format)
13
+ reply = get strip_base(link.url), fhir_headers
14
14
  reply.resource = parse_reply(current.resource_class, @default_format, reply)
15
15
  reply.resource_class = current.resource_class
16
16
  reply
@@ -25,7 +25,7 @@ module FHIR
25
25
 
26
26
  def history(options)
27
27
  options = {format: @default_format}.merge(options)
28
- reply = get resource_url(options), fhir_headers(options).except(:history)
28
+ reply = get resource_url(options), fhir_headers
29
29
  reply.resource = parse_reply(options[:resource], options[:format], reply)
30
30
  reply.resource_class = options[:resource]
31
31
  reply
@@ -10,15 +10,19 @@ module FHIR
10
10
  # Fetch Patient Record [base]/Patient/$everything | [base]/Patient/[id]/$everything
11
11
  # http://hl7.org/implement/standards/FHIR-Develop/patient-operations.html#everything
12
12
  # Fetches resources for a given patient record, scoped by a start and end time, and returns a Bundle of results
13
- def fetch_patient_record(id = nil, startTime = nil, endTime = nil, method = 'GET', format = @default_format)
13
+ def fetch_patient_record(id = nil, startTime = nil, endTime = nil, method = 'GET', format = nil)
14
14
  fetch_record(id, [startTime, endTime], method, versioned_resource_class('Patient'), format)
15
15
  end
16
16
 
17
- def fetch_encounter_record(id = nil, method = 'GET', format = @default_format)
17
+ def fetch_encounter_record(id = nil, method = 'GET', format = nil)
18
18
  fetch_record(id, [nil, nil], method, versioned_resource_class('Encounter'), format)
19
19
  end
20
20
 
21
- def fetch_record(id = nil, time = [nil, nil], method = 'GET', klass = versioned_resource_class('Patient'), format = @default_format)
21
+ def fetch_record(id = nil, time = [nil, nil], method = 'GET', klass = versioned_resource_class('Patient'), format = nil)
22
+ headers = {}
23
+ headers[:accept] = "#{format}" if format
24
+ format ||= @default_format
25
+ headers[:content_type] = format
22
26
  options = { resource: klass, format: format, operation: { name: :fetch_patient_record, method: method } }
23
27
  options.deep_merge!(id: id) unless id.nil?
24
28
  options[:operation][:parameters] = {} if options[:operation][:parameters].nil?
@@ -26,7 +30,7 @@ module FHIR
26
30
  options[:operation][:parameters][:end] = { type: 'Date', value: time.last } unless time.last.nil?
27
31
 
28
32
  if options[:operation][:method] == 'GET'
29
- reply = get resource_url(options), fhir_headers(options)
33
+ reply = get resource_url(options), fhir_headers
30
34
  else
31
35
  # create Parameters body
32
36
  if options[:operation] && options[:operation][:parameters]
@@ -37,7 +41,7 @@ module FHIR
37
41
  p.parameter << parameter
38
42
  end
39
43
  end
40
- reply = post resource_url(options), p, fhir_headers(options)
44
+ reply = post resource_url(options), p, fhir_headers(headers)
41
45
  end
42
46
 
43
47
  reply.resource = parse_reply(versioned_resource_class('Bundle'), format, reply)
@@ -95,11 +99,14 @@ module FHIR
95
99
 
96
100
  def terminology_operation(params = {}, format = @default_format)
97
101
  options = { format: format }
102
+ headers = {}
103
+ headers[:accept] = "#{format}" if format
104
+ format ||= @default_format
98
105
  # params = [id, code, system, version, display, coding, codeableConcept, date, abstract]
99
106
  options.deep_merge!(params)
100
107
 
101
108
  if options[:operation][:method] == 'GET'
102
- reply = get resource_url(options), fhir_headers(options)
109
+ reply = get resource_url(options), fhir_headers(headers)
103
110
  else
104
111
  # create Parameters body
105
112
  if options[:operation] && options[:operation][:parameters]
@@ -110,7 +117,8 @@ module FHIR
110
117
  p.parameter << parameter
111
118
  end
112
119
  end
113
- reply = post resource_url(options), p, fhir_headers(options)
120
+ headers[:content_type] = "#{format}"
121
+ reply = post resource_url(options), p, fhir_headers(headers)
114
122
  end
115
123
 
116
124
  reply.resource = parse_reply(options[:resource], format, reply)
@@ -124,7 +132,8 @@ module FHIR
124
132
  add_resource_parameter(params, 'resource', resource)
125
133
  add_parameter(params, 'onlyCertainMatches', 'Boolean', options[:onlyCertainMatches]) unless options[:onlyCertainMatches].nil?
126
134
  add_parameter(params, 'count', 'Integer', options[:matchCount]) if options[:matchCount].is_a?(Integer)
127
- post resource_url(options), params, fhir_headers(options)
135
+ post resource_url(options), params, fhir_headers({content_type: "#{format || @default_format}",
136
+ accept: "#{format || @default_format}"})
128
137
  end
129
138
 
130
139
  #
@@ -139,18 +148,24 @@ module FHIR
139
148
 
140
149
  def validate(resource, options = {}, format = @default_format)
141
150
  options.merge!(resource: resource.class, validate: true, format: format)
151
+ headers = {}
152
+ headers[:accept] = "#{format}" if format
153
+ headers[:content_type] = "#{format}"
142
154
  params = versioned_resource_class('Parameters').new
143
155
  add_resource_parameter(params, 'resource', resource)
144
156
  add_parameter(params, 'profile', 'Uri', options[:profile_uri]) unless options[:profile_uri].nil?
145
- post resource_url(options), params, fhir_headers(options)
157
+ post resource_url(options), params, fhir_headers(headers)
146
158
  end
147
159
 
148
160
  def validate_existing(resource, id, options = {}, format = @default_format)
149
161
  options.merge!(resource: resource.class, id: id, validate: true, format: format)
162
+ headers = {}
163
+ headers[:accept] = "#{format}" if format
164
+ headers[:content_type] = "#{format}"
150
165
  params = versioned_resource_class('Parameters').new
151
166
  add_resource_parameter(params, 'resource', resource)
152
167
  add_parameter(params, 'profile', 'Uri', options[:profile_uri]) unless options[:profile_uri].nil?
153
- post resource_url(options), params, fhir_headers(options)
168
+ post resource_url(options), params, fhir_headers(headers)
154
169
  end
155
170
 
156
171
  private
@@ -13,9 +13,9 @@ module FHIR
13
13
  options[:format] = format
14
14
 
15
15
  reply = if options[:search] && options[:search][:flag]
16
- post resource_url(options), nil, fhir_headers(options)
16
+ post resource_url(options), nil, fhir_headers({content_type: 'application/x-www-form-urlencoded'})
17
17
  else
18
- get resource_url(options), fhir_headers(options)
18
+ get resource_url(options), fhir_headers
19
19
  end
20
20
  # reply = get resource_url(options), fhir_headers(options)
21
21
  reply.resource = parse_reply(klass, format, reply)
@@ -27,9 +27,9 @@ module FHIR
27
27
  options.merge!(resource: klass, id: id, format: format)
28
28
  # if options[:search][:flag]
29
29
  reply = if options[:search] && options[:search][:flag]
30
- post resource_url(options), nil, fhir_headers(options)
30
+ post resource_url(options), nil, fhir_headers({content_type: 'application/x-www-form-urlencoded'})
31
31
  else
32
- get resource_url(options), fhir_headers(options)
32
+ get resource_url(options), fhir_headers
33
33
  end
34
34
  reply.resource = parse_reply(klass, format, reply)
35
35
  reply.resource_class = klass
@@ -39,9 +39,9 @@ module FHIR
39
39
  def search_all(options = {}, format = @default_format)
40
40
  options[:format] = format
41
41
  reply = if options[:search] && options[:search][:flag]
42
- post resource_url(options), nil, fhir_headers(options)
42
+ post resource_url(options), nil, fhir_headers({content_type: 'application/x-www-form-urlencoded'})
43
43
  else
44
- get resource_url(options), fhir_headers(options)
44
+ get resource_url(options), fhir_headers
45
45
  end
46
46
  reply.resource = parse_reply(nil, format, reply)
47
47
  reply.resource_class = nil
@@ -61,8 +61,10 @@ module FHIR
61
61
  # @return FHIR::ClientReply
62
62
  #
63
63
  def end_batch(format = @default_format)
64
- options = { format: format, 'Prefer' => 'return=representation' }
65
- reply = post resource_url(options), @transaction_bundle, fhir_headers(options)
64
+ headers = {prefer: FHIR::Formats::ReturnPreferences::REPRESENTATION}
65
+ headers[:content_type] = "#{format}"
66
+ options = { format: format}
67
+ reply = post resource_url(options), @transaction_bundle, fhir_headers(headers)
66
68
  begin
67
69
  reply.resource = if format.downcase.include?('xml')
68
70
  versioned_resource_class('Xml').from_xml(reply.body)
@@ -1,5 +1,5 @@
1
1
  module FHIR
2
2
  class Client
3
- VERSION = '3.0.7'
3
+ VERSION = '3.1.0'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fhir_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.7
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andre Quina
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2018-12-10 00:00:00.000000000 Z
13
+ date: 2019-02-12 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -144,14 +144,14 @@ dependencies:
144
144
  requirements:
145
145
  - - "~>"
146
146
  - !ruby/object:Gem::Version
147
- version: '1.13'
147
+ version: '2.0'
148
148
  type: :development
149
149
  prerelease: false
150
150
  version_requirements: !ruby/object:Gem::Requirement
151
151
  requirements:
152
152
  - - "~>"
153
153
  - !ruby/object:Gem::Version
154
- version: '1.13'
154
+ version: '2.0'
155
155
  - !ruby/object:Gem::Dependency
156
156
  name: rake
157
157
  requirement: !ruby/object:Gem::Requirement
@@ -256,6 +256,7 @@ files:
256
256
  - lib/fhir_client/patch_format.rb
257
257
  - lib/fhir_client/resource_address.rb
258
258
  - lib/fhir_client/resource_format.rb
259
+ - lib/fhir_client/return_preferences.rb
259
260
  - lib/fhir_client/sections/crud.rb
260
261
  - lib/fhir_client/sections/feed.rb
261
262
  - lib/fhir_client/sections/history.rb