decisive 0.9.0 → 0.10.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: b945f08db65a5275cf3630aef57ba562763630b8f7d4da3c3774d84f2c403c52
4
- data.tar.gz: a64ba39fba71a016e42506dd03b0db8b3ac7a0e02d93ef44453b072b3175d8d7
3
+ metadata.gz: d43e1342475b84925559c143552a768a720944559d2b24e8291c0bc6f07cf844
4
+ data.tar.gz: 50678ed3c307c531395b1b3ffdd6fe7d7adf0dd001ed4bb889f60c4f9e1fc784
5
5
  SHA512:
6
- metadata.gz: 12271c423c16f11a47fd4fe5475addd94c935350c591d79ebcc9f9314ba0f2855d8018e3b3fb7ee1b376ef22c90da62d3469f96c3b7306bc4576d434ea2cef03
7
- data.tar.gz: e3da77344bbd71dc17a12c344065211b9ac212368701ce1e7c45882b84cee6d4f419fdf7c8c34b36e9e2287c9fa813feb5b5c3cdf64fc8cef4c9277e55934f9a
6
+ metadata.gz: a3b81bd36c87486123533bb0ccb8192d483856509a5806e1112e810076a28748d53590f2932f24f99b58c04b61ed65d5e35958ec14ac133dde005b7c9efd7c83
7
+ data.tar.gz: b9a75085e0161dfa76dbbfe107ee672b5e0c375ee1d5bb7e62df3276e001a0db12caf4a17c0169d2953ecdca9ee13a898ca73ea53e63d24baabd2451286324f5
@@ -5,14 +5,14 @@ jobs:
5
5
  strategy:
6
6
  fail-fast: false
7
7
  matrix:
8
- gemfile: [ rails_7.0, rails_7.1, rails_7.2 ]
9
- ruby: [ 3.1, 3.2, 3.3 ]
8
+ gemfile: [ rails_8.0, rails_8.1 ]
9
+ ruby: [ 3.3, 3.4, '4.0' ]
10
10
 
11
11
  runs-on: ubuntu-latest
12
12
  env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
13
13
  BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
14
14
  steps:
15
- - uses: actions/checkout@v2
15
+ - uses: actions/checkout@v7
16
16
  - uses: ruby/setup-ruby@v1
17
17
  with:
18
18
  ruby-version: ${{ matrix.ruby }}
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-3.3.4
1
+ ruby-4.0.2
data/Appraisals CHANGED
@@ -1,12 +1,7 @@
1
- appraise "rails-7.0" do
2
- gem "actionview", "~>7.0.0"
1
+ appraise "rails-8.0" do
2
+ gem "actionview", "~>8.0.0"
3
3
  end
4
4
 
5
- appraise "rails-7.1" do
6
- gem "actionview", "~>7.1.0"
5
+ appraise "rails-8.1" do
6
+ gem "actionview", "~>8.1.0"
7
7
  end
8
-
9
- appraise "rails-7.2" do
10
- gem "actionview", "~>7.2.0"
11
- end
12
-
data/CLAUDE.md ADDED
@@ -0,0 +1,79 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ bundle exec rspec # run the suite (also `rake spec`, the default task)
9
+ bundle exec rspec spec/csv_spec.rb:87 # single example by line
10
+ bundle exec rspec --only-failures # uses .rspec_status
11
+ bin/console # IRB with decisive loaded
12
+ ```
13
+
14
+ Rails version matrix via appraisal (CI runs rails 8.0/8.1 × ruby 3.3/3.4/4.0):
15
+
16
+ ```bash
17
+ bundle exec appraisal install
18
+ bundle exec appraisal rails-8.1 rake
19
+ BUNDLE_GEMFILE=gemfiles/rails_8.1.gemfile bundle exec rspec
20
+ ```
21
+
22
+ ## Architecture
23
+
24
+ Decisive is an ActionView template handler for the `.decisive` extension. `Decisive::Rails` (an
25
+ engine, only defined when Rails is) registers the handler and the `:xls` mime type.
26
+
27
+ **The compile step is the center of the design.** `TemplateHandler.call` does not render anything —
28
+ it returns a string of Ruby that Rails compiles into the view. That generated code:
29
+
30
+ 1. `extend Decisive::DSL` on the view, then evaluates the *entire template source as one expression*
31
+ whose value is a **context object**;
32
+ 2. sets `Content-Disposition` / `Content-Type` / `Content-Transfer-Encoding` from that context;
33
+ 3. either pumps `context.each` into `response.stream`, or returns `context.to_csv` / `context.to_xls`.
34
+
35
+ So the generated code references `response`, `controller`, and `@stream` as bare names in the view's
36
+ scope. Anything that changes headers or the stream/buffer decision lives in the heredoc in
37
+ `template_handler.rb`, not in the contexts.
38
+
39
+ `DSL#csv` / `DSL#xls` pick one of three contexts:
40
+
41
+ | Context | When | Behavior |
42
+ | --- | --- | --- |
43
+ | `StreamCSVContext` | `csv`, default `stream: true` | Columns are collected once at template-eval time, so headers are fixed up front. Raises unless the controller `is_a?(ActionController::Live)`, and unless the block has arity 0. |
44
+ | `RenderCSVContext` | `csv ..., stream: false` | Buffers everything through `Renderer`. The block is re-run per record, so columns may vary per row. |
45
+ | `RenderXLSContext` | `xls` | Worksheets come either from `worksheet` calls inside the block, or from a `{name => records}` hash argument with one shared column block. Renders with RubyXL. |
46
+
47
+ `Renderer` (used by the two non-streaming contexts) builds a hash per record and takes the **union of
48
+ keys in first-seen order** as the header — that is what makes non-deterministic headers work, and it
49
+ is why the two pending `xit` specs about repeating column names fail: duplicate labels collapse onto
50
+ one key.
51
+
52
+ **`#column` is implemented twice** — `StreamCSVContext#column` (streaming) and `Renderer::Row#column`
53
+ (non-streaming). They must stay behaviorally identical: value resolution is block > `Symbol`
54
+ accessor > literal value > accessor inferred from the label (`"Full name".parameterize.underscore`),
55
+ and every value is `to_s`'d.
56
+
57
+ `ViewDelegation` is `method_missing` forwarding to the view, mixed into `Renderer::Row`,
58
+ `StreamCSVContext`, and `RenderXLSContext`. It is why template blocks can call view helpers even
59
+ though they are `instance_exec`'d against decisive's objects. Instance variables deliberately do
60
+ *not* follow — they belong to the view — so blocks see `nil` for `@foo`. It also scrubs its own
61
+ frames from `NameError` backtraces to keep the template's line on top; a spec asserts on that.
62
+
63
+ XLS specifics in `RenderXLSContext`: sheet names are sanitized (illegal chars → spaces, stripped of
64
+ surrounding quotes, truncated to 31 chars), and cells are always written as **data, never formulas**
65
+ — a leading `=` is intentionally literal (see commit 3bdd657). Do not reintroduce formula detection.
66
+
67
+ ## Testing conventions
68
+
69
+ Specs never boot Rails. Each example fakes a template with `Struct.new(:source)`, then
70
+ `eval(Decisive::TemplateHandler.call(template))` — meaning the RSpec example itself plays the role of
71
+ the view. Consequences worth knowing before writing a spec:
72
+
73
+ - `response` and `controller` are `let` doubles; `@records` / `@worksheets` are ivars set in the example.
74
+ - Methods defined on the example group (`shout`, `whisper`) are the "view helpers" that
75
+ `ViewDelegation` finds — including private ones.
76
+ - Streaming examples assert with `expect(response.stream).to receive(:write).with(...)`, in order.
77
+ - `ActionController::Live` is `stub_const`'d to a bare module, with `controller.is_a?` stubbed.
78
+ - XLS examples write to `tmp/` and read back through `Decisive::XLSHasher`, which is public API for
79
+ downstream test suites, not just an internal helper.
data/README.md CHANGED
@@ -70,6 +70,25 @@ Visiting /users.csv will render a file named "users-2010_01_01.csv" with the fol
70
70
  | frodo@example.com | Frodo Baggins | Yes | | 2002-06-19 |
71
71
  | sam@example.com | Samwise Gamgee | | Frodo | 2008-10-13 |
72
72
 
73
+ ### Helper methods
74
+
75
+ Decisive evaluates the blocks in your template against its own objects, but they
76
+ still reach the view's helper methods, just like the rest of the template:
77
+
78
+ ```ruby
79
+ # app/views/users/index.csv.decisive
80
+
81
+ csv @users, filename: "users.csv", stream: false do
82
+ column "Signed up" do |user|
83
+ l(user.created_at, format: :short) # a view helper
84
+ end
85
+ end
86
+ ```
87
+
88
+ The one thing blocks cannot see is the view's instance variables: `@users` above
89
+ is read when the template runs, but inside a block `@users` is nil. Assign it to
90
+ a local first, or reach for a helper method.
91
+
73
92
  ### Debugging
74
93
 
75
94
  Errors in your decisive template will often be swallowed while streaming is enabled, resulting in only some of the csv being rendered, without any explanation. You can temporarily switch decisive into non-streaming mode to see these errors:
@@ -3,6 +3,6 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "byebug"
6
- gem "actionview", "~>7.0.0"
6
+ gem "actionview", "~>8.0.0"
7
7
 
8
8
  gemspec path: "../"
@@ -3,6 +3,6 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "byebug"
6
- gem "actionview", "~>7.1.0"
6
+ gem "actionview", "~>8.1.0"
7
7
 
8
8
  gemspec path: "../"
@@ -3,9 +3,9 @@ require "active_support/core_ext/string/inflections"
3
3
  require "decisive/renderer"
4
4
 
5
5
  module Decisive
6
- class RenderCSVContext < Struct.new(:records, :filename, :block)
6
+ class RenderCSVContext < Struct.new(:records, :filename, :block, :view)
7
7
  def to_csv(*args, **kwargs)
8
- Renderer.new(records, block).map do |row|
8
+ Renderer.new(records, block, view).map do |row|
9
9
  row.to_csv(*args, **kwargs)
10
10
  end.join
11
11
  end
@@ -1,8 +1,11 @@
1
1
  require "rubyXL"
2
2
  require "decisive/renderer"
3
+ require "decisive/view_delegation"
3
4
 
4
5
  module Decisive
5
- class RenderXLSContext < Struct.new(:worksheets, :filename, :block)
6
+ class RenderXLSContext < Struct.new(:worksheets, :filename, :block, :view)
7
+ include ViewDelegation
8
+
6
9
  class Worksheet < Struct.new(:records, :name, :block); end
7
10
 
8
11
  def initialize *args
@@ -38,7 +41,7 @@ module Decisive
38
41
  def render xls
39
42
  worksheets.each do |worksheet|
40
43
  sheet = xls.add_worksheet(sanitize_name(worksheet.name)).tap do |sheet|
41
- Renderer.new(worksheet.records, worksheet.block).each.with_index do |row, row_index|
44
+ Renderer.new(worksheet.records, worksheet.block, view).each.with_index do |row, row_index|
42
45
  row.each.with_index do |cell, cell_index|
43
46
  sheet.add_cell row_index, cell_index, cell.to_s
44
47
  end
@@ -1,5 +1,7 @@
1
+ require "decisive/view_delegation"
2
+
1
3
  module Decisive
2
- class Renderer < Struct.new(:records, :block)
4
+ class Renderer < Struct.new(:records, :block, :view)
3
5
  include Enumerable
4
6
 
5
7
  def each &block
@@ -24,11 +26,13 @@ module Decisive
24
26
 
25
27
  def hashes
26
28
  @hashes ||= records.map do |record|
27
- Row.new(record, block).to_hash
29
+ Row.new(record, block, view).to_hash
28
30
  end
29
31
  end
30
32
 
31
- class Row < Struct.new(:record, :block)
33
+ class Row < Struct.new(:record, :block, :view)
34
+ include ViewDelegation
35
+
32
36
  module Nothing; end
33
37
 
34
38
  def to_hash
@@ -1,8 +1,11 @@
1
1
  require "csv"
2
2
  require "active_support/core_ext/string/inflections"
3
+ require "decisive/view_delegation"
3
4
 
4
5
  module Decisive
5
- class StreamCSVContext < Struct.new(:records, :filename, :block)
6
+ class StreamCSVContext < Struct.new(:records, :filename, :block, :view)
7
+ include ViewDelegation
8
+
6
9
  class Column < Struct.new(:label, :block); end
7
10
 
8
11
  def initialize *args
@@ -59,14 +59,14 @@ module Decisive
59
59
  if stream
60
60
  raise StreamingNotEnabledByControllerError unless controller.is_a?(ActionController::Live)
61
61
  raise StreamIncompatibleBlockArgumentError if block.arity != 0
62
- StreamCSVContext.new(records, filename, block)
62
+ StreamCSVContext.new(records, filename, block, self)
63
63
  else
64
- RenderCSVContext.new(records, filename, block)
64
+ RenderCSVContext.new(records, filename, block, self)
65
65
  end
66
66
  end
67
67
 
68
68
  def xls worksheets=nil, filename:, &block
69
- RenderXLSContext.new(worksheets, filename, block)
69
+ RenderXLSContext.new(worksheets, filename, block, self)
70
70
  end
71
71
  end
72
72
  end
@@ -1,3 +1,3 @@
1
1
  module Decisive
2
- VERSION = "0.9.0"
2
+ VERSION = "0.10.0"
3
3
  end
@@ -0,0 +1,20 @@
1
+ module Decisive
2
+ # Blocks in a decisive template are evaluated against decisive's own objects,
3
+ # so without this they would be cut off from the view's helper methods.
4
+ module ViewDelegation
5
+ private
6
+
7
+ def method_missing name, *args, **kwargs, &block
8
+ return super unless view.respond_to?(name, true)
9
+ view.send(name, *args, **kwargs, &block)
10
+ rescue NameError => error
11
+ # Keep the template's own line at the top of the backtrace.
12
+ error.set_backtrace(error.backtrace.reject { |line| line.include?(__FILE__) })
13
+ raise
14
+ end
15
+
16
+ def respond_to_missing? name, include_private = false
17
+ view.respond_to?(name, include_private) || super
18
+ end
19
+ end
20
+ end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: decisive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micah Geisel
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-07-15 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: actionview
@@ -121,6 +120,7 @@ files:
121
120
  - ".ruby-gemset"
122
121
  - ".ruby-version"
123
122
  - Appraisals
123
+ - CLAUDE.md
124
124
  - Gemfile
125
125
  - LICENSE.txt
126
126
  - README.md
@@ -129,9 +129,8 @@ files:
129
129
  - bin/setup
130
130
  - decisive.gemspec
131
131
  - gemfiles/.bundle/config
132
- - gemfiles/rails_7.0.gemfile
133
- - gemfiles/rails_7.1.gemfile
134
- - gemfiles/rails_7.2.gemfile
132
+ - gemfiles/rails_8.0.gemfile
133
+ - gemfiles/rails_8.1.gemfile
135
134
  - lib/decisive.rb
136
135
  - lib/decisive/render_csv_context.rb
137
136
  - lib/decisive/render_xls_context.rb
@@ -139,12 +138,12 @@ files:
139
138
  - lib/decisive/stream_csv_context.rb
140
139
  - lib/decisive/template_handler.rb
141
140
  - lib/decisive/version.rb
141
+ - lib/decisive/view_delegation.rb
142
142
  - lib/decisive/xls_hasher.rb
143
143
  homepage: https://github.com/botandrose/decisive
144
144
  licenses:
145
145
  - MIT
146
146
  metadata: {}
147
- post_install_message:
148
147
  rdoc_options: []
149
148
  require_paths:
150
149
  - lib
@@ -159,8 +158,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
159
158
  - !ruby/object:Gem::Version
160
159
  version: '0'
161
160
  requirements: []
162
- rubygems_version: 3.5.11
163
- signing_key:
161
+ rubygems_version: 4.0.6
164
162
  specification_version: 4
165
163
  summary: DSL for rendering CSVs
166
164
  test_files: []
@@ -1,8 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "byebug"
6
- gem "actionview", "~>7.2.0"
7
-
8
- gemspec path: "../"