onc_certification_g10_test_kit 2.0.0.rc1

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