chop 0.35.2 → 0.36.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: 88bedb7f454adc2966b82d3c5748b88ca81498d6de9f46de02f5ee0256b9c641
4
- data.tar.gz: a727470df4d49a8f170724367891caa6f8cd380ef637af75f867b5666924163b
3
+ metadata.gz: 8af1c5a00064df8f6b191c22e8724dcea6f4be1cf6ba940d7976a316efdd9b18
4
+ data.tar.gz: 4c04d4ec5e3ef08fad6f2b7b4534bd182d353427f630526d40d3b2987a7c2912
5
5
  SHA512:
6
- metadata.gz: aa9383445e2c7333c58a107a00c570b20c3bc4f59e47a1b08bbc55682af675af05f9af0a45c61821e04142f99fe6c9a2244ff6c8fa4ab554d77ef5eb2f1e102d
7
- data.tar.gz: 2c1b982d7f1a30cf2a2603927e98cb32b496efbe8c6eb83f3e5c14434f1a8061c33f42a5d50b882e914d55f80c9bd18cadfd4e51a66126f5bfd08bc4a6aa49cd
6
+ metadata.gz: 75c8391e2783ca33dbe20f19e8a0cb0d4f46923c36ea3a0941d4a726f93817b89abcf3c0f33ff2fa40f3a5a8aeaf7eea3bfea563b0453e513360c7c0634659cf
7
+ data.tar.gz: fbd050596b406cd0624e1dd20daf058c713251604103685d678a5a7fd652445fd1f91239dcb901104e7e38f3c8a1cfe302a70c63f0d2d1d5bf2544aac3c3faa2
data/CLAUDE.md CHANGED
@@ -10,14 +10,17 @@ Chop is a Ruby gem that enhances Cucumber tables with three main methods: `#crea
10
10
 
11
11
  ### Testing
12
12
  - `rake spec` or `bundle exec rspec` - Run the full test suite
13
- - `rspec spec/chop/table_spec.rb` - Run a specific test file
13
+ - `rspec spec/chop/table_spec.rb` - Run a specific test file
14
14
  - `rspec spec/chop/table_spec.rb:42` - Run a specific test by line number
15
15
 
16
+ Note: RSpec is configured to run only tests marked with `focus: true`. When no focused tests exist, all tests run automatically.
17
+
16
18
  ### Development Setup
17
19
  - `bin/setup` - Install dependencies after checkout
18
20
  - `bin/console` - Interactive console for experimentation
19
21
 
20
22
  ### Multi-Version Testing
23
+ Tests must pass against Rails 7.0, 7.1, and 7.2 on Ruby 3.1, 3.2, and 3.3:
21
24
  - `bundle exec appraisal install` - Install gems for all Rails versions
22
25
  - `bundle exec appraisal rails-7.0 rspec` - Test against specific Rails version
23
26
  - `bundle exec appraisal rspec` - Test against all Rails versions
@@ -34,12 +37,13 @@ Chop is a Ruby gem that enhances Cucumber tables with three main methods: `#crea
34
37
  - **`Chop::Diff`** (lib/chop/diff.rb) - Base class for HTML element diffing with Capybara
35
38
  - **Element-specific diffing classes**:
36
39
  - `Chop::Table` - HTML table diffing
37
- - `Chop::DefinitionList` - Definition list (`<dl>`) diffing
40
+ - `Chop::DefinitionList` - Definition list (`<dl>`) diffing
41
+ - `Chop::DfnDl` - Definition list with `<dfn>` headings
38
42
  - `Chop::UnorderedList` - Unordered list (`<ul>`) diffing
39
43
  - `Chop::Form` - Form filling functionality
40
44
 
41
45
  ### Monkey-patching Strategy
42
- The gem extends `Cucumber::MultilineArgument::DataTable` by prepending a module that adds the three main methods. The DSL module can also be called directly as `Chop.create!`, `Chop.diff!`, `Chop.fill_in!` to avoid monkey-patching.
46
+ The gem extends `Cucumber::MultilineArgument::DataTable` by prepending the DSL module, which adds the three main methods. The DSL module can also be called directly as `Chop.create!`, `Chop.diff!`, `Chop.fill_in!` to avoid monkey-patching entirely.
43
47
 
44
48
  ### Creation Strategies
45
49
  Supports pluggable creation strategies via `Chop::Create.register_creation_strategy`:
@@ -49,13 +53,25 @@ Supports pluggable creation strategies via `Chop::Create.register_creation_strat
49
53
 
50
54
  ### Transformation DSL
51
55
  The `#create!` method supports a rich DSL for transforming table data:
52
- - Field transformations: `file`, `files`, `has_one`, `has_many`, `default`, `copy`, `rename`, `delete`
53
- - Low-level: `field`, `transformation` for custom logic
54
- - Lifecycle hooks: `after` for post-creation logic
56
+ - **Field transformations**: `file`, `files`, `has_one`, `has_many`, `default`, `copy`, `rename`, `delete`
57
+ - **Low-level**: `field`, `transformation` for custom logic
58
+ - **Lifecycle hooks**: `after` for post-creation logic, `create` to override creation strategy
59
+
60
+ The `#diff!` method also supports transformations:
61
+ - **Capybara finders**: `rows`, `cells`, `text` to override default element selection
62
+ - **High-level transforms**: `image` to extract image filenames, `allow_not_found` for optional elements
63
+ - **Low-level**: `header`, `field`, `hash_transformation`, `transformation` for custom logic
64
+
65
+ ### Diffing Architecture
66
+ Diffing works by:
67
+ 1. Selecting HTML elements using Capybara finders (customizable via `rows`, `cells` blocks)
68
+ 2. Extracting text content from those elements
69
+ 3. Comparing against the expected table using `diff!` from Cucumber
70
+ 4. Supporting element-specific extraction logic (e.g., images in table cells)
55
71
 
56
72
  ## Testing Patterns
57
73
 
58
- - Uses RSpec with `spec_helper.rb` configuring focus and run-all behavior
74
+ - Uses RSpec with `spec_helper.rb` configuring focus filtering
59
75
  - Tests organized by component in `spec/chop/` matching `lib/chop/` structure
60
- - Tests cover both direct method calls and monkey-patched table behavior
61
- - Uses Capybara for integration testing of diffing functionality
76
+ - Tests cover both direct DSL calls and monkey-patched table behavior
77
+ - Uses Capybara for integration testing of diffing functionality with Cuprite headless browser
data/README.md CHANGED
@@ -64,6 +64,48 @@ Overide Capybara finders:
64
64
  High-level declarative transformations:
65
65
  * `#image`: Replaces the specified cell with the filename of the first image within it, stripped of path and cachebusters.
66
66
 
67
+ Regex templates (opt‑in):
68
+ * `#regex(*fields)`: Enables embedded regex templates inside expected cells for flexible matching. By default applies to all fields; optionally whitelist columns by header name (symbol/string) or by 1‑based column index.
69
+
70
+ - Syntax: write literal text with embedded regex tokens using Ruby‑style interpolation markers: `#{/pattern/flags}`. Flags support `i`, `m`, `x`.
71
+ - Matching: builds a single anchored regex for the entire cell by escaping literal segments and splicing regex tokens. The whole cell must match.
72
+ - Multiple tokens: allowed; flags across tokens are OR’ed together.
73
+ - Escaping: write `\#{/…/}` to render a token literally (no matching). The backslash is removed before comparing.
74
+
75
+ Examples:
76
+
77
+ ```ruby
78
+ # All fields enabled (table header present)
79
+ expected = [
80
+ ["Attachments"],
81
+ ['attachment.jpg 23.4 KB browser-report.txt #{/1\.\d{2} KB/}']
82
+ ]
83
+ expected.diff!("table") { regex }
84
+
85
+ # Whitelist by header name (normalized like header keys used elsewhere)
86
+ expected = [
87
+ ["A", "B"],
88
+ ["foo 123", 'bar #{/\d{3}/}']
89
+ ]
90
+ expected.diff!("table") { regex :b }
91
+
92
+ # Whitelist by 1-based column index
93
+ expected = [
94
+ ["A", "B"],
95
+ ["foo 123", 'bar #{/\d{3}/}']
96
+ ]
97
+ expected.diff!("table") { regex 2 }
98
+
99
+ # Non-whitelisted columns treat tokens as literal
100
+ expected = [
101
+ ["A", "B"],
102
+ ['#{/\w+ \d{3}/}', "bar 456"]
103
+ ]
104
+ expect {
105
+ expected.diff!("table") { regex :b }
106
+ }.to raise_error(Cucumber::MultilineArgument::DataTable::Different)
107
+ ```
108
+
67
109
  All these methods are implemented in terms of the following low-level methods, useful for when you need more control over the transformation:
68
110
  * `#header`: add or transform the table header, depending on block arity.
69
111
  * `#header(key)`: transform the specified header column, specified either by numeric index, or by hash key.
@@ -169,4 +211,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/botand
169
211
  ## License
170
212
 
171
213
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
172
-
data/lib/chop/diff.rb CHANGED
@@ -2,6 +2,7 @@ require "active_support/core_ext/string/inflections"
2
2
  require "active_support/core_ext/object/blank"
3
3
  require "active_support/core_ext/class/attribute"
4
4
  require "active_support/hash_with_indifferent_access"
5
+ require "chop/regex_templates"
5
6
 
6
7
  module Chop
7
8
  class Diff < Struct.new(:selector, :table, :session, :timeout, :block)
@@ -27,6 +28,8 @@ module Chop
27
28
 
28
29
  attr_accessor :header_transformations, :transformations
29
30
 
31
+ attr_accessor :regex_templates_enabled, :regex_fields
32
+
30
33
  def initialize selector = nil, table = nil, session = Capybara.current_session, timeout = Capybara.default_max_wait_time, block = nil, &other_block
31
34
  super
32
35
  self.selector ||= default_selector
@@ -66,6 +69,13 @@ module Chop
66
69
  transformations << block
67
70
  end
68
71
 
72
+ # Enable embedded-regex templates within cells.
73
+ # Optionally restrict application to specific fields (by header name or 1-based index).
74
+ def regex *fields
75
+ self.regex_templates_enabled = true
76
+ self.regex_fields = fields unless fields.empty?
77
+ end
78
+
69
79
  def hash_transformation &block
70
80
  transformation do |rows|
71
81
  header = rows[0]
@@ -143,6 +153,9 @@ module Chop
143
153
  actual = to_a
144
154
  # FIXME should just delegate to Cucumber's #diff!. Cucumber needs to handle empty tables better.
145
155
  if !cucumber_table.raw.flatten.empty? && !actual.flatten.empty?
156
+ if regex_templates_enabled
157
+ cucumber_table = Chop::RegexTemplates.apply(cucumber_table, actual, regex_fields)
158
+ end
146
159
  cucumber_table.diff! actual, **kwargs
147
160
  elsif cucumber_table.raw.flatten != actual.flatten
148
161
  raise Cucumber::MultilineArgument::DataTable::Different.new(cucumber_table)
@@ -0,0 +1,97 @@
1
+ module Chop
2
+ module RegexTemplates
3
+ TOKEN = /
4
+ (?<!\\) # not preceded by backslash
5
+ \#\{ # start of token
6
+ \/ # opening slash
7
+ (.*?) # pattern (non-greedy)
8
+ \/ # closing slash
9
+ ([imx]*) # optional flags
10
+ \} # end of token
11
+ /mx
12
+
13
+ module_function
14
+
15
+ def apply(cucumber_table, actual, fields)
16
+ allowed_columns = columns_for(fields, cucumber_table.raw.first)
17
+
18
+ expected = cucumber_table.raw.map.with_index do |row, i|
19
+ row.map.with_index do |cell, j|
20
+ str = cell.to_s
21
+ # De-escape literal token markers so \#{/.../} becomes literal '#{...}'
22
+ deescaped = str.gsub('\\#{', '#{')
23
+
24
+ if allowed?(allowed_columns, j) && deescaped.include?('#{') && deescaped.match?(TOKEN)
25
+ regex = expand_template(deescaped)
26
+ actual_cell = (actual.dig(i, j) || "").to_s
27
+ if regex.match?(actual_cell)
28
+ actual_cell
29
+ else
30
+ deescaped
31
+ end
32
+ else
33
+ deescaped
34
+ end
35
+ end
36
+ end
37
+
38
+ Cucumber::MultilineArgument::DataTable.from(expected)
39
+ end
40
+
41
+ def columns_for(fields, expected_header)
42
+ return :all if fields.nil? || fields.empty?
43
+
44
+ idxs = []
45
+
46
+ fields.each do |f|
47
+ case f
48
+ when Integer
49
+ idxs << (f - 1)
50
+ when Symbol, String
51
+ next unless expected_header
52
+ normalized = f.to_s.parameterize.underscore
53
+ header_keys = expected_header.map.with_index do |text, idx|
54
+ t = text.to_s
55
+ key = t.parameterize.underscore
56
+ key = t if key.blank? && t.present?
57
+ key = (idx + 1).to_s if key.blank?
58
+ key
59
+ end
60
+ header_keys.each_with_index do |key, idx|
61
+ idxs << idx if key == normalized
62
+ end
63
+ end
64
+ end
65
+
66
+ idxs.uniq
67
+ end
68
+
69
+ def allowed?(allowed_columns, j)
70
+ return true if allowed_columns == :all
71
+ allowed_columns.include?(j)
72
+ end
73
+
74
+ def expand_template(str)
75
+ parts = []
76
+ last = 0
77
+
78
+ str.to_enum(:scan, TOKEN).each do
79
+ m = Regexp.last_match
80
+ literal = str[last...m.begin(0)]
81
+ parts << Regexp.escape(literal)
82
+ pattern, flags = m.captures
83
+ if flags.to_s.empty?
84
+ parts << "(?:#{pattern})"
85
+ else
86
+ parts << "(?#{flags}:#{pattern})"
87
+ end
88
+ last = m.end(0)
89
+ end
90
+
91
+ tail = str[last..-1] || ""
92
+ parts << Regexp.escape(tail)
93
+
94
+ Regexp.new("\\A(?:#{parts.join})\\z")
95
+ end
96
+ end
97
+ end
data/lib/chop/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Chop
2
- VERSION = "0.35.2"
2
+ VERSION = "0.36.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chop
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.35.2
4
+ version: 0.36.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micah Geisel
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-09-03 00:00:00.000000000 Z
11
+ date: 2025-11-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -153,6 +153,7 @@ files:
153
153
  - lib/chop/diff.rb
154
154
  - lib/chop/dsl.rb
155
155
  - lib/chop/form.rb
156
+ - lib/chop/regex_templates.rb
156
157
  - lib/chop/table.rb
157
158
  - lib/chop/unordered_list.rb
158
159
  - lib/chop/version.rb