decisive 0.3.0 → 0.4.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/Gemfile.lock +1 -1
- data/README.md +39 -5
- data/lib/decisive/template_handler.rb +87 -22
- data/lib/decisive/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4f0269f515fd1e730866f0d198e2f65af1006e15
|
4
|
+
data.tar.gz: 1c6961707b95dbc84f71ccb77c8d5f33aac1cb0b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 06c93c1c33a9f0b291fa598de426e6d5fe8372668944c0bdbdceb578d8f6703ed6695c016559176de2dc697c2967e21094bb8032b01d7e4115b278b750e26e45
|
7
|
+
data.tar.gz: c675a9f1d1b242bfe0cd4f3badcd0cdac7df8d4d256c5a2bbf49947e3ba7793546d57acd4b448debeb760c1924e4c855d6790bc8015404f5a85e3f0ed8bc0841
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,18 +1,20 @@
|
|
1
1
|
# Decisive
|
2
2
|
|
3
|
-
DSL for rendering CSVs in Rails apps
|
3
|
+
DSL for rendering and streaming CSVs in Rails apps
|
4
4
|
|
5
5
|
[](https://travis-ci.org/botandrose/decisive)
|
6
6
|
[](https://codeclimate.com/github/botandrose/decisive)
|
7
7
|
|
8
8
|
## Usage
|
9
9
|
|
10
|
-
Example usage:
|
10
|
+
### Example usage:
|
11
11
|
|
12
12
|
```ruby
|
13
13
|
# app/controllers/users_controller.rb
|
14
14
|
|
15
15
|
class UsersController < ApplicationController
|
16
|
+
include ActionController::Live # required to stream; decisive will fall back to rendering without it
|
17
|
+
|
16
18
|
def index
|
17
19
|
@users = User.all
|
18
20
|
end
|
@@ -22,20 +24,52 @@ end
|
|
22
24
|
```ruby
|
23
25
|
# app/views/users/index.csv.decisive
|
24
26
|
|
25
|
-
csv @users, filename: "users-#{Time.zone.now.strftime("%Y_%m_%d")}.csv" do
|
27
|
+
csv @users, filename: "users-#{Time.zone.now.strftime("%Y_%m_%d")}.csv" do
|
26
28
|
column "Email" # omitted accessor field gets inferred: user.email
|
27
29
|
column "Full name", :name # explicit accessor field: user.name
|
28
|
-
column "Signed up"
|
30
|
+
column "Signed up" do |user| # accepts a block for doing something special
|
31
|
+
user.created_at.to_date
|
32
|
+
end
|
29
33
|
end
|
30
34
|
```
|
31
35
|
|
32
|
-
Then visit /users.csv to
|
36
|
+
Then visit /users.csv to stream a file named "users-2010_01_01.csv" with the following contents:
|
33
37
|
|
34
38
|
| Email | Full name | Signed up |
|
35
39
|
| ----------------- | -------------- | ---------- |
|
36
40
|
| frodo@example.com | Frodo Baggins | 2002-06-19 |
|
37
41
|
| sam@example.com | Samwise Gamgee | 2008-10-13 |
|
38
42
|
|
43
|
+
### Non-streaming usage for non-deterministic headers:
|
44
|
+
|
45
|
+
Sometimes, we don't know exactly what the headers will be until we've iterated through every record.
|
46
|
+
|
47
|
+
For example, lets say that the Frodo record has a #faqs attribute of `{ "Riddles?" => "Yes" }`, while Sam's is `{ "Hero?" => "Frodo" }`.
|
48
|
+
|
49
|
+
In this case, you can pass `stream: false` to #csv, and the method will yield each record to the block:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
# app/views/users/index.csv.decisive
|
53
|
+
|
54
|
+
csv @users, filename: "users-#{Time.zone.now.strftime("%Y_%m_%d")}.csv", stream: false do |user|
|
55
|
+
column "Email"
|
56
|
+
column "Full name"
|
57
|
+
|
58
|
+
user.faqs.favorite_questions_and_answers.each do |question, answer|
|
59
|
+
column question, answer
|
60
|
+
end
|
61
|
+
|
62
|
+
column "Signed up", user.created_at.to_date # we have access to the user record directly
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
Visiting /users.csv will render a file named "users-2010_01_01.csv" with the following contents:
|
67
|
+
|
68
|
+
| Email | Full name | Riddles? | Hero? | Signed up |
|
69
|
+
| ----------------- | -------------- | -------- | ----- | ---------- |
|
70
|
+
| frodo@example.com | Frodo Baggins | Yes | | 2002-06-19 |
|
71
|
+
| sam@example.com | Samwise Gamgee | | Frodo | 2008-10-13 |
|
72
|
+
|
39
73
|
## Development
|
40
74
|
|
41
75
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -12,19 +12,84 @@ module Decisive
|
|
12
12
|
<<~RUBY
|
13
13
|
extend Decisive::DSL
|
14
14
|
context = (#{template.source})
|
15
|
+
|
16
|
+
response.headers["Content-Type"] = "text/csv"
|
17
|
+
response.headers["Content-Transfer-Encoding"] = "binary"
|
15
18
|
response.headers["Content-Disposition"] = %(attachment; filename="\#{context.filename}")
|
16
|
-
|
19
|
+
|
20
|
+
if controller.respond_to?(:new_controller_thread) # has AC::Live mixed in
|
21
|
+
begin
|
22
|
+
context.each do |row|
|
23
|
+
response.stream.write row.to_csv
|
24
|
+
end
|
25
|
+
ensure
|
26
|
+
response.stream.close
|
27
|
+
end
|
28
|
+
""
|
29
|
+
else
|
30
|
+
context.to_csv
|
31
|
+
end
|
17
32
|
RUBY
|
18
33
|
end
|
19
34
|
end
|
20
35
|
|
36
|
+
class StreamIncompatibleBlockArgumentError < StandardError
|
37
|
+
def message
|
38
|
+
"#csv cannot take a block with a record argument while streaming, because the headers have to be known in advance. Either disable streaming by passing `stream: false` to #csv, or convert the template to yield the record to the block passed to each #column call."
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class StreamingNotEnabledByControllerError < StandardError
|
43
|
+
def message
|
44
|
+
"the controller does not have ActionController::Live included, and thus cannot stream this csv. Either disable streaming by passing `stream: false` to #csv, or include ActionController::Live into the controller."
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
21
48
|
module DSL
|
22
|
-
def csv records, filename:, &block
|
23
|
-
|
49
|
+
def csv records, filename:, stream: true, &block
|
50
|
+
if stream
|
51
|
+
raise StreamingNotEnabledByControllerError unless controller.respond_to?(:new_controller_thread) # has AC::Live mixed in
|
52
|
+
raise StreamIncompatibleBlockArgumentError if block.arity != 0
|
53
|
+
StreamContext.new([], records, filename, &block)
|
54
|
+
else
|
55
|
+
RenderContext.new(records, filename, block)
|
56
|
+
end
|
24
57
|
end
|
25
58
|
end
|
26
59
|
|
27
|
-
class
|
60
|
+
class StreamContext < Struct.new(:columns, :records, :filename)
|
61
|
+
class Column < Struct.new(:label, :block); end
|
62
|
+
|
63
|
+
def initialize *args, &block
|
64
|
+
super
|
65
|
+
instance_eval &block
|
66
|
+
end
|
67
|
+
|
68
|
+
def column label, value=nil, &block # field, label: field.to_s.humanize, &block
|
69
|
+
value ||= label.parameterize.underscore.to_sym
|
70
|
+
block ||= ->(record) { record.send(value) }
|
71
|
+
columns << Column.new(label, block)
|
72
|
+
end
|
73
|
+
|
74
|
+
def each
|
75
|
+
yield header
|
76
|
+
|
77
|
+
records.map do |record|
|
78
|
+
row = columns.map do |column|
|
79
|
+
column.block.call(record).to_s
|
80
|
+
end
|
81
|
+
yield row
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def header
|
88
|
+
columns.map(&:label)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class RenderContext < Struct.new(:records, :filename, :block)
|
28
93
|
def to_csv
|
29
94
|
(header + body).map(&:to_csv).join
|
30
95
|
end
|
@@ -50,27 +115,27 @@ module Decisive
|
|
50
115
|
Row.new(record, block).to_hash
|
51
116
|
end
|
52
117
|
end
|
53
|
-
end
|
54
118
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
119
|
+
class Row < Struct.new(:record, :block)
|
120
|
+
def to_hash
|
121
|
+
@hash = {}
|
122
|
+
instance_exec record, &block
|
123
|
+
@hash
|
124
|
+
end
|
61
125
|
|
62
|
-
|
126
|
+
private
|
63
127
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
128
|
+
def column key, value=nil, &block
|
129
|
+
@hash[key] = if block_given?
|
130
|
+
block.call(record)
|
131
|
+
elsif value.is_a?(Symbol)
|
132
|
+
record.send(value)
|
133
|
+
elsif value.nil?
|
134
|
+
record.send(key.parameterize.underscore.to_sym)
|
135
|
+
else
|
136
|
+
value
|
137
|
+
end.to_s
|
138
|
+
end
|
74
139
|
end
|
75
140
|
end
|
76
141
|
end
|
data/lib/decisive/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: decisive
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.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: 2019-05
|
11
|
+
date: 2019-06-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionview
|