onc_certification_g10_test_kit 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/inferno/exceptions.rb +31 -0
  4. data/lib/inferno/ext/bloomer.rb +24 -0
  5. data/lib/inferno/repositiories/validators.rb +17 -0
  6. data/lib/inferno/repositiories/value_sets.rb +26 -0
  7. data/lib/inferno/terminology/bcp47.rb +95 -0
  8. data/lib/inferno/terminology/bcp_13.rb +26 -0
  9. data/lib/inferno/terminology/codesystem.rb +49 -0
  10. data/lib/inferno/terminology/expected_manifest.yml +1123 -0
  11. data/lib/inferno/terminology/fhir_package_manager.rb +69 -0
  12. data/lib/inferno/terminology/loader.rb +298 -0
  13. data/lib/inferno/terminology/tasks/check_built_terminology.rb +77 -0
  14. data/lib/inferno/terminology/tasks/cleanup.rb +13 -0
  15. data/lib/inferno/terminology/tasks/cleanup_precursors.rb +23 -0
  16. data/lib/inferno/terminology/tasks/count_codes_in_value_set.rb +20 -0
  17. data/lib/inferno/terminology/tasks/create_value_set_validators.rb +34 -0
  18. data/lib/inferno/terminology/tasks/download_fhir_terminology.rb +27 -0
  19. data/lib/inferno/terminology/tasks/download_umls.rb +109 -0
  20. data/lib/inferno/terminology/tasks/download_umls_notice.rb +20 -0
  21. data/lib/inferno/terminology/tasks/expand_value_set_to_file.rb +36 -0
  22. data/lib/inferno/terminology/tasks/process_umls.rb +91 -0
  23. data/lib/inferno/terminology/tasks/process_umls_translations.rb +85 -0
  24. data/lib/inferno/terminology/tasks/run_umls_jar.rb +75 -0
  25. data/lib/inferno/terminology/tasks/temp_dir.rb +27 -0
  26. data/lib/inferno/terminology/tasks/unzip_umls.rb +42 -0
  27. data/lib/inferno/terminology/tasks/validate_code.rb +36 -0
  28. data/lib/inferno/terminology/tasks.rb +11 -0
  29. data/lib/inferno/terminology/terminology_configuration.rb +52 -0
  30. data/lib/inferno/terminology/terminology_validation.rb +42 -0
  31. data/lib/inferno/terminology/validator.rb +64 -0
  32. data/lib/inferno/terminology/value_set.rb +462 -0
  33. data/lib/inferno/terminology.rb +16 -0
  34. data/lib/onc_certification_g10_test_kit/authorization_request_builder.rb +87 -0
  35. data/lib/onc_certification_g10_test_kit/base_token_refresh_group.rb +48 -0
  36. data/lib/onc_certification_g10_test_kit/bulk_data_authorization.rb +235 -0
  37. data/lib/onc_certification_g10_test_kit/bulk_data_group_export.rb +255 -0
  38. data/lib/onc_certification_g10_test_kit/bulk_data_group_export_validation.rb +474 -0
  39. data/lib/onc_certification_g10_test_kit/bulk_data_jwks.json +58 -0
  40. data/lib/onc_certification_g10_test_kit/bulk_export_validation_tester.rb +171 -0
  41. data/lib/onc_certification_g10_test_kit/configuration_checker.rb +104 -0
  42. data/lib/onc_certification_g10_test_kit/export_kick_off_performer.rb +12 -0
  43. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodyheight.json +3772 -0
  44. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodytemp.json +3772 -0
  45. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bodyweight.json +3772 -0
  46. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-bp.json +6034 -0
  47. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-heartrate.json +3756 -0
  48. data/lib/onc_certification_g10_test_kit/igs/StructureDefinition-resprate.json +3756 -0
  49. data/lib/onc_certification_g10_test_kit/limited_scope_grant_test.rb +66 -0
  50. data/lib/onc_certification_g10_test_kit/multi_patient_api.rb +43 -0
  51. data/lib/onc_certification_g10_test_kit/patient_context_test.rb +30 -0
  52. data/lib/onc_certification_g10_test_kit/profile_guesser.rb +69 -0
  53. data/lib/onc_certification_g10_test_kit/resource_access_test.rb +96 -0
  54. data/lib/onc_certification_g10_test_kit/restricted_access_test.rb +12 -0
  55. data/lib/onc_certification_g10_test_kit/restricted_resource_type_access_group.rb +303 -0
  56. data/lib/onc_certification_g10_test_kit/smart_app_launch_invalid_aud_group.rb +136 -0
  57. data/lib/onc_certification_g10_test_kit/smart_ehr_practitioner_app_group.rb +209 -0
  58. data/lib/onc_certification_g10_test_kit/smart_invalid_token_group.rb +197 -0
  59. data/lib/onc_certification_g10_test_kit/smart_limited_app_group.rb +123 -0
  60. data/lib/onc_certification_g10_test_kit/smart_public_standalone_launch_group.rb +113 -0
  61. data/lib/onc_certification_g10_test_kit/smart_scopes_test.rb +153 -0
  62. data/lib/onc_certification_g10_test_kit/smart_standalone_patient_app_group.rb +177 -0
  63. data/lib/onc_certification_g10_test_kit/terminology_binding_validator.rb +140 -0
  64. data/lib/onc_certification_g10_test_kit/token_revocation_group.rb +133 -0
  65. data/lib/onc_certification_g10_test_kit/unauthorized_access_test.rb +25 -0
  66. data/lib/onc_certification_g10_test_kit/unrestricted_resource_type_access_group.rb +375 -0
  67. data/lib/onc_certification_g10_test_kit/version.rb +3 -0
  68. data/lib/onc_certification_g10_test_kit/visual_inspection_and_attestations_group.rb +470 -0
  69. data/lib/onc_certification_g10_test_kit/well_known_capabilities_test.rb +37 -0
  70. data/lib/onc_certification_g10_test_kit.rb +223 -0
  71. metadata +310 -0
@@ -0,0 +1,113 @@
1
+ module ONCCertificationG10TestKit
2
+ class SMARTPublicStandaloneLaunchGroup < SMARTAppLaunch::StandaloneLaunchGroup
3
+ title 'Public Client Standalone Launch with OpenID Connect'
4
+ short_title 'SMART Public Client Launch'
5
+ description %(
6
+ # Background
7
+
8
+ The [Standalone
9
+ Launch](http://hl7.org/fhir/smart-app-launch/#standalone-launch-sequence)
10
+ Sequence allows an app, like Inferno, to be launched independent of an
11
+ existing EHR session. It is one of the two launch methods described in
12
+ the SMART App Launch Framework alongside EHR Launch. The app will
13
+ request authorization for the provided scope from the authorization
14
+ endpoint, ultimately receiving an authorization token which can be
15
+ used to gain access to resources on the FHIR server.
16
+
17
+ # Test Methodology
18
+
19
+ Inferno will redirect the user to the the authorization endpoint so
20
+ that they may provide any required credentials and authorize the
21
+ application. Upon successful authorization, Inferno will exchange the
22
+ authorization code provided for an access token.
23
+
24
+ For more information on the #{title}:
25
+
26
+ * [Standalone Launch Sequence](http://hl7.org/fhir/smart-app-launch/#standalone-launch-sequence)
27
+ )
28
+ id :g10_public_standalone_launch
29
+ run_as_group
30
+
31
+ config(
32
+ inputs: {
33
+ client_id: {
34
+ name: :public_client_id
35
+ },
36
+ client_secret: {
37
+ name: :public_client_secret,
38
+ default: nil,
39
+ optional: true,
40
+ locked: true
41
+ },
42
+ requested_scopes: {
43
+ name: :public_requested_scopes
44
+ },
45
+ url: {
46
+ title: 'Standalone FHIR Endpoint',
47
+ description: 'URL of the FHIR endpoint used by standalone applications'
48
+ },
49
+ code: {
50
+ name: :public_code
51
+ },
52
+ state: {
53
+ name: :public_state
54
+ },
55
+ smart_authorization_url: {
56
+ title: 'OAuth 2.0 Authorize Endpoint',
57
+ description: 'OAuth 2.0 Authorize Endpoint provided during the patient standalone launch'
58
+ },
59
+ smart_token_url: {
60
+ title: 'OAuth 2.0 Token Endpoint',
61
+ description: 'OAuth 2.0 Token Endpoint provided during the patient standalone launch'
62
+ },
63
+ smart_credentials: {
64
+ name: :public_smart_credentials
65
+ }
66
+ },
67
+ outputs: {
68
+ code: { name: :public_code },
69
+ token_retrieval_time: { name: :public_token_retrieval_time },
70
+ state: { name: :public_state },
71
+ id_token: { name: :public_id_token },
72
+ refresh_token: { name: :public_refresh_token },
73
+ access_token: { name: :public_access_token },
74
+ expires_in: { name: :public_expires_in },
75
+ patient_id: { name: :public_patient_id },
76
+ encounter_id: { name: :public_encounter_id },
77
+ received_scopes: { name: :public_received_scopes },
78
+ intent: { name: :public_intent },
79
+ smart_credentials: { name: :public_smart_credentials }
80
+ },
81
+ requests: {
82
+ redirect: { name: :public_redirect },
83
+ token: { name: :public_token }
84
+ }
85
+ )
86
+
87
+ test from: :g10_patient_context,
88
+ config: {
89
+ inputs: {
90
+ patient_id: { name: :public_patient_id },
91
+ smart_credentials: { name: :public_smart_credentials }
92
+ }
93
+ }
94
+
95
+ test do
96
+ title 'OAuth token exchange response contains OpenID Connect id_token'
97
+ description %(
98
+ This test requires that an OpenID Connect id_token is provided to
99
+ demonstrate authentication capabilies for public clients.
100
+ )
101
+ id :g10_public_launch_id_token
102
+
103
+ input :id_token,
104
+ name: :public_id_token,
105
+ locked: true,
106
+ optional: true
107
+
108
+ run do
109
+ assert id_token.present?, 'Token response did not provide an id_token as required.'
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,153 @@
1
+ module ONCCertificationG10TestKit
2
+ class SMARTScopesTest < Inferno::Test
3
+ title 'Patient-level access with OpenID Connect and Refresh Token scopes used.'
4
+ description %(
5
+ The scopes being input must follow the guidelines specified in the
6
+ smart-app-launch guide. All scopes requested are expected to be granted.
7
+ )
8
+ id :g10_smart_scopes
9
+ input :requested_scopes, :received_scopes
10
+ uses_request :token
11
+
12
+ def valid_resource_types
13
+ [
14
+ '*',
15
+ 'Patient',
16
+ 'AllergyIntolerance',
17
+ 'Binary',
18
+ 'CarePlan',
19
+ 'CareTeam',
20
+ 'Condition',
21
+ 'Device',
22
+ 'DiagnosticReport',
23
+ 'DocumentReference',
24
+ 'Encounter',
25
+ 'Goal',
26
+ 'Immunization',
27
+ 'Location',
28
+ 'Medication',
29
+ 'MedicationOrder',
30
+ 'MedicationRequest',
31
+ 'MedicationStatement',
32
+ 'Observation',
33
+ 'Organization',
34
+ 'Person',
35
+ 'Practitioner',
36
+ 'PractitionerRole',
37
+ 'Procedure',
38
+ 'Provenance',
39
+ 'RelatedPerson'
40
+ ]
41
+ end
42
+
43
+ def requested_scope_test(scopes, patient_compartment_resource_types)
44
+ patient_scope_found = false
45
+
46
+ scopes.each do |scope|
47
+ bad_format_message =
48
+ "Requested scope '#{scope}' does not follow the format: `#{scope_type}" \
49
+ '/[ resource | * ].[ read | * ]`'
50
+
51
+ scope_pieces = scope.split('/')
52
+ assert scope_pieces.count == 2, bad_format_message
53
+
54
+ resource_access = scope_pieces[1].split('.')
55
+ bad_resource_message = "'#{resource_access[0]}' must be either a valid resource type or '*'"
56
+
57
+ if scope_type == 'patient' && patient_compartment_resource_types.exclude?(resource_access[0])
58
+ assert ['user', 'patient'].include?(scope_pieces[0]),
59
+ "Requested scope '#{scope}' must begin with either 'user/' or 'patient/'"
60
+ else
61
+ assert scope_pieces[0] == scope_type, bad_format_message
62
+ end
63
+
64
+ assert resource_access.count == 2, bad_format_message
65
+ assert valid_resource_types.include?(resource_access[0]), bad_resource_message
66
+ assert resource_access[1] =~ /^(\*|read)/, bad_format_message
67
+
68
+ patient_scope_found = true
69
+ end
70
+
71
+ assert patient_scope_found,
72
+ "#{scope_type.capitalize}-level scope in the format: " \
73
+ "`#{scope_type}/[ resource | * ].[ read | *]` was not requested."
74
+ end
75
+
76
+ def received_scope_test(scopes, patient_compartment_resource_types)
77
+ granted_resource_types = []
78
+
79
+ scopes.each do |scope|
80
+ scope_pieces = scope.split('/')
81
+ next unless scope_pieces.count == 2
82
+
83
+ resource_access = scope_pieces[1].split('.')
84
+ next unless resource_access.count == 2
85
+
86
+ granted_resource_types << resource_access[0] if resource_access[1] =~ /^(\*|read)/
87
+ end
88
+
89
+ missing_resource_types =
90
+ if granted_resource_types.include?('*')
91
+ []
92
+ else
93
+ patient_compartment_resource_types - granted_resource_types - ['*']
94
+ end
95
+
96
+ assert missing_resource_types.empty?,
97
+ "Request scopes #{missing_resource_types.join(', ')} were not granted by authorization server."
98
+ end
99
+
100
+ run do
101
+ skip_if request.status != 200, 'Token exchange was unsuccessful'
102
+
103
+ patient_compartment_resource_types = [
104
+ '*',
105
+ 'Patient',
106
+ 'AllergyIntolerance',
107
+ 'CarePlan',
108
+ 'CareTeam',
109
+ 'Condition',
110
+ 'DiagnosticReport',
111
+ 'DocumentReference',
112
+ 'Goal',
113
+ 'Immunization',
114
+ 'MedicationRequest',
115
+ 'Observation',
116
+ 'Procedure',
117
+ 'Provenance'
118
+ ].freeze
119
+
120
+ [
121
+ {
122
+ scopes: requested_scopes,
123
+ received_or_requested: 'requested'
124
+ },
125
+ {
126
+ scopes: received_scopes,
127
+ received_or_requested: 'received'
128
+ }
129
+ ].each do |metadata|
130
+ scopes = metadata[:scopes].split
131
+ received_or_requested = metadata[:received_or_requested]
132
+
133
+ missing_scopes = required_scopes - scopes
134
+ assert missing_scopes.empty?,
135
+ "Required scopes were not #{received_or_requested}: #{missing_scopes.join(', ')}"
136
+
137
+ scopes -= required_scopes
138
+
139
+ # Other 'okay' scopes. Also scopes may include both 'launch' and
140
+ # 'launch/patient' for EHR launch and Standalone launch.
141
+ # 'launch/encounter' is mentioned by SMART App Launch though not in
142
+ # (g)(10) test procedure
143
+ scopes -= ['online_access', 'launch', 'launch/patient', 'launch/encounter']
144
+
145
+ if received_or_requested == 'requested'
146
+ requested_scope_test(scopes, patient_compartment_resource_types)
147
+ else
148
+ received_scope_test(scopes, patient_compartment_resource_types)
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,177 @@
1
+ require_relative 'base_token_refresh_group'
2
+ require_relative 'patient_context_test'
3
+ require_relative 'smart_scopes_test'
4
+ require_relative 'unauthorized_access_test'
5
+ require_relative 'unrestricted_resource_type_access_group'
6
+ require_relative 'well_known_capabilities_test'
7
+
8
+ module ONCCertificationG10TestKit
9
+ class SmartStandalonePatientAppGroup < Inferno::TestGroup
10
+ title 'Standalone Patient App - Full Access'
11
+ short_title 'Standalone Patient App'
12
+
13
+ input_instructions %(
14
+ Register Inferno as a standalone application using the following information:
15
+
16
+ * Redirect URI: `#{SMARTAppLaunch::AppRedirectTest.config.options[:redirect_uri]}`
17
+
18
+ Enter in the appropriate scope to enable patient-level access to all
19
+ relevant resources. In addition, support for the OpenID Connect (openid
20
+ fhirUser), refresh tokens (offline_access), and patient context
21
+ (launch/patient) are required.
22
+ )
23
+
24
+ description %(
25
+ This scenario demonstrates the ability of a system to perform a Patient
26
+ Standalone Launch to a [SMART on
27
+ FHIR](http://www.hl7.org/fhir/smart-app-launch/) confidential client
28
+ with a patient context, refresh token, 1 1 and [OpenID Connect
29
+ (OIDC)](https://openid.net/specs/openid-connect-core-1_0.html) identity
30
+ token. After launch, a simple Patient resource read is performed on the
31
+ patient in context. The access token is then refreshed, and the Patient
32
+ resource is read using the new access token to ensure that the refresh
33
+ was successful. The authentication information provided by OpenID
34
+ Connect is decoded and validated, and simple queries are performed to
35
+ ensure that access is granted to all USCDI data elements.
36
+ )
37
+ id :g10_smart_standalone_patient_app
38
+ run_as_group
39
+
40
+ config(
41
+ inputs: {
42
+ client_secret: {
43
+ optional: false
44
+ }
45
+ }
46
+ )
47
+
48
+ group from: :smart_discovery do
49
+ test from: 'g10_smart_well_known_capabilities'
50
+ end
51
+
52
+ group from: :smart_standalone_launch do
53
+ title 'Standalone Launch With Patient Scope'
54
+ description %(
55
+ # Background
56
+
57
+ The [Standalone
58
+ Launch](http://hl7.org/fhir/smart-app-launch/#standalone-launch-sequence)
59
+ Sequence allows an app, like Inferno, to be launched independent of an
60
+ existing EHR session. It is one of the two launch methods described in
61
+ the SMART App Launch Framework alongside EHR Launch. The app will
62
+ request authorization for the provided scope from the authorization
63
+ endpoint, ultimately receiving an authorization token which can be used
64
+ to gain access to resources on the FHIR server.
65
+
66
+ # Test Methodology
67
+
68
+ Inferno will redirect the user to the the authorization endpoint so that
69
+ they may provide any required credentials and authorize the application.
70
+ Upon successful authorization, Inferno will exchange the authorization
71
+ code provided for an access token.
72
+
73
+ For more information on the #{title}:
74
+
75
+ * [Standalone Launch
76
+ Sequence](http://hl7.org/fhir/smart-app-launch/#standalone-launch-sequence)
77
+ )
78
+
79
+ test from: :g10_smart_scopes do
80
+ config(
81
+ inputs: {
82
+ requested_scopes: { name: :standalone_requested_scopes },
83
+ received_scopes: { name: :standalone_received_scopes }
84
+ }
85
+ )
86
+
87
+ def required_scopes
88
+ ['openid', 'fhirUser', 'launch/patient', 'offline_access']
89
+ end
90
+
91
+ def scope_type
92
+ 'patient'
93
+ end
94
+ end
95
+
96
+ test from: :g10_unauthorized_access,
97
+ config: {
98
+ inputs: {
99
+ patient_id: { name: :standalone_patient_id }
100
+ }
101
+ }
102
+
103
+ test from: :g10_patient_context,
104
+ config: {
105
+ inputs: {
106
+ patient_id: { name: :standalone_patient_id },
107
+ smart_credentials: { name: :standalone_smart_credentials }
108
+ }
109
+ }
110
+ end
111
+
112
+ group from: :smart_openid_connect,
113
+ config: {
114
+ inputs: {
115
+ id_token: { name: :standalone_id_token },
116
+ client_id: { name: :standalone_client_id },
117
+ requested_scopes: { name: :standalone_requested_scopes },
118
+ smart_credentials: { name: :standalone_smart_credentials }
119
+ }
120
+ }
121
+
122
+ group from: :g10_token_refresh do
123
+ id :g10_smart_standalone_token_refresh
124
+
125
+ config(
126
+ inputs: {
127
+ refresh_token: { name: :standalone_refresh_token },
128
+ client_id: { name: :standalone_client_id },
129
+ client_secret: { name: :standalone_client_secret },
130
+ received_scopes: { name: :standalone_received_scopes }
131
+ },
132
+ outputs: {
133
+ refresh_token: { name: :standalone_refresh_token },
134
+ received_scopes: { name: :standalone_received_scopes },
135
+ access_token: { name: :standalone_access_token },
136
+ token_retrieval_time: { name: :standalone_token_retrieval_time },
137
+ expires_in: { name: :standalone_expires_in },
138
+ smart_credentials: { name: :standalone_smart_credentials }
139
+ }
140
+ )
141
+
142
+ test from: :g10_patient_context do
143
+ config(
144
+ inputs: {
145
+ patient_id: { name: :standalone_patient_id },
146
+ smart_credentials: { name: :standalone_smart_credentials }
147
+ },
148
+ options: {
149
+ refresh_test: true
150
+ }
151
+ )
152
+ uses_request :token_refresh
153
+ end
154
+ end
155
+
156
+ group from: :g10_unrestricted_resource_type_access,
157
+ config: {
158
+ inputs: {
159
+ received_scopes: { name: :standalone_received_scopes },
160
+ patient_id: { name: :standalone_patient_id },
161
+ smart_credentials: { name: :standalone_smart_credentials }
162
+ }
163
+ }
164
+
165
+ test do
166
+ id :g10_standalone_credentials_export
167
+ title 'Set SMART Credentials to Standalone Launch Credentials'
168
+
169
+ input :standalone_smart_credentials, type: :oauth_credentials
170
+ output :smart_credentials
171
+
172
+ run do
173
+ output smart_credentials: standalone_smart_credentials.to_s
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,140 @@
1
+ require_relative '../inferno/terminology/terminology_validation'
2
+ require_relative '../inferno/exceptions'
3
+
4
+ module ONCCertificationG10TestKit
5
+ class TerminologyBindingValidator
6
+ include USCoreTestKit::FHIRResourceNavigation
7
+ include Inferno::Terminology::TerminologyValidation
8
+
9
+ def self.validate(...)
10
+ new(...).validate
11
+ end
12
+
13
+ attr_reader :resource, :binding_definition, :validation_messages
14
+
15
+ def initialize(resource, binding_definition)
16
+ @resource = resource
17
+ @binding_definition = binding_definition
18
+ @validation_messages = []
19
+ end
20
+
21
+ def validate
22
+ add_error(element_with_invalid_binding) if element_with_invalid_binding.present?
23
+
24
+ validation_messages
25
+ end
26
+
27
+ def path_source
28
+ return resource if binding_definition[:extensions].blank?
29
+
30
+ binding_definition[:extensions].reduce(Array.wrap(resource)) do |elements, extension_url|
31
+ elements.flat_map do |element|
32
+ element.extension.select { |extension| extension.url == extension_url }
33
+ end
34
+ end
35
+ end
36
+
37
+ def element_with_invalid_binding
38
+ @element_with_invalid_binding ||=
39
+ find_a_value_at(path_source, binding_definition[:path]) do |element|
40
+ invalid_binding? element
41
+ end
42
+ end
43
+
44
+ def add_error(element)
45
+ validation_messages << {
46
+ type: 'error',
47
+ message: invalid_binding_message(element)
48
+ }
49
+ end
50
+
51
+ def add_warning(message)
52
+ validation_messages << {
53
+ type: 'warning',
54
+ message: message
55
+ }
56
+ end
57
+
58
+ def element_code(element)
59
+ case element
60
+ when FHIR::CodeableConcept
61
+ element&.coding&.map do |coding|
62
+ "`#{coding.system}|#{coding.code}`"
63
+ end&.join(' or ')
64
+ when FHIR::Coding, FHIR::Quantity
65
+ "`#{element.system}|#{element.code}`"
66
+ else
67
+ "`#{element}`"
68
+ end
69
+ end
70
+
71
+ def resource_type
72
+ resource.resourceType
73
+ end
74
+
75
+ def invalid_binding_message(element)
76
+ system = binding_definition[:system].presence || 'the declared CodeSystem'
77
+
78
+ %(
79
+ #{resource_type}/#{resource.id} at #{resource_type}.#{binding_definition[:path]}
80
+ with code #{element_code(element)} is not in #{system}.
81
+ )
82
+ end
83
+
84
+ def invalid_binding?(element)
85
+ case binding_definition[:type]
86
+ when 'CodeableConcept'
87
+ invalid_codeable_concept? element
88
+ when 'Quantity', 'Coding'
89
+ invalid_coding? element
90
+ when 'code'
91
+ invalid_code? element
92
+ end
93
+ end
94
+
95
+ def invalid_codeable_concept?(element)
96
+ return unless element.is_a? FHIR::CodeableConcept
97
+
98
+ if binding_definition[:system].present?
99
+ element.coding.none? do |coding|
100
+ validate_code(
101
+ value_set_url: binding_definition[:system],
102
+ code: coding.code,
103
+ system: coding.system
104
+ )
105
+ rescue Inferno::ProhibitedSystemException => e
106
+ add_warning(e.message)
107
+ false
108
+ end
109
+ # If we're validating a codesystem (AKA if there's no 'system' URL)
110
+ # We want all of the codes to be in their respective systems
111
+ else
112
+ el.coding.any? do |coding|
113
+ !validate_code(
114
+ value_set_url: nil,
115
+ code: coding.code,
116
+ system: coding.system
117
+ )
118
+ rescue Inferno::ProhibitedSystemException => e
119
+ add_warning(e.message)
120
+ false
121
+ end
122
+ end
123
+ end
124
+
125
+ def invalid_coding?(element)
126
+ !validate_code(
127
+ value_set_url: binding_definition[:system],
128
+ code: element.code,
129
+ system: element.system
130
+ )
131
+ rescue Inferno::ProhibitedSystemException => e
132
+ add_warning(e.message)
133
+ false
134
+ end
135
+
136
+ def invalid_code?(element)
137
+ !validate_code(value_set_url: binding_definition[:system], code: element)
138
+ end
139
+ end
140
+ end