decisive 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d28c31cc0299732df3599ea1902bfa2d6586ae70
4
- data.tar.gz: 5002e3b395a7fb0f56ff77ec1bab7e9280fa9016
3
+ metadata.gz: 4f0269f515fd1e730866f0d198e2f65af1006e15
4
+ data.tar.gz: 1c6961707b95dbc84f71ccb77c8d5f33aac1cb0b
5
5
  SHA512:
6
- metadata.gz: f413f05a6292d19c47d716f8792ff58ee5f3715d13105b1823b450e32b99f696c86cd65f529a6d10a90f2aeef74dec45aaaf64193e65db195095a226de79fb86
7
- data.tar.gz: 4ed224da57a64802dbaeb0126a98ffbc472c33a28b833eb3d69728ada9f5c690a5723ae62a574b45879a49cedada464b7025abd7a4e65e69195210e59b51345d
6
+ metadata.gz: 06c93c1c33a9f0b291fa598de426e6d5fe8372668944c0bdbdceb578d8f6703ed6695c016559176de2dc697c2967e21094bb8032b01d7e4115b278b750e26e45
7
+ data.tar.gz: c675a9f1d1b242bfe0cd4f3badcd0cdac7df8d4d256c5a2bbf49947e3ba7793546d57acd4b448debeb760c1924e4c855d6790bc8015404f5a85e3f0ed8bc0841
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- decisive (0.3.0)
4
+ decisive (0.4.0)
5
5
  actionview
6
6
 
7
7
  GEM
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
  [![Build Status](https://travis-ci.org/botandrose/decisive.svg?branch=master)](https://travis-ci.org/botandrose/decisive)
6
6
  [![Code Climate](https://codeclimate.com/github/botandrose/decisive/badges/gpa.svg)](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 |user|
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", user.created_at.to_date # other values get passed straight through
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 get file named "users-2010_01_01.csv" with the following contents:
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
- context.to_csv
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
- Context.new(records, filename, block)
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 Context < Struct.new(:records, :filename, :block)
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
- class Row < Struct.new(:record, :block)
56
- def to_hash
57
- @hash = {}
58
- instance_exec record, &block
59
- @hash
60
- end
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
- private
126
+ private
63
127
 
64
- def column key, value=nil, &block
65
- @hash[key] = if block_given?
66
- block.call(record)
67
- elsif value.is_a?(Symbol)
68
- record.send(value)
69
- elsif value.nil?
70
- record.send(key.parameterize.underscore.to_sym)
71
- else
72
- value
73
- end.to_s
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
@@ -1,3 +1,3 @@
1
1
  module Decisive
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
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.3.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-08 00:00:00.000000000 Z
11
+ date: 2019-06-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionview