Skip to content

Commit 042c2d5

Browse files
authored
CSV row fan-out (#885)
Prior to #866, the `yielder` was able to `yield` multiple times per item in the collection to "fan-out" one item into multiple rows in the CSV. An example of this would be a CSV that iterates over a collection of invoices, but displays each line item of an invoice as its own row. ```rb class InvoicesCSV < ApplicationCSV def around_row(invoice) invoice.line_items.each do |line_item| super(invoice, line_item) end end def row_template(invoice, line_item) column("Invoice Number", invoice.id) column("SKU", line_item.sku) column("Quantity", line_item.quantity) end end ``` I have not yet performed any benchmarks on this. To get back the old behavior requires adding many more ivar lookups/method calls per row, eliminating a lot of the benefit that was brought about in #866. I attempted to sidestep a lot of that by pulling all the locals into a proc closure, and calling that. I have no idea if this is better/worse/or the same as just doing the ivar lookups/method calls each row. It may be a terrible idea. But it does pass the tests 🎉
2 parents 8260b75 + a84da48 commit 042c2d5

File tree

2 files changed

+87
-7
lines changed

2 files changed

+87
-7
lines changed

lib/phlex/csv.rb

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def initialize(collection)
1313
@collection = collection
1414
@_row_buffer = []
1515
@_headers = []
16+
@_row_appender = nil
1617
end
1718

1819
attr_reader :collection
@@ -49,13 +50,7 @@ def call(buffer = +"", context: nil, delimiter: self.delimiter)
4950
MESSAGE
5051
end
5152

52-
each_item do |record|
53-
if has_yielder
54-
yielder(record) { |*a, **k| row = row_template(*a, **k) }
55-
else
56-
around_row(record)
57-
end
58-
53+
row_appender = -> {
5954
row = row_buffer
6055

6156
if first_row
@@ -108,13 +103,28 @@ def call(buffer = +"", context: nil, delimiter: self.delimiter)
108103
buffer << "\n"
109104

110105
row_buffer.clear
106+
}
107+
108+
if has_yielder
109+
each_item do |record|
110+
yielder(record) do |*a, **k|
111+
row_template(*a, **k)
112+
row_appender.call
113+
end
114+
end
115+
else
116+
@row_appender = row_appender
117+
each_item do |record|
118+
around_row(record)
119+
end
111120
end
112121

113122
buffer
114123
end
115124

116125
def around_row(...)
117126
row_template(...)
127+
@row_appender.call
118128
end
119129

120130
def filename

quickdraw/csv.test.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,76 @@ def row_template(name, price)
138138
CSV
139139
end
140140

141+
test "with an around_row that calls super more than once" do
142+
example = Class.new(Phlex::CSV) do
143+
def escape_csv_injection? = true
144+
def trim_whitespace? = false
145+
146+
def around_row(item)
147+
super(item.name, item.price)
148+
super(item.name, item.price)
149+
end
150+
151+
def row_template(name, price)
152+
column "Name", name
153+
column "Price", price
154+
end
155+
end
156+
157+
assert_equal example.new(products).call, <<~CSV
158+
Name,Price
159+
Apple,1.0
160+
Apple,1.0
161+
" Banana ",2.0
162+
" Banana ",2.0
163+
strawberry,Three pounds
164+
strawberry,Three pounds
165+
"'=SUM(A1:B1)","'=SUM(A1:B1)"
166+
"'=SUM(A1:B1)","'=SUM(A1:B1)"
167+
"Abc, ""def""","Foo\nbar ""baz"""
168+
"Abc, ""def""","Foo\nbar ""baz"""
169+
"",""
170+
"",""
171+
"",""
172+
"",""
173+
CSV
174+
end
175+
176+
test "with a yielder that yields more than once" do
177+
example = Class.new(Phlex::CSV) do
178+
def escape_csv_injection? = true
179+
def trim_whitespace? = false
180+
181+
def yielder(item)
182+
yield(item.name, item.price)
183+
yield(item.name, item.price)
184+
end
185+
186+
def row_template(name, price)
187+
column "Name", name
188+
column "Price", price
189+
end
190+
end
191+
192+
assert_equal example.new(products).call, <<~CSV
193+
Name,Price
194+
Apple,1.0
195+
Apple,1.0
196+
" Banana ",2.0
197+
" Banana ",2.0
198+
strawberry,Three pounds
199+
strawberry,Three pounds
200+
"'=SUM(A1:B1)","'=SUM(A1:B1)"
201+
"'=SUM(A1:B1)","'=SUM(A1:B1)"
202+
"Abc, ""def""","Foo\nbar ""baz"""
203+
"Abc, ""def""","Foo\nbar ""baz"""
204+
"",""
205+
"",""
206+
"",""
207+
"",""
208+
CSV
209+
end
210+
141211
test "with a custom delimiter defined as a method" do
142212
example = Class.new(Phlex::CSV) do
143213
define_method(:escape_csv_injection?) { true }

0 commit comments

Comments
 (0)