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.
@@ -0,0 +1,10 @@
1
+ module FHIR
2
+ module Formats
3
+ class FeedFormat
4
+
5
+ FEED_XML = "application/xml+fhir"
6
+ FEED_JSON = "application/json+fhir"
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,360 @@
1
+ [
2
+ {
3
+ "interaction": "read",
4
+ "path": ["/[type]/[id]"],
5
+ "verb": "GET",
6
+ "request": {
7
+ "headers": {
8
+ "Content-Type": false,
9
+ "Accept": "optional",
10
+ "Prefer": false,
11
+ "ETag": "optional",
12
+ "If-Modified-Since": "optional",
13
+ "If-None-Match": "optional"
14
+ },
15
+ "body": false
16
+ },
17
+ "response": {
18
+ "status": [200,404,410],
19
+ "headers": {
20
+ "Content-Type": true,
21
+ "Location": false,
22
+ "ETag": true,
23
+ "Last-Modified": true
24
+ },
25
+ "body": { "types": ["Resource"] }
26
+ }
27
+ },{
28
+ "interaction": "vread",
29
+ "path": ["/[type]/[id]/_history/[vid]"],
30
+ "verb": "GET",
31
+ "request": {
32
+ "headers": {
33
+ "Content-Type": false,
34
+ "Accept": "optional",
35
+ "Prefer": false
36
+ },
37
+ "body": false
38
+ },
39
+ "response": {
40
+ "status": [200,404],
41
+ "headers": {
42
+ "Content-Type": true,
43
+ "Location": false,
44
+ "ETag": true,
45
+ "Last-Modified": true
46
+ },
47
+ "body": { "types": ["Resource"] }
48
+ }
49
+ },{
50
+ "interaction": "update",
51
+ "path": ["/[type]/[id]"],
52
+ "verb": "PUT",
53
+ "request": {
54
+ "headers": {
55
+ "Content-Type": true,
56
+ "Accept": "optional",
57
+ "Prefer": "optional",
58
+ "If-Match": "optional"
59
+ },
60
+ "body": { "types": ["Resource"] }
61
+ },
62
+ "response": {
63
+ "status": [200,201,400,404,405,409,412,422],
64
+ "headers": {
65
+ "Content-Type": "optional",
66
+ "Location": "optional",
67
+ "ETag": true,
68
+ "Last-Modified": true
69
+ },
70
+ "body": { "types": ["Resource"] }
71
+ }
72
+ },{
73
+ "interaction": "delete",
74
+ "path": ["/[type]/[id]"],
75
+ "verb": "DELETE",
76
+ "request": {
77
+ "headers": {
78
+ "Content-Type": false,
79
+ "Accept": "optional",
80
+ "Prefer": false
81
+ },
82
+ "body": false
83
+ },
84
+ "response": {
85
+ "status": [200,204,404,405,409,412],
86
+ "headers": {
87
+ "Content-Type": "optional",
88
+ "Location": false,
89
+ "ETag": false,
90
+ "Last-Modified": false
91
+ },
92
+ "body": { "types": ["OperationOutcome"] }
93
+ }
94
+ },{
95
+ "interaction": "create",
96
+ "path": ["/[type]"],
97
+ "verb": "POST",
98
+ "request": {
99
+ "headers": {
100
+ "Content-Type": true,
101
+ "Accept": "optional",
102
+ "Prefer": "optional",
103
+ "If-None-Exist": "optional"
104
+ },
105
+ "body": { "types": ["Resource"] }
106
+ },
107
+ "response": {
108
+ "status": [201,400,404,405,422],
109
+ "headers": {
110
+ "Content-Type": true,
111
+ "Location": true,
112
+ "ETag": true,
113
+ "Last-Modified": true
114
+ },
115
+ "body": { "types": ["Resource"] }
116
+ }
117
+ },{
118
+ "interaction": "search",
119
+ "path": ["/[type]?"],
120
+ "verb": "GET",
121
+ "request": {
122
+ "headers": {
123
+ "Content-Type": false,
124
+ "Accept": "optional",
125
+ "Prefer": false
126
+ },
127
+ "body": false
128
+ },
129
+ "response": {
130
+ "status": [200,403],
131
+ "headers": {
132
+ "Content-Type": true,
133
+ "Location": false,
134
+ "ETag": false,
135
+ "Last-Modified": false
136
+ },
137
+ "body": { "types": ["Bundle"] }
138
+ }
139
+ },{
140
+ "interaction": "search",
141
+ "path": ["/[type]/_search?"],
142
+ "verb": "POST",
143
+ "request": {
144
+ "headers": {
145
+ "Content-Type": "application/x-www-form-urlencoded",
146
+ "Accept": "optional",
147
+ "Prefer": false
148
+ },
149
+ "body": "form data"
150
+ },
151
+ "response": {
152
+ "status": [200,403],
153
+ "headers": {
154
+ "Content-Type": true,
155
+ "Location": false,
156
+ "ETag": false,
157
+ "Last-Modified": false
158
+ },
159
+ "body": { "types": ["Bundle"] }
160
+ }
161
+ },{
162
+ "interaction": "search-all",
163
+ "path": ["/_search?"],
164
+ "verb": "GET",
165
+ "request": {
166
+ "headers": {
167
+ "Content-Type": false,
168
+ "Accept": "optional",
169
+ "Prefer": false
170
+ },
171
+ "body": false
172
+ },
173
+ "response": {
174
+ "status": [200,403],
175
+ "headers": {
176
+ "Content-Type": true,
177
+ "Location": false,
178
+ "ETag": false,
179
+ "Last-Modified": false
180
+ },
181
+ "body": { "types": ["Bundle"] }
182
+ }
183
+ },{
184
+ "interaction": "conformance",
185
+ "path": ["/","/metadata"],
186
+ "verb": ["GET","OPTIONS"],
187
+ "request": {
188
+ "headers": {
189
+ "Content-Type": false,
190
+ "Accept": "optional",
191
+ "Prefer": false
192
+ },
193
+ "body": false
194
+ },
195
+ "response": {
196
+ "status": [200,404],
197
+ "headers": {
198
+ "Content-Type": true,
199
+ "Location": false,
200
+ "ETag": false,
201
+ "Last-Modified": false
202
+ },
203
+ "body": { "types": ["Conformance"] }
204
+ }
205
+ },{
206
+ "interaction": "transaction",
207
+ "path": ["/"],
208
+ "verb": "POST",
209
+ "request": {
210
+ "headers": {
211
+ "Content-Type": true,
212
+ "Accept": "optional",
213
+ "Prefer": "optional"
214
+ },
215
+ "body": { "types": ["Bundle"] }
216
+ },
217
+ "response": {
218
+ "status": [200,400,404,405,409,412,422],
219
+ "headers": {
220
+ "Content-Type": true,
221
+ "Location": false,
222
+ "ETag": false,
223
+ "Last-Modified": false
224
+ },
225
+ "body": { "types": ["Bundle"] }
226
+ }
227
+ },{
228
+ "interaction": "history",
229
+ "path": ["/[type]/[id]/_history"],
230
+ "verb": "GET",
231
+ "request": {
232
+ "headers": {
233
+ "Content-Type": false,
234
+ "Accept": "optional",
235
+ "Prefer": false
236
+ },
237
+ "body": false
238
+ },
239
+ "response": {
240
+ "status": [200],
241
+ "headers": {
242
+ "Content-Type": true,
243
+ "Location": false,
244
+ "ETag": false,
245
+ "Last-Modified": false
246
+ },
247
+ "body": { "types": ["Bundle"] }
248
+ }
249
+ },{
250
+ "interaction": "history-type",
251
+ "path": ["/[type]/_history"],
252
+ "verb": "GET",
253
+ "request": {
254
+ "headers": {
255
+ "Content-Type": false,
256
+ "Accept": "optional",
257
+ "Prefer": false
258
+ },
259
+ "body": false
260
+ },
261
+ "response": {
262
+ "status": [200],
263
+ "headers": {
264
+ "Content-Type": true,
265
+ "Location": false,
266
+ "ETag": false,
267
+ "Last-Modified": false
268
+ },
269
+ "body": { "types": ["Bundle"] }
270
+ }
271
+ },{
272
+ "interaction": "history-all",
273
+ "path": ["/_history"],
274
+ "verb": "GET",
275
+ "request": {
276
+ "headers": {
277
+ "Content-Type": false,
278
+ "Accept": "optional",
279
+ "Prefer": false
280
+ },
281
+ "body": false
282
+ },
283
+ "response": {
284
+ "status": [200],
285
+ "headers": {
286
+ "Content-Type": true,
287
+ "Location": false,
288
+ "ETag": false,
289
+ "Last-Modified": false
290
+ },
291
+ "body": { "types": ["Bundle"] }
292
+ }
293
+ },{
294
+ "interaction": "operation",
295
+ "path": ["/$[name]","/[type]/$[name]","/[type]/[id]/$[name]"],
296
+ "verb": "POST",
297
+ "request": {
298
+ "headers": {
299
+ "Content-Type": true,
300
+ "Accept": "optional",
301
+ "Prefer": false
302
+ },
303
+ "body": { "types": ["Parameters"] }
304
+ },
305
+ "response": {
306
+ "status": [200],
307
+ "headers": {
308
+ "Content-Type": true,
309
+ "Location": false,
310
+ "ETag": false,
311
+ "Last-Modified": false
312
+ },
313
+ "body": { "types": ["Parameters","Resource"] }
314
+ }
315
+ },{
316
+ "interaction": "operation",
317
+ "path": ["/$[name]","/[type]/$[name]","/[type]/[id]/$[name]"],
318
+ "verb": "GET",
319
+ "request": {
320
+ "headers": {
321
+ "Content-Type": false,
322
+ "Accept": "optional",
323
+ "Prefer": false
324
+ },
325
+ "body": false
326
+ },
327
+ "response": {
328
+ "status": [200],
329
+ "headers": {
330
+ "Content-Type": true,
331
+ "Location": false,
332
+ "ETag": false,
333
+ "Last-Modified": false
334
+ },
335
+ "body": { "types": ["Parameters","Resource"] }
336
+ }
337
+ },{
338
+ "interaction": "operation",
339
+ "path": ["/$[name]","/[type]/$[name]","/[type]/[id]/$[name]"],
340
+ "verb": "POST",
341
+ "request": {
342
+ "headers": {
343
+ "Content-Type": "application/x-www-form-urlencoded",
344
+ "Accept": "optional",
345
+ "Prefer": false
346
+ },
347
+ "body": { "regex": "([\w\-]+(=[\w\-.:\/\|]*)?(&[\w\-]+(=[\w\-.:\/\|]*)?)*)?" }
348
+ },
349
+ "response": {
350
+ "status": [200],
351
+ "headers": {
352
+ "Content-Type": true,
353
+ "Location": false,
354
+ "ETag": false,
355
+ "Last-Modified": false
356
+ },
357
+ "body": { "types": ["Parameters","Resource"] }
358
+ }
359
+ }
360
+ ]
@@ -0,0 +1,41 @@
1
+ # Top level include file that brings in all the necessary code
2
+ require 'bundler/setup'
3
+ require 'rubygems'
4
+ require 'yaml'
5
+ require 'nokogiri'
6
+ require 'fhir_models'
7
+ require 'rest_client'
8
+ require 'addressable/uri'
9
+ require 'oauth2'
10
+ require 'active_support/core_ext'
11
+
12
+ begin
13
+ generator = FHIR::Boot::Generator.new
14
+ # 1. generate the lists of primitive data types, complex types, and resources
15
+ generator.generate_metadata
16
+ # 2. generate the complex data types
17
+ generator.generate_types
18
+ # 3. generate the base Resources
19
+ generator.generate_resources
20
+ rescue Exception => e
21
+ $LOG.error("Could not re-generate fhir models... this can happen in production, but the code does not need to be re-generated")
22
+ end
23
+
24
+ # Simple and verbose loggers
25
+ RestClient.log = Logger.new("fhir_client.log", 10, 1024000)
26
+ $LOG = Logger.new("fhir_client_verbose.log", 10, 1024000)
27
+
28
+ root = File.expand_path '..', File.dirname(File.absolute_path(__FILE__))
29
+ Dir.glob(File.join(root, 'lib','sections','**','*.rb')).each do |file|
30
+ require file
31
+ end
32
+
33
+ require_relative File.join('.','client_interface.rb')
34
+ require_relative File.join('.','resource_address.rb')
35
+ require_relative File.join('.','resource_format.rb')
36
+ require_relative File.join('.','feed_format.rb')
37
+ require_relative File.join('.','patch_format.rb')
38
+ require_relative File.join('.','model','bundle.rb')
39
+ require_relative File.join('.','model','client_reply.rb')
40
+ require_relative File.join('.','model','tag.rb')
41
+
@@ -0,0 +1,32 @@
1
+ module FHIR
2
+ class Bundle
3
+
4
+ def self_link
5
+ link.select {|n| n.relation == 'self'}.first
6
+ end
7
+
8
+ def first_link
9
+ link.select {|n| n.relation == 'first'}.first
10
+ end
11
+
12
+ def last_link
13
+ link.select {|n| n.relation == 'last'}.first
14
+ end
15
+
16
+ def next_link
17
+ link.select {|n| n.relation == 'next'}.first
18
+ end
19
+
20
+ def previous_link
21
+ link.select {|n| n.relation == 'previous' || n.relation == 'prev'}.first
22
+ end
23
+
24
+ def get_by_id(id)
25
+ entry.each do |item|
26
+ return item.resource if item.id == id || item.resource.id == id
27
+ end
28
+ nil
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,172 @@
1
+ module FHIR
2
+ class ClientReply
3
+
4
+ @@validation_rules = JSON.parse( File.open(File.join(File.expand_path('..',File.dirname(File.absolute_path(__FILE__))),'fhir_api_validation.json'),'r:UTF-8',&:read) )
5
+ @@path_regexes = {
6
+ '[type]' => "(#{FHIR::RESOURCES.join('|')})",
7
+ '[id]' => FHIR::PRIMITIVES['id']['regex'],
8
+ '[vid]' => FHIR::PRIMITIVES['id']['regex'],
9
+ '[name]' => "([A-Za-z\-]+)"
10
+ }
11
+ @@rfs1123 = /\A\s*
12
+ (?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*,\s*)?
13
+ (\d{1,2})\s+
14
+ (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+
15
+ (\d{2,})\s+
16
+ (\d{2})\s*
17
+ :\s*(\d{2})\s*
18
+ (?::\s*(\d{2}))?\s+
19
+ ([+-]\d{4}|
20
+ UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|[A-IK-Z])/ix
21
+
22
+ @@header_regexes = {
23
+ 'Content-Type' => Regexp.new("(#{FHIR::Formats::ResourceFormat::RESOURCE_XML.gsub('+','\\\+')}|#{FHIR::Formats::ResourceFormat::RESOURCE_JSON.gsub('+','\\\+')})(([ ;]+)(charset)([ =]+)(UTF-8|utf-8))?"),
24
+ 'Accept' => Regexp.new("(#{FHIR::Formats::ResourceFormat::RESOURCE_XML.gsub('+','\\\+')}|#{FHIR::Formats::ResourceFormat::RESOURCE_JSON.gsub('+','\\\+')})"),
25
+ 'Prefer' => Regexp.new("(return=minimal|return=representation)"),
26
+ 'ETag' => Regexp.new('(W\/)?"[\dA-Za-z]+"'),
27
+ 'If-Modified-Since' => @@rfs1123,
28
+ 'If-Match' => Regexp.new('(W\/)?"[\dA-Za-z]+"'),
29
+ 'If-None-Match' => Regexp.new('(W\/)?"[\dA-Za-z]+"'),
30
+ 'If-None-Exist' => Regexp.new('([\w\-]+(=[\w\-.:\/\|]*)?(&[\w\-]+(=[\w\-.:\/\|]*)?)*)?'),
31
+ 'Location' => Regexp.new("http(s)?:\/\/[A-Za-z0-9\/\\-\\.]+\/#{@@path_regexes['[type]']}\/#{@@path_regexes['[id]']}\/_history\/#{@@path_regexes['[vid]']}"),
32
+ 'Last-Modified' => @@rfs1123
33
+ }
34
+
35
+ # {
36
+ # :method => :get,
37
+ # :url => 'http://bonfire.mitre.org/fhir/Patient/123/$everything',
38
+ # :path => 'Patient/123/$everything'
39
+ # :headers => {},
40
+ # :payload => nil # body of request goes here in POST
41
+ # }
42
+ attr_accessor :request
43
+ # {
44
+ # :code => '200',
45
+ # :headers => {},
46
+ # :body => '{xml or json here}'
47
+ # }
48
+ attr_accessor :response
49
+ attr_accessor :resource # a FHIR resource
50
+ attr_accessor :resource_class # class of the :resource
51
+
52
+ def initialize(request, response)
53
+ @request = request
54
+ @response = response
55
+ end
56
+
57
+ def code
58
+ @response[:code].to_i unless @response.nil?
59
+ end
60
+
61
+ def id
62
+ return nil if @resource_class.nil?
63
+ (self_link || @request[:url]) =~ %r{(?<=#{@resource_class.name.demodulize}\/)([^\/]+)}
64
+ $1
65
+ end
66
+
67
+ def version
68
+ self_link =~ %r{(?<=_history\/)(\w+)}
69
+ $1
70
+ end
71
+
72
+ def self_link
73
+ (@response[:headers]['content-location'] || @response[:headers]['location']) unless @response.nil? || @response[:headers].nil?
74
+ end
75
+
76
+ def body
77
+ @response[:body] unless @response.nil?
78
+ end
79
+
80
+ def to_hash
81
+ hash = {}
82
+ hash['request'] = @request
83
+ hash['response'] = @response
84
+ hash
85
+ end
86
+
87
+ def is_valid?
88
+ validate.empty?
89
+ end
90
+
91
+ def validate
92
+ errors = []
93
+ @@validation_rules.each do |rule|
94
+ if rule['verb']==@request[:method].to_s.upcase
95
+ rule_match = false
96
+ rule['path'].each do |path|
97
+ rule_regex = path.gsub('/','(\/)').gsub('?','\?')
98
+ @@path_regexes.each do |token,regex|
99
+ rule_regex.gsub!(token,regex)
100
+ end
101
+ rule_match = true if(Regexp.new(rule_regex) =~ @request[:path])
102
+ end
103
+ if rule_match
104
+ # check the request headers
105
+ errors << validate_headers("#{rule['interaction'].upcase} REQUEST",@request[:headers],rule['request']['headers'])
106
+ # check the request body
107
+ errors << validate_body("#{rule['interaction'].upcase} REQUEST",@request[:payload],rule['request']['body'])
108
+ # check the response codes
109
+ if !rule['response']['status'].include?(@response[:code].to_i)
110
+ errors << "#{rule['interaction'].upcase} RESPONSE: Invalid response code: #{@response[:code]}"
111
+ end
112
+ if @response[:code].to_i < 400
113
+ # check the response headers
114
+ errors << validate_headers("#{rule['interaction'].upcase} RESPONSE",@response[:headers],rule['response']['headers'])
115
+ # check the response body
116
+ errors << validate_body("#{rule['interaction'].upcase} RESPONSE",@response[:body],rule['response']['body'])
117
+ end
118
+ end
119
+ end
120
+ end
121
+ errors.flatten
122
+ end
123
+
124
+ def validate_headers(name,headers,header_rules)
125
+ errors = []
126
+ header_rules.each do |header,present|
127
+ value = headers[header]
128
+ if present==true
129
+ if value
130
+ errors << "#{name}: Malformed value for header #{header}: #{value}" if !(@@header_regexes[header] =~ value)
131
+ else
132
+ errors << "#{name}: Missing header: #{header}"
133
+ end
134
+ elsif (present=='optional' && value)
135
+ errors << "#{name}: Malformed value for optional header #{header}: #{value}" if !(@@header_regexes[header] =~ value)
136
+ binding.pry if !(@@header_regexes[header] =~ value)
137
+ elsif !value.nil?
138
+ errors << "#{name}: Should not have header: #{header}"
139
+ end
140
+ end
141
+ errors
142
+ end
143
+
144
+ def validate_body(name,body,body_rules)
145
+ errors = []
146
+ if body && body_rules
147
+ if body_rules['types']
148
+ body_type_match = false
149
+ body_rules['types'].each do |type|
150
+ begin
151
+ content = FHIR.from_contents(body)
152
+ body_type_match = true if content.resourceType==type
153
+ body_type_match = true if type=='Resource' && FHIR::RESOURCES.include?(content.resourceType)
154
+ rescue
155
+ end
156
+ end
157
+ errors << "#{name}: Body does not match allowed types: #{body_rules['types'].join(', ')}" if !body_type_match
158
+ end
159
+ if body_rules['regex']
160
+ regex = Regexp.new(body_rules['regex'])
161
+ errors << "#{name}: Body does not match regular expression: #{body_rules['regex']}" if !(regex =~ body)
162
+ end
163
+ elsif body && !body_rules
164
+ errors "#{name}: Body not allowed"
165
+ end
166
+ errors
167
+ end
168
+
169
+ private :validate_headers, :validate_body
170
+
171
+ end
172
+ end