rbcsv 0.1.4 → 0.1.7

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.
data/DEVELOPMENT.md ADDED
@@ -0,0 +1,491 @@
1
+ # RbCsv 開発ガイド
2
+
3
+ このドキュメントでは、rbcsvの開発環境のセットアップ、ビルド方法、テスト手順、リリース手順について詳しく説明します。
4
+
5
+ ## 必要な環境
6
+
7
+ - **Ruby**: 3.2.0以降(gemspec要件)
8
+ - **Rust**: 最新の安定版を推奨(MSRV: 1.75+)
9
+ - **Bundler**: gem管理
10
+ - **Git**: バージョン管理
11
+ - **RubyGems**: 3.3.11以降
12
+
13
+ ### システム要件
14
+
15
+ - **macOS**: Apple Silicon (arm64) / Intel (x86_64)
16
+ - **Linux**: x86_64 / aarch64
17
+ - **Windows**: x86_64(実験的サポート)
18
+
19
+ ## プロジェクト構成
20
+
21
+ ```
22
+ r_csv/
23
+ ├── lib/
24
+ │ ├── rbcsv.rb # メインのRubyエントリーポイント
25
+ │ └── rbcsv/
26
+ │ ├── version.rb # バージョン定義
27
+ │ └── rbcsv.bundle # コンパイル済みネイティブ拡張(生成される)
28
+ ├── ext/
29
+ │ └── rbcsv/
30
+ │ ├── src/
31
+ │ │ ├── lib.rs # Rust拡張のエントリーポイント、Magnus初期化
32
+ │ │ ├── parser.rs # CSV解析コア、CsvParseOptions定義
33
+ │ │ ├── ruby_api.rs # Ruby APIバインディング、オプション処理
34
+ │ │ └── error.rs # エラーハンドリング
35
+ │ ├── Cargo.toml # Rust依存関係(Magnus 0.8.1使用)
36
+ │ └── extconf.rb # Ruby拡張ビルド設定
37
+ ├── spec/
38
+ │ ├── rbcsv_spec.rb # メインのRubyテスト
39
+ │ └── spec_helper.rb # テスト設定
40
+ ├── docs/ # ドキュメント
41
+ ├── target/ # Rustビルド出力(git無視)
42
+ ├── tmp/ # Ruby拡張ビルド中間ファイル(git無視)
43
+ ├── *.gem # ビルド済みgemファイル
44
+ ├── rbcsv.gemspec # Gem仕様
45
+ ├── Rakefile # ビルドタスク(rb_sys使用)
46
+ ├── Gemfile # Ruby依存関係
47
+ ├── Cargo.toml # ワークスペース設定
48
+ ├── CHANGELOG.md # 変更履歴
49
+ ├── README.md # 使用法ガイド
50
+ └── DEVELOPMENT.md # このファイル
51
+ ```
52
+
53
+ ## 開発環境のセットアップ
54
+
55
+ ### 1. リポジトリのクローン
56
+
57
+ ```bash
58
+ git clone https://github.com/fs0414/rbcsv.git
59
+ cd rbcsv
60
+ ```
61
+
62
+ ### 2. Ruby依存関係のインストール
63
+
64
+ ```bash
65
+ bundle install
66
+ ```
67
+
68
+ ### 3. ネイティブ拡張のビルド
69
+
70
+ ```bash
71
+ # 推奨方法(rb_sys使用)
72
+ rake compile
73
+
74
+ # 代替方法(開発時)
75
+ bundle exec rake compile
76
+ ```
77
+
78
+ ### 4. 動作確認
79
+
80
+ ```bash
81
+ # 基本的な動作確認
82
+ ruby -I lib -e "require 'rbcsv'; p RbCsv.parse('a,b\n1,2', {})"
83
+ # 期待される出力: [["a", "b"], ["1", "2"]]
84
+
85
+ # オプション付きテスト
86
+ ruby -I lib -e "require 'rbcsv'; p RbCsv.parse(' a , b \n 1 , 2 ', {trim: true})"
87
+ # 期待される出力: [["a", "b"], ["1", "2"]]
88
+ ```
89
+
90
+ ## ビルドプロセス
91
+
92
+ ### 自動ビルド(推奨)
93
+
94
+ ```bash
95
+ # 全体ビルド(コンパイル、テスト、リント)
96
+ rake
97
+
98
+ # 拡張のみコンパイル
99
+ rake compile
100
+
101
+ # クリーンビルド
102
+ rake clean
103
+ rake compile
104
+ ```
105
+
106
+ ### 手動ビルド手順
107
+
108
+ ```bash
109
+ # 1. 前回のビルドをクリーン
110
+ rm -rf lib/rbcsv/rbcsv.bundle tmp/ target/
111
+
112
+ # 2. Rust拡張のコンパイル
113
+ cd ext/rbcsv
114
+ cargo build --release
115
+ cd ../..
116
+
117
+ # 3. バンドルファイルのコピー(macOSの場合)
118
+ cp target/release/librbcsv.dylib lib/rbcsv/rbcsv.bundle
119
+
120
+ # Linuxの場合
121
+ # cp target/release/librbcsv.so lib/rbcsv/rbcsv.bundle
122
+ ```
123
+
124
+ ### ビルドのトラブルシューティング
125
+
126
+ #### ABIバージョンの不一致
127
+
128
+ ```bash
129
+ # エラー例: "incompatible ABI version"
130
+ rm -rf lib/rbcsv/rbcsv.bundle tmp/ target/
131
+ bundle exec rake compile
132
+ ```
133
+
134
+ #### Rust/Cargoの問題
135
+
136
+ ```bash
137
+ # Rust依存関係の更新
138
+ cd ext/rbcsv
139
+ cargo update
140
+ cargo clean
141
+ cargo build --release
142
+ cd ../..
143
+ ```
144
+
145
+ #### Magnus APIエラー
146
+
147
+ ```bash
148
+ # 最新のMagnus 0.8.1では、ReprValueトレイトの明示的インポートが必要
149
+ # ruby_api.rs で以下が含まれていることを確認:
150
+ use magnus::{value::ReprValue};
151
+ ```
152
+
153
+ ## テスト手順
154
+
155
+ ### Ruby統合テスト
156
+
157
+ ```bash
158
+ # 全テスト実行
159
+ bundle exec rspec
160
+
161
+ # 特定のテストファイル
162
+ bundle exec rspec spec/rbcsv_spec.rb
163
+
164
+ # 詳細出力
165
+ bundle exec rspec --format documentation
166
+ ```
167
+
168
+ ### Rustユニットテスト
169
+
170
+ ```bash
171
+ # 全Rustテスト
172
+ cd ext/rbcsv
173
+ cargo test
174
+
175
+ # 詳細出力
176
+ cargo test -- --nocapture
177
+
178
+ # 特定のテスト
179
+ cargo test test_parse_basic
180
+ cd ../..
181
+ ```
182
+
183
+ ### パフォーマンステスト
184
+
185
+ ```bash
186
+ # ベンチマーク実行
187
+ ruby benchmark.rb
188
+
189
+ # カスタムテストファイルでのテスト
190
+ ruby test.rb
191
+ ```
192
+
193
+ ### コードスタイルチェック
194
+
195
+ ```bash
196
+ # Rubyコード(RuboCop)
197
+ bundle exec rubocop
198
+
199
+ # Rustコード
200
+ cd ext/rbcsv
201
+ cargo fmt --check
202
+ cargo clippy -- -D warnings
203
+ cd ../..
204
+ ```
205
+
206
+ ## API設計
207
+
208
+ ### 現在のAPI(v0.1.6+)
209
+
210
+ ```ruby
211
+ # 統一されたオプションベースAPI
212
+ RbCsv.parse(csv_string, options = {})
213
+ RbCsv.read(file_path, options = {})
214
+
215
+ # 利用可能なオプション
216
+ options = {
217
+ trim: true/false # 空白文字の除去(デフォルト: false)
218
+ # 将来の拡張:
219
+ # headers: true/false
220
+ # delimiter: ','
221
+ }
222
+ ```
223
+
224
+ ### 実装アーキテクチャ
225
+
226
+ 1. **parser.rs**: CsvParseOptionsと核となるCSV解析機能
227
+ 2. **ruby_api.rs**: Rubyハッシュオプションの処理とMagnus API
228
+ 3. **lib.rs**: Magnus初期化と関数登録
229
+ 4. **error.rs**: エラーハンドリングとRuby例外の変換
230
+
231
+ ## リリース手順
232
+
233
+ ### 1. 準備フェーズ
234
+
235
+ ```bash
236
+ # 開発状況の確認
237
+ git status
238
+ git log --oneline -10
239
+
240
+ # 全テストの実行
241
+ rake clean
242
+ rake
243
+
244
+ # コードスタイルチェック
245
+ bundle exec rubocop
246
+ cd ext/rbcsv && cargo clippy && cd ../..
247
+ ```
248
+
249
+ ### 2. バージョン更新
250
+
251
+ ```bash
252
+ # lib/rbcsv/version.rb を編集
253
+ vim lib/rbcsv/version.rb
254
+ ```
255
+
256
+ ```ruby
257
+ module RbCsv
258
+ VERSION = "x.y.z" # セマンティックバージョニング
259
+ end
260
+ ```
261
+
262
+ ### 3. CHANGELOG.md の更新
263
+
264
+ ```markdown
265
+ ## [x.y.z] - YYYY-MM-DD
266
+
267
+ ### 追加
268
+ - 新機能の説明
269
+
270
+ ### 変更
271
+ - 既存機能の変更点
272
+
273
+ ### 修正
274
+ - バグ修正の説明
275
+
276
+ ### 削除
277
+ - 削除された機能(非互換性のある変更)
278
+
279
+ ### セキュリティ
280
+ - セキュリティ関連の修正
281
+ ```
282
+
283
+ ### 4. ビルドとテスト
284
+
285
+ ```bash
286
+ # フルクリーンビルド
287
+ rake clean
288
+ bundle install
289
+ rake compile
290
+
291
+ # 統合テスト
292
+ rake spec
293
+
294
+ # 動作確認
295
+ ruby -I lib -e "require 'rbcsv'; puts RbCsv::VERSION"
296
+ ruby -I lib -e "require 'rbcsv'; p RbCsv.parse('a,b\n1,2', {})"
297
+ ```
298
+
299
+ ### 5. Gemビルド
300
+
301
+ ```bash
302
+ # Gemファイル生成
303
+ gem build rbcsv.gemspec
304
+
305
+ # 生成確認
306
+ ls -la rbcsv-*.gem
307
+ ```
308
+
309
+ ### 6. 変更のコミット
310
+
311
+ ```bash
312
+ git add -A
313
+ git commit -m "Release v${VERSION}
314
+
315
+ 主な変更:
316
+ - 変更点1の説明
317
+ - 変更点2の説明
318
+ - バグ修正やパフォーマンス改善"
319
+ ```
320
+
321
+ ### 7. タグ作成とプッシュ
322
+
323
+ ```bash
324
+ VERSION=$(ruby -I lib -e "require 'rbcsv/version'; puts RbCsv::VERSION")
325
+ git tag "v${VERSION}"
326
+ git push origin main
327
+ git push origin "v${VERSION}"
328
+ ```
329
+
330
+ ### 8. Gem公開(オプション)
331
+
332
+ ```bash
333
+ # RubyGems.orgへの公開
334
+ gem push rbcsv-${VERSION}.gem
335
+
336
+ # 公開確認
337
+ gem list rbcsv --remote
338
+ ```
339
+
340
+ ## 開発のベストプラクティス
341
+
342
+ ### コードスタイル
343
+
344
+ #### Ruby
345
+ - 標準的なRuby Style Guideに従う
346
+ - RuboCop設定を使用(`.rubocop.yml`)
347
+ - frozen_string_literalを有効化
348
+
349
+ #### Rust
350
+ ```bash
351
+ # フォーマット
352
+ cargo fmt
353
+
354
+ # リント
355
+ cargo clippy -- -D warnings
356
+
357
+ # ドキュメント生成
358
+ cargo doc --open
359
+ ```
360
+
361
+ ### テスト戦略
362
+
363
+ 1. **単体テスト**: 各Rustモジュールに対するcargo test
364
+ 2. **統合テスト**: Ruby APIレベルでのRSpecテスト
365
+ 3. **パフォーマンステスト**: 大きなCSVファイルでのベンチマーク
366
+ 4. **エッジケーステスト**: 不正なCSV、空ファイル、エンコーディング問題
367
+
368
+ ### デバッグ
369
+
370
+ #### Rust側のデバッグ
371
+
372
+ ```rust
373
+ // 開発ビルドでのみ有効
374
+ #[cfg(debug_assertions)]
375
+ eprintln!("Debug: {:?}", variable);
376
+
377
+ // ログ出力(log crateを使用)
378
+ log::debug!("Debug information: {:?}", data);
379
+ ```
380
+
381
+ #### Ruby側のデバッグ
382
+
383
+ ```ruby
384
+ # 詳細エラー情報
385
+ begin
386
+ RbCsv.parse(invalid_csv, {})
387
+ rescue => e
388
+ puts "Error: #{e.class} - #{e.message}"
389
+ puts e.backtrace
390
+ end
391
+ ```
392
+
393
+ ## よくある問題と解決策
394
+
395
+ ### ビルド関連
396
+
397
+ **問題**: "incompatible ABI version"
398
+ ```bash
399
+ # 解決策: クリーンして同じRubyバージョンで再ビルド
400
+ rm -rf lib/rbcsv/rbcsv.bundle tmp/
401
+ bundle exec rake compile
402
+ ```
403
+
404
+ **問題**: Rustコンパイルエラー
405
+ ```bash
406
+ # 解決策: Rust依存関係の更新
407
+ cd ext/rbcsv
408
+ cargo update
409
+ cargo clean
410
+ cargo build --release
411
+ cd ../..
412
+ ```
413
+
414
+ ### 実行時問題
415
+
416
+ **問題**: 空配列が返される
417
+ - **原因**: CSVリーダーの`has_headers`設定
418
+ - **解決策**: 最新バージョン(v0.1.4+)を使用
419
+
420
+ **問題**: 日本語CSV文字化け
421
+ - **原因**: エンコーディング問題
422
+ - **解決策**: UTF-8での保存を確認、またはエンコーディング変換
423
+
424
+ ### パフォーマンス問題
425
+
426
+ **問題**: 大きなファイルでメモリ不足
427
+ - **解決策**: ストリーミング処理の実装を検討(将来の機能)
428
+
429
+ **問題**: 予想より遅い処理速度
430
+ - **チェック項目**:
431
+ - ファイルI/O vs メモリ処理
432
+ - trimオプションの使用
433
+ - デバッグビルド vs リリースビルド
434
+
435
+ ## コントリビューションガイドライン
436
+
437
+ ### 開発フロー
438
+
439
+ 1. **Issue作成**: バグ報告や機能要求
440
+ 2. **フォーク**: 個人リポジトリへのフォーク
441
+ 3. **ブランチ作成**: `feature/new-feature` または `fix/bug-name`
442
+ 4. **開発**: コードの実装とテスト追加
443
+ 5. **テスト**: 全テストの実行と確認
444
+ 6. **プルリクエスト**: 説明と変更内容の詳細
445
+
446
+ ### コミットメッセージ
447
+
448
+ ```
449
+ [種類] 簡潔な説明(50文字以内)
450
+
451
+ 詳細な説明(必要に応じて):
452
+ - 変更の理由
453
+ - 実装方法
454
+ - 影響範囲
455
+
456
+ 関連Issue: #123
457
+ ```
458
+
459
+ 種類の例:
460
+ - `feat`: 新機能
461
+ - `fix`: バグ修正
462
+ - `docs`: ドキュメント
463
+ - `style`: コードスタイル
464
+ - `refactor`: リファクタリング
465
+ - `test`: テスト追加
466
+ - `chore`: その他(依存関係更新など)
467
+
468
+ ## ロードマップ
469
+
470
+ ### 短期目標(v0.2.x)
471
+ - [ ] カスタム区切り文字サポート
472
+ - [ ] ヘッダー行処理
473
+ - [ ] エラーハンドリングの改善
474
+
475
+ ### 中期目標(v0.3.x)
476
+ - [ ] ストリーミング処理
477
+ - [ ] 非同期処理サポート
478
+ - [ ] Windows完全サポート
479
+
480
+ ### 長期目標(v1.0.x)
481
+ - [ ] 安定したAPI
482
+ - [ ] 包括的なドキュメント
483
+ - [ ] パフォーマンス最適化完了
484
+
485
+ ## 参考リンク
486
+
487
+ - [Magnus Documentation](https://docs.rs/magnus/)
488
+ - [rb_sys Documentation](https://docs.rs/rb_sys/)
489
+ - [Ruby Extension Guide](https://docs.ruby-lang.org/en/master/extension_rdoc.html)
490
+ - [Cargo Book](https://doc.rust-lang.org/cargo/)
491
+ - [RubyGems Guides](https://guides.rubygems.org/)
data/ext/rbcsv/Cargo.toml CHANGED
@@ -11,4 +11,9 @@ crate-type = ["cdylib"]
11
11
 
12
12
  [dependencies]
13
13
  csv = "1.3.1"
14
- magnus = { version = "0.6.2" }
14
+ magnus = { version = "0.8.1" }
15
+ # rb-sys = { version = "0.9", features = ["link-ruby"] }
16
+ log = "0.4"
17
+
18
+ [dev-dependencies]
19
+ env_logger = "0.10"
@@ -0,0 +1,87 @@
1
+ use std::error::Error as StdError;
2
+ use std::fmt;
3
+
4
+ #[derive(Debug)]
5
+ pub struct CsvError {
6
+ message: String,
7
+ kind: ErrorKind,
8
+ }
9
+
10
+ #[derive(Debug)]
11
+ pub enum ErrorKind {
12
+ // IO関連エラー
13
+ Io,
14
+ // CSV解析エラー
15
+ Parse,
16
+ // UTF-8エンコーディングエラー
17
+ Encoding,
18
+ // フィールド数の不一致
19
+ FieldCountMismatch,
20
+ // 空のCSVデータ
21
+ EmptyData,
22
+ // その他のエラー
23
+ #[allow(dead_code)]
24
+ Other,
25
+ }
26
+
27
+ impl fmt::Display for CsvError {
28
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
29
+ match self.kind {
30
+ ErrorKind::Io => write!(f, "IO Error: {}", self.message),
31
+ ErrorKind::Parse => write!(f, "Parse Error: {}", self.message),
32
+ ErrorKind::Encoding => write!(f, "Encoding Error: {}", self.message),
33
+ ErrorKind::FieldCountMismatch => write!(f, "Field Count Mismatch: {}", self.message),
34
+ ErrorKind::EmptyData => write!(f, "Empty Data: {}", self.message),
35
+ ErrorKind::Other => write!(f, "Error: {}", self.message),
36
+ }
37
+ }
38
+ }
39
+
40
+ impl StdError for CsvError {}
41
+
42
+ impl CsvError {
43
+ pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
44
+ CsvError {
45
+ message: message.into(),
46
+ kind,
47
+ }
48
+ }
49
+
50
+ pub fn io(message: impl Into<String>) -> Self {
51
+ Self::new(ErrorKind::Io, message)
52
+ }
53
+
54
+ pub fn parse(message: impl Into<String>) -> Self {
55
+ Self::new(ErrorKind::Parse, message)
56
+ }
57
+
58
+ pub fn encoding(message: impl Into<String>) -> Self {
59
+ Self::new(ErrorKind::Encoding, message)
60
+ }
61
+
62
+ #[allow(dead_code)]
63
+ pub fn field_count_mismatch(expected: usize, actual: usize) -> Self {
64
+ Self::new(
65
+ ErrorKind::FieldCountMismatch,
66
+ format!("Expected {} fields, but got {}", expected, actual),
67
+ )
68
+ }
69
+
70
+ pub fn empty_data() -> Self {
71
+ Self::new(ErrorKind::EmptyData, "CSV data is empty")
72
+ }
73
+ }
74
+
75
+ // csv crateのエラーからの変換
76
+ impl From<csv::Error> for CsvError {
77
+ fn from(err: csv::Error) -> Self {
78
+ match err.kind() {
79
+ csv::ErrorKind::Io(_) => CsvError::io(err.to_string()),
80
+ csv::ErrorKind::Utf8 { .. } => CsvError::encoding(err.to_string()),
81
+ csv::ErrorKind::UnequalLengths { .. } => {
82
+ CsvError::new(ErrorKind::FieldCountMismatch, err.to_string())
83
+ }
84
+ _ => CsvError::parse(err.to_string()),
85
+ }
86
+ }
87
+ }
data/ext/rbcsv/src/lib.rs CHANGED
@@ -1,69 +1,19 @@
1
- #[cfg(not(test))]
2
- use magnus::{Error, exception, function, prelude::*, Ruby};
1
+ mod error;
2
+ mod parser;
3
+ mod ruby_api;
3
4
 
4
- #[cfg(test)]
5
- type Error = Box<dyn std::error::Error>;
5
+ use magnus::{Object, Ruby};
6
+ use ruby_api::{parse, parse_trim, read, read_trim};
6
7
 
7
- fn parse(s: String) -> Result<Vec<Vec<String>>, Error> {
8
- let mut reader = csv::ReaderBuilder::new()
9
- .has_headers(false) // ヘッダーを無効にして、すべての行を処理
10
- .from_reader(s.as_bytes());
11
-
12
- let mut records = Vec::new();
13
-
14
- for result in reader.records() {
15
- match result {
16
- Ok(record) => {
17
- let row: Vec<String> = record.iter().map(|field| field.to_string()).collect();
18
- records.push(row);
19
- }
20
- #[cfg(not(test))]
21
- Err(e) => return Err(Error::new(exception::runtime_error(), format!("CSV parse error: {}", e))),
22
- #[cfg(test)]
23
- Err(e) => return Err(Box::new(e)),
24
- }
25
- }
26
-
27
- Ok(records)
28
- }
29
-
30
- #[cfg(not(test))]
31
8
  #[magnus::init]
32
- fn init(ruby: &Ruby) -> Result<(), Error> {
9
+ fn init(ruby: &Ruby) -> Result<(), magnus::Error> {
33
10
  let module = ruby.define_module("RbCsv")?;
34
- module.define_singleton_method("parse", function!(parse, 1))?;
35
- Ok(())
36
- }
37
-
38
- #[cfg(test)]
39
- mod tests {
40
11
 
41
- use super::*;
12
+ module.define_singleton_method("parse", magnus::function!(parse, 1))?;
13
+ module.define_singleton_method("parse!", magnus::function!(parse_trim, 1))?;
14
+ module.define_singleton_method("read", magnus::function!(read, 1))?;
15
+ module.define_singleton_method("read!", magnus::function!(read_trim, 1))?;
42
16
 
43
- #[test]
44
- fn test_parse() {
45
- let csv_data = "name,age,city\nAlice,25,Tokyo\nBob,30,Osaka";
46
- let result = parse(csv_data.to_string());
47
-
48
- assert!(result.is_ok());
49
-
50
- let records = result.unwrap();
51
- assert_eq!(records.len(), 3); // ヘッダー行も含むため3行
52
- assert_eq!(records[0], vec!["name", "age", "city"]);
53
- assert_eq!(records[1], vec!["Alice", "25", "Tokyo"]);
54
- assert_eq!(records[2], vec!["Bob", "30", "Osaka"]);
55
- }
56
-
57
- #[test]
58
- fn test_parse_simple() {
59
- let csv_data = "a,b\n1,2";
60
- let result = parse(csv_data.to_string());
61
-
62
- assert!(result.is_ok());
63
-
64
- let records = result.unwrap();
65
- assert_eq!(records.len(), 2);
66
- assert_eq!(records[0], vec!["a", "b"]);
67
- assert_eq!(records[1], vec!["1", "2"]);
68
- }
17
+ Ok(())
69
18
  }
19
+