fhir_client 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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