Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Changelog

All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- `CHANGELOG.md`.
- Full `README.md`: overview, requirements, installation, DSL usage, development and release flow.
- CLAUDE.md rule: every PR must also update `README.md` and `CHANGELOG.md`.

## [1.2.1] - 2026-10-27

### Added

- Support for Rails 8.0 / 8.1 and Ruby 3.4 (`activesupport` is now allowed up to `< 8.2`; the `csv` gem is an explicit dependency; the `required_ruby_version` upper bound is removed; new `ruby_3.3_rails_8.*` / `ruby_3.4_rails_8.*` gemfiles and CI jobs).
- GitHub Actions `publish.yml`: a new version on `master` is published to RubyGems through Trusted Publishing.
- CLAUDE.md release rules.

### Changed

- Removed stale per-gemfile lockfiles that broke older Rubies in CI.

## [1.2.0] - 2026-01-08

### Added

- Ruby 3.2 and 3.3 test gemfiles (`ruby_3.2_rails_7.0`, `ruby_3.2_rails_7.1`, `ruby_3.3_rails_7.1`) and CI jobs.

### Changed

- Gemspec `required_ruby_version` upper bound raised from `< 3.2.0` to `< 3.4.0`.

## [1.1.2] - 2026-01-07

### Changed

- Updated gemfile lockfiles.

## [1.1.1] - 2026-01-07

### Added

- Multi-version Ruby / Rails test setup: per-version gemfiles, GitHub Actions matrix, `bin/test_all`, `bin/check_dependencies` and `TESTING.md`.

### Fixed

- A test no longer depends on a hard-coded macOS path.

## [1.1.0] - 2023-02-02

### Changed

- XLSX exporter fixes and tweaks (`ExportTo::Exporter::Xlsx`).

## [1.0.0] - 2023-02-02

### Changed

- Rewrite of the internals: export logic moved into `ExportTo::Exporter::Csv` / `Xls` / `Xlsx`; `ExportTo::Base` exposes `to_csv`, `to_xlsx` and `to_xls`.
- `activesupport` is now constrained to `>= 6, < 7`; development dependencies relaxed to `bundler >= 2.0`, `rake >= 13`.

## [0.1.2] - 2019-06-19

### Changed

- CSV output now force-quotes every field.

## [0.1.1] - 2018-12-05

### Fixed

- "File exists" bug when exporting.

## [0.1.0] - 2018-10-21

### Added

- Initial release: `ExportTo::Base` DSL (`set`, `joins`, `presenter`, `each_with`), presenters with row counters, empty join relations, `to_xlsx`.

[Unreleased]: https://github.com/superlanding/export_to/compare/v1.2.1...HEAD
[1.2.1]: https://github.com/superlanding/export_to/compare/v1.2.0...v1.2.1
[1.2.0]: https://github.com/superlanding/export_to/compare/v1.1.2...v1.2.0
[1.1.2]: https://github.com/superlanding/export_to/compare/v1.1.1...v1.1.2
[1.1.1]: https://github.com/superlanding/export_to/releases/tag/v1.1.1
[1.1.0]: https://github.com/superlanding/export_to/commit/48957da
[1.0.0]: https://github.com/superlanding/export_to/commit/8d7f937
[0.1.2]: https://github.com/superlanding/export_to/commit/d276482
[0.1.1]: https://github.com/superlanding/export_to/commit/12dd5c1
[0.1.0]: https://github.com/superlanding/export_to/commit/d605bf9
11 changes: 11 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,14 @@
- 跳版規則(Semantic Versioning):修 bug 跳 patch(x.y.Z);新增相容功能跳 minor(x.Y.0);不相容變更跳 major(X.0.0)。
- 版號只改在 PR 裡。AI 帳號只能開 branch 與 PR,不得合併到 master;只有真人帳號可以合併。合併就等於批准發佈。
- 不得為了讓測試通過而改變程式原本的行為。

## 文件與更新紀錄(README / CHANGELOG)

- **每一個 PR 都必須同步更新文件**:
- 只要行為、用法、設定、相依套件或支援的 Ruby / Rails 版本有變,就要更新 `README.md`。
- 一律要在 `CHANGELOG.md` 新增一筆條目([Keep a Changelog](https://keepachangelog.com/zh-TW/1.1.0/) 格式,分類用 Added / Changed / Deprecated / Removed / Fixed / Security)。
- `CHANGELOG.md` 的寫法:
- 沒有跳版號的 PR:寫在最上方的 `## [Unreleased]` 底下。
- 有跳版號的 PR:把條目寫在新的版本章節 `## [x.y.z] - YYYY-MM-DD`(日期為合併日),並同步更新檔尾的版本比較連結。
- 版本號必須與 rubygems.org 上實際發佈的版本一致,不得寫還沒發佈的版本。
- 只改文件、CI 或 CLAUDE.md 的 PR 不需要跳版號,但仍要在 `CHANGELOG.md` 的 `Unreleased` 補一筆。
139 changes: 115 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,150 @@
# ExportTo

Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/export_to`. To experiment with that code, run `bin/console` for an interactive prompt.
[![Gem Version](https://badge.fury.io/rb/export_to.svg)](https://rubygems.org/gems/export_to)
[![CI](https://github.com/superlanding/export_to/actions/workflows/ci.yml/badge.svg)](https://github.com/superlanding/export_to/actions/workflows/ci.yml)

TODO: Delete this and the text above, and describe your gem
Export a collection of records (ActiveRecord relations, arrays of Structs, anything that responds to `each`) to **CSV**, **XLSX** or **XLS** with a small declarative DSL.

You describe the columns once (title, source attribute, width, format, value transform) in an `ExportTo::Base` subclass, optionally add a presenter for computed columns and a `joins` relation that expands one record into several rows, then call `to_csv` / `to_xlsx` / `to_xls`.

## Requirements

| Ruby | Rails / ActiveSupport tested in CI |
|------|------------------------------------|
| 2.7 | 6.1 |
| 3.0 | 6.1, 7.0 |
| 3.1 | 6.1, 7.0, 7.1 |
| 3.2 | 7.0, 7.1 |
| 3.3 | 7.1, 8.0, 8.1 |
| 3.4 | 8.0, 8.1 |

The gemspec requires Ruby `>= 2.7.0` and `activesupport >= 6.0, < 8.2`. The source of truth is [`.github/workflows/ci.yml`](.github/workflows/ci.yml).

Runtime dependencies: `fast_excel` (XLSX), `spreadsheet` (XLS), `csv`, `iconv`, `activesupport`.

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'export_to'
gem "export_to"
```

And then execute:

$ bundle

Or install it yourself as:
```bash
bundle install
```

$ gem install export_to
Or install it yourself with `gem install export_to`.

## Usage

TODO: Write usage instructions here
### 1. Define an exporter

## Ruby & Rails Version Support
```ruby
class UserExport < ExportTo::Base
# set <header title>, <key>, <column options>, &optional_transform
set "Name", :full_name, width: 30
set "Mobile", :mobile, width: 20, format: { bold: true }
set "Amount", :amount, width: 12 do |value|
value + 100 # the block receives the cell value and returns the final one
end
set "Source", "web" # a String key is written as a fixed value in every row

excel worksheet: "Users", height: 24 # optional XLSX settings (worksheet name, row height)

presenter do # optional: computed columns
def full_name
"#{first_name} #{last_name}" # unknown methods are delegated to the record
end

def amount
relation? ? relation.amount : 0
end
end

joins :wallets # optional: one row per record in record.wallets
end
```

This gem supports:
- Ruby 2.7 with Rails 6.1
- Ruby 3.0 with Rails 6.1, 7.0
- Ruby 3.1 with Rails 6.1, 7.0, 7.1
- Ruby 3.2 with Rails 7.0, 7.1
- Ruby 3.3 with Rails 7.1
- **`set(title, key, options = {}, &block)`**: one column per call, in order.
- `key` as a **Symbol** is read from the presenter (which delegates to the record); `nil` becomes `""`.
- `key` as a **String** is used literally.
- XLSX `options`: `width:` (column width) and `format:` (a `FastExcel` format hash, e.g. `{ bold: true }`).
- The optional block post-processes the cell value.
- **`excel(worksheet: "Name", height: 20)`**: worksheet name (default `"Default"`) and data row height (default `20`). The header row is bold, centered and has a background color.
- **`presenter { ... }`**: defines `YourExport::Presenter` (a subclass of the parent exporter's presenter). Inside it you can use `model`, `relation`, `relation?`, `x` (record index) and `y` (index inside the `joins` relation).
- **`joins(:relation_name)`**: when `record.relation_name` is present, every element becomes its own row and `relation` is that element; the presenter first looks for a method on `relation`, then on `model`. Records with an empty relation produce a single row (`relation?` is `false`).
- **`each_with { |presenter| ... }`**: a hook called with each row's presenter before the columns are read.
- **`self.each_method = :find_in_batches`**: iterate with `records.find_in_batches` instead of `records.each`, for large ActiveRecord relations.

## Development
### 2. Export

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
```ruby
users = User.includes(:wallets).where(active: true) # or any enumerable
export = UserExport.new(users)

### Testing Multiple Ruby Versions
export.to_csv # => String, every field quoted ("Name","Mobile",...)
export.to_xlsx # => binary String of an .xlsx file
```

To test against all supported Ruby versions at once:
Example CSV output for two users (the second has two wallets):

```
"Name","Mobile","Amount","Source"
"Eddie Li","+8869","100","web"
"Vegeta Lu","+8868","1100","web"
"Vegeta Lu","+8868","2100","web"
```

Serving it from a Rails controller:

```ruby
def export
send_data UserExport.new(User.all).to_xlsx,
filename: "users.xlsx",
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
end
```

### Known limitations

These describe the current behavior of the code:

- `ExportTo::Base#to_xls` currently delegates to the **XLSX** exporter, so it returns xlsx data. For a real legacy `.xls` file use `ExportTo::Exporter::Xls.new(export).to_xls`.
- `ExportTo::Base#each` calls an undefined `rows!` and raises `NoMethodError`. Iterate with `each!` (yields `columns, record, index`, with the header row first) or use one of the `to_*` methods.
- `width:` / `format:` and `excel` options only affect XLSX output.
- Subclasses share the column definitions arrays of their parent class (`class_attribute`), so do not call `set` on a subclass of an exporter that is used on its own.

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then run the tests:

```bash
bin/test_all
bundle exec rake test # also: rake t / rake (default)
bin/console # interactive prompt
```

To check dependency compatibility:
Each Ruby / Rails combination has its own gemfile in `gemfiles/` (CI sets `BUNDLE_GEMFILE` to it):

```bash
bin/check_dependencies
BUNDLE_GEMFILE=$PWD/gemfiles/ruby_3.4_rails_8.1.gemfile bundle install
BUNDLE_GEMFILE=$PWD/gemfiles/ruby_3.4_rails_8.1.gemfile bundle exec rake test
```

For more detailed testing instructions, see [TESTING.md](TESTING.md).
`bin/test_all` runs every supported Ruby (needs rbenv or rvm) and `bin/check_dependencies` checks dependency compatibility; see [TESTING.md](TESTING.md) for details (its version lists can lag behind CI).

## Releasing

Releases are automated with RubyGems [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/) (no API key is stored in the repo):

1. In your PR, bump `ExportTo::VERSION` in `lib/export_to/version.rb` ([Semantic Versioning](https://semver.org/): patch for fixes, minor for compatible features, major for breaking changes).
2. Add a matching `## [x.y.z] - YYYY-MM-DD` section to `CHANGELOG.md`.
3. A human reviews and merges the PR into `master` (AI accounts can only open PRs).
4. [`.github/workflows/publish.yml`](.github/workflows/publish.yml) runs on `master`: if the gemspec version is not on rubygems.org yet it runs `rake release` (builds the gem, tags `vX.Y.Z`, publishes). A merge without a version bump publishes nothing.

To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
Docs/CI-only PRs do not need a version bump. See [CLAUDE.md](CLAUDE.md): every PR must also update `README.md` (when behavior or usage changes) and add an entry to `CHANGELOG.md`.

## Contributing

Expand Down
Loading