leftovers 0.2.2 → 0.4.2

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 (146) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/README.md +35 -49
  4. data/docs/Configuration.md +627 -0
  5. data/exe/leftovers +2 -2
  6. data/leftovers.gemspec +13 -7
  7. data/lib/config/attr_encrypted.yml +3 -4
  8. data/lib/config/audited.yml +9 -4
  9. data/lib/config/datagrid.yml +1 -1
  10. data/lib/config/flipper.yml +1 -3
  11. data/lib/config/graphql.yml +15 -13
  12. data/lib/config/okcomputer.yml +1 -3
  13. data/lib/config/parser.yml +89 -91
  14. data/lib/config/rails.yml +160 -97
  15. data/lib/config/redcarpet.yml +35 -38
  16. data/lib/config/rollbar.yml +1 -3
  17. data/lib/config/rspec.yml +18 -10
  18. data/lib/config/ruby.yml +42 -49
  19. data/lib/config/selenium-webdriver.yml +19 -0
  20. data/lib/config/sidekiq.yml +9 -0
  21. data/lib/config/will_paginate.yml +12 -14
  22. data/lib/leftovers.rb +61 -43
  23. data/lib/leftovers/ast.rb +8 -0
  24. data/lib/leftovers/ast/builder.rb +6 -3
  25. data/lib/leftovers/ast/node.rb +66 -107
  26. data/lib/leftovers/backports.rb +24 -40
  27. data/lib/leftovers/cli.rb +17 -14
  28. data/lib/leftovers/collector.rb +7 -11
  29. data/lib/leftovers/config.rb +38 -13
  30. data/lib/leftovers/config_validator.rb +60 -0
  31. data/lib/leftovers/config_validator/error_processor.rb +196 -0
  32. data/lib/leftovers/config_validator/schema_hash.rb +496 -0
  33. data/lib/leftovers/definition.rb +16 -41
  34. data/lib/leftovers/definition_node.rb +36 -0
  35. data/lib/leftovers/definition_set.rb +17 -24
  36. data/lib/leftovers/dynamic_processors.rb +11 -0
  37. data/lib/leftovers/dynamic_processors/call.rb +25 -0
  38. data/lib/leftovers/dynamic_processors/call_definition.rb +31 -0
  39. data/lib/leftovers/dynamic_processors/definition.rb +26 -0
  40. data/lib/leftovers/dynamic_processors/each.rb +19 -0
  41. data/lib/leftovers/dynamic_processors/null.rb +9 -0
  42. data/lib/leftovers/erb.rb +2 -2
  43. data/lib/leftovers/file.rb +3 -5
  44. data/lib/leftovers/file_collector.rb +82 -62
  45. data/lib/leftovers/file_list.rb +9 -15
  46. data/lib/leftovers/haml.rb +9 -12
  47. data/lib/leftovers/matcher_builders.rb +24 -0
  48. data/lib/leftovers/matcher_builders/and.rb +19 -0
  49. data/lib/leftovers/matcher_builders/and_not.rb +14 -0
  50. data/lib/leftovers/matcher_builders/argument_node_value.rb +21 -0
  51. data/lib/leftovers/matcher_builders/name.rb +29 -0
  52. data/lib/leftovers/matcher_builders/node.rb +40 -0
  53. data/lib/leftovers/matcher_builders/node_has_argument.rb +71 -0
  54. data/lib/leftovers/matcher_builders/node_has_keyword_argument.rb +22 -0
  55. data/lib/leftovers/matcher_builders/node_has_positional_argument.rb +24 -0
  56. data/lib/leftovers/matcher_builders/node_name.rb +15 -0
  57. data/lib/leftovers/matcher_builders/node_pair_name.rb +18 -0
  58. data/lib/leftovers/matcher_builders/node_pair_value.rb +16 -0
  59. data/lib/leftovers/matcher_builders/node_path.rb +14 -0
  60. data/lib/leftovers/matcher_builders/node_type.rb +28 -0
  61. data/lib/leftovers/matcher_builders/or.rb +73 -0
  62. data/lib/leftovers/matcher_builders/path.rb +15 -0
  63. data/lib/leftovers/matcher_builders/string.rb +11 -0
  64. data/lib/leftovers/matcher_builders/string_pattern.rb +19 -0
  65. data/lib/leftovers/matcher_builders/unless.rb +13 -0
  66. data/lib/leftovers/matchers.rb +26 -0
  67. data/lib/leftovers/matchers/all.rb +25 -0
  68. data/lib/leftovers/matchers/and.rb +24 -0
  69. data/lib/leftovers/matchers/any.rb +27 -0
  70. data/lib/leftovers/matchers/node_has_any_keyword_argument.rb +28 -0
  71. data/lib/leftovers/matchers/node_has_any_positional_argument_with_value.rb +25 -0
  72. data/lib/leftovers/matchers/node_has_positional_argument.rb +23 -0
  73. data/lib/leftovers/matchers/node_has_positional_argument_with_value.rb +25 -0
  74. data/lib/leftovers/matchers/node_name.rb +23 -0
  75. data/lib/leftovers/matchers/node_pair_value.rb +23 -0
  76. data/lib/leftovers/matchers/node_path.rb +23 -0
  77. data/lib/leftovers/matchers/node_scalar_value.rb +25 -0
  78. data/lib/leftovers/matchers/node_type.rb +23 -0
  79. data/lib/leftovers/matchers/not.rb +23 -0
  80. data/lib/leftovers/matchers/or.rb +26 -0
  81. data/lib/leftovers/merged_config.rb +24 -14
  82. data/lib/leftovers/parser.rb +2 -5
  83. data/lib/leftovers/processor_builders.rb +22 -0
  84. data/lib/leftovers/processor_builders/action.rb +63 -0
  85. data/lib/leftovers/processor_builders/add_prefix.rb +20 -0
  86. data/lib/leftovers/processor_builders/add_suffix.rb +20 -0
  87. data/lib/leftovers/processor_builders/argument.rb +25 -0
  88. data/lib/leftovers/processor_builders/dynamic.rb +27 -0
  89. data/lib/leftovers/processor_builders/each.rb +36 -0
  90. data/lib/leftovers/processor_builders/each_action.rb +51 -0
  91. data/lib/leftovers/processor_builders/each_dynamic.rb +54 -0
  92. data/lib/leftovers/processor_builders/each_for_definition_set.rb +36 -0
  93. data/lib/leftovers/processor_builders/itself.rb +13 -0
  94. data/lib/leftovers/processor_builders/keyword.rb +24 -0
  95. data/lib/leftovers/processor_builders/keyword_argument.rb +14 -0
  96. data/lib/leftovers/processor_builders/transform.rb +55 -0
  97. data/lib/leftovers/processor_builders/transform_chain.rb +24 -0
  98. data/lib/leftovers/processor_builders/transform_set.rb +47 -0
  99. data/lib/leftovers/processor_builders/value.rb +13 -0
  100. data/lib/leftovers/rake_task.rb +4 -4
  101. data/lib/leftovers/reporter.rb +1 -1
  102. data/lib/leftovers/value_processors.rb +40 -0
  103. data/lib/leftovers/value_processors/add_dynamic_prefix.rb +31 -0
  104. data/lib/leftovers/value_processors/add_dynamic_suffix.rb +31 -0
  105. data/lib/leftovers/value_processors/add_prefix.rb +20 -0
  106. data/lib/leftovers/value_processors/add_suffix.rb +20 -0
  107. data/lib/leftovers/value_processors/camelize.rb +24 -0
  108. data/lib/leftovers/value_processors/capitalize.rb +19 -0
  109. data/lib/leftovers/value_processors/deconstantize.rb +24 -0
  110. data/lib/leftovers/value_processors/delete_after.rb +22 -0
  111. data/lib/leftovers/value_processors/delete_before.rb +22 -0
  112. data/lib/leftovers/value_processors/delete_prefix.rb +26 -0
  113. data/lib/leftovers/value_processors/delete_suffix.rb +26 -0
  114. data/lib/leftovers/value_processors/demodulize.rb +24 -0
  115. data/lib/leftovers/value_processors/downcase.rb +19 -0
  116. data/lib/leftovers/value_processors/each.rb +21 -0
  117. data/lib/leftovers/value_processors/each_for_definition_set.rb +29 -0
  118. data/lib/leftovers/value_processors/each_keyword.rb +27 -0
  119. data/lib/leftovers/value_processors/each_keyword_argument.rb +27 -0
  120. data/lib/leftovers/value_processors/each_positional_argument.rb +24 -0
  121. data/lib/leftovers/value_processors/itself.rb +17 -0
  122. data/lib/leftovers/value_processors/keyword.rb +36 -0
  123. data/lib/leftovers/value_processors/keyword_argument.rb +36 -0
  124. data/lib/leftovers/value_processors/parameterize.rb +24 -0
  125. data/lib/leftovers/value_processors/placeholder.rb +18 -0
  126. data/lib/leftovers/value_processors/pluralize.rb +24 -0
  127. data/lib/leftovers/value_processors/positional_argument.rb +26 -0
  128. data/lib/leftovers/value_processors/replace_value.rb +18 -0
  129. data/lib/leftovers/value_processors/return_definition.rb +26 -0
  130. data/lib/leftovers/value_processors/return_string.rb +14 -0
  131. data/lib/leftovers/value_processors/singularize.rb +24 -0
  132. data/lib/leftovers/value_processors/split.rb +22 -0
  133. data/lib/leftovers/value_processors/swapcase.rb +19 -0
  134. data/lib/leftovers/value_processors/titleize.rb +24 -0
  135. data/lib/leftovers/value_processors/underscore.rb +24 -0
  136. data/lib/leftovers/value_processors/upcase.rb +19 -0
  137. data/lib/leftovers/version.rb +1 -1
  138. metadata +190 -28
  139. data/lib/config/selenium.yml +0 -21
  140. data/lib/leftovers/argument_rule.rb +0 -219
  141. data/lib/leftovers/core_ext.rb +0 -13
  142. data/lib/leftovers/hash_rule.rb +0 -40
  143. data/lib/leftovers/name_rule.rb +0 -53
  144. data/lib/leftovers/rule.rb +0 -74
  145. data/lib/leftovers/transform_rule.rb +0 -171
  146. data/lib/leftovers/value_rule.rb +0 -56
@@ -1,21 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'yaml'
4
- require_relative 'rule'
5
4
 
6
5
  module Leftovers
7
6
  class Config
8
7
  # :nocov:
9
- using ::Leftovers::YAMLSymbolizeNames if defined?(::Leftovers::YAMLSymbolizeNames)
8
+ using ::Leftovers::Backports::SetCaseEq if defined?(::Leftovers::Backports::SetCaseEq)
10
9
  # :nocov:
11
10
 
12
11
  attr_reader :name
13
12
 
14
- def initialize(
15
- name,
16
- path: ::File.join(__dir__, '..', 'config', "#{name}.yml"),
17
- content: (::File.exist?(path) ? ::File.read(path) : '')
18
- )
13
+ def initialize(name, path: nil, content: nil)
19
14
  @name = name.to_sym
20
15
  @path = path
21
16
  @content = content
@@ -37,17 +32,47 @@ module Leftovers
37
32
  @test_paths ||= Array(yaml[:test_paths])
38
33
  end
39
34
 
40
- def rules
41
- @rules ||= Rule.wrap(yaml[:rules])
35
+ def dynamic
36
+ @dynamic ||= ::Leftovers::ProcessorBuilders::Dynamic.build(yaml[:dynamic])
37
+ end
38
+
39
+ def keep
40
+ @keep ||= ::Leftovers::MatcherBuilders::Node.build(yaml[:keep])
41
+ end
42
+
43
+ def test_only
44
+ @test_only ||= ::Leftovers::MatcherBuilders::Node.build(yaml[:test_only])
45
+ end
46
+
47
+ def requires
48
+ @requires ||= Array(yaml[:requires])
42
49
  end
43
50
 
44
51
  private
45
52
 
53
+ def content
54
+ @content ||= ::File.exist?(path) ? ::File.read(path) : ''
55
+ end
56
+
57
+ def path
58
+ @path ||= ::File.expand_path("../config/#{name}.yml", __dir__)
59
+ end
60
+
46
61
  def yaml
47
- @yaml ||= YAML.safe_load(@content, symbolize_names: true) || {}
48
- rescue Psych::SyntaxError => e
49
- warn "\e[31mError with config #{path}: #{e.message}\e[0m"
50
- exit 1
62
+ @yaml ||= ::Leftovers::ConfigValidator.validate_and_process!(parse_yaml, path)
63
+ end
64
+
65
+ def parse_yaml
66
+ # :nocov:
67
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.6')
68
+ Psych.safe_load(content, filename: path) || {}
69
+ else
70
+ Psych.safe_load(content, [], [], false, path) || {}
71
+ end
72
+ # :nocov:
73
+ rescue ::Psych::SyntaxError => e
74
+ warn "\e[31mConfig SyntaxError: #{e.message}\e[0m"
75
+ Leftovers.exit 1
51
76
  end
52
77
  end
53
78
  end
@@ -0,0 +1,60 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'json_schemer'
4
+
5
+ module Leftovers
6
+ module ConfigValidator
7
+ autoload(:ErrorProcessor, "#{__dir__}/config_validator/error_processor")
8
+ autoload(:SCHEMA_HASH, "#{__dir__}/config_validator/schema_hash")
9
+
10
+ def self.default_schema
11
+ @default_schema ||= ::JSONSchemer.schema(::Leftovers::ConfigValidator::SCHEMA_HASH)
12
+ end
13
+
14
+ def self.validate(obj, validator = default_schema)
15
+ validator.validate(obj)
16
+ end
17
+
18
+ def self.validate_and_process!(yaml, path)
19
+ errors = validate(yaml)
20
+ print_validation_errors_and_exit(errors, path) unless errors.first.nil?
21
+ post_process!(yaml)
22
+ end
23
+
24
+ def self.print_validation_errors_and_exit(errors, path)
25
+ ::Leftovers::ConfigValidator::ErrorProcessor.process(errors).each do |message|
26
+ warn "\e[31mConfig SchemaError: (#{path}): #{message}\e[0m"
27
+ end
28
+
29
+ ::Leftovers.exit 1
30
+ end
31
+
32
+ def self.post_process!(obj)
33
+ case obj
34
+ when Hash
35
+ obj.keys.each do |key| # rubocop:disable Style/HashEachMethods # each_key never finishes.
36
+ obj[symbolize_name(key)] = post_process!(obj.delete(key))
37
+ end
38
+ when Array
39
+ obj.map! { |ea| post_process!(ea) }
40
+ end
41
+ obj
42
+ end
43
+
44
+ def self.symbolize_name(name) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
45
+ case name
46
+ when 'matches' then :match
47
+ when 'defines' then :define
48
+ when 'calls' then :call
49
+ when 'name' then :names
50
+ when 'keyword' then :keywords
51
+ when 'argument' then :arguments
52
+ when 'has_argument' then :has_arguments
53
+ when 'path' then :paths
54
+ when 'unless' then :unless_arg
55
+ when 'require' then :requires
56
+ else name.to_sym
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,196 @@
1
+ # frozen-string-literal: true
2
+
3
+ ::Leftovers.try_require 'did_you_mean'
4
+
5
+ module Leftovers
6
+ module ConfigValidator
7
+ module ErrorProcessor # rubocop:disable Metrics/ModuleLength
8
+ LENGTH_TYPE = %w{minItems minLength minProperties}.freeze
9
+ TYPE_TYPE = %w{null string boolean integer number array object}.freeze
10
+ VALUE_TYPE = %w{enum const}.freeze
11
+ REQUIRED_TYPE = %w{required}.freeze
12
+
13
+ class << self
14
+ def process(errors) # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize
15
+ error_data_pointers = []
16
+ errors = group_errors(errors)
17
+ errors.flat_map do |data_pointer, error_group| # rubocop:disable Metrics/BlockLength
18
+ next if error_data_pointers.find { |x| x.start_with?(data_pointer) }
19
+
20
+ # original_error_group = error_group.dup
21
+ length_errors = error_group.select { |x| LENGTH_TYPE.include?(x['type']) }
22
+ value_errors = error_group.select { |x| VALUE_TYPE.include?(x['type']) }
23
+ type_errors = error_group.select { |x| TYPE_TYPE.include?(x['type']) }
24
+ required_errors = error_group.select { |x| REQUIRED_TYPE.include?(x['type']) }
25
+ messages = []
26
+
27
+ if !type_errors.empty? && (error_group - type_errors).empty?
28
+ error_group -= type_errors
29
+ error_data_pointers << data_pointer
30
+ any_of = group_by_same_any_of(type_errors)
31
+ if any_of.length == 1
32
+ was_class = to_json_type(type_errors.first['data'])
33
+ error_types = type_errors.map { |x| x['type'] }
34
+
35
+ messages << <<~MESSAGE.chomp
36
+ #{data_pointer}: must be #{an to_sentence(error_types, 'or')} (was #{an was_class})
37
+ MESSAGE
38
+ # :nocov:
39
+ else
40
+ nil
41
+ # :nocov:
42
+ end
43
+ elsif !required_errors.empty? && error_group.first['data'].is_a?(Hash)
44
+ error_data_pointers << data_pointer
45
+ group_by_same_any_of(required_errors).each do |_any_of_key, any_of_value|
46
+ required_keywords = any_of_value.flat_map { |x| x['details']['missing_keys'] }
47
+ error_group -= any_of_value
48
+ if any_of_value.length > 1
49
+ messages << <<~MESSAGE
50
+ #{data_pointer}: requires at least one of these keywords: #{to_sentence(required_keywords, 'or')}
51
+ MESSAGE
52
+
53
+ # :nocov:
54
+ else
55
+ nil
56
+ # :nocov:
57
+ # messages << "#{data_pointer}: requires keyword: #{required_keywords.first}"
58
+ end
59
+ end
60
+ elsif !length_errors.empty?
61
+ error_data_pointers << data_pointer
62
+ messages << "#{data_pointer}: can't be empty"
63
+ end
64
+
65
+ error_group.each do |error| # rubocop:disable Metrics/BlockLength
66
+ type = error['type']
67
+ case type
68
+ when 'schema'
69
+ error_data_pointers << data_pointer
70
+ parent_pointer = parent(data_pointer)
71
+ if ::File.basename(error['schema_pointer']) == 'additionalProperties'
72
+ keyword = tail(data_pointer)
73
+ parent_schema_pointer = parent(error['schema_pointer'])
74
+ actual_keywords = schema_hash_dig(parent_schema_pointer)['properties'].keys
75
+ corrections = did_you_mean(keyword, actual_keywords)
76
+
77
+ messages << <<~MESSAGE.chomp
78
+ #{parent_pointer}: invalid property keyword: #{keyword}
79
+ Valid keywords: #{to_sentence actual_keywords}
80
+ #{"Did you mean? #{to_sentence corrections, 'or'}" unless corrections.empty?}
81
+ MESSAGE
82
+ # :nocov:
83
+ else
84
+ nil
85
+ # :nocov:
86
+ end
87
+ when 'enum'
88
+ next if error['data'].is_a?(Hash)
89
+
90
+ error_data_pointers << data_pointer
91
+ given_value = error['data']
92
+ valid_values = value_errors.first['schema']['enum']
93
+ corrections = did_you_mean(given_value, valid_values)
94
+ messages << <<~MESSAGE
95
+ #{data_pointer}: can't be: #{given_value}
96
+ Valid values: #{to_sentence valid_values, 'or'}
97
+ #{"Did you mean? #{to_sentence corrections, 'or'}" unless corrections.empty?}
98
+ MESSAGE
99
+ when 'not'
100
+ next unless error['data'].is_a?(Hash)
101
+
102
+ error_data_pointers << data_pointer
103
+ if error['schema']['required']
104
+ invalid_combinations = error['schema']['required'] & error['data'].keys
105
+ messages << <<~MESSAGE
106
+ #{data_pointer}: use only one of: #{to_sentence invalid_combinations, 'or'}
107
+ MESSAGE
108
+ # :nocov:
109
+ else
110
+ end
111
+ else
112
+ end
113
+ # :nocov:
114
+ end
115
+
116
+ if messages.empty?
117
+ error_data_pointers << data_pointer
118
+ "#{data_pointer} is invalid"
119
+ else
120
+ messages
121
+ end
122
+ end.compact.map(&:strip)
123
+ end
124
+
125
+ private
126
+
127
+ def did_you_mean(word, dictionary)
128
+ # :nocov:
129
+ if defined?(::DidYouMean::SpellChecker)
130
+ ::DidYouMean::SpellChecker.new(dictionary: dictionary).correct(word)
131
+ else
132
+ []
133
+ end
134
+ # :nocov:
135
+ end
136
+
137
+ def schema_hash_dig(schema_pointer)
138
+ ::Leftovers::ConfigValidator::SCHEMA_HASH.dig(
139
+ *schema_pointer.split('/').drop(1).map { |x| x.match?(/\A\d+\z/) ? x.to_i : x }
140
+ )
141
+ end
142
+
143
+ def parent(pointer)
144
+ ::File.dirname(pointer)
145
+ end
146
+
147
+ def tail(pointer)
148
+ ::File.basename(pointer)
149
+ end
150
+
151
+ def to_json_type(value)
152
+ case value
153
+ when Hash then 'object'
154
+ when Float then 'number'
155
+ when true, false then 'boolean'
156
+ when nil then 'null'
157
+ else value.class.name.downcase
158
+ end
159
+ end
160
+
161
+ def an(str)
162
+ case str[0]
163
+ # when nil then ""
164
+ when 'a', 'e', 'i', 'o', 'u' then "an #{str}"
165
+ else "a #{str}"
166
+ end
167
+ end
168
+
169
+ def to_sentence(ary, join_word = 'and')
170
+ case ary.length
171
+ when 1 then ary.first
172
+ when 2 then ary.join(" #{join_word} ")
173
+ else
174
+ ary = ary.dup
175
+ last = ary.pop(2)
176
+ [*ary, last.join(", #{join_word} ")].join(', ')
177
+ end
178
+ end
179
+
180
+ def group_errors(errors)
181
+ errors = errors.map do |x|
182
+ x.delete('root_schema')
183
+ x
184
+ end
185
+ errors.group_by { |x| x['data_pointer'] }.sort.reverse.to_h
186
+ end
187
+
188
+ def group_by_same_any_of(errors)
189
+ errors.group_by do |x|
190
+ x['schema_pointer'].match?(%r{/anyOf/\d+$}) && parent(parent(x['schema_pointer']))
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,496 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Leftovers
4
+ module ConfigValidator # rubocop:disable Metrics/ModuleLength
5
+ SCHEMA_HASH = {
6
+ '$schema' => 'http://json-schema.org/draft-06/schema#',
7
+ 'type' => 'object',
8
+ 'definitions' => {
9
+ 'true' => { 'enum' => [true, 'true'] },
10
+ 'string' => {
11
+ 'type' => 'string',
12
+ 'minLength' => 1
13
+ },
14
+ 'stringPattern' => {
15
+ 'type' => 'object',
16
+ 'properties' => {
17
+ 'match' => { '$ref' => '#/definitions/string' },
18
+ 'matches' => { '$ref' => '#/definitions/string' },
19
+ 'has_prefix' => { '$ref' => '#/definitions/string' },
20
+ 'has_suffix' => { '$ref' => '#/definitions/string' }
21
+ },
22
+ 'minProperties' => 0,
23
+ 'additionalProperties' => true,
24
+ 'allOf' => [
25
+ # synonyms
26
+ { 'not' => { 'required' => %w{match matches} } },
27
+ # incompatible groups
28
+ { 'not' => { 'required' => %w{match has_prefix} } },
29
+ { 'not' => { 'required' => %w{match has_suffix} } },
30
+ { 'not' => { 'required' => %w{matches has_prefix} } },
31
+ { 'not' => { 'required' => %w{matches has_suffix} } }
32
+ ]
33
+ },
34
+ 'stringList' => {
35
+ 'anyOf' => [
36
+ {
37
+ 'type' => 'array',
38
+ 'items' => { '$ref' => '#/definitions/string' },
39
+ 'minItems' => 1,
40
+ 'uniqueItems' => true
41
+ },
42
+ { '$ref' => '#/definitions/string' }
43
+ ]
44
+ },
45
+ 'name' => {
46
+ 'anyOf' => [
47
+ { '$ref' => '#/definitions/string' },
48
+ { 'allOf' => [
49
+ { '$ref' => '#/definitions/stringPattern' },
50
+ {
51
+ 'type' => 'object',
52
+ 'properties' => {
53
+ 'match' => true, 'matches' => true,
54
+ 'has_prefix' => true, 'has_suffix' => true,
55
+ 'unless' => { '$ref' => '#/definitions/nameList' }
56
+ },
57
+ 'minProperties' => 1,
58
+ 'additionalProperties' => false
59
+ }
60
+ ] }
61
+ ]
62
+ },
63
+ 'nameList' => {
64
+ 'anyOf' => [
65
+ {
66
+ 'type' => 'array',
67
+ 'items' => { '$ref' => '#/definitions/name' },
68
+ 'minItems' => 1,
69
+ 'uniqueItems' => true
70
+ },
71
+ { '$ref' => '#/definitions/name' }
72
+ ]
73
+ },
74
+ 'argumentPosition' => {
75
+ 'anyOf' => [
76
+ { '$ref' => '#/definitions/string' },
77
+ { 'type' => 'integer', 'minimum' => 0 },
78
+ { '$ref' => '#/definitions/name' }
79
+ ]
80
+ },
81
+ 'argumentPositionList' => {
82
+ 'anyOf' => [
83
+ { '$ref' => '#/definitions/argumentPosition' },
84
+ {
85
+ 'type' => 'array',
86
+ 'items' => { '$ref' => '#/definitions/argumentPosition' },
87
+ 'minItems' => 1,
88
+ 'uniqueItems' => true
89
+ }
90
+ ]
91
+ },
92
+ 'valueType' => {
93
+ 'type' => 'string',
94
+ 'enum' => %w{String Symbol Integer Float}
95
+ },
96
+ 'valueTypeList' => {
97
+ 'anyOf' => [
98
+ { '$ref' => '#/definitions/valueType' },
99
+ {
100
+ 'type' => 'array',
101
+ 'items' => { '$ref' => '#/definitions/valueType' },
102
+ 'minItems' => 1,
103
+ 'uniqueItems' => true
104
+ }
105
+ ]
106
+ },
107
+ 'hasValue' => {
108
+ 'anyOf' => [
109
+ { 'type' => 'string' },
110
+ { 'type' => 'integer' },
111
+ { 'type' => 'number' },
112
+ { 'type' => 'boolean' },
113
+ { 'type' => 'null' }
114
+ ]
115
+ },
116
+ 'hasValueList' => {
117
+ 'anyOf' => [
118
+ { '$ref' => '#/definitions/hasValue' },
119
+ {
120
+ 'type' => 'array',
121
+ 'items' => { '$ref' => '#/definitions/hasValue' },
122
+ 'minItems' => 1,
123
+ 'uniqueItems' => true
124
+ }
125
+ ]
126
+ },
127
+ 'hasArgument' => {
128
+ 'anyOf' => [
129
+ { '$ref' => '#/definitions/string' },
130
+ { 'type' => 'integer', 'minimum' => 0 },
131
+ {
132
+ 'type' => 'object',
133
+ 'properties' => {
134
+ 'at' => { '$ref' => '#/definitions/argumentPositionList' },
135
+ 'has_value' => { '$ref' => '#/definitions/hasValueList' },
136
+ 'has_value_type' => { '$ref' => '#/definitions/valueTypeList' },
137
+ 'unless' => { '$ref' => '#/definitions/hasArgumentList' }
138
+ },
139
+ 'minProperties' => 1,
140
+ 'additionalProperties' => false
141
+ }
142
+ ]
143
+ },
144
+ 'hasArgumentList' => {
145
+ 'anyOf' => [
146
+ { '$ref' => '#/definitions/hasArgument' },
147
+ {
148
+ 'type' => 'array',
149
+ 'items' => { '$ref' => '#/definitions/hasArgument' },
150
+ 'minItems' => 1,
151
+ 'uniqueItems' => true
152
+ }
153
+ ]
154
+ },
155
+ 'rulePattern' => {
156
+ 'type' => 'object',
157
+ 'properties' => {
158
+ 'name' => { '$ref' => '#/definitions/nameList' },
159
+ 'names' => { '$ref' => '#/definitions/nameList' },
160
+ 'path' => { '$ref' => '#/definitions/stringList' },
161
+ 'paths' => { '$ref' => '#/definitions/stringList' },
162
+ 'has_argument' => { '$ref' => '#/definitions/hasArgumentList' },
163
+ 'has_arguments' => { '$ref' => '#/definitions/hasArgumentList' }
164
+ },
165
+ 'minProperties' => 1,
166
+ 'allOf' => [
167
+ # synonyms
168
+ { 'not' => { 'required' => %w{name names} } },
169
+ { 'not' => { 'required' => %w{path paths} } },
170
+ { 'not' => { 'required' => %w{has_argument has_arguments} } }
171
+ ]
172
+ },
173
+ 'transformProperties' => {
174
+ 'type' => 'object',
175
+ 'properties' => {
176
+ 'original' => { '$ref' => '#/definitions/true' },
177
+ 'pluralize' => { '$ref' => '#/definitions/true' },
178
+ 'singularize' => { '$ref' => '#/definitions/true' },
179
+ 'camelize' => { '$ref' => '#/definitions/true' },
180
+ 'camelcase' => { '$ref' => '#/definitions/true' },
181
+ 'underscore' => { '$ref' => '#/definitions/true' },
182
+ 'titleize' => { '$ref' => '#/definitions/true' },
183
+ 'titlecase' => { '$ref' => '#/definitions/true' },
184
+ 'demodulize' => { '$ref' => '#/definitions/true' },
185
+ 'deconstantize' => { '$ref' => '#/definitions/true' },
186
+ 'parameterize' => { '$ref' => '#/definitions/true' },
187
+ 'downcase' => { '$ref' => '#/definitions/true' },
188
+ 'upcase' => { '$ref' => '#/definitions/true' },
189
+ 'capitalize' => { '$ref' => '#/definitions/true' },
190
+ 'swapcase' => { '$ref' => '#/definitions/true' },
191
+
192
+ 'add_prefix' => { '$ref' => '#/definitions/actionList' },
193
+ 'add_suffix' => { '$ref' => '#/definitions/actionList' },
194
+
195
+ 'split' => { '$ref' => '#/definitions/string' },
196
+ 'delete_prefix' => { '$ref' => '#/definitions/string' },
197
+ 'delete_suffix' => { '$ref' => '#/definitions/string' },
198
+ 'delete_before' => { '$ref' => '#/definitions/string' },
199
+ 'delete_after' => { '$ref' => '#/definitions/string' }
200
+ }
201
+ },
202
+ 'transform' => {
203
+ 'anyOf' => [
204
+ {
205
+ 'type' => 'string',
206
+ 'enum' => %w{
207
+ original
208
+ pluralize
209
+ singularize
210
+ camelize
211
+ camelcase
212
+ underscore
213
+ titleize
214
+ titlecase
215
+ demodulize
216
+ deconstantize
217
+ parameterize
218
+ downcase
219
+ upcase
220
+ capitalize
221
+ swapcase
222
+ }
223
+ },
224
+ { 'allOf' => [
225
+ { '$ref' => '#/definitions/transformProperties' },
226
+ {
227
+ 'type' => 'object',
228
+ 'properties' => {
229
+ 'original' => true,
230
+ 'pluralize' => true,
231
+ 'singularize' => true,
232
+ 'camelize' => true,
233
+ 'camelcase' => true,
234
+ 'underscore' => true,
235
+ 'titleize' => true,
236
+ 'titlecase' => true,
237
+ 'demodulize' => true,
238
+ 'deconstantize' => true,
239
+ 'parameterize' => true,
240
+ 'downcase' => true,
241
+ 'upcase' => true,
242
+ 'capitalize' => true,
243
+ 'swapcase' => true,
244
+ 'add_prefix' => true,
245
+ 'add_suffix' => true,
246
+ 'split' => true,
247
+ 'delete_prefix' => true,
248
+ 'delete_suffix' => true,
249
+ 'delete_before' => true,
250
+ 'delete_after' => true
251
+ },
252
+ 'additionalProperties' => false
253
+ }
254
+ ] }
255
+ ]
256
+ },
257
+ 'transformList' => {
258
+ 'anyOf' => [
259
+ { '$ref' => '#/definitions/transform' },
260
+ {
261
+ 'type' => 'array',
262
+ 'items' => { '$ref' => '#/definitions/transform' },
263
+ 'minItems' => 1,
264
+ 'uniqueItems' => true
265
+ }
266
+ ]
267
+ },
268
+ 'keyword' => {
269
+ 'anyOf' => [
270
+ { '$ref' => '#/definitions/name' }
271
+ ]
272
+ },
273
+ 'keywordList' => {
274
+ 'anyOf' => [
275
+ { '$ref' => '#/definitions/keyword' },
276
+ {
277
+ 'type' => 'array',
278
+ 'items' => { '$ref' => '#/definitions/keyword' },
279
+ 'minItems' => 1,
280
+ 'uniqueItems' => true
281
+ }
282
+ ]
283
+ },
284
+ 'action' => {
285
+ 'anyOf' => [
286
+ { '$ref' => '#/definitions/string' },
287
+ { 'type' => 'integer', 'minimum' => 0 },
288
+ { 'allOf' => [
289
+ { '$ref' => '#/definitions/transformProperties' },
290
+ {
291
+ 'type' => 'object',
292
+ 'properties' => {
293
+ 'argument' => { '$ref' => '#/definitions/argumentPositionList' },
294
+ 'arguments' => { '$ref' => '#/definitions/argumentPositionList' },
295
+ 'keyword' => { '$ref' => '#/definitions/keywordList' },
296
+ 'keywords' => { '$ref' => '#/definitions/keywordList' },
297
+ 'itself' => { '$ref' => '#/definitions/true' },
298
+ 'value' => { '$ref' => '#/definitions/string' },
299
+ 'nested' => { '$ref' => '#/definitions/actionList' },
300
+ 'recursive' => { '$ref' => '#/definitions/true' },
301
+ 'transforms' => { '$ref' => '#/definitions/transformList' },
302
+ 'pluralize' => true,
303
+ 'singularize' => true,
304
+ 'camelize' => true,
305
+ 'camelcase' => true,
306
+ 'underscore' => true,
307
+ 'titleize' => true,
308
+ 'titlecase' => true,
309
+ 'demodulize' => true,
310
+ 'deconstantize' => true,
311
+ 'parameterize' => true,
312
+ 'downcase' => true,
313
+ 'upcase' => true,
314
+ 'capitalize' => true,
315
+ 'swapcase' => true,
316
+ 'add_prefix' => true,
317
+ 'add_suffix' => true,
318
+ 'split' => true,
319
+ 'delete_prefix' => true,
320
+ 'delete_suffix' => true,
321
+ 'delete_before' => true,
322
+ 'delete_after' => true
323
+ },
324
+ 'additionalProperties' => false,
325
+ 'allOf' => [
326
+ # synonyms
327
+ { 'not' => { 'required' => %w{keyword keywords} } },
328
+ { 'not' => { 'required' => %w{argument arguments} } },
329
+ # any of
330
+ { 'anyOf' => [
331
+ { 'required' => ['argument'] },
332
+ { 'required' => ['arguments'] },
333
+ { 'required' => ['keyword'] },
334
+ { 'required' => ['keywords'] },
335
+ { 'required' => ['itself'] },
336
+ { 'required' => ['value'] }
337
+ ] }
338
+ ]
339
+ }
340
+ ] }
341
+ ]
342
+ },
343
+ 'actionList' => {
344
+ 'anyOf' => [
345
+ { '$ref' => '#/definitions/action' },
346
+ {
347
+ 'type' => 'array',
348
+ 'items' => { '$ref' => '#/definitions/action' },
349
+ 'minItems' => 1,
350
+ 'uniqueItems' => true
351
+ }
352
+ ]
353
+ },
354
+ 'dynamicAction' => {
355
+ 'type' => 'object',
356
+ 'properties' => {
357
+ 'call' => { '$ref' => '#/definitions/actionList' },
358
+ 'calls' => { '$ref' => '#/definitions/actionList' },
359
+ 'define' => { '$ref' => '#/definitions/actionList' },
360
+ 'defines' => { '$ref' => '#/definitions/actionList' }
361
+ },
362
+ 'additionalProperties' => true,
363
+ 'minProperties' => 1,
364
+ 'allOf' => [
365
+ # synonyms
366
+ { 'not' => { 'required' => %w{call calls} } },
367
+ { 'not' => { 'required' => %w{define defines} } },
368
+ # At least one of
369
+ { 'anyOf' => [
370
+ { 'required' => ['call'] }, { 'required' => ['calls'] },
371
+ { 'required' => ['define'] }, { 'required' => ['defines'] }
372
+ ] }
373
+ ]
374
+ },
375
+ 'ruleMatcherList' => {
376
+ 'anyOf' => [
377
+ {
378
+ 'type' => 'array',
379
+ 'items' => { '$ref' => '#/definitions/ruleMatcher' },
380
+ 'minItems' => 1,
381
+ 'uniqueItems' => true
382
+ },
383
+ { '$ref' => '#/definitions/ruleMatcher' }
384
+ ]
385
+ },
386
+ 'ruleMatcher' => {
387
+ 'allOf' => [
388
+ { '$ref' => '#/definitions/rulePattern' },
389
+ { 'anyOf' => [
390
+ { 'required' => ['name'] }, { 'required' => ['names'] },
391
+ { 'required' => ['path'] }, { 'required' => ['paths'] },
392
+ { 'required' => ['has_argument'] }, { 'required' => ['has_arguments'] },
393
+ { 'required' => ['unless'] }
394
+ ] },
395
+ {
396
+ 'type' => 'object',
397
+ 'properties' => {
398
+ # unfortunately this repetition is necessary to use additionalProperties: false
399
+ 'name' => true, 'names' => true,
400
+ 'path' => true, 'paths' => true,
401
+ 'has_argument' => true, 'has_arguments' => true,
402
+ 'unless' => { '$ref' => '#/definitions/ruleMatcherList' }
403
+
404
+ },
405
+ 'additionalProperties' => false,
406
+ 'minProperties' => 1
407
+ }
408
+ ]
409
+ },
410
+ 'dynamic' => {
411
+ 'allOf' => [
412
+ { '$ref' => '#/definitions/rulePattern' },
413
+ { '$ref' => '#/definitions/dynamicAction' },
414
+ { 'anyOf' => [
415
+ { 'required' => ['name'] }, { 'required' => ['names'] },
416
+ { 'required' => ['path'] }, { 'required' => ['paths'] },
417
+ { 'required' => ['has_argument'] }, { 'required' => ['has_arguments'] },
418
+ { 'required' => ['unless'] }
419
+ ] },
420
+ {
421
+ 'type' => 'object',
422
+ 'properties' => {
423
+ # unfortunately this repetition is necessary to use additionalProperties: false
424
+ 'name' => true, 'names' => true,
425
+ 'path' => true, 'paths' => true,
426
+ 'has_argument' => true, 'has_arguments' => true,
427
+ 'unless' => { '$ref' => '#/definitions/ruleMatcherList' },
428
+
429
+ 'call' => true, 'calls' => true,
430
+ 'define' => true, 'defines' => true
431
+ },
432
+ 'additionalProperties' => false,
433
+ 'minProperties' => 1
434
+ }
435
+ ]
436
+ },
437
+ 'dynamicList' => {
438
+ 'anyOf' => [
439
+ {
440
+ 'type' => 'array',
441
+ 'items' => { '$ref' => '#/definitions/dynamic' },
442
+ 'minItems' => 1,
443
+ 'uniqueItems' => true
444
+ },
445
+ { '$ref' => '#/definitions/dynamic' }
446
+ ]
447
+ },
448
+ 'keepTestOnly' => {
449
+ 'anyOf' => [
450
+ { '$ref' => '#/definitions/string' },
451
+ {
452
+ 'allOf' => [
453
+ { '$ref' => '#/definitions/stringPattern' },
454
+ { '$ref' => '#/definitions/rulePattern' },
455
+ {
456
+ 'type' => 'object',
457
+ 'properties' => {
458
+ # unfortunately this repetition is necessary to use additionalProperties: false
459
+ 'name' => true, 'names' => true,
460
+ 'has_prefix' => true, 'has_suffix' => true, 'matches' => true,
461
+ 'path' => true, 'paths' => true,
462
+ 'has_argument' => true, 'has_arguments' => true,
463
+ 'unless' => { '$ref' => '#/definitions/keepTestOnlyList' }
464
+ },
465
+ 'additionalProperties' => false,
466
+ 'minProperties' => 1
467
+ }
468
+ ]
469
+ }
470
+ ]
471
+ },
472
+ 'keepTestOnlyList' => {
473
+ 'anyOf' => [
474
+ {
475
+ 'type' => 'array',
476
+ 'items' => { '$ref' => '#/definitions/keepTestOnly' },
477
+ 'minItems' => 1,
478
+ 'uniqueItems' => true
479
+ },
480
+ { '$ref' => '#/definitions/keepTestOnly' }
481
+ ]
482
+ }
483
+ },
484
+ 'properties' => {
485
+ 'include_paths' => { '$ref' => '#/definitions/stringList' },
486
+ 'exclude_paths' => { '$ref' => '#/definitions/stringList' },
487
+ 'test_paths' => { '$ref' => '#/definitions/stringList' },
488
+ 'requires' => { '$ref' => '#/definitions/stringList' },
489
+ 'gems' => { '$ref' => '#/definitions/stringList' },
490
+ 'keep' => { '$ref' => '#/definitions/keepTestOnlyList' },
491
+ 'test_only' => { '$ref' => '#/definitions/keepTestOnlyList' },
492
+ 'dynamic' => { '$ref' => '#/definitions/dynamicList' }
493
+ }
494
+ }.freeze
495
+ end
496
+ end