inferno_core 0.6.7 → 0.6.9

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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/lib/inferno/apps/cli/evaluate.rb +6 -1
  3. data/lib/inferno/apps/cli/templates/lib/%library_name%/suite.rb.tt +2 -2
  4. data/lib/inferno/apps/web/controllers/requirements/show.rb +18 -0
  5. data/lib/inferno/apps/web/controllers/test_suites/requirements/index.rb +29 -0
  6. data/lib/inferno/apps/web/router.rb +7 -0
  7. data/lib/inferno/apps/web/serializers/requirement.rb +18 -0
  8. data/lib/inferno/apps/web/serializers/requirement_set.rb +13 -0
  9. data/lib/inferno/apps/web/serializers/test_suite.rb +10 -0
  10. data/lib/inferno/config/boot/requirements.rb +40 -0
  11. data/lib/inferno/dsl/fhir_evaluation/rules/all_defined_extensions_have_examples.rb +58 -0
  12. data/lib/inferno/dsl/fhir_evaluation/rules/all_extensions_used.rb +76 -0
  13. data/lib/inferno/dsl/fhir_evaluation/rules/all_profiles_have_examples.rb +49 -0
  14. data/lib/inferno/dsl/fhir_evaluation/rules/all_search_parameters_have_examples.rb +79 -0
  15. data/lib/inferno/dsl/fhir_evaluation/rules/differential_content_has_examples.rb +124 -0
  16. data/lib/inferno/dsl/fhir_evaluation/rules/value_sets_demonstrate.rb +233 -0
  17. data/lib/inferno/dsl/fhir_resource_navigation.rb +11 -2
  18. data/lib/inferno/dsl/must_support_assessment.rb +15 -3
  19. data/lib/inferno/dsl/requirement_set.rb +82 -0
  20. data/lib/inferno/dsl/runnable.rb +22 -0
  21. data/lib/inferno/dsl/suite_requirements.rb +46 -0
  22. data/lib/inferno/entities/ig.rb +4 -0
  23. data/lib/inferno/entities/requirement.rb +63 -0
  24. data/lib/inferno/entities/test_suite.rb +2 -0
  25. data/lib/inferno/public/bundle.js +3 -3
  26. data/lib/inferno/repositories/igs.rb +1 -2
  27. data/lib/inferno/repositories/requirements.rb +116 -0
  28. data/lib/inferno/version.rb +1 -1
  29. metadata +17 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 48f096486362742aa31ed9f7edec9df07786be3ac7e4f5dfdf121c65b61da1e5
4
- data.tar.gz: d6edfd6063f0ba5b83708b86ee5ccc9e0302a32e9a1f96473bbabeabc2d18184
3
+ metadata.gz: 5740a0c7e8d54767851b4f7e9ee7c5c6ca9ac175211d63b4990f99bbbe9266ca
4
+ data.tar.gz: 4fd1b9c5cb4f37c441a668ae58c8b6cd49aeea18ad393b2d7bbb9699d86a0060
5
5
  SHA512:
6
- metadata.gz: e065c783d7def8ca873e964f61b9ab4cfcf01a352c99e63157d11eb5ca909f617ee031d5421e26b244caa4f36831ae6574160ac59e2d99647aa2d6d99a152fa0
7
- data.tar.gz: 68c8b17c1c635fc4e2986d1e56b06225aa858cf0c049447957c33982f16d0587a0f0407f24c4164ca8b9c08fdbbe6adae693f7a6b1abf0fd9ca258efc50c7db1
6
+ metadata.gz: f6adc91f5db1ba2a6535c847c0fe7b691cd85455fb0f34a44bd525428dee3ddcd4c414d7989ea4e13fdd50b9c39778a19d26e97473f1617b85691b4683776f49
7
+ data.tar.gz: 463d68cf3e187f992bd134bbc177072bfaa9ec625a2006ca1bb23c49591a4bc9521ca027232c68c5d1ebcf499dcca84f3f722ca2afd9bd1b6038bbb19daf4835
@@ -1,7 +1,6 @@
1
1
  require_relative '../../dsl/fhir_evaluation/evaluator'
2
2
  require_relative '../../dsl/fhir_evaluation/config'
3
3
  require_relative '../../entities'
4
- require_relative '../../repositories'
5
4
  require_relative '../../utils/ig_downloader'
6
5
 
7
6
  require 'tempfile'
@@ -10,6 +9,12 @@ module Inferno
10
9
  module CLI
11
10
  class Evaluate < Thor::Group
12
11
  def evaluate(ig_path, data_path, _log_level)
12
+ # NOTE: repositories is required here rather than at the top of the file because
13
+ # the tree of requires means that this file and its requires get required by every CLI app.
14
+ # Sequel::Model, used in some repositories, fetches the table schema at instantiation.
15
+ # This breaks the `migrate` task by fetching a table before the task runs/creates it.
16
+ require_relative '../../repositories'
17
+
13
18
  validate_args(ig_path, data_path)
14
19
  ig = Inferno::Repositories::IGs.new.find_or_load(ig_path)
15
20
 
@@ -13,13 +13,13 @@ module <%= module_name %>
13
13
 
14
14
  input :credentials,
15
15
  title: 'OAuth Credentials',
16
- type: :oauth_credentials,
16
+ type: :auth_info,
17
17
  optional: true
18
18
 
19
19
  # All FHIR requests in this suite will use this FHIR client
20
20
  fhir_client do
21
21
  url :url
22
- oauth_credentials :credentials
22
+ auth_info :credentials
23
23
  end
24
24
 
25
25
  # All FHIR validation requests will use this FHIR validator
@@ -0,0 +1,18 @@
1
+ require_relative '../../serializers/requirement'
2
+
3
+ module Inferno
4
+ module Web
5
+ module Controllers
6
+ module Requirements
7
+ class Show < Controller
8
+ def handle(req, res)
9
+ requirement = repo.find(req.params[:id])
10
+ halt 404 if requirement.nil?
11
+
12
+ res.body = serialize(requirement)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,29 @@
1
+ module Inferno
2
+ module Web
3
+ module Controllers
4
+ module TestSuites
5
+ module Requirements
6
+ class Index < Controller
7
+ include Import[test_suites_repo: 'inferno.repositories.test_suites']
8
+ include Import[test_sessions_repo: 'inferno.repositories.test_sessions']
9
+
10
+ def handle(req, res)
11
+ test_suite = test_suites_repo.find(req.params[:id])
12
+ halt 404, "Test Suite `#{req.params[:id]}` not found" if test_suite.nil?
13
+
14
+ test_session = nil
15
+ if req.params[:session_id]
16
+ test_session = test_sessions_repo.find(req.params[:session_id])
17
+ halt 404, "Test session `#{req.params[:session_id]}` not found" if test_session.nil?
18
+ end
19
+
20
+ requirements = repo.requirements_for_suite(test_suite.id, test_session&.id)
21
+
22
+ res.body = serialize(requirements)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -47,6 +47,13 @@ module Inferno
47
47
  put '/:id/check_configuration',
48
48
  to: Inferno::Web::Controllers::TestSuites::CheckConfiguration,
49
49
  as: :check_configuration
50
+ get ':id/requirements',
51
+ to: Inferno::Web::Controllers::TestSuites::Requirements::Index,
52
+ as: :requirements
53
+ end
54
+
55
+ scope 'requirements' do
56
+ get '/:id', to: Inferno::Web::Controllers::Requirements::Show, as: :show
50
57
  end
51
58
 
52
59
  get '/requests/:id', to: Inferno::Web::Controllers::Requests::Show, as: :requests_show
@@ -0,0 +1,18 @@
1
+ require_relative 'serializer'
2
+
3
+ module Inferno
4
+ module Web
5
+ module Serializers
6
+ class Requirement < Serializer
7
+ identifier :id
8
+
9
+ field :requirement
10
+ field :conformance
11
+ field :actor
12
+ field :sub_requirements
13
+ field :conditionality
14
+ field :url, if: :field_present?
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'serializer'
2
+
3
+ module Inferno
4
+ module Web
5
+ module Serializers
6
+ class RequirementSet < Serializer
7
+ identifier :identifier
8
+
9
+ field :title
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,4 +1,5 @@
1
1
  require_relative 'preset'
2
+ require_relative 'requirement_set'
2
3
  require_relative 'suite_option'
3
4
  require_relative 'test_group'
4
5
 
@@ -27,6 +28,7 @@ module Inferno
27
28
 
28
29
  view :full do
29
30
  include_view :summary
31
+
30
32
  field :test_groups do |suite, options|
31
33
  suite_options = options[:suite_options]
32
34
  TestGroup.render_as_hash(suite.groups(suite_options), suite_options:)
@@ -36,6 +38,14 @@ module Inferno
36
38
  suite_options = options[:suite_options]
37
39
  Input.render_as_hash(suite.available_inputs(suite_options).values)
38
40
  end
41
+ field :requirement_sets, if: :field_present? do |suite, options|
42
+ selected_options = options[:suite_options] || []
43
+ requirement_sets = suite.requirement_sets.select do |requirement_set|
44
+ requirement_set.suite_options.all? { |suite_option| selected_options.include? suite_option }
45
+ end
46
+
47
+ RequirementSet.render_as_hash(requirement_sets)
48
+ end
39
49
  end
40
50
  end
41
51
  end
@@ -0,0 +1,40 @@
1
+ require_relative '../../repositories/requirements'
2
+
3
+ Inferno::Application.register_provider(:requirements) do
4
+ prepare do
5
+ target_container.start :suites
6
+
7
+ requirements_repo = Inferno::Repositories::Requirements.new
8
+
9
+ test_kit_gems =
10
+ Bundler
11
+ .definition
12
+ .specs
13
+ .select { |spec| spec.metadata.fetch('inferno_test_kit', 'false').casecmp? 'true' }
14
+
15
+ files_to_load = Dir.glob(['lib/*test_kit/requirements/*.csv'])
16
+
17
+ if ENV['LOAD_DEV_SUITES'].present?
18
+ ENV['LOAD_DEV_SUITES'].split(',').map(&:strip).reject(&:empty?).each do |suite|
19
+ files_to_load.concat Dir.glob(File.join(Inferno::Application.root, 'dev_suites', suite, 'requirements',
20
+ '*.csv'))
21
+ end
22
+ end
23
+
24
+ files_to_load +=
25
+ test_kit_gems.flat_map do |gem|
26
+ [
27
+ Dir.glob([File.join(gem.full_gem_path, 'lib', '*test_kit', 'requirements', '*.csv')])
28
+ ].flatten
29
+ end
30
+
31
+ files_to_load.compact!
32
+ files_to_load.reject! { |file| file.include?('out_of_scope') }
33
+ files_to_load.map! { |path| File.realpath(path) }
34
+ files_to_load.uniq!
35
+
36
+ files_to_load.each do |path|
37
+ requirements_repo.insert_from_file(path)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inferno
4
+ module DSL
5
+ module FHIREvaluation
6
+ module Rules
7
+ class AllDefinedExtensionsHaveExamples < Rule
8
+ attr_accessor :used_extensions, :unused_extensions
9
+
10
+ def check(context)
11
+ @used_extensions = context.data.map { |e| extension_urls(e) }.flatten.uniq
12
+ @unused_extensions = []
13
+
14
+ get_unused_extensions(context.ig.extensions) do |extension|
15
+ next true if extension.context.any? do |ctx|
16
+ # Skip extensions that are defined for definitional artifacts.
17
+ # For example, US Core's uscdi-requirement extension is applied to
18
+ # the profiles and extensions of the IG, not data that conforms to the IG.
19
+ # There may eventually be cases where SD/ED are data, so this may become configurable.
20
+ ctx.expression == 'StructureDefinition' || ctx.expression == 'ElementDefinition'
21
+ end
22
+
23
+ versioned_url = "#{extension.url}|#{extension.version}"
24
+ used_extensions.include?(extension.url) || used_extensions.include?(versioned_url)
25
+ end
26
+
27
+ if unused_extensions.any?
28
+ message = "Found unused extensions defined in the IG: \n\t #{unused_extensions.join(', ')}"
29
+ result = EvaluationResult.new(message, rule: self)
30
+ else
31
+ message = 'All defined extensions are represented in examples'
32
+ result = EvaluationResult.new(message, severity: 'success', rule: self)
33
+ end
34
+
35
+ context.add_result result
36
+ end
37
+
38
+ def extension_urls(resource)
39
+ urls = []
40
+ resource.each_element do |value, _metadata, path|
41
+ path_elements = path.split('.')
42
+ next unless path_elements.length > 1
43
+
44
+ urls << value if path_elements[-2].include?('extension') && path_elements[-1] == 'url'
45
+ end
46
+ urls.uniq
47
+ end
48
+
49
+ def get_unused_extensions(extensions, &extension_filter)
50
+ extensions.each do |extension|
51
+ unused_extensions.push extension.url unless extension_filter.call(extension)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inferno
4
+ module DSL
5
+ module FHIREvaluation
6
+ module Rules
7
+ class AllExtensionsUsed < Rule
8
+ def check(context)
9
+ all_extensions = collect_profile_extensions(context.ig.profiles)
10
+ unused_extensions = remove_found_resource_extensions(all_extensions, context.data)
11
+ if unused_extensions.any? { |_profile, extensions| !extensions.empty? }
12
+ message = get_fail_message(unused_extensions)
13
+ result = EvaluationResult.new(message, rule: self)
14
+ else
15
+ message = 'All extensions specified in profiles are represented in instances.'
16
+ result = EvaluationResult.new(message, severity: 'success', rule: self)
17
+ end
18
+
19
+ context.add_result result
20
+ end
21
+
22
+ def collect_profile_extensions(profiles)
23
+ extensions = Hash.new { |extension, profile| extension[profile] = Set.new }
24
+ profiles.each do |profile|
25
+ profile.each_element do |value, metadata|
26
+ next unless metadata['type'] == 'ElementDefinition'
27
+
28
+ path_end = value.id.split('.')[-1]
29
+ next unless path_end.include?('extension')
30
+
31
+ value.type.each do |element_definition|
32
+ element_definition.profile.each do |extension_url|
33
+ extensions[profile.url].add(extension_url)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ extensions
39
+ end
40
+
41
+ # rubocop:disable Metrics/CyclomaticComplexity
42
+ def remove_found_resource_extensions(extensions, examples)
43
+ unused_extensions = extensions.dup
44
+ examples.each do |resource|
45
+ resource.each_element do |value, _metadata, path|
46
+ path_elements = path.split('.')
47
+ next unless path_elements.length > 1
48
+
49
+ next unless path_elements[-2].include?('extension') && path_elements[-1] == 'url'
50
+
51
+ profiles = resource&.meta&.profile || []
52
+ update_unused_extensions(profiles, value, unused_extensions, extensions)
53
+ end
54
+ end
55
+ unused_extensions
56
+ end
57
+ # rubocop:enable Metrics/CyclomaticComplexity
58
+
59
+ def update_unused_extensions(profiles, value, unused_extensions, extensions)
60
+ profiles.each do |profile|
61
+ unused_extensions[profile].delete(value) if extensions.key?(profile)
62
+ end
63
+ end
64
+
65
+ def get_fail_message(extensions)
66
+ message = 'Found extensions specified in profiles, but not used in instances:'
67
+ extensions.each do |profile, extension|
68
+ message += "\n Profile: #{profile}, \n\tExtensions: #{extension.join(', ')}" unless extension.empty?
69
+ end
70
+ message
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,49 @@
1
+ module Inferno
2
+ module DSL
3
+ module FHIREvaluation
4
+ module Rules
5
+ class AllProfilesHaveExamples < Rule
6
+ include ProfileConformanceHelper
7
+
8
+ attr_accessor :context, :unused_profile_urls, :all_resources
9
+
10
+ def check(context)
11
+ @context = context
12
+ @unused_profile_urls = []
13
+ @all_resources = []
14
+ options = context.config.data['Rule']['AllProfilesHaveExamples']['ConformanceOptions'].to_options
15
+
16
+ context.data.map { |entry| extract_resources(entry) }
17
+ all_resources.uniq!
18
+
19
+ context.ig.profiles.each do |profile|
20
+ profile_used = all_resources.any? do |resource|
21
+ conforms_to_profile?(resource, profile, options, context.validator)
22
+ end
23
+ unused_profile_urls << profile.url unless profile_used
24
+ end
25
+
26
+ unused_profile_urls.uniq!
27
+
28
+ if unused_profile_urls.any?
29
+ message = "Found profiles without examples: \n\t #{unused_profile_urls.join(', ')}"
30
+ result = EvaluationResult.new(message, rule: self)
31
+ else
32
+ message = 'All profiles have example instances.'
33
+ result = EvaluationResult.new(message, severity: 'success', rule: self)
34
+ end
35
+
36
+ context.add_result result
37
+ end
38
+
39
+ def extract_resources(resource)
40
+ all_resources << resource
41
+ return unless resource.resourceType == 'Bundle'
42
+
43
+ resource.entry.map { |entry| extract_resources(entry.resource) }.flatten
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,79 @@
1
+ require_relative '../../fhirpath_evaluation'
2
+
3
+ module Inferno
4
+ module DSL
5
+ module FHIREvaluation
6
+ module Rules
7
+ class AllSearchParametersHaveExamples < Rule
8
+ include FhirpathEvaluation
9
+
10
+ def check(context)
11
+ unless ENV['FHIRPATH_URL']
12
+ message = 'FHIRPATH_URL is not found. Skipping rule AllSearchParametersHaveExamples.'
13
+ result = EvaluationResult.new(message, severity: 'warning', rule: self)
14
+ context.add_result result
15
+ return
16
+ end
17
+
18
+ unused_resource_urls = []
19
+ search_params = context.ig.resources_by_type['SearchParameter']
20
+
21
+ search_params.each do |search_param|
22
+ unused_resource_urls.push search_param.url unless param_is_used?(search_param, context)
23
+ end
24
+
25
+ if unused_resource_urls.any?
26
+ message = "Found SearchParameters with no searchable data: \n\t#{unused_resource_urls.join(' ,')}"
27
+ result = EvaluationResult.new(message, rule: self)
28
+ elsif !search_params.empty?
29
+ message = 'All SearchParameters have examples'
30
+ result = EvaluationResult.new(message, severity: 'success', rule: self)
31
+ else
32
+ message = 'IG contains no SearchParameter'
33
+ result = EvaluationResult.new(message, severity: 'information', rule: self)
34
+ end
35
+
36
+ context.add_result result
37
+ end
38
+
39
+ def param_is_used?(param, context)
40
+ # Assume that all params have an expression (fhirpath)
41
+ # This is not guaranteed since the field is 0..1
42
+ # but without it there's no other way to select a value
43
+ # Return warning if params don't include expression
44
+ unless param.expression
45
+ message = "Search parameter #{param.url} doesn't have an expression."
46
+ result = EvaluationResult.new(message, severity: 'warning', rule: self)
47
+ context.add_result result
48
+ return false
49
+ end
50
+
51
+ used = false
52
+
53
+ context.data.each do |resource|
54
+ next unless param.base.include? resource.resourceType
55
+
56
+ begin
57
+ result = evaluate_fhirpath(resource: resource, path: param.expression)
58
+ 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)
62
+ context.add_result result
63
+
64
+ used = true
65
+ break
66
+ end
67
+
68
+ if result.present?
69
+ used = true
70
+ break
71
+ end
72
+ end
73
+ used
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inferno
4
+ module DSL
5
+ module FHIREvaluation
6
+ module Rules
7
+ class DifferentialContentHasExamples < Rule
8
+ def check(context)
9
+ unused_differential = Hash.new { |field, url| field[url] = Set.new }
10
+ collect_profile_differential_content(unused_differential, context.ig.profiles)
11
+ collect_profile_differential_content(unused_differential, context.ig.extensions)
12
+ remove_found_differential_content(unused_differential, context.data)
13
+
14
+ if unused_differential.any? { |_url, diff| !diff.empty? }
15
+ message = gen_differential_fail_message(unused_differential)
16
+ result = EvaluationResult.new(message, rule: self)
17
+ else
18
+ message = 'All differential fields are represented in instances'
19
+ result = EvaluationResult.new(message, severity: 'success', rule: self)
20
+ end
21
+
22
+ context.add_result result
23
+ end
24
+
25
+ def collect_profile_differential_content(unused_differential, profiles)
26
+ profiles.each do |profile|
27
+ profile.each_element do |value, _metadata, path|
28
+ next unless path.start_with? 'differential'
29
+
30
+ next unless value.is_a? FHIR::ElementDefinition
31
+ next unless value.id.include? '.'
32
+
33
+ # Skip fields that are disallowed by the profile (cardinality 0..0)
34
+ # Note that max is a string to allow for "*", not an int
35
+ next if value.max == '0'
36
+
37
+ # TODO: discriminate between extensions
38
+ # if you have field.extension:A and field.extension:B
39
+ # only field.extension is recorded and checked for
40
+ # if A and B are not defined in the IG,they may be missed
41
+ clean_val = clean_value(value)
42
+
43
+ unused_differential[profile.url].add(clean_val)
44
+ end
45
+ end
46
+ end
47
+
48
+ # rubocop:disable Metrics/CyclomaticComplexity
49
+ def remove_found_differential_content(unused_differential, examples)
50
+ examples.each do |resource|
51
+ extension_base_path = ''
52
+ extension_url = ''
53
+ resource.each_element do |value, _metadata, path|
54
+ profiles = resource&.meta&.profile || []
55
+ profiles.each do |profile|
56
+ processed_path = plain_value(path)
57
+ processed_path = rm_brackets(processed_path)
58
+
59
+ if path.match?('extension\[\d+\]\.url$')
60
+ extension_base_path = path.rpartition('.').first
61
+ extension_url = value
62
+ unused_differential[extension_url].delete('url') if unused_differential.key?(extension_url)
63
+ unused_differential[extension_url].delete('extension') if unused_differential.key?(extension_url)
64
+ unused_differential.delete(extension_url) if unused_differential[extension_url].empty?
65
+ elsif path.start_with?(extension_base_path) && !extension_base_path.empty?
66
+ if unused_differential.key?(extension_url)
67
+ unused_differential[extension_url].delete(processed_path.rpartition('.').last)
68
+ end
69
+ unused_differential.delete(extension_url) if unused_differential[extension_url].empty?
70
+ else
71
+ unused_differential[profile].delete(processed_path) if unused_differential.key?(profile)
72
+ unused_differential.delete(profile) if unused_differential[profile].empty?
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ # rubocop:enable Metrics/CyclomaticComplexity
79
+
80
+ def clean_value(value)
81
+ stripped_val = value.id.partition('.').last
82
+ stripped_val = stripped_val.partition('[').first if stripped_val.end_with? ']'
83
+ stripped_val.split('.').map do |field|
84
+ field = field.partition(':').first if field.include?(':')
85
+ field = field.partition('[').first if field.include?('[')
86
+ field
87
+ end.join('.')
88
+ end
89
+
90
+ def plain_value(path)
91
+ if path.include? '.'
92
+ path_array = path.split('.').map! do |field|
93
+ field.start_with?('value') ? 'value' : field
94
+ end
95
+ path_array.join('.')
96
+ elsif path.start_with?('value')
97
+ 'value'
98
+ elsif path.end_with?(']')
99
+ path.rpartition('[').first
100
+ else
101
+ path
102
+ end
103
+ end
104
+
105
+ def rm_brackets(path)
106
+ path_array = path.split('.').map! do |field|
107
+ field.include?('[') ? field.partition('[').first : field
108
+ end
109
+ path_array.join('.')
110
+ end
111
+
112
+ def gen_differential_fail_message(unused_differential)
113
+ "Found fields highlighted in the differential view, but not used in instances: #{
114
+ unused_differential.map do |url, field|
115
+ next if field.empty?
116
+
117
+ "\n Profile/Extension: #{url} \n\tFields: #{field.join(', ')}"
118
+ end.compact.join}"
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end