comma 5.0.0 → 5.1.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +10 -1
  3. data/.rubocop_todo.yml +5 -4
  4. data/Appraisals +20 -22
  5. data/Gemfile.lock +1 -1
  6. data/README.md +75 -3
  7. data/gemfiles/{active6.0.6.gemfile → active7.1.6.gemfile} +2 -2
  8. data/gemfiles/{active7.1.3.gemfile.lock → active7.1.6.gemfile.lock} +79 -43
  9. data/gemfiles/{active7.0.8.gemfile → active7.2.3.2.gemfile} +2 -2
  10. data/gemfiles/active7.2.3.2.gemfile.lock +182 -0
  11. data/gemfiles/{active6.1.7.6.gemfile → active8.0.5.1.gemfile} +2 -2
  12. data/gemfiles/{active6.1.7.6.gemfile.lock → active8.0.5.1.gemfile.lock} +87 -47
  13. data/gemfiles/{active7.1.3.gemfile → active8.1.3.1.gemfile} +2 -2
  14. data/gemfiles/{active7.0.8.gemfile.lock → active8.1.3.1.gemfile.lock} +87 -45
  15. data/gemfiles/{rails7.0.8.gemfile → rails7.1.6.gemfile} +1 -1
  16. data/gemfiles/{rails7.1.3.gemfile.lock → rails7.1.6.gemfile.lock} +155 -126
  17. data/gemfiles/{rails7.1.3.gemfile → rails7.2.3.2.gemfile} +1 -1
  18. data/gemfiles/rails7.2.3.2.gemfile.lock +321 -0
  19. data/gemfiles/{rails6.0.6.gemfile → rails8.0.5.1.gemfile} +1 -2
  20. data/gemfiles/rails8.0.5.1.gemfile.lock +319 -0
  21. data/gemfiles/{rails6.1.7.6.gemfile → rails8.1.3.1.gemfile} +1 -2
  22. data/gemfiles/rails8.1.3.1.gemfile.lock +322 -0
  23. data/lib/comma/array.rb +1 -3
  24. data/lib/comma/collection_export.rb +9 -0
  25. data/lib/comma/data_extractor.rb +16 -20
  26. data/lib/comma/data_mapper_collection.rb +1 -3
  27. data/lib/comma/extractor.rb +33 -2
  28. data/lib/comma/generator.rb +4 -8
  29. data/lib/comma/header_extractor.rb +10 -19
  30. data/lib/comma/mongoid.rb +1 -3
  31. data/lib/comma/object.rb +37 -11
  32. data/lib/comma/options.rb +18 -0
  33. data/lib/comma/rails/renderer.rb +21 -0
  34. data/lib/comma/rails.rb +16 -0
  35. data/lib/comma/version.rb +1 -1
  36. data/lib/comma.rb +4 -43
  37. data/spec/comma/comma_spec.rb +93 -34
  38. data/spec/comma/data_extractor_spec.rb +3 -3
  39. data/spec/comma/header_extractor_spec.rb +1 -1
  40. data/spec/comma/options_spec.rb +63 -0
  41. data/spec/comma/rails/active_record_spec.rb +0 -1
  42. data/spec/controllers/users_controller_spec.rb +13 -28
  43. data/spec/support/comma_class_helper.rb +12 -0
  44. data/spec/support/csv_expectation_helper.rb +14 -0
  45. metadata +28 -18
  46. data/gemfiles/active6.0.6.gemfile.lock +0 -144
  47. data/gemfiles/rails6.0.6.gemfile.lock +0 -276
  48. data/gemfiles/rails6.1.7.6.gemfile.lock +0 -279
  49. data/gemfiles/rails7.0.8.gemfile.lock +0 -274
data/lib/comma.rb CHANGED
@@ -11,50 +11,11 @@ module Comma
11
11
  }.freeze
12
12
  end
13
13
 
14
- require 'active_support'
15
- require 'active_support/lazy_load_hooks'
16
- ActiveSupport.on_load(:active_record) do
17
- require 'comma/relation' if defined?(ActiveRecord::Relation)
18
- end
19
-
20
- ActiveSupport.on_load(:mongoid) do
21
- require 'comma/mongoid'
22
- end
23
-
24
- require 'comma/data_mapper_collection' if defined? DataMapper
25
-
14
+ require 'comma/options'
26
15
  require 'comma/generator'
16
+ require 'comma/collection_export'
17
+ require 'comma/data_mapper_collection' if defined? DataMapper
27
18
  require 'comma/array'
28
19
  require 'comma/object'
29
20
 
30
- # Load into Rails controllers
31
- ActiveSupport.on_load(:action_controller) do
32
- if defined?(ActionController::Renderers) && ActionController::Renderers.respond_to?(:add)
33
- ActionController::Renderers.add :csv do |obj, options|
34
- filename = options[:filename] || 'data'
35
- extension = options[:extension] || 'csv'
36
- mime_type = if Rails.version >= '5.0.0'
37
- options[:mime_type] || Mime[:csv]
38
- else
39
- options[:mime_type] || Mime::CSV
40
- end
41
- with_bom = options.delete(:with_bom) || false
42
-
43
- # Capture any CSV optional settings passed to comma or comma specific options
44
- csv_options = options.slice(*CSV_HANDLER::DEFAULT_OPTIONS.merge(Comma::DEFAULT_OPTIONS).keys)
45
- csv_options = csv_options.each_with_object({}) do |(k, v), h|
46
- # XXX: Convert string to boolean
47
- h[k] = case k
48
- when :write_headers
49
- (v != 'false') if v.is_a?(String)
50
- else
51
- v
52
- end
53
- end
54
- data = obj.to_comma(csv_options)
55
- data = "\xEF\xBB\xBF#{data}" if with_bom
56
- disposition = "attachment; filename=\"#{filename}.#{extension}\""
57
- send_data data, type: mime_type, disposition: disposition
58
- end
59
- end
60
- end
21
+ require 'comma/rails'
@@ -40,8 +40,9 @@ describe Comma, 'generating CSV' do # rubocop:disable Metrics/BlockLength
40
40
  end
41
41
 
42
42
  it 'should extend Array to add a #to_comma method which will return CSV content for objects within the array' do
43
- expected = "Title,Description,Issuer,ISBN-10,ISBN-13\nSmalltalk-80,Language and Implementation,ISBN,123123123,321321321\n" # rubocop:disable Layout/LineLength
44
- expect(@books.to_comma).to eq(expected)
43
+ expect_csv(@books.to_comma,
44
+ headers: %w[Title Description Issuer ISBN-10 ISBN-13],
45
+ rows: [['Smalltalk-80', 'Language and Implementation', 'ISBN', '123123123', '321321321']])
45
46
  end
46
47
 
47
48
  it 'should return an empty string when generating CSV from an empty array' do
@@ -49,7 +50,9 @@ describe Comma, 'generating CSV' do # rubocop:disable Metrics/BlockLength
49
50
  end
50
51
 
51
52
  it 'should change the style when specified' do
52
- expect(@books.to_comma(:brief)).to eq("Name,Description\nSmalltalk-80,Language and Implementation\n")
53
+ expect_csv(@books.to_comma(:brief),
54
+ headers: %w[Name Description],
55
+ rows: [['Smalltalk-80', 'Language and Implementation']])
53
56
  end
54
57
 
55
58
  describe 'with :filename specified' do
@@ -57,31 +60,39 @@ describe Comma, 'generating CSV' do # rubocop:disable Metrics/BlockLength
57
60
 
58
61
  it 'should write to the file' do
59
62
  @books.to_comma(filename: 'comma.csv')
60
- expected = "Title,Description,Issuer,ISBN-10,ISBN-13\nSmalltalk-80,Language and Implementation,ISBN,123123123,321321321\n" # rubocop:disable Layout/LineLength
61
- expect(File.read('comma.csv')).to eq(expected)
63
+ expect_csv(File.read('comma.csv'),
64
+ headers: %w[Title Description Issuer ISBN-10 ISBN-13],
65
+ rows: [['Smalltalk-80', 'Language and Implementation', 'ISBN', '123123123', '321321321']])
62
66
  end
63
67
 
64
68
  it 'should accept FasterCSV options' do
65
69
  @books.to_comma(filename: 'comma.csv', col_sep: ';', force_quotes: true)
66
- expected = "\"Title\";\"Description\";\"Issuer\";\"ISBN-10\";\"ISBN-13\"\n\"Smalltalk-80\";\"Language and Implementation\";\"ISBN\";\"123123123\";\"321321321\"\n" # rubocop:disable Layout/LineLength
67
- expect(File.read('comma.csv')).to eq(expected)
70
+ expect_csv(File.read('comma.csv'),
71
+ headers: %w[Title Description Issuer ISBN-10 ISBN-13],
72
+ rows: [['Smalltalk-80', 'Language and Implementation', 'ISBN', '123123123', '321321321']],
73
+ col_sep: ';', force_quotes: true)
68
74
  end
69
75
  end
70
76
 
71
77
  describe 'with FasterCSV options' do
72
78
  it 'should not change when options are empty' do
73
- expected = "Title,Description,Issuer,ISBN-10,ISBN-13\nSmalltalk-80,Language and Implementation,ISBN,123123123,321321321\n" # rubocop:disable Layout/LineLength
74
- expect(@books.to_comma({})).to eq(expected)
79
+ expect_csv(@books.to_comma({}),
80
+ headers: %w[Title Description Issuer ISBN-10 ISBN-13],
81
+ rows: [['Smalltalk-80', 'Language and Implementation', 'ISBN', '123123123', '321321321']])
75
82
  end
76
83
 
77
84
  it 'should accept the options in #to_comma and generate the appropriate CSV' do
78
- expected = "\"Title\";\"Description\";\"Issuer\";\"ISBN-10\";\"ISBN-13\"\n\"Smalltalk-80\";\"Language and Implementation\";\"ISBN\";\"123123123\";\"321321321\"\n" # rubocop:disable Layout/LineLength
79
- expect(@books.to_comma(col_sep: ';', force_quotes: true)).to eq(expected)
85
+ expect_csv(@books.to_comma(col_sep: ';', force_quotes: true),
86
+ headers: %w[Title Description Issuer ISBN-10 ISBN-13],
87
+ rows: [['Smalltalk-80', 'Language and Implementation', 'ISBN', '123123123', '321321321']],
88
+ col_sep: ';', force_quotes: true)
80
89
  end
81
90
 
82
91
  it 'should change the style when specified' do
83
- expect(@books.to_comma(style: :brief, col_sep: ';', force_quotes: true))
84
- .to eq("\"Name\";\"Description\"\n\"Smalltalk-80\";\"Language and Implementation\"\n")
92
+ expect_csv(@books.to_comma(style: :brief, col_sep: ';', force_quotes: true),
93
+ headers: %w[Name Description],
94
+ rows: [['Smalltalk-80', 'Language and Implementation']],
95
+ col_sep: ';', force_quotes: true)
85
96
  end
86
97
  end
87
98
  end
@@ -89,29 +100,29 @@ end
89
100
  describe Comma, 'defining CSV descriptions' do
90
101
  describe 'with an unnamed description' do
91
102
  before do
92
- class Foo
103
+ @foo_class = define_comma_class do
93
104
  comma do; end
94
105
  end
95
106
  end
96
107
 
97
108
  it 'should name the current description :default if no name has been provided' do
98
- expect(Foo.comma_formats).not_to be_empty
99
- expect(Foo.comma_formats[:default]).not_to be_nil
109
+ expect(@foo_class.comma_formats).not_to be_empty
110
+ expect(@foo_class.comma_formats[:default]).not_to be_nil
100
111
  end
101
112
  end
102
113
 
103
114
  describe 'with a named description' do
104
115
  before do
105
- class Bar
116
+ @bar_class = define_comma_class do
106
117
  comma do; end
107
118
  comma :detailed do; end
108
119
  end
109
120
  end
110
121
 
111
122
  it 'should use the provided name to index the comma format' do
112
- expect(Bar.comma_formats).not_to be_empty
113
- expect(Bar.comma_formats[:default]).not_to be_nil
114
- expect(Bar.comma_formats[:detailed]).not_to be_nil
123
+ expect(@bar_class.comma_formats).not_to be_empty
124
+ expect(@bar_class.comma_formats[:default]).not_to be_nil
125
+ expect(@bar_class.comma_formats[:detailed]).not_to be_nil
115
126
  end
116
127
  end
117
128
  end
@@ -119,7 +130,7 @@ end
119
130
  describe Comma, 'to_comma data/headers object extensions' do # rubocop:disable Metrics/BlockLength
120
131
  describe 'with unnamed descriptions' do
121
132
  before do
122
- class Foo
133
+ foo_class = define_comma_class do
123
134
  attr_accessor :content
124
135
  comma do; content; end
125
136
 
@@ -128,7 +139,7 @@ describe Comma, 'to_comma data/headers object extensions' do # rubocop:disable M
128
139
  end
129
140
  end
130
141
 
131
- @foo = Foo.new('content')
142
+ @foo = foo_class.new('content')
132
143
  end
133
144
 
134
145
  it 'should return and array of data content, using the :default CSV description if none requested' do
@@ -146,7 +157,7 @@ describe Comma, 'to_comma data/headers object extensions' do # rubocop:disable M
146
157
 
147
158
  describe 'with named descriptions' do
148
159
  before do
149
- class Foo
160
+ foo_class = define_comma_class do
150
161
  attr_accessor :content
151
162
  comma :detailed do; content; end
152
163
 
@@ -155,7 +166,7 @@ describe Comma, 'to_comma data/headers object extensions' do # rubocop:disable M
155
166
  end
156
167
  end
157
168
 
158
- @foo = Foo.new('content')
169
+ @foo = foo_class.new('content')
159
170
  end
160
171
 
161
172
  it 'should return and array of data content, using the :default CSV description if none requested' do
@@ -179,7 +190,7 @@ describe Comma, 'to_comma data/headers object extensions' do # rubocop:disable M
179
190
 
180
191
  describe 'with block' do # rubocop:disable Metrics/BlockLength
181
192
  before do
182
- class Foo
193
+ foo_class = define_comma_class do
183
194
  attr_accessor :content, :created_at, :updated_at
184
195
  comma do
185
196
  content
@@ -199,7 +210,7 @@ describe Comma, 'to_comma data/headers object extensions' do # rubocop:disable M
199
210
 
200
211
  @time = Time.now
201
212
  @content = 'content ' * 5
202
- @foo = Foo.new @content, @time, @time
213
+ @foo = foo_class.new @content, @time, @time
203
214
  end
204
215
 
205
216
  it 'should return yielded values by block' do
@@ -263,9 +274,9 @@ describe Comma, 'to_comma data/headers object extensions' do # rubocop:disable M
263
274
  end
264
275
  end
265
276
 
266
- describe 'on objects using Single Table Inheritance' do
277
+ describe 'on objects using Single Table Inheritance' do # rubocop:disable Metrics/BlockLength
267
278
  before do
268
- class MySuperClass
279
+ super_class = define_comma_class do
269
280
  attr_accessor :content
270
281
  comma do; content end
271
282
 
@@ -274,7 +285,7 @@ describe Comma, 'to_comma data/headers object extensions' do # rubocop:disable M
274
285
  end
275
286
  end
276
287
 
277
- class ChildClassComma < MySuperClass
288
+ child_class_comma = define_comma_class(super_class) do
278
289
  comma do; content end
279
290
 
280
291
  def initialize(content)
@@ -282,11 +293,10 @@ describe Comma, 'to_comma data/headers object extensions' do # rubocop:disable M
282
293
  end
283
294
  end
284
295
 
285
- class ChildClassNoComma < MySuperClass
286
- end
296
+ child_class_no_comma = define_comma_class(super_class) {}
287
297
 
288
- @childComma = ChildClassComma.new('content')
289
- @childNoComma = ChildClassNoComma.new('content')
298
+ @childComma = child_class_comma.new('content')
299
+ @childNoComma = child_class_no_comma.new('content')
290
300
  end
291
301
 
292
302
  it 'should return and array of data content, as defined in comma block in child class' do
@@ -296,12 +306,34 @@ describe Comma, 'to_comma data/headers object extensions' do # rubocop:disable M
296
306
  it 'should return and array of data content, as defined in comma block in super class, if not present in child' do
297
307
  expect(@childNoComma.to_comma).to eq(%w[super-content])
298
308
  end
309
+
310
+ it 'should reflect changes to the superclass format made after the subclass was defined' do
311
+ reopened_super_class = define_comma_class do
312
+ attr_accessor :content
313
+ comma do; content end
314
+
315
+ def initialize(content)
316
+ @content = 'super-' + content
317
+ end
318
+ end
319
+
320
+ reopened_child_no_comma = define_comma_class(reopened_super_class) {}
321
+
322
+ reopened_super_class.class_eval do
323
+ comma do
324
+ content(&:upcase)
325
+ end
326
+ end
327
+
328
+ child = reopened_child_no_comma.new('content')
329
+ expect(child.to_comma).to eq(%w[SUPER-CONTENT])
330
+ end
299
331
  end
300
332
  end
301
333
 
302
334
  describe Comma, '__use__ keyword' do
303
335
  before(:all) do
304
- @obj = Class.new(Struct.new(:id, :title, :description)) do
336
+ @obj = define_comma_class(Struct.new(:id, :title, :description)) do
305
337
  comma do
306
338
  title
307
339
  __use__ :description
@@ -322,3 +354,30 @@ describe Comma, '__use__ keyword' do
322
354
  its(:size) { should eq(3) }
323
355
  it { should eq(['Programming Ruby', 'Foo, Inc.', 'The Pickaxe book']) }
324
356
  end
357
+
358
+ describe Comma, '__use__ keyword with a circular reference' do
359
+ it 'should raise Comma::CircularStyleReference instead of overflowing the stack' do
360
+ obj = define_comma_class(Struct.new(:id, :title)) do
361
+ comma :a do
362
+ title
363
+ __use__ :b
364
+ end
365
+
366
+ comma :b do
367
+ __use__ :a
368
+ end
369
+ end.new(1, 'Programming Ruby')
370
+
371
+ expect { obj.to_comma(:a) }.to raise_error(Comma::CircularStyleReference, /a -> b -> a/)
372
+ end
373
+
374
+ it 'should raise Comma::CircularStyleReference for direct self-reference' do
375
+ obj = define_comma_class(Struct.new(:id)) do
376
+ comma :a do
377
+ __use__ :a
378
+ end
379
+ end.new(1)
380
+
381
+ expect { obj.to_comma(:a) }.to raise_error(Comma::CircularStyleReference, /a -> a/)
382
+ end
383
+ end
@@ -45,7 +45,7 @@ end
45
45
 
46
46
  describe Comma::DataExtractor, 'id attribute' do
47
47
  before do
48
- @data = Class.new(Struct.new(:id)) do
48
+ @data = define_comma_class(Struct.new(:id)) do
49
49
  comma do
50
50
  id 'ID' do |_id| '42' end
51
51
  end
@@ -59,7 +59,7 @@ end
59
59
 
60
60
  describe Comma::DataExtractor, 'with static column method' do
61
61
  before do
62
- @data = Class.new(Struct.new(:id, :name)) do
62
+ @data = define_comma_class(Struct.new(:id, :name)) do
63
63
  comma do
64
64
  __static_column__
65
65
  __static_column__ 'STATIC'
@@ -76,7 +76,7 @@ end
76
76
 
77
77
  describe Comma::DataExtractor, 'nil value' do
78
78
  before do
79
- @data = Class.new(Struct.new(:id, :name)) do
79
+ @data = define_comma_class(Struct.new(:id, :name)) do
80
80
  comma do
81
81
  name
82
82
  name 'Name'
@@ -46,7 +46,7 @@ end
46
46
 
47
47
  describe Comma::HeaderExtractor, 'with static column method' do
48
48
  before do
49
- @headers = Class.new(Struct.new(:id, :name)) do
49
+ @headers = define_comma_class(Struct.new(:id, :name)) do
50
50
  comma do
51
51
  __static_column__
52
52
  __static_column__ 'STATIC'
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Comma::Options do # rubocop:disable Metrics/BlockLength
6
+ describe '.parse' do # rubocop:disable Metrics/BlockLength
7
+ it 'treats a bare symbol as the style, with no filename or csv options' do
8
+ expect(described_class.parse(:brief)).to eq(style: :brief, filename: nil, csv: {})
9
+ end
10
+
11
+ it 'passes a bare nil through unchanged rather than defaulting it' do
12
+ expect(described_class.parse(nil)).to eq(style: nil, filename: nil, csv: {})
13
+ end
14
+
15
+ it 'passes a bare false through unchanged rather than defaulting it' do
16
+ expect(described_class.parse(false)).to eq(style: false, filename: nil, csv: {})
17
+ end
18
+
19
+ it 'extracts :style from a hash, defaulting to :default when absent' do
20
+ result = described_class.parse(col_sep: ';')
21
+ expect(result[:style]).to eq(:default)
22
+ expect(result[:csv]).to eq(col_sep: ';')
23
+ end
24
+
25
+ it 'extracts an explicit :style from a hash' do
26
+ result = described_class.parse(style: :brief, col_sep: ';')
27
+ expect(result[:style]).to eq(:brief)
28
+ expect(result[:csv]).to eq(col_sep: ';')
29
+ end
30
+
31
+ it 'extracts :filename from a hash and excludes it from :csv' do
32
+ result = described_class.parse(filename: 'export.csv', col_sep: ';')
33
+ expect(result[:filename]).to eq('export.csv')
34
+ expect(result[:csv]).to eq(col_sep: ';')
35
+ end
36
+
37
+ it 'passes through a real boolean write_headers unchanged' do
38
+ result = described_class.parse(write_headers: false)
39
+ expect(result[:csv]).to eq(write_headers: false)
40
+ end
41
+
42
+ it 'coerces the string "false" for write_headers to boolean false' do
43
+ result = described_class.parse(write_headers: 'false')
44
+ expect(result[:csv]).to eq(write_headers: false)
45
+ end
46
+
47
+ it 'coerces any other string for write_headers to boolean true' do
48
+ result = described_class.parse(write_headers: 'true')
49
+ expect(result[:csv]).to eq(write_headers: true)
50
+ end
51
+
52
+ it 'does not mutate the input hash' do
53
+ input = { style: :brief, filename: 'f.csv', col_sep: ';' }
54
+ described_class.parse(input)
55
+ expect(input).to eq(style: :brief, filename: 'f.csv', col_sep: ';')
56
+ end
57
+
58
+ it 'does not raise when given a frozen hash' do
59
+ input = { style: :brief, col_sep: ';' }.freeze
60
+ expect { described_class.parse(input) }.not_to raise_error
61
+ end
62
+ end
63
+ end
@@ -191,7 +191,6 @@ if defined? ActiveRecord
191
191
  expect(@dog.to_comma).to eq %w[Dog-Rex]
192
192
  end
193
193
 
194
- # FIXME: this one is failing - the comma block from Dog is executed instead of the one from the super class
195
194
  it 'should return and array of data content, as defined in comma block in super class, if not present in child' do
196
195
  expect(@cat.to_comma).to eq %w[Super-Kitty]
197
196
  end
@@ -37,13 +37,9 @@ if defined?(Rails)
37
37
  expect(response.media_type).to eq 'text/csv'
38
38
  expect(response.header['Content-Disposition']).to include('filename="data.csv"')
39
39
 
40
- expected_content = <<-CSV.gsub(/^\s+/, '')
41
- First name,Last name,Name
42
- Fred,Flintstone,Fred Flintstone
43
- Wilma,Flintstone,Wilma Flintstone
44
- CSV
45
-
46
- expect(response.body).to eq expected_content
40
+ expect_csv(response.body,
41
+ headers: ['First name', 'Last name', 'Name'],
42
+ rows: [['Fred', 'Flintstone', 'Fred Flintstone'], ['Wilma', 'Flintstone', 'Wilma Flintstone']])
47
43
  end
48
44
 
49
45
  describe 'with comma options' do
@@ -53,13 +49,9 @@ if defined?(Rails)
53
49
 
54
50
  get :with_custom_style, format: :csv
55
51
 
56
- expected_content = <<-CSV.gsub(/^\s+/, '')
57
- First name,Last name
58
- Fred,Flintstone
59
- Wilma,Flintstone
60
- CSV
61
-
62
- expect(response.body).to eq expected_content
52
+ expect_csv(response.body,
53
+ headers: ['First name', 'Last name'],
54
+ rows: [%w[Fred Flintstone], %w[Wilma Flintstone]])
63
55
  end
64
56
  end
65
57
 
@@ -131,13 +123,9 @@ if defined?(Rails)
131
123
  expect(response.status).to eq 200
132
124
  expect(response.media_type).to eq 'text/csv'
133
125
 
134
- expected_content = <<-CSV.gsub(/^\s+/, '')
135
- First name,Last name,Name
136
- Fred,Flintstone,Fred Flintstone
137
- Wilma,Flintstone,Wilma Flintstone
138
- CSV
139
-
140
- expect(response.body).to eq expected_content
126
+ expect_csv(response.body,
127
+ headers: ['First name', 'Last name', 'Name'],
128
+ rows: [['Fred', 'Flintstone', 'Fred Flintstone'], ['Wilma', 'Flintstone', 'Wilma Flintstone']])
141
129
  end
142
130
 
143
131
  it 'should allow toggling off' do
@@ -161,13 +149,10 @@ if defined?(Rails)
161
149
  expect(response.status).to eq 200
162
150
  expect(response.media_type).to eq 'text/csv'
163
151
 
164
- expected_content = <<-CSV.gsub(/^\s+/, '')
165
- "First name","Last name","Name"
166
- "Fred","Flintstone","Fred Flintstone"
167
- "Wilma","Flintstone","Wilma Flintstone"
168
- CSV
169
-
170
- expect(response.body).to eq expected_content
152
+ expect_csv(response.body,
153
+ headers: ['First name', 'Last name', 'Name'],
154
+ rows: [['Fred', 'Flintstone', 'Fred Flintstone'], ['Wilma', 'Flintstone', 'Wilma Flintstone']],
155
+ force_quotes: true)
171
156
  end
172
157
 
173
158
  it 'should allow combinations of options' do
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Defines an anonymous class for use in a single example, avoiding
4
+ # constants leaking into the global namespace across the spec suite.
5
+ # Pass `base` to build on a superclass (e.g. a Struct).
6
+ #
7
+ # book_class = define_comma_class(Struct.new(:title)) do
8
+ # comma { title }
9
+ # end
10
+ def define_comma_class(base = Object, &block)
11
+ Class.new(base, &block)
12
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Builds the expected CSV string from headers/rows so specs don't hand-roll
4
+ # comma-joined strings, then asserts it against the actual output.
5
+ # `csv_options` are forwarded to CSV.generate (e.g. col_sep:, force_quotes:).
6
+ #
7
+ # expect_csv(books.to_comma, headers: %w[Title Author], rows: [['Smalltalk-80', 'Kay']])
8
+ def expect_csv(output, headers:, rows:, **csv_options)
9
+ expected = CSV.generate(**csv_options) do |csv|
10
+ csv << headers
11
+ rows.each { |row| csv << row }
12
+ end
13
+ expect(output).to eq(expected)
14
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: comma
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.0
4
+ version: 5.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcus Crafter
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2026-08-22 00:00:00.000000000 Z
12
+ date: 2026-09-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -258,25 +258,26 @@ files:
258
258
  - README.md
259
259
  - Rakefile
260
260
  - comma.gemspec
261
- - gemfiles/active6.0.6.gemfile
262
- - gemfiles/active6.0.6.gemfile.lock
263
- - gemfiles/active6.1.7.6.gemfile
264
- - gemfiles/active6.1.7.6.gemfile.lock
265
- - gemfiles/active7.0.8.gemfile
266
- - gemfiles/active7.0.8.gemfile.lock
267
- - gemfiles/active7.1.3.gemfile
268
- - gemfiles/active7.1.3.gemfile.lock
269
- - gemfiles/rails6.0.6.gemfile
270
- - gemfiles/rails6.0.6.gemfile.lock
271
- - gemfiles/rails6.1.7.6.gemfile
272
- - gemfiles/rails6.1.7.6.gemfile.lock
273
- - gemfiles/rails7.0.8.gemfile
274
- - gemfiles/rails7.0.8.gemfile.lock
275
- - gemfiles/rails7.1.3.gemfile
276
- - gemfiles/rails7.1.3.gemfile.lock
261
+ - gemfiles/active7.1.6.gemfile
262
+ - gemfiles/active7.1.6.gemfile.lock
263
+ - gemfiles/active7.2.3.2.gemfile
264
+ - gemfiles/active7.2.3.2.gemfile.lock
265
+ - gemfiles/active8.0.5.1.gemfile
266
+ - gemfiles/active8.0.5.1.gemfile.lock
267
+ - gemfiles/active8.1.3.1.gemfile
268
+ - gemfiles/active8.1.3.1.gemfile.lock
269
+ - gemfiles/rails7.1.6.gemfile
270
+ - gemfiles/rails7.1.6.gemfile.lock
271
+ - gemfiles/rails7.2.3.2.gemfile
272
+ - gemfiles/rails7.2.3.2.gemfile.lock
273
+ - gemfiles/rails8.0.5.1.gemfile
274
+ - gemfiles/rails8.0.5.1.gemfile.lock
275
+ - gemfiles/rails8.1.3.1.gemfile
276
+ - gemfiles/rails8.1.3.1.gemfile.lock
277
277
  - init.rb
278
278
  - lib/comma.rb
279
279
  - lib/comma/array.rb
280
+ - lib/comma/collection_export.rb
280
281
  - lib/comma/data_extractor.rb
281
282
  - lib/comma/data_mapper_collection.rb
282
283
  - lib/comma/extractor.rb
@@ -284,11 +285,15 @@ files:
284
285
  - lib/comma/header_extractor.rb
285
286
  - lib/comma/mongoid.rb
286
287
  - lib/comma/object.rb
288
+ - lib/comma/options.rb
289
+ - lib/comma/rails.rb
290
+ - lib/comma/rails/renderer.rb
287
291
  - lib/comma/relation.rb
288
292
  - lib/comma/version.rb
289
293
  - spec/comma/comma_spec.rb
290
294
  - spec/comma/data_extractor_spec.rb
291
295
  - spec/comma/header_extractor_spec.rb
296
+ - spec/comma/options_spec.rb
292
297
  - spec/comma/rails/active_record_spec.rb
293
298
  - spec/comma/rails/data_mapper_collection_spec.rb
294
299
  - spec/comma/rails/mongoid_spec.rb
@@ -302,6 +307,8 @@ files:
302
307
  - spec/rails_app/rails_app.rb
303
308
  - spec/rails_app/tmp/.gitkeep
304
309
  - spec/spec_helper.rb
310
+ - spec/support/comma_class_helper.rb
311
+ - spec/support/csv_expectation_helper.rb
305
312
  homepage: http://github.com/comma-csv/comma
306
313
  licenses:
307
314
  - MIT
@@ -329,6 +336,7 @@ test_files:
329
336
  - spec/comma/comma_spec.rb
330
337
  - spec/comma/data_extractor_spec.rb
331
338
  - spec/comma/header_extractor_spec.rb
339
+ - spec/comma/options_spec.rb
332
340
  - spec/comma/rails/active_record_spec.rb
333
341
  - spec/comma/rails/data_mapper_collection_spec.rb
334
342
  - spec/comma/rails/mongoid_spec.rb
@@ -342,3 +350,5 @@ test_files:
342
350
  - spec/rails_app/rails_app.rb
343
351
  - spec/rails_app/tmp/.gitkeep
344
352
  - spec/spec_helper.rb
353
+ - spec/support/comma_class_helper.rb
354
+ - spec/support/csv_expectation_helper.rb