cqm-converter 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ require 'active_support/inflector'
2
+ require 'health-data-standards'
3
+ require 'cqm/models'
4
+
5
+ # Base CQM module.
6
+ module CQM
7
+ # Base CQM Converter module.
8
+ module Converter
9
+ end
10
+ end
11
+
12
+ require_relative 'converter/utils'
13
+ require_relative 'converter/hds_record'
14
+ require_relative 'converter/qdm_patient'
@@ -0,0 +1,125 @@
1
+ require 'execjs'
2
+ require 'sprockets'
3
+
4
+ # CQM Converter module for HDS models.
5
+ module CQM::Converter
6
+ # CQM Converter class for HDS based records.
7
+ class HDSRecord
8
+ # Initialize a new HDSRecord converter. NOTE: This should be done once, and then
9
+ # used for every HDS Record you want to convert, since it takes a few seconds
10
+ # to initialize the conversion environment using Sprockets.
11
+ def initialize
12
+ # Create a new sprockets environment.
13
+ environment = Sprockets::Environment.new
14
+
15
+ # Populate the JavaScript environment with the cql_qdm_patientapi mappings and
16
+ # its dependencies.
17
+ cql_qdm_patientapi_spec = Gem::Specification.find_by_name('cql_qdm_patientapi')
18
+ momentjs_rails_spec = Gem::Specification.find_by_name('momentjs-rails')
19
+ environment.append_path(cql_qdm_patientapi_spec.gem_dir + '/app/assets/javascripts')
20
+ environment.append_path(cql_qdm_patientapi_spec.gem_dir + '/vendor/assets/javascripts')
21
+ environment.append_path(momentjs_rails_spec.gem_dir + '/vendor/assets/javascripts')
22
+ @js_dependencies = environment['moment'].to_s
23
+ @js_dependencies += environment['cql4browsers'].to_s
24
+ @js_dependencies += environment['cql_qdm_patientapi'].to_s
25
+ @qdm_model_attrs = Utils.gather_qdm_model_attrs
26
+ end
27
+
28
+ # Given an HDS record, return a corresponding QDM patient.
29
+ def to_qdm(record)
30
+ # Start with a new QDM patient.
31
+ patient = QDM::Patient.new
32
+
33
+ # Build and execute JavaScript that will create a 'CQL_QDM.Patient'
34
+ # JavaScript version of the HDS record. Specifically, we will use
35
+ # this to build our patient's 'dataElements'.
36
+ cql_qdm_patient = ExecJS.exec Utils.hds_to_qdm_js(@js_dependencies, record, @qdm_model_attrs)
37
+
38
+ # Make sure all date times are in the correct form.
39
+ Utils.date_time_adjuster(cql_qdm_patient) if cql_qdm_patient
40
+
41
+ # Grab the results from the CQL_QDM.Patient and add a new 'data_element'
42
+ # for each datatype found on the CQL_QDM.Patient to the new QDM Patient.
43
+ cql_qdm_patient.keys.each do |dc_type|
44
+ cql_qdm_patient[dc_type].each do |dc|
45
+ # Convert snake_case to camelCase
46
+ dc_fixed_keys = dc.deep_transform_keys { |key| key.to_s.camelize(:lower) }
47
+
48
+ # Our Code model uses 'codeSystem' to describe the code system (since system is
49
+ # a reserved keyword). The cql.Code calls this 'system', so make sure the proper
50
+ # conversion is made. Also do this for 'display', where we call this descriptor.
51
+ dc_fixed_keys = dc_fixed_keys.deep_transform_keys { |key| key.to_s == 'system' ? :codeSystem : key }
52
+ dc_fixed_keys = dc_fixed_keys.deep_transform_keys { |key| key.to_s == 'display' ? :descriptor : key }
53
+
54
+ patient.dataElements << QDM.const_get(dc_type).new.from_json(dc_fixed_keys.to_json)
55
+ end
56
+ end
57
+
58
+ # Convert patient characteristic birthdate.
59
+ birthdate = record.birthdate
60
+ if birthdate
61
+ birth_datetime = DateTime.strptime(birthdate.to_s, '%s')
62
+ code = QDM::Code.new('21112-8', 'LOINC')
63
+ patient.dataElements << QDM::PatientCharacteristicBirthdate.new(birthDatetime: birth_datetime, dataElementCodes: [code])
64
+ end
65
+
66
+ # Convert patient characteristic clinical trial participant.
67
+ # TODO, Adam 4/1: The Bonnie team is working on implementing this in HDS. When that work
68
+ # is complete, this should be updated to reflect how that looks in HDS.
69
+ # patient.dataElements << QDM::PatientCharacteristicClinicalTrialParticipant.new
70
+
71
+ # Convert patient characteristic ethnicity.
72
+ ethnicity = record.ethnicity
73
+ if ethnicity
74
+ code = QDM::Code.new(ethnicity['code'], ethnicity['codeSystem'], ethnicity['name'], Utils.code_system_helper(ethnicity['codeSystem']))
75
+ patient.dataElements << QDM::PatientCharacteristicEthnicity.new(dataElementCodes: [code])
76
+ end
77
+
78
+ # Convert patient characteristic expired.
79
+ expired = record.deathdate
80
+ if expired
81
+ expired_datetime = DateTime.strptime(expired.to_s, '%s')
82
+ code = QDM::Code.new('419099009', 'SNOMED-CT')
83
+ patient.dataElements << QDM::PatientCharacteristicExpired.new(expiredDatetime: expired_datetime, dataElementCodes: [code])
84
+ end
85
+
86
+ # Convert patient characteristic race.
87
+ race = record.race
88
+ if race
89
+ code = QDM::Code.new(race['code'], race['codeSystem'], race['name'], Utils.code_system_helper(race['codeSystem']))
90
+ patient.dataElements << QDM::PatientCharacteristicRace.new(dataElementCodes: [code])
91
+ end
92
+
93
+ # Convert patient characteristic sex.
94
+ sex = record.gender
95
+ if sex
96
+ code = QDM::Code.new(sex, 'AdministrativeSex', Utils.code_system_helper('AdministrativeSex'))
97
+ patient.dataElements << QDM::PatientCharacteristicSex.new(dataElementCodes: [code])
98
+ end
99
+
100
+ # Convert remaining metadata.
101
+ patient.birthDatetime = DateTime.strptime(record.birthdate.to_s, '%s') if record.birthdate
102
+ patient.givenNames = record.first ? [record.first] : []
103
+ patient.familyName = record.last if record.last
104
+ patient.bundleId = record.bundle_id if record.bundle_id
105
+
106
+ # Convert extended_data.
107
+ patient.extendedData = {}
108
+ patient.extendedData['type'] = record.type if record.respond_to?('type')
109
+ patient.extendedData['measure_ids'] = record.measure_ids if record.respond_to?('measure_ids')
110
+ patient.extendedData['source_data_criteria'] = record.source_data_criteria if record.respond_to?('source_data_criteria')
111
+ patient.extendedData['expected_values'] = record.expected_values if record.respond_to?('expected_values')
112
+ patient.extendedData['notes'] = record.notes if record.respond_to?('notes')
113
+ patient.extendedData['is_shared'] = record.is_shared if record.respond_to?('is_shared')
114
+ patient.extendedData['origin_data'] = record.origin_data if record.respond_to?('origin_data')
115
+ patient.extendedData['test_id'] = record.test_id if record.respond_to?('test_id')
116
+ patient.extendedData['medical_record_number'] = record.medical_record_number if record.respond_to?('medical_record_number')
117
+ patient.extendedData['medical_record_assigner'] = record.medical_record_assigner if record.respond_to?('medical_record_assigner')
118
+ patient.extendedData['description'] = record.description if record.respond_to?('description')
119
+ patient.extendedData['description_category'] = record.description_category if record.respond_to?('description_category')
120
+ patient.extendedData['insurance_providers'] = record.insurance_providers.to_json(except: '_id') if record.respond_to?('insurance_providers')
121
+
122
+ patient
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,325 @@
1
+ # CQM Converter module for QDM models.
2
+ module CQM::Converter
3
+ # CQM Converter class for QDM based patients.
4
+ class QDMPatient
5
+ # Initialize a new QDMPatient converter. NOTE: This should be done once, and then
6
+ # used for every QDM Patient you want to convert, since it takes a few seconds
7
+ # to initialize the conversion environment.
8
+ def initialize
9
+ @qdm_model_attrs = Utils.gather_qdm_model_attrs
10
+ @qdm_to_hds_mappings = Utils.gather_qdm_to_hds_mappings(@qdm_model_attrs)
11
+ end
12
+
13
+ # Given a QDM patient, return a corresponding HDS record.
14
+ def to_hds(patient)
15
+ # Start with a new HDS record.
16
+ record = Record.new
17
+
18
+ # Loop over the QDM Patient's data elements, and create the corresponding
19
+ # HDS Record Entry models on the newly created record.
20
+ patient.dataElements.each do |data_element|
21
+ category = data_element.category if data_element.fields.include? 'category'
22
+ next unless category
23
+ # Handle patient characteristics seperately.
24
+ next if data_element.category == 'patient_characteristic'
25
+
26
+ # Grab the QDM datatype name of this data element.
27
+ qdm_model_name = data_element.class.name.demodulize
28
+
29
+ # Handle mismatched HDS names for QDM things.
30
+ category = Utils.qdm_to_hds_class_type(category)
31
+
32
+ # Grab the corresponding HDS model we will cast this QDM model into.
33
+ hds_model = category.camelize.constantize
34
+
35
+ # Start with a new HDS entry.
36
+ hds_entry = hds_model.new
37
+
38
+ # Populate codes.
39
+ hds_entry.codes = Utils.qdm_codes_to_hds_codes(data_element.dataElementCodes)
40
+
41
+ # Populate OID.
42
+ hds_entry.oid = data_element.hqmfOid
43
+
44
+ # Populate description.
45
+ hds_entry.description = data_element.description
46
+
47
+ # Set type.
48
+ hds_entry.set(_type: hds_model.to_s)
49
+
50
+ # Set status_code.
51
+ status = data_element.fields.include?('qdmStatus') ? data_element[:qdmStatus] : nil
52
+ status = 'ordered' if status == 'order'
53
+ hds_entry.status_code = { 'HL7 ActStatus': [status] }
54
+
55
+ # Grab the QDM attribute mappings for this data element, and construct the
56
+ # corresponding HDS attributes.
57
+ hds_attrs = {}
58
+ @qdm_to_hds_mappings[qdm_model_name].each do |qdm_attr, hds_attr|
59
+ next if data_element[qdm_attr].nil?
60
+ extracted_value = extractor(data_element[qdm_attr].as_json)
61
+ if hds_attr.is_a?(Hash) && hds_attr[:low]
62
+ # Handle something that has multiple parts.
63
+ hds_attrs[hds_attr[:low]] = extracted_value.first
64
+ hds_attrs[hds_attr[:high]] = extracted_value.last if hds_attr[:high]
65
+ elsif extracted_value.is_a?(Array) && extracted_value.any? && extracted_value.first.is_a?(Hash) && extracted_value.first[:code]
66
+ # Handle a result that is returning multiple codes.
67
+ hds_attrs[hds_attr] = [CodedResultValue.new(codes: Utils.qdm_codes_to_hds_codes(extracted_value), description: extracted_value.first[:title])]
68
+ elsif extracted_value.is_a?(Array)
69
+ # Handle simple Arrays.
70
+ hds_attrs[hds_attr] = { values: extracted_value } if extracted_value.any?
71
+ elsif hds_attr == 'values' && extracted_value.key?(:scalar)
72
+ # Handle a Quantity.
73
+ hds_attrs[hds_attr] = [PhysicalQuantityResultValue.new(extracted_value)]
74
+ elsif hds_attr == 'values' && extracted_value.key?(:code)
75
+ # Handle a Code result.
76
+ hds_attrs[hds_attr] = [CodedResultValue.new(codes: Utils.qdm_codes_to_hds_codes([extracted_value]), description: extracted_value[:title])]
77
+ elsif hds_attr == 'dose'
78
+ # Handle a dosage.
79
+ hds_attrs[hds_attr] = dose_extractor(extracted_value)
80
+ else
81
+ # Nothing special.
82
+ hds_attrs[hds_attr] = extracted_value
83
+ end
84
+ end
85
+
86
+ # If there is an actual negationReason, set negationInd to true.
87
+ if hds_attrs.key?('negationReason') && !hds_attrs['negationReason'].nil?
88
+ hds_attrs['negationInd'] = true
89
+ end
90
+
91
+ # Unpack references.
92
+ unpack_references(hds_attrs)
93
+
94
+ # Unpack facility.
95
+ unpack_facility(hds_attrs)
96
+
97
+ # Unpack diagnosis.
98
+ unpack_diagnosis(hds_attrs)
99
+
100
+ # Unpack components.
101
+ unpack_components(hds_attrs)
102
+
103
+ # Communication entries need direction, which we can get from the QDM model name.
104
+ if hds_entry._type == 'Communication'
105
+ hds_attrs['direction'] = qdm_model_name.underscore
106
+ end
107
+
108
+ # Ignore infinity dates.
109
+ Utils.fix_infinity_dates(hds_attrs)
110
+
111
+ # Apply the attributes to the entry.
112
+ hds_entry.set(hds_attrs)
113
+
114
+ # Add entry to HDS record, make sure to handle tricky plural types.
115
+ plural_category = ['medical_equipment'].include?(category) ? category : category.pluralize
116
+ record.send(plural_category) << hds_entry unless hds_entry.codes.empty?
117
+ end
118
+
119
+ # Unpack patient characteristics.
120
+ unpack_patient_characteristics(patient, record)
121
+
122
+ # Unpack extended_data.
123
+ unpack_extended_data(patient, record)
124
+
125
+ record
126
+ end
127
+
128
+ private
129
+
130
+ # Given something QDM model based, return a corresponding HDS
131
+ # representation. This will operate recursively.
132
+ def extractor(qdm_thing)
133
+ keys = qdm_thing.symbolize_keys.keys if qdm_thing.class.to_s == 'Hash'
134
+ if qdm_thing.nil? # Is nothing.
135
+ nil
136
+ elsif ['Time', 'DateTime', 'Date'].include? qdm_thing.class.to_s # Is a DateTime.
137
+ date_time_converter(qdm_thing)
138
+ elsif qdm_thing.is_a?(String) && date_time?(qdm_thing) # Is a DateTime.
139
+ date_time_converter(qdm_thing)
140
+ elsif qdm_thing.is_a?(Hash) && keys.include?(:low) # Is a QDM::Interval.
141
+ interval_extractor(qdm_thing.symbolize_keys)
142
+ elsif qdm_thing.is_a?(Hash) && keys.include?(:code) # Is a QDM::Code.
143
+ code_extractor(qdm_thing.symbolize_keys)
144
+ elsif qdm_thing.is_a?(Hash) && keys.include?(:unit) # Is a QDM::Quantity.
145
+ quantity_extractor(qdm_thing.symbolize_keys)
146
+ elsif qdm_thing.is_a?(Array) # Is an Array.
147
+ qdm_thing.collect { |item| extractor(item) }
148
+ elsif qdm_thing.is_a?(Numeric) # Is an Number.
149
+ { units: '', scalar: qdm_thing.to_s }
150
+ elsif qdm_thing.is_a?(String) # Is an String.
151
+ qdm_thing
152
+ elsif qdm_thing.is_a?(Hash)
153
+ qdm_thing.each { |k, v| qdm_thing[k] = extractor(v) }
154
+ else
155
+ raise 'Unsupported type! Found: ' + qdm_thing.class.to_s
156
+ end
157
+ end
158
+
159
+ # Extract a QDM::Code to something usable in HDS.
160
+ def code_extractor(code)
161
+ { code: code[:code], code_system: code[:codeSystem], title: code[:descriptor] }
162
+ end
163
+
164
+ # Extract a QDM::Interval to something usable in HDS.
165
+ def interval_extractor(interval)
166
+ # If this interval has a high of inifinity, nil it out.
167
+ interval[:high] = nil if interval[:high] == '9999-12-31T23:59:59.99+0000'
168
+ [extractor(interval[:low]), extractor(interval[:high])]
169
+ end
170
+
171
+ # Extract a QDM::Quantity to something usable in HDS.
172
+ def quantity_extractor(quantity)
173
+ { units: quantity[:unit], scalar: quantity[:value].to_s }
174
+ end
175
+
176
+ # Extract a Dose to something usable in HDS.
177
+ def dose_extractor(dose)
178
+ { unit: dose[:units], value: dose[:scalar] }
179
+ end
180
+
181
+ # Convert a DateTime to something usable in HDS.
182
+ def date_time_converter(date_time)
183
+ date_time = DateTime.parse(date_time) if date_time.class.to_s == 'String'
184
+ date_time.to_i
185
+ end
186
+
187
+ # Grab the data elements on the patient. This should only be used when there
188
+ # is no active Mongo connection.
189
+ def get_data_elements(patient, category, status = nil)
190
+ matches = []
191
+ patient.dataElements.each do |data_element|
192
+ matches << data_element if data_element[:category] == category && (data_element[:qdmStatus] == status || status.nil?)
193
+ end
194
+ matches
195
+ end
196
+
197
+ # Grab the data elements on the patient by _type. This should only be used when there
198
+ # is no active Mongo connection.
199
+ def get_data_elements_by_type(patient, type)
200
+ matches = []
201
+ patient.dataElements.each do |data_element|
202
+ matches << data_element if data_element[:_type] == type
203
+ end
204
+ matches
205
+ end
206
+
207
+ # Check if the given string is a DateTime.
208
+ def date_time?(date_time)
209
+ true if DateTime.parse(date_time)
210
+ rescue ArgumentError
211
+ false
212
+ end
213
+
214
+ # Unpack components.
215
+ def unpack_components(hds_attrs)
216
+ return unless hds_attrs.key?('components') && !hds_attrs['components'].nil?
217
+ hds_attrs['components']['type'] = 'COL'
218
+ hds_attrs['components'][:values]&.collect do |code_value|
219
+ code_value['code'] = code_value.delete('Code')
220
+ code_value['result'] = { code: code_value.delete('Result'), title: code_value['code'][:title] }
221
+ code_value['code'].delete(:title)
222
+ code_value['result'][:code].delete(:title)
223
+ { code: code_value }
224
+ end
225
+ end
226
+
227
+ # Unpack diagnosis.
228
+ def unpack_diagnosis(hds_attrs)
229
+ if hds_attrs.key?('diagnosis') && !hds_attrs['diagnosis'].empty?
230
+ unpacked = {}
231
+ unpacked['type'] = 'COL'
232
+ unpacked['values'] = hds_attrs['diagnosis'].collect do |diag|
233
+ code = Utils.hds_codes_to_qdm_codes(diag.codes).first
234
+ {
235
+ code_system: code[:codeSystem],
236
+ code: code[:code],
237
+ title: diag.description
238
+ }
239
+ end
240
+ hds_attrs['diagnosis'] = unpacked
241
+ end
242
+ # Remove diagnosis if principalDiagnosis is equivalent.
243
+ return unless hds_attrs.key?('diagnosis') && hds_attrs.key?('principalDiagnosis')
244
+ return unless hds_attrs['diagnosis']['values'] && Hash[hds_attrs['diagnosis']['values'].first.sort] == Hash[hds_attrs['principalDiagnosis'].sort]
245
+ hds_attrs.delete('diagnosis')
246
+ end
247
+
248
+ # Unpack facility.
249
+ def unpack_facility(hds_attrs)
250
+ return unless hds_attrs.key?('facility') && !hds_attrs['facility'].empty?
251
+ hds_attrs['facility']['type'] = 'COL'
252
+ hds_attrs['facility'][:values]&.each do |value|
253
+ value['code'] = value.delete('Code')
254
+ value[:display] = value['code'].delete(:title) if value['code']
255
+ value[:locationPeriodHigh] = Time.at(value['Locationperiod'].last).utc.strftime('%m/%d/%Y %l:%M %p').split.join(' ')
256
+ value[:locationPeriodLow] = Time.at(value['Locationperiod'].first).utc.strftime('%m/%d/%Y %l:%M %p').split.join(' ')
257
+ value.delete('Locationperiod')
258
+ end
259
+ end
260
+
261
+ # Unpack references.
262
+ def unpack_references(hds_attrs)
263
+ return unless hds_attrs.key?('references') && !hds_attrs['references'].empty?
264
+ hds_attrs['references'] = hds_attrs['references'][:values].collect { |value| { referenced_id: value['value'], referenced_type: value['referencedType'], type: value['type'] } }
265
+ end
266
+
267
+ # Unpack patient characteristics.
268
+ def unpack_patient_characteristics(patient, record)
269
+ # Convert patient characteristic birthdate.
270
+ birthdate = get_data_elements(patient, 'patient_characteristic', 'birthdate').first
271
+ record.birthdate = birthdate.birthDatetime if birthdate
272
+
273
+ # Convert patient characteristic clinical trial participant.
274
+ # TODO, Adam 4/9: The Bonnie team is working on implementing this in HDS. When that work
275
+ # is complete, this should be updated to reflect how that looks in HDS.
276
+ # clinical_trial_participant = get_data_elements(patient, 'patient_characteristic', 'clinical_trial_participant').first
277
+
278
+ # Convert patient characteristic ethnicity.
279
+ ethnicity = get_data_elements(patient, 'patient_characteristic', 'ethnicity').first
280
+ ethnicity_code = ethnicity.dataElementCodes.first.symbolize_keys if ethnicity.dataElementCodes.any?
281
+ record.ethnicity = { code: ethnicity_code[:code], name: ethnicity_code[:descriptor], codeSystem: ethnicity_code[:codeSystem] } if ethnicity_code
282
+
283
+ # Convert patient characteristic expired.
284
+ expired = get_data_elements_by_type(patient, 'QDM::PatientCharacteristicExpired').first
285
+ record.deathdate = date_time_converter(expired.expiredDatetime) if expired
286
+ record.expired = record.deathdate if record.deathdate
287
+
288
+ # Convert patient characteristic race.
289
+ race = get_data_elements(patient, 'patient_characteristic', 'race').first
290
+ race_code = race.dataElementCodes.first.symbolize_keys if race.dataElementCodes.any?
291
+ record.race = { code: race_code[:code], name: race_code[:descriptor], codeSystem: race_code[:codeSystem] } if race_code
292
+
293
+ # Convert patient characteristic sex.
294
+ sex = get_data_elements_by_type(patient, 'QDM::PatientCharacteristicSex').first
295
+ sex_code = sex.dataElementCodes.first.symbolize_keys if sex.dataElementCodes.any?
296
+ record.gender = sex_code[:code] if sex
297
+
298
+ # Convert remaining metadata.
299
+ record.birthdate = date_time_converter(patient.birthDatetime) unless record.birthdate
300
+ record.first = patient.givenNames.first if patient.givenNames.any?
301
+ record.last = patient.familyName if patient.familyName
302
+ record.bundle_id = patient.bundleId if patient.bundleId
303
+ end
304
+
305
+ # Unpack extended data.
306
+ def unpack_extended_data(patient, record)
307
+ record['type'] = patient.extendedData['type'] if patient.extendedData['type']
308
+ record['measure_ids'] = patient.extendedData['measure_ids'] if patient.extendedData['measure_ids']
309
+ record['source_data_criteria'] = patient.extendedData['source_data_criteria'] if patient.extendedData['source_data_criteria']
310
+ record['expected_values'] = patient.extendedData['expected_values'] if patient.extendedData['expected_values'].is_a?(Array)
311
+ record['notes'] = patient.extendedData['notes'] if patient.extendedData['notes']
312
+ record['is_shared'] = patient.extendedData['is_shared'] if patient.extendedData['is_shared']
313
+ record['origin_data'] = patient.extendedData['origin_data'] if patient.extendedData['origin_data']
314
+ record['test_id'] = patient.extendedData['test_id'] if patient.extendedData['test_id']
315
+ record['medical_record_number'] = patient.extendedData['medical_record_number'] if patient.extendedData['medical_record_number']
316
+ record['medical_record_assigner'] = patient.extendedData['medical_record_assigner'] if patient.extendedData['medical_record_assigner']
317
+ record['description'] = patient.extendedData['description'] if patient.extendedData['description']
318
+ record['description_category'] = patient.extendedData['description_category'] if patient.extendedData['description_category']
319
+ insurance_providers = JSON.parse(patient.extendedData['insurance_providers']).collect do |insurance_provider|
320
+ InsuranceProvider.new.from_json(insurance_provider.to_json)
321
+ end
322
+ record['insurance_providers'] = insurance_providers
323
+ end
324
+ end
325
+ end