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.
- 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
|