├── .github └── workflows │ └── rspec.yml ├── .gitignore ├── .rspec ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── csb.gemspec ├── gemfiles ├── rails71.gemfile ├── rails72.gemfile └── rails80.gemfile ├── lib ├── csb.rb └── csb │ ├── builder.rb │ ├── col.rb │ ├── cols.rb │ ├── configuration.rb │ ├── handler.rb │ ├── railtie.rb │ ├── template.rb │ ├── testing.rb │ └── version.rb └── spec ├── csb_spec.rb ├── lib └── csb │ ├── builder_spec.rb │ ├── col_spec.rb │ ├── cols_spec.rb │ ├── template_spec.rb │ └── testing_spec.rb └── spec_helper.rb /.github/workflows/rspec.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | jobs: 9 | rspec: 10 | runs-on: ubuntu-latest 11 | env: 12 | BUNDLE_JOBS: 4 13 | BUNDLE_RETRY: 3 14 | BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | ruby: ["3.1", "3.2", "3.3", "3.4"] 19 | gemfile: ["rails71", "rails72", "rails80"] 20 | exclude: 21 | # rails 8.0: support ruby 3.2+ 22 | - ruby: "3.1" 23 | gemfile: "rails80" 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Set up Ruby 28 | uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{ matrix.ruby }} 31 | bundler-cache: true 32 | 33 | - name: Run rspec 34 | run: bundle exec rspec 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | Gemfile.lock 13 | /vendor/bundle 14 | .vscode/settings.json 15 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.12.0 2 | 3 | - Drop support for Rails versions below 7.1.4 4 | - Drop support for ruby 3.0 5 | 6 | ## 0.11.0 7 | 8 | - Consider `Csb.configuration` as initial value for `Csb::Builder` 9 | 10 | ## 0.10.0 11 | 12 | - Drop support for rails 6.1 13 | 14 | ## 0.9.0 15 | 16 | - Fixing Issue with config.streaming Enabled in Rails 7.1 (https://github.com/aki77/csb/pull/17) 17 | 18 | ## 0.8.0 19 | 20 | - Add `csv_options` as an option to pass to `CSV.generate_line` (https://github.com/aki77/csb/pull/18) 21 | 22 | ## 0.7.0 23 | 24 | - Drop support for ruby 2.7 25 | - Drop support for rails 6.0 26 | 27 | ## 0.6.0 28 | 29 | - Drop support for ruby 2.6 30 | - Drop support for rails 5.1 31 | 32 | ## 0.5.1 33 | 34 | - Update dependency 35 | 36 | ## 0.5.0 37 | 38 | - Add `as_table` testing helper 39 | 40 | ## 0.4.0 41 | 42 | - Csb::Cols#initialize supports block arguments 43 | - Add Testing Helper 44 | 45 | ## 0.3.1 46 | 47 | - Refactor 48 | 49 | ## 0.3.0 50 | 51 | - Add ignore_class_names config 52 | 53 | ## 0.2.4 54 | 55 | - Clean up rack@2.2.x monkey patch. 56 | 57 | ## 0.2.3 58 | 59 | - Fix rack@2.2.x bug https://github.com/rack/rack/issues/1619 60 | 61 | ## 0.2.2 62 | 63 | - Update dependency 64 | 65 | ## 0.2.1 66 | 67 | - Update dependency 68 | 69 | ## 0.2.0 70 | 71 | - Rename 72 | - Changed API 73 | 74 | ## 0.1.1 75 | 76 | - Add after_streaming_error config 77 | 78 | ## 0.1.0 79 | 80 | - First Release 81 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at lala.akira@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in csb.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 aki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/csb.svg)](https://rubygems.org/gems/csb) 2 | [![Build](https://github.com/aki77/csb/workflows/Build/badge.svg)](https://github.com/aki77/csb/actions) 3 | 4 | # Csb 5 | 6 | A simple and streaming support CSV template engine for Ruby on Rails. 7 | 8 | ## Features 9 | 10 | - Support for streaming downloads 11 | - Output in UTF-8 with BOM 12 | - Readable code 13 | - High testability 14 | 15 | ## Usage 16 | 17 | ### Template handler 18 | 19 | In app/controllers/reports_controller.rb: 20 | 21 | ```ruby 22 | def index 23 | @reports = Report.preload(:categories) 24 | end 25 | ``` 26 | 27 | In app/views/reports/index.csv.csb: 28 | 29 | ```ruby 30 | csv.items = @reports 31 | 32 | # When there are many records 33 | # csv.items = @reports.find_each 34 | 35 | # When there are many records with decorator 36 | # csv.items = @reports.find_each.lazy.map(&:decorate) 37 | 38 | # csv.filename = "reports_#{Time.current.to_i}.csv" 39 | # csv.streaming = false 40 | # csv.csv_options = { col_sep: "\t" } 41 | 42 | csv.cols.add('Update date') { |r| l(r.updated_at.to_date) } 43 | csv.cols.add('Categories') { |r| r.categories.pluck(:name).join(' ') } 44 | csv.cols.add('Content', :content) 45 | csv.cols.add('Empty') 46 | csv.cols.add('Static', 'dummy') 47 | ``` 48 | 49 | Output: 50 | 51 | ```csv 52 | Update date,Categories,Content,Empty,Static 53 | 2019/06/01,category1 category2,content1,,dummy 54 | 2019/06/02,category3,content2,,dummy 55 | ``` 56 | 57 | ### Directly 58 | 59 | ```ruby 60 | csv = Csb::Builder.new(items: items) 61 | csv.cols.add('Update date') { |r| l(r.updated_at.to_date) } 62 | csv.cols.add('Categories') { |r| r.categories.pluck(:name).join(' ') } 63 | csv.cols.add('Content', :content) 64 | csv.cols.add('Empty') 65 | csv.cols.add('Static', 'dummy') 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 72 | ``` 73 | 74 | ### Testing 75 | 76 | ```ruby 77 | # Your view 78 | csv.items = @articles 79 | csv.cols = Article.csb_cols 80 | 81 | # Your Model 82 | def self.csb_cols 83 | Csb::Cols.new do |cols| 84 | cols.add('Update date') { |r| I18n.l(r.updated_at.to_date) } 85 | cols.add('Categories') { |r| r.categories.pluck(:name).join(' ') } 86 | cols.add('Title', :title) 87 | end 88 | end 89 | 90 | # Your test 91 | require 'csb/testing' 92 | 93 | expect(Article.csb_cols.col_pairs(article)).to eq [ 94 | ['Update date', '2020-01-01'], 95 | ['Categories', 'test rspec'], 96 | ['Title', 'Testing'], 97 | ] 98 | 99 | expect(Article.csb_cols.as_table(articles)).to eq [ 100 | ['Update date', 'Categories', 'Title'], 101 | ['2020-01-01', 'test rspec', 'Testing'], 102 | ['2020-02-01', 'rails gem', 'Rails 6.2'], 103 | ] 104 | ``` 105 | 106 | ## Installation 107 | 108 | Add this line to your application's Gemfile: 109 | 110 | ```ruby 111 | gem 'csb' 112 | ``` 113 | 114 | And then execute: 115 | 116 | ```bash 117 | $ bundle 118 | ``` 119 | 120 | Or install it yourself as: 121 | 122 | ```bash 123 | $ gem install csb 124 | ``` 125 | 126 | ## Configuration 127 | 128 | In `config/initializers/csb.rb`, you can configure the following values. 129 | 130 | ```ruby 131 | Csb.configure do |config| 132 | config.utf8_bom = true # default: false 133 | config.streaming = false # default: true 134 | config.csv_options = { col_sep: "\t" } # default: {} 135 | config.after_streaming_error = ->(error) do # default: nil 136 | Rails.logger.error(error) 137 | Bugsnag.notify(error) 138 | end 139 | end 140 | ``` 141 | 142 | ## Contributing 143 | 144 | Bug reports and pull requests are welcome on GitHub at https://github.com/aki77/csb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 145 | 146 | ## License 147 | 148 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 149 | 150 | ## Code of Conduct 151 | 152 | Everyone interacting in the Csb project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/aki77/csb/blob/master/CODE_OF_CONDUCT.md). 153 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "csb" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /csb.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("../lib", __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require "csb/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "csb" 7 | spec.version = Csb::VERSION 8 | spec.authors = ["aki77"] 9 | spec.email = ["aki77@users.noreply.github.com"] 10 | 11 | spec.summary = %q{A simple and streaming support CSV template engine for Ruby on Rails.} 12 | spec.description = %q{A simple and streaming support CSV template engine for Ruby on Rails.} 13 | spec.homepage = "https://github.com/aki77/csb" 14 | spec.license = "MIT" 15 | 16 | # Specify which files should be added to the gem when it is released. 17 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 18 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 19 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 20 | end 21 | spec.bindir = "exe" 22 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 23 | spec.require_paths = ["lib"] 24 | 25 | spec.required_ruby_version = '>= 3.1.0' 26 | 27 | spec.add_dependency "rails", ">= 7.1.4" 28 | spec.add_dependency "csv" 29 | 30 | spec.add_development_dependency "bundler", "~> 2.0" 31 | spec.add_development_dependency "rake" 32 | spec.add_development_dependency "rspec" 33 | spec.add_development_dependency "ostruct" 34 | end 35 | -------------------------------------------------------------------------------- /gemfiles/rails71.gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'rails', '~> 7.1.4' 4 | 5 | gemspec path: '../' 6 | -------------------------------------------------------------------------------- /gemfiles/rails72.gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'rails', '~> 7.2.0' 4 | 5 | gemspec path: '../' 6 | -------------------------------------------------------------------------------- /gemfiles/rails80.gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gem 'rails', '~> 8.0.0' 4 | 5 | gemspec path: '../' 6 | -------------------------------------------------------------------------------- /lib/csb.rb: -------------------------------------------------------------------------------- 1 | require 'csb/version' 2 | require 'csb/railtie' 3 | require 'csb/configuration' 4 | require 'csb/builder' 5 | require 'csb/cols' 6 | 7 | module Csb 8 | class << self 9 | def configure 10 | yield(configuration) 11 | end 12 | 13 | def configuration 14 | @configuration ||= Configuration.new 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/csb/builder.rb: -------------------------------------------------------------------------------- 1 | require 'csv' 2 | require 'csb/cols' 3 | 4 | module Csb 5 | class Builder 6 | UTF8_BOM = "\xEF\xBB\xBF".freeze 7 | 8 | attr_reader :output, :utf8_bom, :items, :cols, :csv_options 9 | attr_accessor :items 10 | 11 | def initialize(output = '', items: [], **kwargs) 12 | @output = output 13 | @cols = Cols.new 14 | @items = items 15 | @utf8_bom = kwargs.fetch(:utf8_bom) { Csb.configuration.utf8_bom } 16 | @csv_options = kwargs.fetch(:csv_options) { Csb.configuration.csv_options } 17 | end 18 | 19 | def build 20 | output << UTF8_BOM if utf8_bom 21 | output << CSV.generate_line(cols.headers, **csv_options) 22 | items.each do |item| 23 | output << CSV.generate_line(cols.values_by_item(item), **csv_options) 24 | rescue => error 25 | break if Csb.configuration.ignore_class_names.include?(error.class.name) 26 | 27 | raise error 28 | end 29 | output 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/csb/col.rb: -------------------------------------------------------------------------------- 1 | module Csb 2 | class Col 3 | attr_reader :name 4 | 5 | def initialize(name, value = nil, &block) 6 | @name = name 7 | @value = block ? block : value 8 | end 9 | 10 | def value_by_item(item) 11 | case value 12 | when ::Symbol 13 | item.public_send(value) 14 | when ::Proc 15 | value.call(item) 16 | else 17 | value 18 | end 19 | end 20 | 21 | private 22 | 23 | attr_reader :value 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/csb/cols.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/object/deep_dup' 2 | require 'csb/col' 3 | 4 | module Csb 5 | class Cols 6 | include Enumerable 7 | 8 | attr_reader :cols 9 | 10 | def initialize 11 | @cols = [] 12 | yield(self) if block_given? 13 | end 14 | 15 | def copy!(other) 16 | @cols = other.cols.deep_dup 17 | end 18 | 19 | def each(&block) 20 | cols.each(&block) 21 | end 22 | 23 | def add(*args, &block) 24 | cols << Col.new(*args, &block) 25 | end 26 | 27 | def headers 28 | map(&:name) 29 | end 30 | 31 | def values_by_item(item) 32 | map do |col| 33 | col.value_by_item(item) 34 | end 35 | end 36 | alias_method :values, :values_by_item 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/csb/configuration.rb: -------------------------------------------------------------------------------- 1 | module Csb 2 | class Configuration 3 | attr_accessor :utf8_bom, :streaming, :after_streaming_error, :ignore_class_names, :csv_options 4 | 5 | def initialize 6 | @utf8_bom = false 7 | @streaming = true 8 | @csv_options = {} 9 | @ignore_class_names = %w[Puma::ConnectionError] 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/csb/handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'csb/template' 4 | 5 | module Csb 6 | class Handler 7 | class_attribute :default_format 8 | self.default_format = :csv 9 | 10 | def self.call(template, source = nil) 11 | source ||= template.source 12 | 13 | <<~RUBY 14 | csv = ::Csb::Template.new( 15 | streaming: ::Csb.configuration.streaming, 16 | ) 17 | #{source} 18 | controller.send(:send_file_headers!, type: 'text/csv', filename: csv.filename) 19 | if csv.streaming? 20 | response.headers['Cache-Control'] = 'no-cache' 21 | response.headers['X-Accel-Buffering'] = 'no' 22 | # SEE: https://github.com/rack/rack/issues/1619 23 | if Gem::Version.new('2.2.0') <= Gem::Version.new(Rack::RELEASE) 24 | response.headers['Last-Modified'] = '0' 25 | end 26 | end 27 | csv.build 28 | RUBY 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/csb/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'rails/railtie' 2 | 3 | module Csb 4 | class Railtie < Rails::Railtie 5 | initializer :csb do 6 | ActiveSupport.on_load :action_view do 7 | require 'csb/handler' 8 | ActionView::Template.register_template_handler :csb, Csb::Handler 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/csb/template.rb: -------------------------------------------------------------------------------- 1 | module Csb 2 | class Template 3 | attr_accessor :utf8_bom, :filename, :streaming, :items, :cols, :csv_options 4 | 5 | def initialize(streaming:) 6 | @streaming = streaming 7 | @cols = Cols.new 8 | @items = [] 9 | end 10 | 11 | def build 12 | streaming ? build_enumerator : build_string 13 | end 14 | 15 | def streaming? 16 | !!streaming 17 | end 18 | 19 | private 20 | 21 | def builder_options 22 | { 23 | items: items, 24 | utf8_bom: utf8_bom, 25 | csv_options: csv_options, 26 | }.compact 27 | end 28 | 29 | def build_string 30 | builder = Builder.new(**builder_options) 31 | builder.cols.copy!(cols) 32 | builder.build 33 | end 34 | 35 | def build_enumerator 36 | Enumerator.new do |y| 37 | begin 38 | builder = Builder.new(y, **builder_options) 39 | builder.cols.copy!(cols) 40 | builder.build 41 | rescue => error 42 | if Csb.configuration.after_streaming_error.respond_to?(:call) 43 | Csb.configuration.after_streaming_error.call(error) 44 | end 45 | raise error 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/csb/testing.rb: -------------------------------------------------------------------------------- 1 | module Csb 2 | class Cols 3 | def col_pairs(item) 4 | headers.zip(values(item)) 5 | end 6 | 7 | def as_table(items) 8 | [headers] + items.map { |item| values(item) } 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/csb/version.rb: -------------------------------------------------------------------------------- 1 | module Csb 2 | VERSION = '0.13.0' 3 | end 4 | -------------------------------------------------------------------------------- /spec/csb_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Csb do 2 | it "has a version number" do 3 | expect(Csb::VERSION).not_to be nil 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/lib/csb/builder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | RSpec.describe Csb::Builder do 4 | describe '#build' do 5 | subject { builder.build } 6 | 7 | let(:items) do 8 | [ 9 | OpenStruct.new(name: 'tester1', email: 'dummy1@dummy.test'), 10 | OpenStruct.new(name: 'tester2', email: 'dummy2@dummy.test') 11 | ] 12 | end 13 | 14 | before do 15 | builder.cols.add('Name') { |item| item.name } 16 | builder.cols.add('Email', :email) 17 | builder.cols.add('Dummy') 18 | end 19 | 20 | context 'default' do 21 | let(:builder) { Csb::Builder.new(items: items) } 22 | 23 | it { is_expected.to eq "Name,Email,Dummy\ntester1,dummy1@dummy.test,\ntester2,dummy2@dummy.test,\n" } 24 | end 25 | 26 | context 'with utf8 bom' do 27 | let(:builder) { Csb::Builder.new(items: items, utf8_bom: true) } 28 | 29 | it { is_expected.to eq "\xEF\xBB\xBFName,Email,Dummy\ntester1,dummy1@dummy.test,\ntester2,dummy2@dummy.test,\n" } 30 | end 31 | 32 | context 'with csv_options' do 33 | let(:builder) { Csb::Builder.new(items: items, csv_options: { col_sep: "\t" }) } 34 | 35 | it { is_expected.to eq "Name\tEmail\tDummy\ntester1\tdummy1@dummy.test\t\ntester2\tdummy2@dummy.test\t\n" } 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/lib/csb/col_spec.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | RSpec.describe Csb::Col do 4 | describe '#value_by_item' do 5 | subject { col.value_by_item(item) } 6 | 7 | let(:item) { OpenStruct.new(name: 'tester', email: 'dummy@dummy.test') } 8 | 9 | context 'block' do 10 | let(:col) { Csb::Col.new('Name') { |item| item.name } } 11 | 12 | it { is_expected.to eq 'tester' } 13 | end 14 | 15 | context 'symbol' do 16 | let(:col) { Csb::Col.new('Name', :name) } 17 | 18 | it { is_expected.to eq 'tester' } 19 | end 20 | 21 | context 'string' do 22 | let(:col) { Csb::Col.new('Name', 'dummy') } 23 | 24 | it { is_expected.to eq 'dummy' } 25 | end 26 | 27 | context 'nil' do 28 | let(:col) { Csb::Col.new('Name') } 29 | 30 | it { is_expected.to eq nil } 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/lib/csb/cols_spec.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | RSpec.describe Csb::Cols do 4 | let(:cols) { Csb::Cols.new } 5 | 6 | before do 7 | cols.add('Name') { |item| item.name } 8 | cols.add('Email', :email) 9 | cols.add('Dummy') 10 | end 11 | 12 | describe '#headers' do 13 | subject { cols.headers } 14 | 15 | it { is_expected.to eq %w[Name Email Dummy] } 16 | end 17 | 18 | describe '#values_by_item' do 19 | subject { cols.values_by_item(item) } 20 | 21 | let(:item) { OpenStruct.new(name: 'tester', email: 'dummy@dummy.test') } 22 | 23 | it { is_expected.to eq ['tester', 'dummy@dummy.test', nil] } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/lib/csb/template_spec.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'csb/template' 3 | 4 | RSpec.describe Csb::Template do 5 | describe '#build' do 6 | before do 7 | template.items = [ 8 | OpenStruct.new(name: 'tester1', email: 'dummy1@dummy.test'), 9 | OpenStruct.new(name: 'tester2', email: 'dummy2@dummy.test') 10 | ] 11 | template.cols.add('Name') { |item| item.name } 12 | template.cols.add('Email', :email) 13 | template.cols.add('Dummy') 14 | end 15 | 16 | context 'Streaming' do 17 | subject(:enum) { template.build } 18 | 19 | let(:template) { Csb::Template.new(streaming: true) } 20 | 21 | it 'Is a Enumerator' do 22 | expect(enum).to be_a Enumerator 23 | expect(enum.next).to eq "Name,Email,Dummy\n" 24 | expect(enum.next).to eq "tester1,dummy1@dummy.test,\n" 25 | expect(enum.next).to eq "tester2,dummy2@dummy.test,\n" 26 | end 27 | end 28 | 29 | context 'Not streaming' do 30 | subject { template.build } 31 | 32 | let(:template) { Csb::Template.new(streaming: false) } 33 | 34 | it { is_expected.to eq "Name,Email,Dummy\ntester1,dummy1@dummy.test,\ntester2,dummy2@dummy.test,\n" } 35 | end 36 | 37 | context 'with csv_options' do 38 | subject { template.build } 39 | 40 | let(:template) do 41 | template = Csb::Template.new(streaming: false) 42 | template.csv_options = { row_sep: "\r\n" } 43 | template 44 | end 45 | 46 | it { is_expected.to eq "Name,Email,Dummy\r\ntester1,dummy1@dummy.test,\r\ntester2,dummy2@dummy.test,\r\n" } 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/lib/csb/testing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'csb/testing' 3 | 4 | RSpec.describe Csb::Cols do 5 | let(:cols) { Csb::Cols.new } 6 | 7 | before do 8 | cols.add('Name') { |item| item.name } 9 | cols.add('Email', :email) 10 | cols.add('Dummy') 11 | end 12 | 13 | describe '#col_pairs' do 14 | subject { cols.col_pairs(item) } 15 | 16 | let(:item) { OpenStruct.new(name: 'tester', email: 'dummy@dummy.test') } 17 | 18 | it { is_expected.to eq [%w[Name tester], %w[Email dummy@dummy.test], ['Dummy', nil]] } 19 | end 20 | 21 | describe '#as_table' do 22 | subject { cols.as_table(items) } 23 | 24 | let(:items) do 25 | [ 26 | OpenStruct.new(name: 'tester1', email: 'dummy1@dummy.test'), 27 | OpenStruct.new(name: 'tester2', email: 'dummy2@dummy.test') 28 | ] 29 | end 30 | 31 | it { is_expected.to eq [%w[Name Email Dummy], ['tester1', 'dummy1@dummy.test', nil], ['tester2', 'dummy2@dummy.test', nil]] } 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "csb" 3 | 4 | RSpec.configure do |config| 5 | # Enable flags like --only-failures and --next-failure 6 | config.example_status_persistence_file_path = ".rspec_status" 7 | 8 | # Disable RSpec exposing methods globally on `Module` and `main` 9 | config.disable_monkey_patching! 10 | 11 | config.expect_with :rspec do |c| 12 | c.syntax = :expect 13 | end 14 | end 15 | --------------------------------------------------------------------------------