csb 0.14.0 → 0.16.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/.github/workflows/rspec.yml +3 -7
- data/CHANGELOG.md +4 -0
- data/README.md +64 -29
- data/csb.gemspec +7 -4
- data/gemfiles/rails72.gemfile +1 -1
- data/gemfiles/rails80.gemfile +1 -1
- data/gemfiles/rails81.gemfile +5 -0
- data/lib/csb/railtie.rb +1 -1
- data/lib/csb/version.rb +1 -1
- metadata +11 -22
- data/gemfiles/rails71.gemfile +0 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dd0789e534f43fca7c3b34921ca5aa2c151a8385237a4f1509222a3bcf76852f
|
|
4
|
+
data.tar.gz: cce9f025f3fdaba91ea2e201585ac914d81191d99100eb971ba4266dd8e46111
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 160a693da16312599fe0f905be34c5086e5a54dc85234ac46fe5e758dd505d550830048aa98f3b9c31d0bd99d5827fd2affe1db8cd3a6cfa859f3d93fdca37f9
|
|
7
|
+
data.tar.gz: d9d703f251f3c29009c94002af15fcd6a0a6df80e268d241d9227c6ca8ef1e7a25f77a954e8c42a4fc26ecc91c2a199755aa552b178fd10384d2843503186e3c
|
data/.github/workflows/rspec.yml
CHANGED
|
@@ -16,14 +16,10 @@ jobs:
|
|
|
16
16
|
strategy:
|
|
17
17
|
fail-fast: false
|
|
18
18
|
matrix:
|
|
19
|
-
ruby: ["3.
|
|
20
|
-
gemfile: ["
|
|
21
|
-
exclude:
|
|
22
|
-
# rails 8.0: support ruby 3.2+
|
|
23
|
-
- ruby: "3.1"
|
|
24
|
-
gemfile: "rails80"
|
|
19
|
+
ruby: ["3.3", "3.4", "4.0"]
|
|
20
|
+
gemfile: ["rails72", "rails80", "rails81"]
|
|
25
21
|
steps:
|
|
26
|
-
- uses: actions/checkout@
|
|
22
|
+
- uses: actions/checkout@v6
|
|
27
23
|
|
|
28
24
|
- name: Set up Ruby
|
|
29
25
|
uses: ruby/setup-ruby@v1
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -3,20 +3,39 @@
|
|
|
3
3
|
|
|
4
4
|
# Csb
|
|
5
5
|
|
|
6
|
-
A simple
|
|
6
|
+
A simple, streaming CSV template engine for Ruby on Rails. (The name is short for **CSV builder**.)
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Why csb?
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
10
|
+
Writing CSV downloads in Rails by hand looks easy, but the naive approach has recurring problems:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
# app/views/posts/index.csv.erb (the typical hand-written version)
|
|
14
|
+
CSV.generate do |csv|
|
|
15
|
+
csv << %w[Date Category Title Content]
|
|
16
|
+
@posts.each do |post|
|
|
17
|
+
csv << [l(post.created_at.to_date), post.category.name, post.title, post.content]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- **Garbled in Excel** — UTF-8 without a BOM shows up as mojibake.
|
|
23
|
+
- **Memory / timeout errors** — loading and building the whole CSV in memory breaks on large datasets.
|
|
24
|
+
- **Hard to maintain** — headers and values are defined far apart, so adding columns hurts readability.
|
|
25
|
+
- **Hard to test** — the export logic is buried in a view, leaving you stuck with slow system tests.
|
|
26
|
+
|
|
27
|
+
csb solves each of these:
|
|
28
|
+
|
|
29
|
+
- **Excel-friendly** — output UTF-8 with a BOM so Excel opens it without garbling.
|
|
30
|
+
- **Streaming download** — stream row by row to handle hundreds of thousands of records without memory or timeout errors.
|
|
31
|
+
- **Readable** — define each column's header and value together on one line.
|
|
32
|
+
- **Testable** — extract column definitions into a model and unit-test them directly.
|
|
14
33
|
|
|
15
34
|
## Usage
|
|
16
35
|
|
|
17
36
|
### Template handler
|
|
18
37
|
|
|
19
|
-
In app/controllers/reports_controller.rb
|
|
38
|
+
In `app/controllers/reports_controller.rb`:
|
|
20
39
|
|
|
21
40
|
```ruby
|
|
22
41
|
def index
|
|
@@ -24,61 +43,66 @@ def index
|
|
|
24
43
|
end
|
|
25
44
|
```
|
|
26
45
|
|
|
27
|
-
In app/views/reports/index.csv.csb
|
|
46
|
+
In `app/views/reports/index.csv.csb`:
|
|
28
47
|
|
|
29
48
|
```ruby
|
|
30
49
|
csv.items = @reports
|
|
31
50
|
|
|
32
|
-
#
|
|
51
|
+
# For large datasets, pass an Enumerator so streaming starts immediately
|
|
52
|
+
# instead of waiting for every record to load:
|
|
33
53
|
# csv.items = @reports.find_each
|
|
34
54
|
|
|
35
|
-
#
|
|
55
|
+
# Combine with a decorator (e.g. Draper) while keeping it lazy:
|
|
36
56
|
# csv.items = @reports.find_each.lazy.map(&:decorate)
|
|
37
57
|
|
|
58
|
+
# Optional per-view overrides:
|
|
38
59
|
# csv.filename = "reports_#{Time.current.to_i}.csv"
|
|
39
60
|
# csv.streaming = false
|
|
40
61
|
# csv.csv_options = { col_sep: "\t" }
|
|
41
62
|
|
|
42
|
-
csv.cols.add('Update date') { |r| l(r.updated_at.to_date) }
|
|
63
|
+
csv.cols.add('Update date') { |r| l(r.updated_at.to_date) } # block receives the record
|
|
43
64
|
csv.cols.add('Categories') { |r| r.categories.pluck(:name).join(' ') }
|
|
44
|
-
csv.cols.add('Content', :content)
|
|
45
|
-
csv.cols.add('
|
|
46
|
-
csv.cols.add('
|
|
65
|
+
csv.cols.add('Content', :content) # a Symbol calls the method on the record
|
|
66
|
+
csv.cols.add('Static', 'dummy') # a String is output verbatim
|
|
67
|
+
csv.cols.add('Empty') # no value -> empty column
|
|
68
|
+
csv.cols.add('Dup', :col1) # the same header may be added more than once
|
|
69
|
+
csv.cols.add('Dup', :col2) # (columns are output in definition order)
|
|
47
70
|
```
|
|
48
71
|
|
|
49
72
|
Output:
|
|
50
73
|
|
|
51
74
|
```csv
|
|
52
|
-
Update date,Categories,Content,Empty,
|
|
53
|
-
2019/06/01,category1 category2,content1,,
|
|
54
|
-
2019/06/02,category3,content2,,
|
|
75
|
+
Update date,Categories,Content,Static,Empty,Dup,Dup
|
|
76
|
+
2019/06/01,category1 category2,content1,dummy,,a,b
|
|
77
|
+
2019/06/02,category3,content2,dummy,,c,d
|
|
55
78
|
```
|
|
56
79
|
|
|
80
|
+
A link such as `link_to 'Download CSV', reports_path(format: :csv)` triggers the streaming download automatically.
|
|
81
|
+
|
|
57
82
|
### Directly
|
|
58
83
|
|
|
84
|
+
When you want to generate the CSV outside of a request (e.g. in a background job), use `Csb::Builder`:
|
|
85
|
+
|
|
59
86
|
```ruby
|
|
60
87
|
csv = Csb::Builder.new(items: items)
|
|
61
88
|
csv.cols.add('Update date') { |r| l(r.updated_at.to_date) }
|
|
62
89
|
csv.cols.add('Categories') { |r| r.categories.pluck(:name).join(' ') }
|
|
63
90
|
csv.cols.add('Content', :content)
|
|
64
|
-
csv.
|
|
65
|
-
|
|
66
|
-
csv.build
|
|
67
|
-
|
|
68
|
-
# =>
|
|
69
|
-
# Update date,Categories,Content,Empty,Static
|
|
70
|
-
# 2019/06/01,category1 category2,content1,,dummy
|
|
71
|
-
# 2019/06/02,category3,content2,,dummy
|
|
91
|
+
csv.build # => returns the CSV string
|
|
92
|
+
|
|
93
|
+
# File.write('reports.csv', csv.build)
|
|
72
94
|
```
|
|
73
95
|
|
|
74
96
|
### Testing
|
|
75
97
|
|
|
98
|
+
Move the column definitions out of the view so you can unit-test them:
|
|
99
|
+
|
|
76
100
|
```ruby
|
|
77
|
-
#
|
|
101
|
+
# app/views/articles/index.csv.csb
|
|
78
102
|
csv.items = @articles
|
|
79
103
|
csv.cols = Article.csb_cols
|
|
80
104
|
|
|
81
|
-
#
|
|
105
|
+
# app/models/article.rb
|
|
82
106
|
def self.csb_cols
|
|
83
107
|
Csb::Cols.new do |cols|
|
|
84
108
|
cols.add('Update date') { |r| I18n.l(r.updated_at.to_date) }
|
|
@@ -86,16 +110,20 @@ def self.csb_cols
|
|
|
86
110
|
cols.add('Title', :title)
|
|
87
111
|
end
|
|
88
112
|
end
|
|
113
|
+
```
|
|
89
114
|
|
|
90
|
-
|
|
91
|
-
|
|
115
|
+
```ruby
|
|
116
|
+
# spec/models/article_spec.rb
|
|
117
|
+
require 'csb/testing' # adds col_pairs and as_table
|
|
92
118
|
|
|
119
|
+
# Assert a single record, row by row:
|
|
93
120
|
expect(Article.csb_cols.col_pairs(article)).to eq [
|
|
94
121
|
['Update date', '2020-01-01'],
|
|
95
122
|
['Categories', 'test rspec'],
|
|
96
123
|
['Title', 'Testing'],
|
|
97
124
|
]
|
|
98
125
|
|
|
126
|
+
# Assert the whole table (header row + value rows):
|
|
99
127
|
expect(Article.csb_cols.as_table(articles)).to eq [
|
|
100
128
|
['Update date', 'Categories', 'Title'],
|
|
101
129
|
['2020-01-01', 'test rspec', 'Testing'],
|
|
@@ -132,10 +160,17 @@ Csb.configure do |config|
|
|
|
132
160
|
config.utf8_bom = true # default: false
|
|
133
161
|
config.streaming = false # default: true
|
|
134
162
|
config.csv_options = { col_sep: "\t" } # default: {}
|
|
163
|
+
|
|
164
|
+
# Called when an error is raised during streaming. Without this, errors that
|
|
165
|
+
# happen mid-stream are not reported even if you use a tool like Bugsnag.
|
|
135
166
|
config.after_streaming_error = ->(error) do # default: nil
|
|
136
167
|
Rails.logger.error(error)
|
|
137
168
|
Bugsnag.notify(error)
|
|
138
169
|
end
|
|
170
|
+
|
|
171
|
+
# Error classes to ignore (not re-raise) during streaming, e.g. when the
|
|
172
|
+
# client disconnects before the download finishes.
|
|
173
|
+
config.ignore_class_names = %w[Puma::ConnectionError] # default: %w[Puma::ConnectionError]
|
|
139
174
|
end
|
|
140
175
|
```
|
|
141
176
|
|
data/csb.gemspec
CHANGED
|
@@ -13,21 +13,24 @@ Gem::Specification.new do |spec|
|
|
|
13
13
|
spec.homepage = "https://github.com/aki77/csb"
|
|
14
14
|
spec.license = "MIT"
|
|
15
15
|
|
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
17
|
+
spec.metadata["source_code_uri"] = "https://github.com/aki77/csb"
|
|
18
|
+
spec.metadata["changelog_uri"] = "https://github.com/aki77/csb/releases"
|
|
19
|
+
|
|
16
20
|
# Specify which files should be added to the gem when it is released.
|
|
17
21
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
18
22
|
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
|
19
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
23
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|\.claude)/}) }
|
|
20
24
|
end
|
|
21
25
|
spec.bindir = "exe"
|
|
22
26
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
23
27
|
spec.require_paths = ["lib"]
|
|
24
28
|
|
|
25
|
-
spec.required_ruby_version = '>= 3.
|
|
29
|
+
spec.required_ruby_version = '>= 3.3.0'
|
|
26
30
|
|
|
27
|
-
spec.add_dependency "rails", ">= 7.
|
|
31
|
+
spec.add_dependency "rails", ">= 7.2.0"
|
|
28
32
|
spec.add_dependency "csv"
|
|
29
33
|
|
|
30
|
-
spec.add_development_dependency "bundler", "~> 2.0"
|
|
31
34
|
spec.add_development_dependency "rake"
|
|
32
35
|
spec.add_development_dependency "rspec"
|
|
33
36
|
spec.add_development_dependency "ostruct"
|
data/gemfiles/rails72.gemfile
CHANGED
data/gemfiles/rails80.gemfile
CHANGED
data/lib/csb/railtie.rb
CHANGED
data/lib/csb/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: csb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- aki77
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rails
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 7.
|
|
18
|
+
version: 7.2.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 7.
|
|
25
|
+
version: 7.2.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: csv
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -37,20 +37,6 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: bundler
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - "~>"
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '2.0'
|
|
47
|
-
type: :development
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - "~>"
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '2.0'
|
|
54
40
|
- !ruby/object:Gem::Dependency
|
|
55
41
|
name: rake
|
|
56
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -112,9 +98,9 @@ files:
|
|
|
112
98
|
- bin/console
|
|
113
99
|
- bin/setup
|
|
114
100
|
- csb.gemspec
|
|
115
|
-
- gemfiles/rails71.gemfile
|
|
116
101
|
- gemfiles/rails72.gemfile
|
|
117
102
|
- gemfiles/rails80.gemfile
|
|
103
|
+
- gemfiles/rails81.gemfile
|
|
118
104
|
- lib/csb.rb
|
|
119
105
|
- lib/csb/builder.rb
|
|
120
106
|
- lib/csb/col.rb
|
|
@@ -128,7 +114,10 @@ files:
|
|
|
128
114
|
homepage: https://github.com/aki77/csb
|
|
129
115
|
licenses:
|
|
130
116
|
- MIT
|
|
131
|
-
metadata:
|
|
117
|
+
metadata:
|
|
118
|
+
homepage_uri: https://github.com/aki77/csb
|
|
119
|
+
source_code_uri: https://github.com/aki77/csb
|
|
120
|
+
changelog_uri: https://github.com/aki77/csb/releases
|
|
132
121
|
rdoc_options: []
|
|
133
122
|
require_paths:
|
|
134
123
|
- lib
|
|
@@ -136,14 +125,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
136
125
|
requirements:
|
|
137
126
|
- - ">="
|
|
138
127
|
- !ruby/object:Gem::Version
|
|
139
|
-
version: 3.
|
|
128
|
+
version: 3.3.0
|
|
140
129
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
130
|
requirements:
|
|
142
131
|
- - ">="
|
|
143
132
|
- !ruby/object:Gem::Version
|
|
144
133
|
version: '0'
|
|
145
134
|
requirements: []
|
|
146
|
-
rubygems_version:
|
|
135
|
+
rubygems_version: 4.0.10
|
|
147
136
|
specification_version: 4
|
|
148
137
|
summary: A simple and streaming support CSV template engine for Ruby on Rails.
|
|
149
138
|
test_files: []
|