inferno_core 0.6.9 → 0.6.10

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/lib/inferno/apps/cli/evaluate/database.yml +15 -0
  3. data/lib/inferno/apps/cli/evaluate/docker-compose.evaluate.yml +16 -0
  4. data/lib/inferno/apps/cli/evaluate.rb +52 -4
  5. data/lib/inferno/apps/cli/main.rb +5 -1
  6. data/lib/inferno/apps/cli/requirements.rb +28 -0
  7. data/lib/inferno/apps/cli/requirements_exporter.rb +194 -0
  8. data/lib/inferno/apps/cli/suite.rb +21 -0
  9. data/lib/inferno/apps/cli/templates/lib/%library_name%/example_suite/patient_group.rb.tt +141 -0
  10. data/lib/inferno/apps/cli/templates/lib/%library_name%/example_suite.rb.tt +128 -0
  11. data/lib/inferno/apps/cli/templates/lib/%library_name%/metadata.rb.tt +65 -3
  12. data/lib/inferno/apps/cli/templates/lib/%library_name%/version.rb.tt +1 -0
  13. data/lib/inferno/apps/cli/templates/lib/%library_name%.rb.tt +1 -1
  14. data/lib/inferno/apps/web/serializers/input.rb +1 -0
  15. data/lib/inferno/apps/web/serializers/test.rb +1 -0
  16. data/lib/inferno/apps/web/serializers/test_group.rb +1 -0
  17. data/lib/inferno/apps/web/serializers/test_suite.rb +1 -0
  18. data/lib/inferno/config/boot/suites.rb +3 -0
  19. data/lib/inferno/dsl/fhir_evaluation/default.yml +68 -0
  20. data/lib/inferno/dsl/fhir_evaluation/evaluator.rb +3 -5
  21. data/lib/inferno/dsl/fhir_evaluation/rules/all_defined_extensions_have_examples.rb +2 -2
  22. data/lib/inferno/dsl/fhir_evaluation/rules/all_extensions_used.rb +2 -2
  23. data/lib/inferno/dsl/fhir_evaluation/rules/all_must_supports_present.rb +1 -1
  24. data/lib/inferno/dsl/fhir_evaluation/rules/all_profiles_have_examples.rb +1 -1
  25. data/lib/inferno/dsl/fhir_evaluation/rules/all_references_resolve.rb +2 -2
  26. data/lib/inferno/dsl/fhir_evaluation/rules/all_resources_reachable.rb +2 -2
  27. data/lib/inferno/dsl/fhir_evaluation/rules/all_search_parameters_have_examples.rb +22 -11
  28. data/lib/inferno/dsl/fhir_evaluation/rules/differential_content_has_examples.rb +2 -2
  29. data/lib/inferno/dsl/fhir_evaluation/rules/value_sets_demonstrate.rb +4 -4
  30. data/lib/inferno/dsl/fhir_resource_validation.rb +25 -3
  31. data/lib/inferno/dsl/fhirpath_evaluation.rb +25 -1
  32. data/lib/inferno/dsl/input_output_handling.rb +1 -0
  33. data/lib/inferno/dsl/runnable.rb +5 -0
  34. data/lib/inferno/dsl/short_id_manager.rb +55 -0
  35. data/lib/inferno/entities/input.rb +14 -5
  36. data/lib/inferno/entities/requirement.rb +15 -3
  37. data/lib/inferno/entities/test.rb +3 -1
  38. data/lib/inferno/entities/test_group.rb +3 -1
  39. data/lib/inferno/entities/test_suite.rb +2 -0
  40. data/lib/inferno/exceptions.rb +6 -0
  41. data/lib/inferno/public/237.bundle.js +1 -1
  42. data/lib/inferno/public/bundle.js +54 -54
  43. data/lib/inferno/public/bundle.js.LICENSE.txt +3 -36
  44. data/lib/inferno/repositories/requirements.rb +6 -2
  45. data/lib/inferno/version.rb +1 -1
  46. data/spec/shared/test_kit_examples.rb +32 -0
  47. metadata +24 -3
  48. data/lib/inferno/apps/cli/templates/lib/%library_name%/patient_group.rb.tt +0 -44
@@ -5,11 +5,73 @@ module <%= module_name %>
5
5
  id :<%= test_kit_id %>
6
6
  title '<%= title_name %>'
7
7
  description <<~DESCRIPTION
8
- This is a big markdown description of the test kit.
8
+ The Example Test Kit is a testing tool for Health IT systems seeking to meet the requirements of API Criterion within the Example Certification Program.
9
+
10
+ *or*
11
+
12
+ The Example Test Kit validates the conformance of a server implementation to a specific version of the [Example IG](https://example.com/example).
13
+ Currently, Inferno can test against implementations of following versions of the Example IG: v1.0.0, v1.3.0, v2.0.0, and v3.0.1.
14
+
15
+ <!-- break -->
16
+
17
+ ## Getting Started
18
+
19
+ Please select which approved version of each standard to use, and click ‘Create Test Session’ to begin testing.
20
+
21
+ This test kit includes a [simulated conformant FHIR API](https://inferno.healthit.gov/reference-server/)
22
+ that can be used to demonstrate success for all tests. This simulated API is open source and is available on GitHub.
23
+ Visit the [walkthrough](https://example.com/Walkthrough) for a demonstration
24
+ of using these tests against the provided simulated FHIR API.
25
+
26
+ ## Status
27
+
28
+ The Example Test Kit is actively developed and updates are released monthly.
29
+
30
+ *or*
31
+
32
+ These tests are a **DRAFT**. Future versions of these tests may verify other requirements and may change how these requirements are tested.
33
+
34
+ ## Conformance
35
+
36
+ The test kit currently tests all requirements for the
37
+ [API Criterion within the Example Certification Program](https://example.com/api-criterion).
38
+ This includes:
39
+ - The Lorum IG [v1.0.0](https://example.com/lorum/1.0.0)
40
+ - The Ipsum IG [v2.0.0](https://example.com/ipsum/2.0.0), [v3.0.1](https://example.com/ipsum/3.0.1)
41
+ - The Dolor IG [v2.0.2](https://example.com/dolor/2.0.2)
42
+
43
+ *or*
44
+
45
+ The test kit currently tests the following requirements:
46
+ - Vel mattis erat semper ut
47
+ - Suspendisse eget tempor
48
+ - Nulla eu cursus turpis
49
+ - Praesent orci diam
50
+
51
+
52
+ ## Repository
53
+
54
+ The Example Kit can be
55
+ [downloaded from its GitHub repository](https://example.com/example-test-kit-repo),
56
+ where additional resources and documentation are also available to help users get
57
+ started with the testing process. The repository [Wiki](https://example.com/example-test-kit-repo/wiki/)
58
+ provides a [FAQ](https://example.com/example-test-kit-repo/wiki/FAQ) for testers,
59
+ and the [Releases](https://example.com/example-test-kit-repo/releases) page provides information about each new release.
60
+
61
+ ## Providing Feedback and Reporting Issues
62
+
63
+ We welcome feedback on the tests, including but not limited to the following areas:
64
+
65
+ - Validation logic, such as potential bugs, lax checks, and unexpected failures.
66
+ - Requirements coverage, such as requirements that have been missed, tests that necessitate features that the IG does not require, or other issues with the interpretation of the IG’s requirements.
67
+ - User experience, such as confusing or missing information in the test UI.
68
+
69
+ Please report any issues with this set of tests in the [issues section](https://example.com/example-test-kit-repo/issues) of the repository.
9
70
  DESCRIPTION
71
+
10
72
  suite_ids [:<%= test_suite_id %>]
11
- # tags ['SMART App Launch', 'US Core']
12
- # last_updated '2024-03-07'
73
+ tags [] # E.g., ['SMART App Launch', 'US Core']
74
+ last_updated LAST_UPDATED
13
75
  version VERSION
14
76
  maturity 'Low'
15
77
  authors <%= authors %>
@@ -1,3 +1,4 @@
1
1
  module <%= module_name %>
2
2
  VERSION = '0.0.0'.freeze
3
+ LAST_UPDATED = '<%= Date.today.strftime("%Y-%m-%d") %>'.freeze # TODO: update next release
3
4
  end
@@ -1 +1 @@
1
- require_relative '<%= library_name %>/suite'
1
+ require_relative '<%= library_name %>/example_suite'
@@ -14,6 +14,7 @@ module Inferno
14
14
  field :optional, if: :field_present?
15
15
  field :options, if: :field_present?
16
16
  field :locked, if: :field_present?
17
+ field :hidden, if: :field_present?
17
18
  field :value, if: :field_present?
18
19
  end
19
20
  end
@@ -20,6 +20,7 @@ module Inferno
20
20
  field :input_instructions
21
21
  field :user_runnable?, name: :user_runnable
22
22
  field :optional?, name: :optional
23
+ field :verifies_requirements, if: :field_present?
23
24
  end
24
25
  end
25
26
  end
@@ -32,6 +32,7 @@ module Inferno
32
32
  Input.render_as_hash(group.available_inputs(suite_options).values)
33
33
  end
34
34
  field :output_definitions, name: :outputs, extractor: HashValueExtractor
35
+ field :verifies_requirements, if: :field_present?
35
36
  end
36
37
  end
37
38
  end
@@ -46,6 +46,7 @@ module Inferno
46
46
 
47
47
  RequirementSet.render_as_hash(requirement_sets)
48
48
  end
49
+ field :verifies_requirements, if: :field_present?
49
50
  end
50
51
  end
51
52
  end
@@ -45,6 +45,9 @@ Inferno::Application.register_provider(:suites) do
45
45
  if descendant.id.blank? || descendant.id == 'Inferno::Entities::TestSuite'
46
46
  raise StandardError, "Error initializing test suite #{descendant.name}: test suite ID is not set"
47
47
  end
48
+
49
+ # This will lock the short IDs if a short ID map for this suite is present
50
+ descendant.assign_short_ids
48
51
  end
49
52
  end
50
53
  end
@@ -0,0 +1,68 @@
1
+ Environment:
2
+ ExternalValidator:
3
+ Enabled: false
4
+ Url: ''
5
+ VSAC:
6
+ Username: 'apikey'
7
+ Password: ''
8
+
9
+ Rule:
10
+ AllReferencesResolve:
11
+ Description: 'All References in Examples resolve to each other'
12
+ Enabled: true
13
+ AllResourcesReachable:
14
+ Description: 'All resources in Examples are reachable'
15
+ Enabled: true
16
+ AllMustSupportsPresent:
17
+ Description: 'An instance of all MustSupport elements, extensions, and slices is present in the given resources'
18
+ Enabled: true
19
+ # RequirementExtensionUrl accepts an extension URL which tags element definitions as required for the purposes of this test, even if not Must Support.
20
+ # For instance, US Core elements with the "http://hl7.org/fhir/us/core/StructureDefinition/uscdi-requirement" extension
21
+ # may be considered required even if not necessarily tagged Must Support.
22
+ # An instance of the extension must have valueBoolean=true to be recognized.
23
+ RequirementExtensionUrl: null
24
+
25
+ # Set WriteMetadataForDebugging:true to have the test write out the metadata it used to a temporary file
26
+ WriteMetadataForDebugging: false
27
+
28
+ ConformanceOptions:
29
+ # ConformanceOptions allows selecting from a few approaches to determine which subset of resources
30
+ # should be used to search for the MustSupport elements from each profile.
31
+ # Resources that are not the same type as the target of the profile are never searched, regardless of option.
32
+
33
+ # - If considerMetaProfile, the search will include resources that declare the current profile in meta.profile
34
+ considerMetaProfile: true
35
+
36
+ # - If considerValidationResults, resources will be validated against each profile to determine which they should be checked against.
37
+ # The search will include resources that validate against the current profile
38
+ # (in other words, resources for which a validation request produces no errors).
39
+ considerValidationResults: false
40
+
41
+ # - If considerOnlyResourceType, the search will include resources of the same type as the profile target type (StructureDefintion.type)
42
+ considerOnlyResourceType: false
43
+ AllExtensionsUsed:
44
+ Description: 'All extensions specified in profiles are represented in Examples'
45
+ Enabled: true
46
+ DifferentialContentHasExamples:
47
+ Enabled: true
48
+ ValueSetsDemonstrate:
49
+ Descripton: 'Examples demonstrate reasonable coverage of valuesets defines in an IG. (Note this probably only makes sense for small valuesets such as status options, not something like disease codes from SNOMED)'
50
+ Enabled: true
51
+ IgnoreUnloadableValueset: false
52
+ Exclude:
53
+ URN: true # Exclude if system is provided as Uniform Resource Name "urn:"
54
+ Filter: true # Exclude filter
55
+ SystemOnly: true # Exclude if only system is provided (e.g. http://loing.org)
56
+ AllSearchParametersHaveExamples:
57
+ Description: 'Examples cover all search parameters defined in an IG'
58
+ Enabled: true
59
+ AllDefinedExtensionsHaveExamples:
60
+ Description: 'All defined extensions are represented in Examples'
61
+ Enabled: true
62
+ AllProfilesHaveExamples:
63
+ Description: 'All profiles defined in an IG have at least one example instance'
64
+ Enabled: true
65
+ ConformanceOptions:
66
+ considerMetaProfile: true
67
+ considerValidationResults: false
68
+ considerOnlyResourceType: false
@@ -24,13 +24,11 @@ module Inferno
24
24
  def evaluate(data, config = Config.new)
25
25
  context = EvaluationContext.new(@ig, data, config, validator)
26
26
 
27
- active_rules = []
28
27
  config.data['Rule'].each do |rulename, rule_details|
29
- active_rules << rulename if rule_details['Enabled']
30
- end
28
+ next unless rule_details['Enabled']
31
29
 
32
- Rule.descendants.each do |rule|
33
- rule.new.check(context) if active_rules.include?(rule.name.demodulize)
30
+ Rule.descendants.select { |rule| rule.name.demodulize == rulename }
31
+ .each { |rule| rule.new.check(context) }
34
32
  end
35
33
 
36
34
  context.results
@@ -25,10 +25,10 @@ module Inferno
25
25
  end
26
26
 
27
27
  if unused_extensions.any?
28
- message = "Found unused extensions defined in the IG: \n\t #{unused_extensions.join(', ')}"
28
+ message = "Found defined extensions in the IG without examples: \n\t #{unused_extensions.join(', ')}"
29
29
  result = EvaluationResult.new(message, rule: self)
30
30
  else
31
- message = 'All defined extensions are represented in examples'
31
+ message = 'All defined extensions in the IG have examples.'
32
32
  result = EvaluationResult.new(message, severity: 'success', rule: self)
33
33
  end
34
34
 
@@ -12,7 +12,7 @@ module Inferno
12
12
  message = get_fail_message(unused_extensions)
13
13
  result = EvaluationResult.new(message, rule: self)
14
14
  else
15
- message = 'All extensions specified in profiles are represented in instances.'
15
+ message = 'All extensions specified in profiles are used in examples.'
16
16
  result = EvaluationResult.new(message, severity: 'success', rule: self)
17
17
  end
18
18
 
@@ -63,7 +63,7 @@ module Inferno
63
63
  end
64
64
 
65
65
  def get_fail_message(extensions)
66
- message = 'Found extensions specified in profiles, but not used in instances:'
66
+ message = 'Found extensions specified in profiles, but NOT used in examples:'
67
67
  extensions.each do |profile, extension|
68
68
  message += "\n Profile: #{profile}, \n\tExtensions: #{extension.join(', ')}" unless extension.empty?
69
69
  end
@@ -48,7 +48,7 @@ module Inferno
48
48
  else
49
49
  message = 'Found Profiles with not all MustSupports represented:'
50
50
  missing_items_by_profile.each do |profile_url, missing_items|
51
- message += "\n\t\t#{profile_url}: #{missing_items.join(', ')}"
51
+ message += "\n\t#{profile_url}: #{missing_items.join(', ')}"
52
52
  end
53
53
  result = EvaluationResult.new(message, rule: self)
54
54
  end
@@ -29,7 +29,7 @@ module Inferno
29
29
  message = "Found profiles without examples: \n\t #{unused_profile_urls.join(', ')}"
30
30
  result = EvaluationResult.new(message, rule: self)
31
31
  else
32
- message = 'All profiles have example instances.'
32
+ message = 'All profiles have examples.'
33
33
  result = EvaluationResult.new(message, severity: 'success', rule: self)
34
34
  end
35
35
 
@@ -29,7 +29,7 @@ module Inferno
29
29
  message = gen_reference_fail_message(unresolved_references)
30
30
  result = EvaluationResult.new(message, rule: self)
31
31
  else
32
- message = 'All references resolve'
32
+ message = 'All references in examples resolve.'
33
33
  result = EvaluationResult.new(message, severity: 'success', rule: self)
34
34
  end
35
35
 
@@ -44,7 +44,7 @@ module Inferno
44
44
  "\n Resource (id): #{resource_id} #{reference_detail}"
45
45
  end.join(',')
46
46
 
47
- "Found unresolved references: #{result_message}"
47
+ "Found unresolved references in examples: #{result_message}"
48
48
  end
49
49
  end
50
50
  end
@@ -27,11 +27,11 @@ module Inferno
27
27
  island_resources.to_a.sort!
28
28
 
29
29
  if island_resources.any?
30
- message = "Found resources that have no resolved references and are not referenced: #{
30
+ message = "Found resources in examples have no resolved references and are not referenced: #{
31
31
  island_resources.join(', ')}"
32
32
  result = EvaluationResult.new(message, rule: self)
33
33
  else
34
- message = 'All resources are reachable'
34
+ message = 'All resources in examples are reachable.'
35
35
  result = EvaluationResult.new(message, severity: 'success', rule: self)
36
36
  end
37
37
 
@@ -23,13 +23,14 @@ module Inferno
23
23
  end
24
24
 
25
25
  if unused_resource_urls.any?
26
- message = "Found SearchParameters with no searchable data: \n\t#{unused_resource_urls.join(' ,')}"
26
+ unused_resource_list = unused_resource_urls.join("\n\t")
27
+ message = "Found SearchParameters with no searchable data in examples: \n\t#{unused_resource_list}"
27
28
  result = EvaluationResult.new(message, rule: self)
28
29
  elsif !search_params.empty?
29
- message = 'All SearchParameters have examples'
30
+ message = 'All SearchParameters have examples.'
30
31
  result = EvaluationResult.new(message, severity: 'success', rule: self)
31
32
  else
32
- message = 'IG contains no SearchParameter'
33
+ message = 'IG contains no SearchParameter.'
33
34
  result = EvaluationResult.new(message, severity: 'information', rule: self)
34
35
  end
35
36
 
@@ -42,13 +43,13 @@ module Inferno
42
43
  # but without it there's no other way to select a value
43
44
  # Return warning if params don't include expression
44
45
  unless param.expression
45
- message = "Search parameter #{param.url} doesn't have an expression."
46
+ message = "Search parameter #{param.url} doesn't include an expression."
46
47
  result = EvaluationResult.new(message, severity: 'warning', rule: self)
47
48
  context.add_result result
48
49
  return false
49
50
  end
50
51
 
51
- used = false
52
+ param_used = false
52
53
 
53
54
  context.data.each do |resource|
54
55
  next unless param.base.include? resource.resourceType
@@ -56,21 +57,31 @@ module Inferno
56
57
  begin
57
58
  result = evaluate_fhirpath(resource: resource, path: param.expression)
58
59
  rescue StandardError => e
59
- message = "SearchParameter #{param.url} failed to evaluate due to an error. " \
60
- "Expression: #{param.expression}. #{e}"
61
- result = EvaluationResult.new(message)
60
+ if e.to_s.include? 'Unable to connect to FHIRPath service'
61
+ result = EvaluationResult.new(e.to_s, severity: 'error', rule: self)
62
+ else
63
+ message = "SearchParameter #{param.url} failed to evaluate due to an error. " \
64
+ "Expression: #{param.expression}. #{e}"
65
+ result = EvaluationResult.new(message, severity: 'warning', rule: self)
66
+ end
67
+
62
68
  context.add_result result
63
69
 
64
- used = true
70
+ param_used = true
65
71
  break
66
72
  end
67
73
 
68
74
  if result.present?
69
- used = true
75
+ param_used = true
70
76
  break
71
77
  end
72
78
  end
73
- used
79
+ param_used
80
+ end
81
+
82
+ def add_message(message_type, message)
83
+ # No implementation but to prevent error from evaluate_fhirpath().
84
+ # Without this, will throw "undefined method" error since it expects to be called from a Runnable.
74
85
  end
75
86
  end
76
87
  end
@@ -15,7 +15,7 @@ module Inferno
15
15
  message = gen_differential_fail_message(unused_differential)
16
16
  result = EvaluationResult.new(message, rule: self)
17
17
  else
18
- message = 'All differential fields are represented in instances'
18
+ message = 'All differential fields are used in examples.'
19
19
  result = EvaluationResult.new(message, severity: 'success', rule: self)
20
20
  end
21
21
 
@@ -110,7 +110,7 @@ module Inferno
110
110
  end
111
111
 
112
112
  def gen_differential_fail_message(unused_differential)
113
- "Found fields highlighted in the differential view, but not used in instances: #{
113
+ "Found fields highlighted in the differential view, but not used in examples: #{
114
114
  unused_differential.map do |url, field|
115
115
  next if field.empty?
116
116
 
@@ -72,7 +72,7 @@ module Inferno
72
72
  # rubocop:disable Metrics/CyclomaticComplexity
73
73
  def create_result_message
74
74
  if value_set_unused.none?
75
- message = 'All Value sets are used in Examples:'
75
+ message = 'All ValueSets are used in examples:'
76
76
  value_set_used.map { |value_set| message += "\n\t#{value_set}" }
77
77
 
78
78
  if value_set_unevaluated.any?
@@ -82,14 +82,14 @@ module Inferno
82
82
 
83
83
  EvaluationResult.new(message, severity: 'success', rule: self)
84
84
  else
85
- message = 'Value sets with all codes used at least once in Examples:'
85
+ message = 'Found ValueSets with all codes used (at least once) in examples:'
86
86
  value_set_used.map { |url| message += "\n\t#{url}" }
87
87
 
88
- message += "\nFound unused Value Sets: "
88
+ message += "\nFound unused ValueSets: "
89
89
  value_set_unused.map { |url| message += "\n\t#{url}" }
90
90
 
91
91
  if value_set_unevaluated.any?
92
- message += "\nFound unevaluated Value Sets: "
92
+ message += "\nFound unevaluated ValueSets: "
93
93
  value_set_unevaluated.map { |url| message += "\n\t#{url}" }
94
94
  end
95
95
 
@@ -175,10 +175,10 @@ module Inferno
175
175
  begin
176
176
  response = call_validator(resource, profile_url)
177
177
  rescue StandardError => e
178
- # This could be a complete failure to connect (validator isn't running)
179
- # or a timeout (validator took too long to respond).
180
178
  runnable.add_message('error', e.message)
181
- raise Inferno::Exceptions::ErrorInValidatorException, "Unable to connect to validator at #{url}."
179
+ Application[:logger].error(e.message)
180
+
181
+ raise Inferno::Exceptions::ErrorInValidatorException, validator_error_message(e)
182
182
  end
183
183
 
184
184
  outcome = operation_outcome_from_validator_response(response, runnable)
@@ -340,6 +340,28 @@ module Inferno
340
340
  'Validator response was an unexpected format. ' \
341
341
  'Review Messages tab or validator service logs for more information.'
342
342
  end
343
+
344
+ # Add a specific error message for specific network problems to help the user
345
+ #
346
+ # @private
347
+ # @param error [Exception] An error exception that happened during evaluator connection
348
+ # @return [String] A readable error message describing the specific network problem
349
+ def validator_error_message(error)
350
+ case error
351
+ when Faraday::ConnectionFailed
352
+ "Connection failed to validator at #{url}."
353
+ when Faraday::TimeoutError
354
+ "Timeout while connecting to validator at #{url}."
355
+ when Faraday::SSLError
356
+ "SSL error connecting to validator at #{url}."
357
+ when Faraday::ClientError # these are 400s
358
+ "Client error (4xx) connecting to validator at #{url}."
359
+ when Faraday::ServerError # these are 500s
360
+ "Server error (5xx) from validator at #{url}."
361
+ else
362
+ "Unable to connect to validator at #{url}."
363
+ end
364
+ end
343
365
  end
344
366
 
345
367
  # @private
@@ -67,7 +67,9 @@ module Inferno
67
67
  # This could be a complete failure to connect (fhirpath service isn't running)
68
68
  # or a timeout (fhirpath service took too long to respond).
69
69
  runnable.add_message('error', e.message)
70
- raise Inferno::Exceptions::ErrorInFhirpathException, "Unable to connect to FHIRPath service at #{url}."
70
+ Application[:logger].error(e.message)
71
+
72
+ raise Inferno::Exceptions::ErrorInFhirpathException, evaluator_error_message(e)
71
73
  end
72
74
 
73
75
  sanitized_body = remove_invalid_characters(response.body)
@@ -108,6 +110,28 @@ module Inferno
108
110
  def remove_invalid_characters(string)
109
111
  string.gsub(/[^[:print:]\r\n]+/, '')
110
112
  end
113
+
114
+ # Add a specific error message for specific network problems to help the user
115
+ #
116
+ # @private
117
+ # @param error [Exception] An error exception that happened during evaluator connection
118
+ # @return [String] A readable error message describing the specific network problem
119
+ def evaluator_error_message(error)
120
+ case error
121
+ when Faraday::ConnectionFailed
122
+ "Connection failed to evaluator at #{url}."
123
+ when Faraday::TimeoutError
124
+ "Timeout while connecting to evaluator at #{url}."
125
+ when Faraday::SSLError
126
+ "SSL error connecting to evaluator at #{url}."
127
+ when Faraday::ClientError # these are 400s
128
+ "Client error (4xx) connecting to evaluator at #{url}."
129
+ when Faraday::ServerError # these are 500s
130
+ "Server error (5xx) from evaluator at #{url}."
131
+ else
132
+ "Unable to connect to FHIRPath service at #{url}."
133
+ end
134
+ end
111
135
  end
112
136
 
113
137
  module ClassMethods
@@ -12,6 +12,7 @@ module Inferno
12
12
  # @option input_params [String] :default The default value for the input
13
13
  # @option input_params [Boolean] :optional Set to true to not require input for test execution
14
14
  # @option input_params [Boolean] :locked If true, the user can not alter the value
15
+ # @option input_params [Boolean] :hidden If true, the input will not be visible to the user in the UI
15
16
  # @option input_params [Hash] :options Possible input option formats based on input type
16
17
  # @option options [Array] :list_options Array of options for input formats
17
18
  # that require a list of possible values (radio and checkbox)
@@ -346,6 +346,11 @@ module Inferno
346
346
  @all_children ||= []
347
347
  end
348
348
 
349
+ # @private
350
+ def all_descendants
351
+ children.flat_map { |child| [child] + child.all_descendants }
352
+ end
353
+
349
354
  # @private
350
355
  def suite
351
356
  return self if ancestors.include? Inferno::Entities::TestSuite
@@ -0,0 +1,55 @@
1
+ module Inferno
2
+ module DSL
3
+ # This module manages and locks short IDs, ensuring that short IDs
4
+ # remain stable and do not change unexpectedly.
5
+ module ShortIDManager
6
+ def base_short_id_file_folder
7
+ File.join(Dir.pwd, 'lib', name.split('::').first.underscore)
8
+ end
9
+
10
+ def short_id_file_name
11
+ "#{name.demodulize.underscore}_short_id_map.yml"
12
+ end
13
+
14
+ def short_id_file_path
15
+ File.join(base_short_id_file_folder, short_id_file_name).freeze
16
+ end
17
+
18
+ # Loads and memoizes the short ID map from the YAML file.
19
+ #
20
+ # @return [Hash] mapping of runnable IDs to their locked short IDs
21
+ def short_id_map
22
+ return unless File.exist?(short_id_file_path)
23
+
24
+ @short_id_map ||= YAML.load_file(short_id_file_path)
25
+ end
26
+
27
+ # @private
28
+ # Assigns locked short IDs to all descendant runnables based on the short ID map.
29
+ #
30
+ # This method is called at boot time.
31
+ #
32
+ # @return [void]
33
+ def assign_short_ids
34
+ return unless short_id_map
35
+
36
+ all_descendants.each do |runnable|
37
+ new_short_id = short_id_map.fetch(runnable.id)
38
+ runnable.short_id(new_short_id)
39
+ rescue KeyError
40
+ Inferno::Application['logger'].warn("No short id defined for #{runnable.id}")
41
+ end
42
+ end
43
+
44
+ # Builds and memoizes the current mapping of runnable IDs to their short IDs.
45
+ #
46
+ # @return [Hash] current short ID mapping
47
+ def current_short_id_map
48
+ @current_short_id_map ||=
49
+ all_descendants.each_with_object({}) do |runnable, mapping|
50
+ mapping[runnable.id] = runnable.short_id
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -14,6 +14,7 @@ module Inferno
14
14
  :optional,
15
15
  :options,
16
16
  :locked,
17
+ :hidden,
17
18
  :value
18
19
  ].freeze
19
20
  include Entities::Attributes
@@ -21,15 +22,15 @@ module Inferno
21
22
  # These attributes require special handling when merging input
22
23
  # definitions.
23
24
  UNINHERITABLE_ATTRIBUTES = [
24
- # Locking an input only has meaning at the level it is locked.
25
+ # Locking or hiding an input only has meaning at the level it is applied.
25
26
  # Consider:
26
27
  # - ParentGroup
27
28
  # - Group 1, input :a
28
- # - Group 2, input :a, locked: true
29
- # The input 'a' should be only be locked when running Group 2 in
30
- # isolation. It should not be locked when running Group 1 or the
31
- # ParentGroup.
29
+ # - Group 2, input :a, locked: true, hidden: true, optional: true
30
+ # The input 'a' should only be locked or hidden when running Group 2 in isolation.
31
+ # It should not be locked or hidden when running Group 1 or the ParentGroup.
32
32
  :locked,
33
+ :hidden,
33
34
  # Input type is sometimes only a UI concern (e.g. text vs. textarea), so
34
35
  # it is common to not redeclare the type everywhere it's used and needs
35
36
  # special handling to avoid clobbering the type with the default (text)
@@ -50,6 +51,14 @@ module Inferno
50
51
 
51
52
  raise Exceptions::UnknownAttributeException.new(bad_params, self.class) if bad_params.present?
52
53
 
54
+ if params[:hidden] && !params[:optional] && !params[:locked]
55
+ raise Exceptions::InvalidAttributeException.new(
56
+ :hidden,
57
+ self.class,
58
+ "Input '#{params[:name]}' cannot be hidden unless it is optional or locked."
59
+ )
60
+ end
61
+
53
62
  params
54
63
  .compact
55
64
  .each { |key, value| send("#{key}=", value) }