fhir_client 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +38 -0
- data/.travis.yml +10 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +98 -0
- data/LICENSE +201 -0
- data/README.md +135 -0
- data/Rakefile +25 -0
- data/fhir_client.gemspec +21 -0
- data/lib/client_interface.rb +525 -0
- data/lib/feed_format.rb +10 -0
- data/lib/fhir_api_validation.json +360 -0
- data/lib/fhir_client.rb +41 -0
- data/lib/model/bundle.rb +32 -0
- data/lib/model/client_reply.rb +172 -0
- data/lib/model/tag.rb +57 -0
- data/lib/patch_format.rb +10 -0
- data/lib/resource_address.rb +133 -0
- data/lib/resource_format.rb +10 -0
- data/lib/sections/crud.rb +170 -0
- data/lib/sections/feed.rb +23 -0
- data/lib/sections/history.rb +78 -0
- data/lib/sections/operations.rb +118 -0
- data/lib/sections/search.rb +53 -0
- data/lib/sections/tags.rb +64 -0
- data/lib/sections/transactions.rb +83 -0
- data/lib/sections/validate.rb +49 -0
- data/lib/tasks/tasks.rake +73 -0
- data/test/fixtures/bundle-example.xml +79 -0
- data/test/fixtures/parameters-example.json +18 -0
- data/test/fixtures/parameters-example.xml +17 -0
- data/test/simplecov.rb +17 -0
- data/test/test_helper.rb +8 -0
- data/test/unit/basic_test.rb +17 -0
- data/test/unit/bundle_test.rb +21 -0
- data/test/unit/parameters_test.rb +44 -0
- metadata +177 -0
data/lib/model/tag.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
module FHIR
|
2
|
+
class Tag
|
3
|
+
# Each Tag is part of an HTTP header named "Category" with three parts: term, scheme, and label.
|
4
|
+
# Each Tag can be in an individual "Category" header, or they can all be concatentated (with comma
|
5
|
+
# separation) inside a single "Category" header.
|
6
|
+
|
7
|
+
# Term is a URI:
|
8
|
+
# General tags:
|
9
|
+
# Bundle / FHIR Documents: "http://hl7.org/fhir/tag/document"
|
10
|
+
# Bundle / FHIR Messages: "http://hl7.org/fhir/tag/message"
|
11
|
+
# Profile tags: URL that references a profile resource.
|
12
|
+
attr_accessor :term
|
13
|
+
|
14
|
+
# Scheme is a URI:
|
15
|
+
# "http://hl7.org/fhir/tag" A general tag
|
16
|
+
# "http://hl7.org/fhir/tag/profile" A profile tag - a claim that the Resource conforms to the profile identified in the term
|
17
|
+
# "http://hl7.org/fhir/tag/security" A security label
|
18
|
+
attr_accessor :scheme
|
19
|
+
|
20
|
+
# Label is an OPTIONAL human-readable label for the tag for use when displaying in end-user applications
|
21
|
+
attr_accessor :label
|
22
|
+
|
23
|
+
def to_header
|
24
|
+
s = "#{term}; scheme=#{scheme}"
|
25
|
+
s += "; label=#{label}" if !label.nil?
|
26
|
+
s
|
27
|
+
end
|
28
|
+
|
29
|
+
# Parses a string named "header" and returns a Tag object.
|
30
|
+
def self.parse_tag(header)
|
31
|
+
h = FHIR::Tag.new
|
32
|
+
regex = /\s*;\s*/
|
33
|
+
tokens = header.strip.split(regex)
|
34
|
+
h.term = tokens.shift
|
35
|
+
tokens.each do |token|
|
36
|
+
if !token.strip.index('scheme').nil?
|
37
|
+
token.strip =~ %r{(?<=scheme)(\s*)=(\s*)([\".:_\-\/\w]+)}
|
38
|
+
h.scheme = $3
|
39
|
+
elsif !token.strip.index('label').nil?
|
40
|
+
token.strip =~ %r{(?<=label)(\s*)=(\s*)([\".:_\-\/\w\s]+)}
|
41
|
+
h.label = $3
|
42
|
+
end
|
43
|
+
end
|
44
|
+
h
|
45
|
+
end
|
46
|
+
|
47
|
+
# Parses a string named "header" and returns an Array of Tag objects.
|
48
|
+
def self.parse_tags(header)
|
49
|
+
tags = []
|
50
|
+
regex = /\s*,\s*/
|
51
|
+
tokens = header.strip.split(regex)
|
52
|
+
tokens.each { |token| tags << FHIR::Tag.parse_tag(token) }
|
53
|
+
tags
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
data/lib/patch_format.rb
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
module FHIR
|
2
|
+
class ResourceAddress
|
3
|
+
|
4
|
+
DEFAULTS = {
|
5
|
+
id: nil,
|
6
|
+
resource: nil,
|
7
|
+
format: 'application/xml+fhir',
|
8
|
+
}
|
9
|
+
|
10
|
+
DEFAULT_CHARSET = 'UTF-8'
|
11
|
+
|
12
|
+
def fhir_headers(options, use_format_param=false)
|
13
|
+
options = DEFAULTS.merge(options)
|
14
|
+
|
15
|
+
format = options[:format] || FHIR::Formats::ResourceFormat::RESOURCE_XML
|
16
|
+
fhir_headers = {
|
17
|
+
'User-Agent' => 'Ruby FHIR Client',
|
18
|
+
'Content-Type' => format + ';charset=' + DEFAULT_CHARSET,
|
19
|
+
'Accept-Charset' => DEFAULT_CHARSET
|
20
|
+
}
|
21
|
+
# remove the content-type header if the format is 'xml' or 'json' because
|
22
|
+
# even those are valid _format parameter options, they are not valid MimeTypes.
|
23
|
+
fhir_headers.delete('Content-Type') if ['xml','json'].include?(format.downcase)
|
24
|
+
|
25
|
+
if(options[:category])
|
26
|
+
# options[:category] should be an Array of FHIR::Tag objects
|
27
|
+
tags = {
|
28
|
+
'Category' => options[:category].collect { |h| h.to_header }.join(',')
|
29
|
+
}
|
30
|
+
fhir_headers.merge!(tags)
|
31
|
+
options.delete(:category)
|
32
|
+
end
|
33
|
+
|
34
|
+
if use_format_param
|
35
|
+
fhir_headers.delete('Accept')
|
36
|
+
options.delete('Accept')
|
37
|
+
options.delete(:accept)
|
38
|
+
else
|
39
|
+
fhir_headers['Accept'] = format
|
40
|
+
end
|
41
|
+
|
42
|
+
fhir_headers.merge!(options) unless options.empty?
|
43
|
+
fhir_headers[:operation] = options[:operation][:name] if options[:operation] && options[:operation][:name]
|
44
|
+
fhir_headers
|
45
|
+
end
|
46
|
+
|
47
|
+
def resource_url(options, use_format_param=false)
|
48
|
+
options = DEFAULTS.merge(options)
|
49
|
+
|
50
|
+
params = {}
|
51
|
+
url = ""
|
52
|
+
# handle requests for resources by class or string; useful for testing nonexistent resource types
|
53
|
+
url += "/#{ options[:resource].try(:name).try(:demodulize) || options[:resource].split("::").last }" if options[:resource]
|
54
|
+
url += "/#{options[:id]}" if options[:id]
|
55
|
+
url += "/$validate" if options[:validate]
|
56
|
+
|
57
|
+
if(options[:operation])
|
58
|
+
opr = options[:operation]
|
59
|
+
p = opr[:parameters]
|
60
|
+
p = p.each{|k,v|p[k]=v[:value]} if p
|
61
|
+
params.merge!(p) if p && opr[:method]=='GET'
|
62
|
+
|
63
|
+
if (opr[:name] == :fetch_patient_record)
|
64
|
+
url += "/$everything"
|
65
|
+
elsif (opr[:name] == :value_set_expansion)
|
66
|
+
url += "/$expand"
|
67
|
+
elsif (opr && opr[:name]== :value_set_based_validation)
|
68
|
+
url += "/$validate-code"
|
69
|
+
elsif (opr && opr[:name]== :code_system_lookup)
|
70
|
+
url += "/$lookup"
|
71
|
+
elsif (opr && opr[:name]== :concept_map_translate)
|
72
|
+
url += "/$translate"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
if (options[:history])
|
77
|
+
history = options[:history]
|
78
|
+
url += "/_history/#{history[:id]}"
|
79
|
+
params[:_count] = history[:count] if history[:count]
|
80
|
+
params[:_since] = history[:since].iso8601 if history[:since]
|
81
|
+
end
|
82
|
+
|
83
|
+
if(options[:search])
|
84
|
+
search_options = options[:search]
|
85
|
+
url += '/_search' if search_options[:flag]
|
86
|
+
url += "/#{search_options[:compartment]}" if search_options[:compartment]
|
87
|
+
|
88
|
+
if search_options[:parameters]
|
89
|
+
search_options[:parameters].each do |key,value|
|
90
|
+
params[key.to_sym] = value
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# options[:params] is simply appended at the end of a url and is used by testscripts
|
96
|
+
url += options[:params] if options[:params]
|
97
|
+
|
98
|
+
if(options[:summary])
|
99
|
+
params[:_summary] = options[:summary]
|
100
|
+
end
|
101
|
+
|
102
|
+
if use_format_param && options[:format]
|
103
|
+
params[:_format] = options[:format]
|
104
|
+
end
|
105
|
+
|
106
|
+
uri = Addressable::URI.parse(url)
|
107
|
+
# params passed in options takes precidence over params calculated in this method
|
108
|
+
# for use by testscript primarily
|
109
|
+
uri.query_values = params unless options[:params] && options[:params].include?("?")
|
110
|
+
uri.normalize.to_str
|
111
|
+
end
|
112
|
+
|
113
|
+
# Get the resource ID out of the URL (e.g. Bundle.entry.response.location)
|
114
|
+
def self.pull_out_id(resourceType,url)
|
115
|
+
id = nil
|
116
|
+
if !resourceType.nil? && !url.nil?
|
117
|
+
token = "#{resourceType}/"
|
118
|
+
start = url.index(token) + token.length
|
119
|
+
t = url[start..-1]
|
120
|
+
stop = (t.index("/") || 0)-1
|
121
|
+
stop = -1 if stop.nil?
|
122
|
+
id = t[0..stop]
|
123
|
+
end
|
124
|
+
id
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.append_forward_slash_to_path(path)
|
128
|
+
path += '/' unless path.last == '/'
|
129
|
+
path
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
module FHIR
|
2
|
+
module Sections
|
3
|
+
module Crud
|
4
|
+
|
5
|
+
#
|
6
|
+
# Read the current state of a resource.
|
7
|
+
#
|
8
|
+
def read(klass, id, format=@default_format, summary=nil, options = {})
|
9
|
+
options = { resource: klass, id: id, format: format }.merge(options)
|
10
|
+
options[:summary] = summary if summary
|
11
|
+
reply = get resource_url(options), fhir_headers(options)
|
12
|
+
reply.resource = parse_reply(klass, format, reply)
|
13
|
+
reply.resource_class = klass
|
14
|
+
reply
|
15
|
+
end
|
16
|
+
|
17
|
+
#
|
18
|
+
# Read a resource bundle (an XML ATOM feed)
|
19
|
+
#
|
20
|
+
def read_feed(klass, format=@default_format_bundle)
|
21
|
+
options = { resource: klass, format: format }
|
22
|
+
reply = get resource_url(options), fhir_headers(options)
|
23
|
+
reply.resource = parse_reply(klass, format, reply)
|
24
|
+
reply.resource_class = klass
|
25
|
+
reply
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Read the state of a specific version of the resource
|
30
|
+
#
|
31
|
+
def vread(klass, id, version_id, format=@default_format)
|
32
|
+
options = { resource: klass, id: id, format: format, history: {id: version_id} }
|
33
|
+
reply = get resource_url(options), fhir_headers(options)
|
34
|
+
reply.resource = parse_reply(klass, format, reply)
|
35
|
+
reply.resource_class = klass
|
36
|
+
reply
|
37
|
+
end
|
38
|
+
|
39
|
+
def raw_read(options)
|
40
|
+
reply = get resource_url(options), fhir_headers(options)
|
41
|
+
reply.body
|
42
|
+
end
|
43
|
+
|
44
|
+
def raw_read_url(url)
|
45
|
+
reply = get url, fhir_headers({})
|
46
|
+
reply.body
|
47
|
+
end
|
48
|
+
|
49
|
+
#
|
50
|
+
# Update an existing resource by its id or create it if it is a new resource, not present on the server
|
51
|
+
#
|
52
|
+
def update(resource, id, format=@default_format)
|
53
|
+
base_update(resource, id, nil, format)
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# Update an existing resource by its id or create it if it is a new resource, not present on the server
|
58
|
+
#
|
59
|
+
def conditional_update(resource, id, searchParams, format=@default_format)
|
60
|
+
options = {
|
61
|
+
:search => {
|
62
|
+
:flag => false,
|
63
|
+
:compartment => nil,
|
64
|
+
:parameters => {}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
searchParams.each do |key,value|
|
68
|
+
options[:search][:parameters][key] = value
|
69
|
+
end
|
70
|
+
base_update(resource, id, options, format)
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# Update an existing resource by its id or create it if it is a new resource, not present on the server
|
75
|
+
#
|
76
|
+
def base_update(resource, id, options, format)
|
77
|
+
options = {} if options.nil?
|
78
|
+
options[:resource] = resource.class
|
79
|
+
options[:format] = format
|
80
|
+
options[:id] = id
|
81
|
+
reply = put resource_url(options), resource, fhir_headers(options)
|
82
|
+
reply.resource = parse_reply(resource.class, format, reply)
|
83
|
+
reply.resource_class = resource.class
|
84
|
+
reply
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# Partial update using a patchset (PATCH)
|
89
|
+
#
|
90
|
+
def partial_update(klass, id, patchset, options={}, format=@default_format)
|
91
|
+
options = { resource: klass, id: id, format: format }.merge options
|
92
|
+
|
93
|
+
if (format == FHIR::Formats::ResourceFormat::RESOURCE_XML)
|
94
|
+
options[:format] = FHIR::Formats::PatchFormat::PATCH_XML
|
95
|
+
options[:Accept] = format
|
96
|
+
elsif (format == FHIR::Formats::ResourceFormat::RESOURCE_JSON)
|
97
|
+
options[:format] = FHIR::Formats::PatchFormat::PATCH_JSON
|
98
|
+
options[:Accept] = format
|
99
|
+
end
|
100
|
+
|
101
|
+
reply = patch resource_url(options), patchset, fhir_headers(options)
|
102
|
+
reply.resource = parse_reply(klass, format, reply)
|
103
|
+
reply.resource_class = klass
|
104
|
+
reply
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# Delete the resource with the given ID.
|
109
|
+
#
|
110
|
+
def destroy(klass, id=nil, options={})
|
111
|
+
options = { resource: klass, id: id, format: nil }.merge options
|
112
|
+
reply = delete resource_url(options), fhir_headers(options)
|
113
|
+
reply.resource_class = klass
|
114
|
+
reply
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# Create a new resource with a server assigned id. Return the newly created
|
119
|
+
# resource with the id the server assigned.
|
120
|
+
#
|
121
|
+
def create(resource, format=@default_format)
|
122
|
+
base_create(resource, nil ,format)
|
123
|
+
end
|
124
|
+
|
125
|
+
#
|
126
|
+
# Conditionally create a new resource with a server assigned id.
|
127
|
+
#
|
128
|
+
def conditional_create(resource, ifNoneExistParameters, format=@default_format)
|
129
|
+
query = ''
|
130
|
+
ifNoneExistParameters.each do |key,value|
|
131
|
+
query += "#{key.to_s}=#{value.to_s}&"
|
132
|
+
end
|
133
|
+
query = query[0..-2] # strip off the trailing ampersand
|
134
|
+
options = {}
|
135
|
+
options['If-None-Exist'] = query
|
136
|
+
base_create(resource, options, format)
|
137
|
+
end
|
138
|
+
|
139
|
+
#
|
140
|
+
# Create a new resource with a server assigned id. Return the newly created
|
141
|
+
# resource with the id the server assigned.
|
142
|
+
#
|
143
|
+
def base_create(resource, options, format)
|
144
|
+
options = {} if options.nil?
|
145
|
+
options[:resource] = resource.class
|
146
|
+
options[:format] = format
|
147
|
+
reply = post resource_url(options), resource, fhir_headers(options)
|
148
|
+
if [200,201].include? reply.code
|
149
|
+
type = reply.response[:headers][:content_type]
|
150
|
+
if !type.nil?
|
151
|
+
if type.include?('xml') && !reply.body.empty?
|
152
|
+
reply.resource = resource.class.from_xml(reply.body)
|
153
|
+
elsif type.include?('json') && !reply.body.empty?
|
154
|
+
reply.resource = resource.class.from_fhir_json(reply.body)
|
155
|
+
else
|
156
|
+
reply.resource = resource # just send back the submitted resource
|
157
|
+
end
|
158
|
+
else
|
159
|
+
reply.resource = resource # don't know the content type, so return the resource provided
|
160
|
+
end
|
161
|
+
else
|
162
|
+
reply.resource = resource # just send back the submitted resource
|
163
|
+
end
|
164
|
+
reply.resource_class = resource.class
|
165
|
+
reply
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module FHIR
|
2
|
+
module Sections
|
3
|
+
module Feed
|
4
|
+
|
5
|
+
FORWARD = :next_link
|
6
|
+
BACKWARD = :previous_link
|
7
|
+
FIRST = :first_link
|
8
|
+
LAST = :last_link
|
9
|
+
|
10
|
+
def next_page(current, page=FORWARD)
|
11
|
+
bundle = current.resource
|
12
|
+
link = bundle.method(page).call
|
13
|
+
return nil unless link
|
14
|
+
reply = get strip_base(link.url), fhir_headers
|
15
|
+
reply.resource = parse_reply(current.resource_class, @default_format_bundle, reply)
|
16
|
+
reply.resource_class = current.resource_class
|
17
|
+
reply
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module FHIR
|
2
|
+
module Sections
|
3
|
+
module History
|
4
|
+
#
|
5
|
+
# Create a new resource with a server assigned id. Return the newly created
|
6
|
+
# resource with the id the server assigned. Associates tags with newly created resource.
|
7
|
+
#
|
8
|
+
# @param resourceClass
|
9
|
+
# @param resource
|
10
|
+
# @return
|
11
|
+
#
|
12
|
+
# public <T extends Resource> AtomEntry<OperationOutcome> create(Class<T> resourceClass, T resource, List<AtomCategory> tags);
|
13
|
+
|
14
|
+
#
|
15
|
+
# Retrieve the update history for a resource with given id since last update time.
|
16
|
+
# Last update may be null TODO - ensure this is the case.
|
17
|
+
#
|
18
|
+
# @param lastUpdate
|
19
|
+
# @param resourceClass
|
20
|
+
# @param id
|
21
|
+
# @return
|
22
|
+
#
|
23
|
+
# public <T extends Resource> AtomFeed history(Calendar lastUpdate, Class<T> resourceClass, String id);
|
24
|
+
# public <T extends Resource> AtomFeed history(DateAndTime lastUpdate, Class<T> resourceClass, String id);
|
25
|
+
|
26
|
+
def history(options)
|
27
|
+
reply = get resource_url(options), fhir_headers(options).except(:history)
|
28
|
+
reply.resource = parse_reply(options[:resource], @default_format_bundle, reply)
|
29
|
+
reply.resource_class = options[:resource]
|
30
|
+
reply
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Retrieve the entire update history for a resource with the given id.
|
35
|
+
# Last update may be null TODO - ensure this is the case.
|
36
|
+
#
|
37
|
+
# @param resourceClass
|
38
|
+
# @param id
|
39
|
+
# @param lastUpdate
|
40
|
+
# @return
|
41
|
+
#
|
42
|
+
def resource_instance_history_as_of(klass, id, lastUpdate)
|
43
|
+
history(resource: klass, id: id, history:{since: lastUpdate})
|
44
|
+
end
|
45
|
+
|
46
|
+
def resource_instance_history(klass, id)
|
47
|
+
history(resource: klass, id: id, history:{})
|
48
|
+
end
|
49
|
+
|
50
|
+
def resource_history(klass)
|
51
|
+
history(resource: klass, history:{})
|
52
|
+
end
|
53
|
+
|
54
|
+
def resource_history_as_of(klass, lastUpdate)
|
55
|
+
history(resource: klass, history:{since: lastUpdate})
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Retrieve the update history for all resource types since the start of server records.
|
60
|
+
#
|
61
|
+
def all_history
|
62
|
+
history(history:{})
|
63
|
+
end
|
64
|
+
|
65
|
+
#
|
66
|
+
# Retrieve the update history for all resource types since a specific last update date/time.
|
67
|
+
#
|
68
|
+
# Note:
|
69
|
+
# @param lastUpdate
|
70
|
+
# @return
|
71
|
+
#
|
72
|
+
def all_history_as_of(lastUpdate)
|
73
|
+
history(history:{since: lastUpdate})
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|