gherkin_checker 1.2.0 → 1.3.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 187d4e241a76c4044dc0f41c640c4b3b5022508614482d6c6369dd72b4c9888a
4
- data.tar.gz: 3e679ba099d82061788ae12270bb0d73f15673289e089ca35436a2d0418934d3
3
+ metadata.gz: 842d35c08788f9b36bab530ea6278e57ab3e3b073a1685bcfab5aeec07de3079
4
+ data.tar.gz: 38b86c8550dc83042c76d3903d8a51e25f23ee65749fb3b54da62b2e6658b6d7
5
5
  SHA512:
6
- metadata.gz: 9184c1441b70cd8f8f8f6db7a1499b3f6cf476556e762a5f3b1318235e9a0ed1bf3dfbc7417aa6b1ff42f78ec4d1ca9ee278bd4edaec7bbfd57821063cea99fe
7
- data.tar.gz: 3ba183a3a7af38e70823945f7228ac21311f3462b7d4f5be8c18047263b0f8f8e6d977ae560b61113c02148f0acb7a9dbc29b7b0b84cc87d22c6bc49b1846d25
6
+ metadata.gz: b80cfb47d90b9e6e48b1789661862c8246a03738a14e4d2cf68cc13e430d178bb4380588a675f469a12fb3dbf2ed20310c9d87e211c1be1dfe86ca2a48159cc2
7
+ data.tar.gz: c166eff07a845109578d9561546cf7d016a0d25a47dff9e40cd2693ecd1d4ab0bb249cd42ee11ec09a3fa0e6feb21a382a3fc2a3029dd0cad772a7b85fc72580
data/exe/gherkin_checker CHANGED
@@ -8,4 +8,3 @@ checker = GherkinChecker::Checker.new("gherkin_checker.yml")
8
8
 
9
9
  # Run the check and print the results
10
10
  checker.check_feature_files
11
-
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module GherkinChecker
6
+ # Config class to handle configuration loading
7
+ class Config
8
+ attr_reader :feature_files_path, :one_of_tags, :must_be_tags
9
+
10
+ def initialize(config_file = "gherkin_checker.yml")
11
+ @config = load_config(config_file)
12
+ @feature_files_path = @config.fetch("feature_files_path", "./features")
13
+ @one_of_tags = @config.dig("mandatory_tags", "one_of")
14
+ @must_be_tags = @config.dig("mandatory_tags", "must_be")
15
+ end
16
+
17
+ private
18
+
19
+ def load_config(file)
20
+ return {} unless File.exist?(file)
21
+
22
+ YAML.load_file(file) || {}
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "gherkin/parser"
4
+ require "gherkin/errors"
5
+
6
+ module GherkinChecker
7
+ # Parser class to handle Gherkin file parsing
8
+ class Parser
9
+ def initialize
10
+ @parser = Gherkin::Parser.new
11
+ end
12
+
13
+ def parse(file_path)
14
+ content = File.read(file_path)
15
+ document = @parser.parse(content)
16
+ feature = document.feature
17
+
18
+ return [] unless feature
19
+
20
+ feature_name = feature.name
21
+ feature_tags = feature.tags.map(&:name).map { |tag| tag.delete_prefix("@") }
22
+
23
+ feature.children.map do |child|
24
+ process_child(child, feature_name, feature_tags)
25
+ end.compact
26
+ rescue Gherkin::CompositeParserException => e
27
+ e.errors.map do |error|
28
+ { error: error.message, type: :syntax_error }
29
+ end
30
+ rescue Gherkin::ParserError => e
31
+ [{ error: e.message, type: :syntax_error }]
32
+ rescue StandardError => e
33
+ [{ error: e.message, type: :unknown_error }]
34
+ end
35
+
36
+ private
37
+
38
+ def process_child(child, feature_name, feature_tags)
39
+ return unless child.respond_to?(:scenario) && child.scenario
40
+
41
+ scenario = child.scenario
42
+ {
43
+ feature_name: feature_name,
44
+ feature_tags: feature_tags,
45
+ line: scenario.location.line,
46
+ column: scenario.location.column,
47
+ name: scenario.name,
48
+ tags: scenario.tags.map(&:name).map { |tag| tag.delete_prefix("@") }
49
+ }
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GherkinChecker
4
+ # Validator class to check tags against rules
5
+ class Validator
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def validate(scenario_data)
11
+ errors = []
12
+ return errors if scenario_data[:error]
13
+
14
+ errors << check_one_of_tags(scenario_data)
15
+ errors << check_must_be_tags(scenario_data)
16
+ errors.compact
17
+ end
18
+
19
+ private
20
+
21
+ def check_one_of_tags(data)
22
+ return nil unless @config.one_of_tags
23
+ return nil if @config.one_of_tags.any? { |tag| data[:tags].include?(tag) }
24
+
25
+ format_error(:one_of_tags, @config.one_of_tags, data)
26
+ end
27
+
28
+ def check_must_be_tags(data)
29
+ return nil unless @config.must_be_tags
30
+ return nil if @config.must_be_tags.all? { |tag| data[:tags].include?(tag) }
31
+
32
+ format_error(:must_be_tags, @config.must_be_tags, data)
33
+ end
34
+
35
+ def format_error(type, expected, data)
36
+ {
37
+ check: type,
38
+ file_line: data[:line], # We will need to prepend file path in runner
39
+ scenario_name: data[:name],
40
+ scenario_tags: data[:tags],
41
+ expected: expected
42
+ }
43
+ end
44
+ end
45
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GherkinChecker
4
- VERSION = "1.2.0"
4
+ VERSION = "1.3.0"
5
5
  end
@@ -1,44 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "find"
4
- require "yaml"
5
- require "gherkin/parser"
6
-
7
4
  require_relative "gherkin_checker/version"
5
+ require_relative "gherkin_checker/config"
6
+ require_relative "gherkin_checker/parser"
7
+ require_relative "gherkin_checker/validator"
8
8
 
9
9
  module GherkinChecker
10
- # Checker class
10
+ # Checker class - The main runner
11
11
  class Checker
12
12
  def initialize(config_file = "gherkin_checker.yml")
13
- unless File.exist?(config_file)
14
- log_message("Error: Configuration file #{config_file} not found.", level: :error)
15
- exit(1) # Exit with a status of 1 to indicate an error
16
- end
17
-
18
- @config = YAML.load_file(config_file)
19
- if @config
20
- @skip_check = false
21
- else
22
- log_message("Warning: The gherkin_checker.yml not define", level: :warn)
23
- @skip_check = true
24
- end
25
-
26
- @feature_files_path = @config&.key?("feature_files_path") ? @config.fetch("feature_files_path") : "./features"
27
- @one_of_tags = @config.dig("mandatory_tags", "one_of") if @config
28
- @must_be_tags = @config.dig("mandatory_tags", "must_be") if @config
29
-
30
- # Flags to ensure warnings are only printed once
31
- @one_of_tags_warning_shown = false
32
- @must_be_tags_warning_shown = false
13
+ @config = Config.new(config_file)
14
+ @parser = Parser.new
15
+ @validator = Validator.new(@config)
33
16
  end
34
17
 
35
18
  def check_feature_files
19
+ return log_skip_message if skip_check?
20
+
36
21
  errors = []
37
- Find.find(@feature_files_path) do |file|
22
+ Find.find(@config.feature_files_path) do |file|
38
23
  next unless File.extname(file) == ".feature"
39
24
 
40
- errors += check_one_of_tags(file)
41
- errors += check_must_be_tags(file)
25
+ scenarios = @parser.parse(file)
26
+ scenarios.each do |scenario|
27
+ if scenario[:error]
28
+ errors << scenario
29
+ else
30
+ validation_errors = @validator.validate(scenario)
31
+ # Prepend file path to the error
32
+ validation_errors.each { |err| err[:file_line] = "#{file}:#{err[:file_line]}:#{scenario[:column]}" }
33
+ errors += validation_errors
34
+ end
35
+ end
42
36
  end
43
37
 
44
38
  report_errors(errors)
@@ -46,188 +40,62 @@ module GherkinChecker
46
40
 
47
41
  private
48
42
 
49
- def check_one_of_tags(file)
50
- return log_one_of_tags_warning if @one_of_tags.nil?
51
-
52
- errors = []
53
- extract_scenario_data(file).each do |data|
54
- is_tags_nil = data[:scenario_tags].nil?
55
- tags = data[:scenario_tags]
56
- file_line = data[:file_line]
57
- scenario = data[:scenario_name]
58
- error = data[:error]
59
-
60
- if is_tags_nil
61
- errors << {
62
- check: :one_of_tags,
63
- file_line: file_line,
64
- scenario_name: scenario,
65
- scenario_tags: tags,
66
- error: error
67
- }
68
- end
69
-
70
- next if is_tags_nil
71
-
72
- next if @one_of_tags.any? { |tag| tags.include?(tag) }
73
-
74
- errors << {
75
- check: :one_of_tags,
76
- file_line: file_line,
77
- scenario_name: scenario,
78
- scenario_tags: tags,
79
- error: error
80
- }
81
- end
82
-
83
- errors
84
- end
85
-
86
- def check_must_be_tags(file)
87
- return log_must_be_tags_warning if @must_be_tags.nil?
88
-
89
- errors = []
90
- extract_scenario_data(file).each do |data|
91
- is_tags_nil = data[:scenario_tags].nil?
92
- tags = data[:scenario_tags]
93
- file_line = data[:file_line]
94
- scenario = data[:scenario_name]
95
- error = data[:error]
96
-
97
- if is_tags_nil
98
- errors << {
99
- check: :must_be_tags,
100
- file_line: file_line,
101
- scenario_name: scenario,
102
- scenario_tags: tags,
103
- error: error
104
- }
105
- end
106
-
107
- next if is_tags_nil
108
-
109
- next if @must_be_tags.all? { |item| tags.include?(item) }
110
-
111
- errors << {
112
- check: :must_be_tags,
113
- file_line: file_line,
114
- scenario_name: scenario,
115
- scenario_tags: tags,
116
- error: error
117
- }
118
- end
119
-
120
- errors
121
- end
122
-
123
- def log_one_of_tags_warning
124
- return [] if @one_of_tags_warning_shown
125
-
126
- log_message("Warning: Optional tags not set in 'gherkin_checker.yml'.", level: :warn) unless @skip_check
127
- @one_of_tags_warning_shown = true
128
- [] # Return an empty array to maintain consistency
43
+ def skip_check?
44
+ # If no config was loaded (meaning no file found or empty), we might want to warn
45
+ # But based on original logic, if config is missing, we warn and skip.
46
+ # Config class handles loading. If attributes are nil, it means load failed or empty.
47
+ # Let's check if critical config is present.
48
+ @config.one_of_tags.nil? && @config.must_be_tags.nil?
129
49
  end
130
50
 
131
- def log_must_be_tags_warning
132
- return [] if @must_be_tags_warning_shown
133
-
134
- log_message("Warning: Mandatory tags not set in 'gherkin_checker.yml'.", level: :warn) unless @skip_check
135
- @must_be_tags_warning_shown = true
136
- [] # Return an empty array to maintain consistency
51
+ def log_skip_message
52
+ log_message("Warning: The gherkin_checker.yml not define or no tags configured", level: :warn)
53
+ log_message("Skip gherkin checking", level: :warn)
137
54
  end
138
55
 
139
56
  def report_errors(errors)
140
57
  if errors.empty?
141
- if @skip_check
142
- log_message("Skip gherkin checking", level: :warn)
143
- else
144
- log_message("All scenarios have the required tags.")
145
- end
58
+ log_message("All scenarios have the required tags.")
146
59
  else
147
60
  log_message("Gherkin Checker found Error:", level: :error)
148
61
  errors.each do |error|
149
- error_message = error[:error]
150
- if error_message.nil?
151
- scenario_tags = error[:scenario_tags]
152
- tags = scenario_tags
153
- tags = scenario_tags.nil? ? "Tagging not set" : "Just found '#{tags}'"
154
-
155
- message = case error[:check]
156
- when :one_of_tags
157
- "one_of_tags '#{@one_of_tags}' not found!, #{tags}"
158
- when :must_be_tags
159
- "must_be_tags '#{@must_be_tags}' not found!, #{tags}"
160
- else
161
- "error undefined"
162
- end
163
-
164
- log_message("#{error[:file_line]}: #{error[:scenario_name]} - #{message}", level: :error)
62
+ if error[:error]
63
+ log_syntax_error(error)
165
64
  else
166
- log_message("Error: #{error_message}", level: :error)
65
+ log_validation_error(error)
167
66
  end
168
67
  end
169
68
  end
170
69
  end
171
70
 
172
- # Define a method to log messages with different levels
71
+ def log_syntax_error(error)
72
+ type = error[:type] == :syntax_error ? "Syntax Error" : "Error"
73
+ log_message("#{type}: #{error[:error]}", level: :error)
74
+ end
75
+
76
+ def log_validation_error(error)
77
+ tags_str = if error[:scenario_tags].nil? || error[:scenario_tags].empty?
78
+ "Tagging not set"
79
+ else
80
+ "Just found '#{error[:scenario_tags]}'"
81
+ end
82
+ check_type = error[:check] == :one_of_tags ? "one_of_tags" : "must_be_tags"
83
+
84
+ message = "#{check_type} '#{error[:expected]}' not found!, #{tags_str}"
85
+ log_message("#{error[:file_line]}: #{error[:scenario_name]} - #{message}", level: :error)
86
+ end
87
+
173
88
  def log_message(message, level: :info)
174
- # Define color codes
175
89
  colors = {
176
- debug: "\e[36m", # Cyan
177
- info: "\e[32m", # Green
178
- warn: "\e[33m", # Yellow
179
- error: "\e[31m", # Red
180
- fatal: "\e[35m", # Magenta
181
- reset: "\e[0m" # Reset to default color
90
+ debug: "\e[36m",
91
+ info: "\e[32m",
92
+ warn: "\e[33m",
93
+ error: "\e[31m",
94
+ fatal: "\e[35m",
95
+ reset: "\e[0m"
182
96
  }
183
-
184
97
  color = colors[level] || colors[:reset]
185
-
186
98
  puts "#{color}#{message}#{colors[:reset]}"
187
99
  end
188
-
189
- def extract_scenario_data(file)
190
- gherkin_parser = Gherkin::Parser.new
191
- scenario_data = []
192
-
193
- begin
194
- content = File.read(file)
195
- document = gherkin_parser.parse(content)
196
- feature = document.feature
197
- feature_name = feature.name
198
- feature_tags = feature.tags.map(&:name)
199
-
200
- feature.children.each do |child|
201
- raise "Error: read scenario data" unless child.respond_to?(:scenario) && child.scenario
202
-
203
- scenario = child.scenario
204
- scenario_name = scenario.name
205
- scenario_tags = scenario.tags.map(&:name)
206
- scenario_tags = scenario_tags.map { |tag| tag.delete_prefix("@") }
207
- location_line = scenario.location.line
208
- location_column = scenario.location.column
209
-
210
- scenario_data << {
211
- feature_name: feature_name,
212
- feature_tags: feature_tags,
213
- file_line: "#{file}:#{location_line}:#{location_column}",
214
- scenario_name: scenario_name,
215
- scenario_tags: scenario_tags,
216
- error: nil
217
- }
218
- end
219
- rescue StandardError => e
220
- scenario_data << {
221
- feature_name: nil,
222
- feature_tags: nil,
223
- file_line: nil,
224
- scenario_name: nil,
225
- scenario_tags: nil,
226
- error: e.message
227
- }
228
- end
229
-
230
- scenario_data
231
- end
232
100
  end
233
101
  end
metadata CHANGED
@@ -1,14 +1,70 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gherkin_checker
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dikakoko
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-07-25 00:00:00.000000000 Z
11
- dependencies: []
10
+ date: 2026-01-12 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: cucumber-gherkin
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '33.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '33.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rubocop
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.21'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.21'
12
68
  description: Checking .feature files
13
69
  email:
14
70
  - dikakoko@icloud.com
@@ -20,6 +76,9 @@ files:
20
76
  - README.md
21
77
  - exe/gherkin_checker
22
78
  - lib/gherkin_checker.rb
79
+ - lib/gherkin_checker/config.rb
80
+ - lib/gherkin_checker/parser.rb
81
+ - lib/gherkin_checker/validator.rb
23
82
  - lib/gherkin_checker/version.rb
24
83
  homepage: https://github.com/dikako/gherkin_checker
25
84
  licenses: