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 +4 -4
- data/.github/workflows/ci.yml +3 -3
- data/.ruby-version +1 -1
- data/Appraisals +4 -9
- data/CLAUDE.md +79 -0
- data/README.md +19 -0
- data/gemfiles/{rails_7.0.gemfile → rails_8.0.gemfile} +1 -1
- data/gemfiles/{rails_7.1.gemfile → rails_8.1.gemfile} +1 -1
- data/lib/decisive/render_csv_context.rb +2 -2
- data/lib/decisive/render_xls_context.rb +5 -2
- data/lib/decisive/renderer.rb +7 -3
- data/lib/decisive/stream_csv_context.rb +4 -1
- data/lib/decisive/template_handler.rb +3 -3
- data/lib/decisive/version.rb +1 -1
- data/lib/decisive/view_delegation.rb +20 -0
- metadata +7 -9
- data/gemfiles/rails_7.2.gemfile +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d43e1342475b84925559c143552a768a720944559d2b24e8291c0bc6f07cf844
|
|
4
|
+
data.tar.gz: 50678ed3c307c531395b1b3ffdd6fe7d7adf0dd001ed4bb889f60c4f9e1fc784
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a3b81bd36c87486123533bb0ccb8192d483856509a5806e1112e810076a28748d53590f2932f24f99b58c04b61ed65d5e35958ec14ac133dde005b7c9efd7c83
|
|
7
|
+
data.tar.gz: b9a75085e0161dfa76dbbfe107ee672b5e0c375ee1d5bb7e62df3276e001a0db12caf4a17c0169d2953ecdca9ee13a898ca73ea53e63d24baabd2451286324f5
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -5,14 +5,14 @@ jobs:
|
|
|
5
5
|
strategy:
|
|
6
6
|
fail-fast: false
|
|
7
7
|
matrix:
|
|
8
|
-
gemfile: [
|
|
9
|
-
ruby: [ 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@
|
|
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-
|
|
1
|
+
ruby-4.0.2
|
data/Appraisals
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
appraise "rails-
|
|
2
|
-
gem "actionview", "~>
|
|
1
|
+
appraise "rails-8.0" do
|
|
2
|
+
gem "actionview", "~>8.0.0"
|
|
3
3
|
end
|
|
4
4
|
|
|
5
|
-
appraise "rails-
|
|
6
|
-
gem "actionview", "~>
|
|
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,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
|
data/lib/decisive/renderer.rb
CHANGED
|
@@ -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
|
data/lib/decisive/version.rb
CHANGED
|
@@ -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.
|
|
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:
|
|
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/
|
|
133
|
-
- gemfiles/
|
|
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:
|
|
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: []
|