fhir_client 1.0.1

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.
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'fileutils'
4
+ require 'pry'
5
+
6
+ require_relative 'lib/fhir_client'
7
+
8
+ # Pull in any rake task defined in lib/tasks
9
+ Dir['lib/tasks/**/*.rake'].sort.each do |ext|
10
+ load ext
11
+ end
12
+
13
+ desc "Run basic tests"
14
+ Rake::TestTask.new(:test_unit) do |t|
15
+ t.libs << "test"
16
+ t.test_files = FileList['test/**/*_test.rb']
17
+ t.verbose = true
18
+ t.warning = false
19
+ end
20
+
21
+ task :test => [:test_unit] do
22
+ system("open coverage/index.html")
23
+ end
24
+
25
+ task :default => [:test]
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "fhir_client"
5
+ s.summary = "A Gem for handling FHIR client requests in ruby"
6
+ s.description = "A Gem for handling FHIR client requests in ruby"
7
+ s.email = "aquina@mitre.org"
8
+ s.homepage = "https://github.com/hl7-fhir/fhir-svn"
9
+ s.authors = ["Andre Quina", "Jason Walonoski", "Janoo Fernandes"]
10
+ s.version = '1.0.1'
11
+
12
+ s.files = s.files = `git ls-files`.split("\n")
13
+
14
+ s.add_dependency 'fhir_models', '~> 0.3'
15
+ s.add_dependency 'tilt', '>= 1.1'
16
+ s.add_dependency 'rest-client', '~> 1.8'
17
+ s.add_dependency 'oauth2', '~> 1.1'
18
+ s.add_dependency 'activesupport', '>= 3'
19
+ s.add_dependency 'addressable', '>= 2.3'
20
+ s.add_development_dependency 'pry'
21
+ end
@@ -0,0 +1,525 @@
1
+ module FHIR
2
+
3
+ class Client
4
+
5
+ include FHIR::Sections::History
6
+ include FHIR::Sections::Crud
7
+ include FHIR::Sections::Validate
8
+ include FHIR::Sections::Tags
9
+ include FHIR::Sections::Feed
10
+ include FHIR::Sections::Search
11
+ include FHIR::Sections::Operations
12
+ include FHIR::Sections::Transactions
13
+
14
+ attr_accessor :reply
15
+ attr_accessor :use_format_param
16
+ attr_accessor :use_basic_auth
17
+ attr_accessor :use_oauth2_auth
18
+ attr_accessor :security_headers
19
+ attr_accessor :client
20
+
21
+ attr_accessor :default_format
22
+ attr_accessor :default_format_bundle
23
+
24
+ attr_accessor :cached_conformance
25
+
26
+ # Call method to initialize FHIR client. This method must be invoked
27
+ # with a valid base server URL prior to using the client.
28
+ #
29
+ # @param baseServiceUrl Base service URL for FHIR Service.
30
+ # @return
31
+ #
32
+ def initialize(baseServiceUrl)
33
+ $LOG.info "Initializing client with #{@baseServiceUrl}"
34
+ @baseServiceUrl = baseServiceUrl
35
+ @use_format_param = false
36
+ @default_format = FHIR::Formats::ResourceFormat::RESOURCE_XML
37
+ @default_format_bundle = FHIR::Formats::FeedFormat::FEED_XML
38
+ set_no_auth
39
+ end
40
+
41
+ # Set the client to use no authentication mechanisms
42
+ def set_no_auth
43
+ $LOG.info "Configuring the client to use no authentication."
44
+ @use_oauth2_auth = false
45
+ @use_basic_auth = false
46
+ @security_headers = {}
47
+ @client = RestClient
48
+ end
49
+
50
+ # Set the client to use HTTP Basic Authentication
51
+ def set_basic_auth(client,secret)
52
+ $LOG.info "Configuring the client to use HTTP Basic authentication."
53
+ token = Base64.encode64("#{client}:#{secret}")
54
+ value = "Basic #{token}"
55
+ @security_headers = { 'Authorization' => value }
56
+ @use_oauth2_auth = false
57
+ @use_basic_auth = true
58
+ @client = RestClient
59
+ end
60
+
61
+ # Set the client to use Bearer Token Authentication
62
+ def set_bearer_token(token)
63
+ $LOG.info "Configuring the client to use Bearer Token authentication."
64
+ value = "Bearer #{token}"
65
+ @security_headers = { 'Authorization' => value }
66
+ @use_oauth2_auth = false
67
+ @use_basic_auth = true
68
+ @client = RestClient
69
+ end
70
+
71
+ # Set the client to use OpenID Connect OAuth2 Authentication
72
+ # client -- client id
73
+ # secret -- client secret
74
+ # authorizePath -- absolute path of authorization endpoint
75
+ # tokenPath -- absolute path of token endpoint
76
+ def set_oauth2_auth(client,secret,authorizePath,tokenPath)
77
+ $LOG.info "Configuring the client to use OpenID Connect OAuth2 authentication."
78
+ @use_oauth2_auth = true
79
+ @use_basic_auth = false
80
+ @security_headers = {}
81
+ options = {
82
+ :site => @baseServiceUrl,
83
+ :authorize_url => authorizePath,
84
+ :token_url => tokenPath,
85
+ :raise_errors => true
86
+ }
87
+ client = OAuth2::Client.new(client,secret,options)
88
+ @client = client.client_credentials.get_token
89
+ end
90
+
91
+ # Get the OAuth2 server and endpoints from the conformance statement
92
+ # (the server should not require OAuth2 or other special security to access
93
+ # the conformance statement).
94
+ # <rest>
95
+ # <mode value="server"/>
96
+ # <documentation value="All the functionality defined in FHIR"/>
97
+ # <security>
98
+ # <extension url="http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris">
99
+ # <extension url="register">
100
+ # <valueUri value="https://authorize-dstu2.smarthealthit.org/register"/>
101
+ # </extension>
102
+ # <extension url="authorize">
103
+ # <valueUri value="https://authorize-dstu2.smarthealthit.org/authorize"/>
104
+ # </extension>
105
+ # <extension url="token">
106
+ # <valueUri value="https://authorize-dstu2.smarthealthit.org/token"/>
107
+ # </extension>
108
+ # </extension>
109
+ # <service>
110
+ # <coding>
111
+ # <system value="http://hl7.org/fhir/vs/restful-security-service"/>
112
+ # <code value="OAuth2"/>
113
+ # </coding>
114
+ # <text value="OAuth version 2 (see oauth.net)."/>
115
+ # </service>
116
+ # <description value="SMART on FHIR uses OAuth2 for authorization"/>
117
+ # </security>
118
+ def get_oauth2_metadata_from_conformance
119
+ options = {
120
+ :authorize_url => nil,
121
+ :token_url => nil
122
+ }
123
+ oauth_extension = 'http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris'
124
+ authorize_extension = 'authorize'
125
+ token_extension = 'token'
126
+ begin
127
+ conformance = conformanceStatement
128
+ conformance.rest.each do |rest|
129
+ rest.security.service.each do |service|
130
+ service.coding.each do |coding|
131
+ if coding.code == 'SMART-on-FHIR'
132
+ rest.security.extension.where({url: oauth_extension}).first.extension.each do |ext|
133
+ case ext.url
134
+ when authorize_extension
135
+ options[:authorize_url] = ext.value.value
136
+ when "#{oauth_extension}\##{authorize_extension}"
137
+ options[:authorize_url] = ext.value.value
138
+ when token_extension
139
+ options[:token_url] = ext.value.value
140
+ when "#{oauth_extension}\##{token_extension}"
141
+ options[:token_url] = ext.value.value
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ rescue Exception => e
149
+ $LOG.error 'Failed to locate SMART-on-FHIR OAuth2 Security Extensions.'
150
+ end
151
+ options.delete_if{|k,v|v.nil?}
152
+ options.clear if options.keys.size!=2
153
+ options
154
+ end
155
+
156
+ # Method returns a conformance statement for the system queried.
157
+ # @return
158
+ def conformanceStatement(format=FHIR::Formats::ResourceFormat::RESOURCE_XML)
159
+ if (@cached_conformance.nil? || format!=@default_format)
160
+ format = try_conformance_formats(format)
161
+ end
162
+ @cached_conformance
163
+ end
164
+
165
+ def try_conformance_formats(default_format)
166
+ formats = [ FHIR::Formats::ResourceFormat::RESOURCE_XML,
167
+ FHIR::Formats::ResourceFormat::RESOURCE_JSON,
168
+ 'application/xml',
169
+ 'application/json']
170
+ formats.insert(0,default_format)
171
+
172
+ @cached_conformance = nil
173
+ @default_format = nil
174
+ @default_format_bundle = nil
175
+
176
+ formats.each do |frmt|
177
+ reply = get 'metadata', fhir_headers({format: frmt})
178
+ if reply.code == 200
179
+ @cached_conformance = parse_reply(FHIR::Conformance, frmt, reply)
180
+ @default_format = frmt
181
+ @default_format_bundle = frmt
182
+ break
183
+ end
184
+ end
185
+ @default_format = default_format if @default_format.nil?
186
+ @default_format
187
+ end
188
+
189
+ def resource_url(options)
190
+ FHIR::ResourceAddress.new.resource_url(options, @use_format_param)
191
+ end
192
+
193
+ def full_resource_url(options)
194
+ @baseServiceUrl + resource_url(options)
195
+ end
196
+
197
+ def fhir_headers(options={})
198
+ FHIR::ResourceAddress.new.fhir_headers(options, @use_format_param)
199
+ end
200
+
201
+ def parse_reply(klass, format, response)
202
+ $LOG.info "Parsing response with {klass: #{klass}, format: #{format}, code: #{response.code}}."
203
+ return nil if ![200,201].include? response.code
204
+ res = nil
205
+ begin
206
+ res = nil
207
+ if(format.downcase.include?('xml'))
208
+ res = FHIR::Xml.from_xml(response.body)
209
+ else
210
+ res = FHIR::Json.from_json(response.body)
211
+ end
212
+ $LOG.warn "Expected #{klass} but got #{res.class}" if res.class!=klass
213
+ rescue Exception => e
214
+ $LOG.error "Failed to parse #{format} as resource #{klass}: #{e.message} %n #{e.backtrace.join("\n")} #{response}"
215
+ nil
216
+ end
217
+ res
218
+ end
219
+
220
+ def strip_base(path)
221
+ path.gsub(@baseServiceUrl, '')
222
+ end
223
+
224
+ def reissue_request(request)
225
+ if [:get, :delete, :head].include?(request['method'])
226
+ method(request['method']).call(request['url'], request['headers'])
227
+ elsif [:post, :put].include?(request['method'])
228
+ resource = request['headers']['resource'].constantize.from_xml(request['payload'])
229
+ method(request['method']).call(request['url'], resource, request['headers'])
230
+ end
231
+ end
232
+
233
+ private
234
+
235
+ def base_path(path)
236
+ if path.start_with?('/')
237
+ if @baseServiceUrl.end_with?('/')
238
+ @baseServiceUrl.chop
239
+ else
240
+ @baseServiceUrl
241
+ end
242
+ else
243
+ @baseServiceUrl + '/'
244
+ end
245
+ end
246
+
247
+ # Extract the request payload in the specified format, defaults to XML
248
+ def request_payload(resource, headers)
249
+ if headers
250
+ case headers["format"]
251
+ when FHIR::Formats::ResourceFormat::RESOURCE_XML
252
+ resource.to_xml
253
+ when FHIR::Formats::ResourceFormat::RESOURCE_JSON
254
+ resource.to_json
255
+ else
256
+ resource.to_xml
257
+ end
258
+ else
259
+ resource.to_xml
260
+ end
261
+ end
262
+
263
+ def request_patch_payload(patchset, format)
264
+ if (format == FHIR::Formats::PatchFormat::PATCH_JSON)
265
+ patchset.each do |patch|
266
+ # remove the resource name from the patch path, since the JSON representation doesn't have that
267
+ patch[:path] = patch[:path].slice(patch[:path].index('/')..-1)
268
+ end
269
+ patchset.to_json
270
+ elsif (format == FHIR::Formats::PatchFormat::PATCH_XML)
271
+ builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
272
+ patchset.each do |patch|
273
+ xml.diff {
274
+ # TODO: support other kinds besides just replace
275
+ xml.replace(patch[:value], sel: patch[:path] + '/@value') if patch[:op] == 'replace'
276
+ }
277
+ end
278
+ end
279
+ builder.to_xml
280
+ end
281
+ end
282
+
283
+ def clean_headers(headers)
284
+ headers.delete_if{|k,v|(k.nil? || v.nil?)}
285
+ headers.inject({}){|h,(k,v)| h[k.to_s]=v.to_s; h}
286
+ end
287
+
288
+ def scrubbed_response_headers(result)
289
+ result.each_key do |k|
290
+ v = result[k]
291
+ result[k] = v[0] if (v.is_a? Array)
292
+ end
293
+ end
294
+
295
+ def get(path, headers)
296
+ url = URI(build_url(path)).to_s
297
+ puts "GETTING: #{url}"
298
+ headers = clean_headers(headers)
299
+ if @use_oauth2_auth
300
+ # @client.refresh!
301
+ begin
302
+ response = @client.get(url, {:headers=>headers})
303
+ rescue Exception => e
304
+ response = e.response if e.response
305
+ end
306
+ req = {
307
+ :method => :get,
308
+ :url => url,
309
+ :path => url.gsub(@baseServiceUrl,''),
310
+ :headers => headers,
311
+ :payload => nil
312
+ }
313
+ res = {
314
+ :code => response.status.to_s,
315
+ :headers => response.headers,
316
+ :body => response.body
317
+ }
318
+ $LOG.info "GET - Request: #{req.to_s}, Response: #{response.body.force_encoding("UTF-8")}"
319
+ @reply = FHIR::ClientReply.new(req, res)
320
+ else
321
+ headers.merge!(@security_headers) if @use_basic_auth
322
+ @client.get(url, headers){ |response, request, result|
323
+ $LOG.info "GET - Request: #{request.to_json}, Response: #{response.force_encoding("UTF-8")}"
324
+ request.args[:path] = url.gsub(@baseServiceUrl,'')
325
+ res = {
326
+ :code => result.code,
327
+ :headers => scrubbed_response_headers(result.each_key{}),
328
+ :body => response
329
+ }
330
+ @reply = FHIR::ClientReply.new(request.args, res)
331
+ }
332
+ end
333
+ end
334
+
335
+ def post(path, resource, headers)
336
+ url = URI(build_url(path)).to_s
337
+ puts "POSTING: #{url}"
338
+ headers = clean_headers(headers)
339
+ payload = request_payload(resource, headers) if resource
340
+ if @use_oauth2_auth
341
+ # @client.refresh!
342
+ begin
343
+ response = @client.post(url, {:headers=>headers,:body=>payload})
344
+ rescue Exception => e
345
+ response = e.response if e.response
346
+ end
347
+ req = {
348
+ :method => :post,
349
+ :url => url,
350
+ :path => url.gsub(@baseServiceUrl,''),
351
+ :headers => headers,
352
+ :payload => payload
353
+ }
354
+ res = {
355
+ :code => response.status.to_s,
356
+ :headers => response.headers,
357
+ :body => response.body
358
+ }
359
+ $LOG.info "POST - Request: #{req.to_s}, Response: #{response.body.force_encoding("UTF-8")}"
360
+ @reply = FHIR::ClientReply.new(req, res)
361
+ else
362
+ headers.merge!(@security_headers) if @use_basic_auth
363
+ @client.post(url, payload, headers){ |response, request, result|
364
+ $LOG.info "POST - Request: #{request.to_json}, Response: #{response.force_encoding("UTF-8")}"
365
+ request.args[:path] = url.gsub(@baseServiceUrl,'')
366
+ res = {
367
+ :code => result.code,
368
+ :headers => scrubbed_response_headers(result.each_key{}),
369
+ :body => response
370
+ }
371
+ @reply = FHIR::ClientReply.new(request.args, res)
372
+ }
373
+ end
374
+ end
375
+
376
+ def put(path, resource, headers)
377
+ url = URI(build_url(path)).to_s
378
+ puts "PUTTING: #{url}"
379
+ headers = clean_headers(headers)
380
+ payload = request_payload(resource, headers) if resource
381
+ if @use_oauth2_auth
382
+ # @client.refresh!
383
+ begin
384
+ response = @client.put(url, {:headers=>headers,:body=>payload})
385
+ rescue Exception => e
386
+ response = e.response if e.response
387
+ end
388
+ req = {
389
+ :method => :put,
390
+ :url => url,
391
+ :path => url.gsub(@baseServiceUrl,''),
392
+ :headers => headers,
393
+ :payload => payload
394
+ }
395
+ res = {
396
+ :code => response.status.to_s,
397
+ :headers => response.headers,
398
+ :body => response.body
399
+ }
400
+ $LOG.info "PUT - Request: #{req.to_s}, Response: #{response.body.force_encoding("UTF-8")}"
401
+ @reply = FHIR::ClientReply.new(req, res)
402
+ else
403
+ headers.merge!(@security_headers) if @use_basic_auth
404
+ @client.put(url, payload, headers){ |response, request, result|
405
+ $LOG.info "PUT - Request: #{request.to_json}, Response: #{response.force_encoding("UTF-8")}"
406
+ request.args[:path] = url.gsub(@baseServiceUrl,'')
407
+ res = {
408
+ :code => result.code,
409
+ :headers => scrubbed_response_headers(result.each_key{}),
410
+ :body => response
411
+ }
412
+ @reply = FHIR::ClientReply.new(request.args, res)
413
+ }
414
+ end
415
+ end
416
+
417
+ def patch(path, patchset, headers)
418
+ url = URI(build_url(path)).to_s
419
+ puts "PATCHING: #{url}"
420
+ headers = clean_headers(headers)
421
+ payload = request_patch_payload(patchset, headers['format'])
422
+ if @use_oauth2_auth
423
+ # @client.refresh!
424
+ begin
425
+ response = @client.patch(url, {:headers=>headers,:body=>payload})
426
+ rescue Exception => e
427
+ response = e.response if e.response
428
+ end
429
+ req = {
430
+ :method => :patch,
431
+ :url => url,
432
+ :path => url.gsub(@baseServiceUrl,''),
433
+ :headers => headers,
434
+ :payload => payload
435
+ }
436
+ res = {
437
+ :code => response.status.to_s,
438
+ :headers => response.headers,
439
+ :body => response.body
440
+ }
441
+ $LOG.info "PATCH - Request: #{req.to_s}, Response: #{response.body.force_encoding("UTF-8")}"
442
+ @reply = FHIR::ClientReply.new(req, res)
443
+ else
444
+ headers.merge!(@security_headers) if @use_basic_auth
445
+ # url = 'http://requestb.in/o8juy3o8'
446
+ @client.patch(url, payload, headers){ |response, request, result|
447
+ $LOG.info "PATCH - Request: #{request.to_json}, Response: #{response.force_encoding("UTF-8")}"
448
+ request.args[:path] = url.gsub(@baseServiceUrl,'')
449
+ res = {
450
+ :code => result.code,
451
+ :headers => scrubbed_response_headers(result.each_key{}),
452
+ :body => response
453
+ }
454
+ @reply = FHIR::ClientReply.new(request.args, res)
455
+ }
456
+ end
457
+ end
458
+
459
+ def delete(path, headers)
460
+ url = URI(build_url(path)).to_s
461
+ puts "DELETING: #{url}"
462
+ headers = clean_headers(headers)
463
+ if @use_oauth2_auth
464
+ # @client.refresh!
465
+ begin
466
+ response = @client.delete(url, {:headers=>headers})
467
+ rescue Exception => e
468
+ response = e.response if e.response
469
+ end
470
+ req = {
471
+ :method => :delete,
472
+ :url => url,
473
+ :path => url.gsub(@baseServiceUrl,''),
474
+ :headers => headers,
475
+ :payload => nil
476
+ }
477
+ res = {
478
+ :code => response.status.to_s,
479
+ :headers => response.headers,
480
+ :body => response.body
481
+ }
482
+ $LOG.info "DELETE - Request: #{req.to_s}, Response: #{response.body.force_encoding("UTF-8")}"
483
+ @reply = FHIR::ClientReply.new(req, res)
484
+ else
485
+ headers.merge!(@security_headers) if @use_basic_auth
486
+ @client.delete(url, headers){ |response, request, result|
487
+ $LOG.info "DELETE - Request: #{request.to_json}, Response: #{response.force_encoding("UTF-8")}"
488
+ request.args[:path] = url.gsub(@baseServiceUrl,'')
489
+ res = {
490
+ :code => result.code,
491
+ :headers => scrubbed_response_headers(result.each_key{}),
492
+ :body => response
493
+ }
494
+ @reply = FHIR::ClientReply.new(request.args, res)
495
+ }
496
+ end
497
+ end
498
+
499
+ def head(path, headers)
500
+ headers.merge!(@security_headers) unless @security_headers.blank?
501
+ url = URI(build_url(path)).to_s
502
+ puts "HEADING: #{url}"
503
+ RestClient.head(url, headers){ |response, request, result|
504
+ $LOG.info "HEAD - Request: #{request.to_json}, Response: #{response.force_encoding("UTF-8")}"
505
+ request.args[:path] = url.gsub(@baseServiceUrl,'')
506
+ res = {
507
+ :code => result.code,
508
+ :headers => scrubbed_response_headers(result.each_key{}),
509
+ :body => response
510
+ }
511
+ @reply = FHIR::ClientReply.new(request.args, res)
512
+ }
513
+ end
514
+
515
+ def build_url(path)
516
+ if path =~ /^\w+:\/\//
517
+ path
518
+ else
519
+ "#{base_path(path)}#{path}"
520
+ end
521
+ end
522
+
523
+ end
524
+
525
+ end