fhir_client 1.0.1

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