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
65 changes: 42 additions & 23 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,88 +1,107 @@
# Changelog
# 更新紀錄(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).
格式參考 [Keep a Changelog](https://keepachangelog.com/zh-TW/1.1.0/),版本號遵循 [Semantic Versioning](https://semver.org/lang/zh-TW/)。

## [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`.
- 新增 `CHANGELOG.md`。
- 完整的 `README.md`:概述、環境需求、安裝、DSL 用法、開發與發版流程。
- CLAUDE.md 規則:每個 PR 都必須同步更新 `README.md` 與 `CHANGELOG.md`;`README.md` 與 `CHANGELOG.md` 一律用繁體中文撰寫。

### Changed

- `README.md`、`CHANGELOG.md` 改以繁體中文(zh-TW)撰寫。

## [1.3.0] - 2026-10-27

### Fixed

- `ExportTo::Base#each` 每次呼叫都會 raise `NoMethodError (undefined method 'rows!')`。現在會回傳所有列組成的 Array(第一列是標題列,內容與 `#to_csv` 相同);帶 block 時會逐列 yield 並回傳 exporter 本身,因此 Enumerable 方法(`map`、`to_a` 等)也能使用。`#each!` 沒有變動。
- `TESTING.md` 與 `bin/test_all` 已過時(只涵蓋 Ruby 2.7–3.1)。現在它們的說明與執行內容都與 `.github/workflows/ci.yml` 的矩陣一致。

### Deprecated

- `ExportTo::Base#to_xls`:自 1.0.0 起它回傳的就是 **XLSX** 資料(與 `#to_xlsx` 相同),而不是舊版 `.xls` 檔。為了不讓既有呼叫端壞掉,回傳值維持不變,但現在會印出一次性的 `[DEPRECATION]` 警告(輸出到 `$stderr`,每個 process 一次)。請改用 `#to_xlsx`;若要真正的 `.xls`,請用 `ExportTo::Exporter::Xls.new(export).to_xls`。未來的 major 版本可能會讓 `#to_xls` 回傳真正的 `.xls` 檔,或將它移除。

### Added

- 測試:`#each`、`#to_xls` 的相容性(仍回傳 XLSX)與其棄用警告、`#to_xlsx` / `ExportTo::Exporter::Xls` 的輸出格式。

## [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.
- 支援 Rails 8.0 / 8.1 與 Ruby 3.4(`activesupport` 現在放寬到 `< 8.2`;`csv` gem 改為明確相依;移除 `required_ruby_version` 的上限;新增 `ruby_3.3_rails_8.*` / `ruby_3.4_rails_8.*` gemfile 與對應的 CI job)。
- GitHub Actions `publish.yml`:新版本進到 `master` 時,透過 Trusted Publishing 自動發佈到 RubyGems。
- CLAUDE.md 加入發版規則。

### Changed

- Removed stale per-gemfile lockfiles that broke older Rubies in CI.
- 移除每個 gemfile 各自的舊 lockfile,這些 lockfile 會讓 CI 在較舊的 Ruby 上失敗。

## [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.
- 新增 Ruby 3.2 與 3.3 的測試 gemfile(`ruby_3.2_rails_7.0`、`ruby_3.2_rails_7.1`、`ruby_3.3_rails_7.1`)與 CI job。

### Changed

- Gemspec `required_ruby_version` upper bound raised from `< 3.2.0` to `< 3.4.0`.
- gemspec 的 `required_ruby_version` 上限由 `< 3.2.0` 提高到 `< 3.4.0`。

## [1.1.2] - 2026-01-07

### Changed

- Updated gemfile lockfiles.
- 更新各 gemfile 的 lockfile。

## [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`.
- 多版本 Ruby / Rails 測試環境:每個版本各自的 gemfile、GitHub Actions 矩陣、`bin/test_all`、`bin/check_dependencies` 與 `TESTING.md`。

### Fixed

- A test no longer depends on a hard-coded macOS path.
- 測試不再依賴寫死的 macOS 路徑。

## [1.1.0] - 2023-02-02

### Changed

- XLSX exporter fixes and tweaks (`ExportTo::Exporter::Xlsx`).
- 修正並微調 XLSX exporter(`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`.
- 內部改寫:匯出邏輯移到 `ExportTo::Exporter::Csv` / `Xls` / `Xlsx`;`ExportTo::Base` 提供 `to_csv`、`to_xlsx` 與 `to_xls`。
- `activesupport` 限制為 `>= 6, < 7`;開發相依套件放寬為 `bundler >= 2.0`、`rake >= 13`。

## [0.1.2] - 2019-06-19

### Changed

- CSV output now force-quotes every field.
- CSV 輸出現在會強制幫每個欄位加上引號。

## [0.1.1] - 2018-12-05

### Fixed

- "File exists" bug when exporting.
- 修正匯出時出現 "File exists" 的 bug。

## [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`.
- 首次發佈:`ExportTo::Base` DSL(`set`、`joins`、`presenter`、`each_with`)、帶有列計數器的 presenter、空的 join relation 處理,以及 `to_xlsx`。

[Unreleased]: https://github.com/superlanding/export_to/compare/v1.2.1...HEAD
[Unreleased]: https://github.com/superlanding/export_to/compare/v1.3.0...HEAD
[1.3.0]: https://github.com/superlanding/export_to/compare/v1.2.1...v1.3.0
[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
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- **每一個 PR 都必須同步更新文件**:
- 只要行為、用法、設定、相依套件或支援的 Ruby / Rails 版本有變,就要更新 `README.md`。
- 一律要在 `CHANGELOG.md` 新增一筆條目([Keep a Changelog](https://keepachangelog.com/zh-TW/1.1.0/) 格式,分類用 Added / Changed / Deprecated / Removed / Fixed / Security)。
- **語言規則**:`README.md` 與 `CHANGELOG.md` 一律用繁體中文(zh-TW,台灣用語)撰寫;程式碼、識別字(class / 方法 / 變數名稱)、指令、檔名與路徑維持英文,不要翻譯。
- `CHANGELOG.md` 的寫法:
- 沒有跳版號的 PR:寫在最上方的 `## [Unreleased]` 底下。
- 有跳版號的 PR:把條目寫在新的版本章節 `## [x.y.z] - YYYY-MM-DD`(日期為合併日),並同步更新檔尾的版本比較連結。
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
export_to (1.2.1)
export_to (1.3.0)
activesupport (>= 6.0, < 8.2)
csv
fast_excel (~> 0.4)
Expand Down
117 changes: 59 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
[![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)

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.
用一套簡單的宣告式 DSL,把一批資料(ActiveRecord relation、Struct 陣列,或任何有 `each` 的物件)匯出成 **CSV** 或 **XLSX**(透過較底層的 exporter,也能匯出舊版 **XLS**)。

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`.
你只要在 `ExportTo::Base` 的子類別裡宣告一次欄位(標題、來源屬性、寬度、格式、值的轉換),需要計算欄位時可以加上 presenter,也可以加上 `joins` relation 把一筆資料展開成多列,最後呼叫 `to_csv` / `to_xlsx` 即可。

## Requirements
## 環境需求

| Ruby | Rails / ActiveSupport tested in CI |
| Ruby | CI 測試的 Rails / ActiveSupport 版本 |
|------|------------------------------------|
| 2.7 | 6.1 |
| 3.0 | 6.1, 7.0 |
Expand All @@ -18,78 +18,80 @@ You describe the columns once (title, source attribute, width, format, value tra
| 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).
gemspec 要求 Ruby `>= 2.7.0` 與 `activesupport >= 6.0, < 8.2`。實際支援範圍以 [`.github/workflows/ci.yml`](.github/workflows/ci.yml) 為準。

Runtime dependencies: `fast_excel` (XLSX), `spreadsheet` (XLS), `csv`, `iconv`, `activesupport`.
Runtime 相依套件:`fast_excel`(XLSX)、`spreadsheet`(XLS)、`csv`、`iconv`、`activesupport`。

## Installation
## 安裝

Add this line to your application's Gemfile:
在應用程式的 Gemfile 加入這一行:

```ruby
gem "export_to"
```

And then execute:
然後執行:

```bash
bundle install
```

Or install it yourself with `gem install export_to`.
或自行執行 `gem install export_to` 安裝。

## Usage
## 使用方式

### 1. Define an exporter
### 1. 定義 exporter

```ruby
class UserExport < ExportTo::Base
# set <header title>, <key>, <column options>, &optional_transform
# set <標題>, <key>, <欄位選項>, &可選的轉換 block
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
value + 100 # block 會收到儲存格的值,並回傳最終要輸出的值
end
set "Source", "web" # a String key is written as a fixed value in every row
set "Source", "web" # key 為 String 時,每一列都會寫入這個固定值

excel worksheet: "Users", height: 24 # optional XLSX settings (worksheet name, row height)
excel worksheet: "Users", height: 24 # 可選的 XLSX 設定(工作表名稱、列高)

presenter do # optional: computed columns
presenter do # 可選:計算欄位
def full_name
"#{first_name} #{last_name}" # unknown methods are delegated to the record
"#{first_name} #{last_name}" # 找不到的方法會委派給該筆 record
end

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

joins :wallets # optional: one row per record in record.wallets
joins :wallets # 可選:record.wallets 裡每一筆各佔一列
end
```

- **`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.
- **`set(title, key, options = {}, &block)`**:每次呼叫新增一個欄位,依呼叫順序排列。
- `key` 為 **Symbol** 時,會從 presenter 讀取(presenter 會委派給 record);`nil` 會變成 `""`。
- `key` 為 **String** 時,直接當作固定內容使用。
- XLSX 專用的 `options`:`width:`(欄寬)與 `format:`(`FastExcel` 的 format hash,例如 `{ bold: true }`)。
- 可選的 block 用來後處理儲存格的值。
- **`excel(worksheet: "Name", height: 20)`**:工作表名稱(預設 `"Default"`)與資料列的列高(預設 `20`)。標題列為粗體、置中並帶有背景色。
- **`presenter { ... }`**:定義 `YourExport::Presenter`(是上層 exporter 的 presenter 的子類別)。在裡面可以使用 `model`、`relation`、`relation?`、`x`(record 的索引)與 `y`(`joins` relation 內的索引)。
- **`joins(:relation_name)`**:當 `record.relation_name` 有資料時,每個元素各自成為一列,且 `relation` 就是該元素;presenter 會先找 `relation` 上的方法,找不到再找 `model`。relation 為空的 record 會產生單獨一列(此時 `relation?` 為 `false`)。
- **`each_with { |presenter| ... }`**:hook,會在讀取各欄位之前,以每一列的 presenter 呼叫一次。
- **`self.each_method = :find_in_batches`**:改用 `records.find_in_batches` 而不是 `records.each` 來迭代,適合很大的 ActiveRecord relation。

### 2. Export
### 2. 匯出

```ruby
users = User.includes(:wallets).where(active: true) # or any enumerable
users = User.includes(:wallets).where(active: true) # 或任何可列舉的物件
export = UserExport.new(users)

export.to_csv # => String, every field quoted ("Name","Mobile",...)
export.to_xlsx # => binary String of an .xlsx file
export.to_csv # => String,每個欄位都有加引號("Name","Mobile",...)
export.to_xlsx # => .xlsx 檔案的二進位 String
export.each # => 所有列組成的 Array,第一列是標題列:[["Name", "Mobile", ...], ["Eddie Li", ...], ...]
export.each { |columns| ... } # 逐列 yield(第一列是標題列),並回傳 exporter 本身;Enumerable 方法也能用
```

Example CSV output for two users (the second has two wallets):
兩位使用者的 CSV 輸出範例(第二位有兩個 wallet):

```
"Name","Mobile","Amount","Source"
Expand All @@ -98,7 +100,7 @@ Example CSV output for two users (the second has two wallets):
"Vegeta Lu","+8868","2100","web"
```

Serving it from a Rails controller:
在 Rails controller 裡提供下載:

```ruby
def export
Expand All @@ -108,48 +110,47 @@ def export
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.
- `ExportTo::Base#to_xls` **已棄用,而且名稱有誤導性**:自 1.0.0 起它回傳的是與 `#to_xlsx` 相同的 **XLSX** 資料(不是舊版 `.xls` 檔)。保留這個行為是為了不讓既有呼叫端壞掉;呼叫時會印出一次性的 `[DEPRECATION]` 警告。請改用 `#to_xlsx`。若需要真正的舊版 `.xls` 檔,請用 `ExportTo::Exporter::Xls.new(export).to_xls`(不會套用任何樣式選項)。
- `width:` / `format:` 與 `excel` 選項只對 XLSX 輸出有效。
- 子類別會共用上層 class 的欄位定義陣列(`class_attribute`),所以如果某個 exporter 本身也會單獨使用,就不要在它的子類別裡呼叫 `set`。

## Development
## 開發

After checking out the repo, run `bin/setup` to install dependencies. Then run the tests:
clone 下來之後,先執行 `bin/setup` 安裝相依套件,接著跑測試:

```bash
bundle exec rake test # also: rake t / rake (default)
bin/console # interactive prompt
bundle exec rake test # 也可以用 rake t / rake(預設 task)
bin/console # 互動式 console
```

Each Ruby / Rails combination has its own gemfile in `gemfiles/` (CI sets `BUNDLE_GEMFILE` to it):
每個 Ruby / Rails 組合在 `gemfiles/` 內都有各自的 gemfile(CI 會把 `BUNDLE_GEMFILE` 指到它):

```bash
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
```

`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).
`bin/test_all` 會跑過所有支援的 Ruby 版本(需要 rbenv 或 rvm),`bin/check_dependencies` 會檢查相依套件的相容性;詳見 [TESTING.md](TESTING.md)。

## Releasing
## 發版

Releases are automated with RubyGems [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/) (no API key is stored in the repo):
發版透過 RubyGems [Trusted Publishing](https://guides.rubygems.org/trusted-publishing/) 自動完成(repo 內不存放任何 API key):

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.
1. 在你的 PR 裡修改 `lib/export_to/version.rb` 的 `ExportTo::VERSION`(遵循 [Semantic Versioning](https://semver.org/lang/zh-TW/):修 bug 跳 patch、相容的新功能跳 minor、不相容的變更跳 major)。
2. 在 `CHANGELOG.md` 新增對應的 `## [x.y.z] - YYYY-MM-DD` 區塊。
3. 由真人 review 後合併進 `master`(AI 帳號只能開 PR)。
4. [`.github/workflows/publish.yml`](.github/workflows/publish.yml) 會在 `master` 上執行:如果 gemspec 的版本還沒上 rubygems.org,就執行 `rake release`(打包 gem、打 `vX.Y.Z` tag、發佈)。沒有跳版號的合併不會發佈任何東西。

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`.
只改文件 / CI 的 PR 不需要跳版號。請見 [CLAUDE.md](CLAUDE.md):每個 PR 也都必須更新 `README.md`(行為或用法有變時)並在 `CHANGELOG.md` 新增一筆條目。

## Contributing
## 貢獻

Bug reports and pull requests are welcome on GitHub at https://github.com/superlanding/export_to.
歡迎到 GitHub 回報問題與提交 pull request:https://github.com/superlanding/export_to。

## License
## 授權

The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
本 gem 以 [MIT License](https://opensource.org/licenses/MIT) 條款開源。
Loading
Loading