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 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