rbcsv 0.1.7 → 0.1.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 351d11d92922c2f119bf0e273abddd2cfdbe55fa46eaf2577710f0ef241ceacb
4
- data.tar.gz: adee292996af62f55eaf70ab0d927c9fc4b2a5c85cc1687775e6c6507d1851a4
3
+ metadata.gz: 6081f5083fa0f62ff08812aed7b6ff8fe47fbbc2a2170fc3d5ff3a4c6ef21661
4
+ data.tar.gz: a73748f6fefbf4a8fced9bf15f1f5db5ae3b4ef4fc11512e0f30a4ceb09b2e3f
5
5
  SHA512:
6
- metadata.gz: ea495c0d2d329c59b31fe64a9de7643bfb7e6a7d70ec3bbb18c4316659004bb30b88f1ec7a8b197888f877bcf4b1cc335e9042644b1d1e377efdee3cb1912a3c
7
- data.tar.gz: d20567b6ca4f1650190ffc74d16405bf6e68d2606dd873e978806c0e132c97b7ca102bb1a00e81d2298d5a539de1c09227596fb5f2a94971beace577bad7fa7c
6
+ metadata.gz: f2bc15f8586443f6be96a47525688312938141f5d509fe1365b0c83c6e4776c2624678e4e2bdabb926b319875218d03e6e519e317acb42ab7749861197e72a8e
7
+ data.tar.gz: c0e37e6c24bae12583a538f79b9a0bba74d9b7ce06b16198c9b8764de1a2dd87cbcd1ca0681f580b65220d2a9c532c98fe7cc6fe04ad72cd6fb5a403ead1024a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.8] - 2025-01-28
4
+
5
+ ### Added
6
+ - CSV file writing functionality with `RbCsv.write(file_path, data)` method
7
+ - Comprehensive data validation (empty data check, field count consistency)
8
+ - Enhanced error handling for write operations (permission errors, invalid data)
9
+ - Full test coverage for write functionality with executable test script
10
+
11
+ ### Fixed
12
+ - **CRITICAL**: Fixed special character handling in CSV parsing
13
+ - Removed problematic `escape_sanitize` function that interfered with standard CSV escaping
14
+ - Now properly preserves backslashes, newlines, tabs, and other special characters
15
+ - Ensures perfect round-trip fidelity for write/read operations
16
+ - Updated RSpec tests to reflect correct CSV parsing behavior
17
+
3
18
  ## [0.1.7] - 2025-01-28
4
19
 
5
20
  ### Changed
data/DEVELOPMENT.md CHANGED
@@ -16,39 +16,6 @@
16
16
  - **Linux**: x86_64 / aarch64
17
17
  - **Windows**: x86_64(実験的サポート)
18
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
19
 
53
20
  ## 開発環境のセットアップ
54
21
 
@@ -77,16 +44,100 @@ bundle exec rake compile
77
44
 
78
45
  ### 4. 動作確認
79
46
 
47
+ #### CSV パース機能
48
+
80
49
  ```bash
81
- # 基本的な動作確認
82
- ruby -I lib -e "require 'rbcsv'; p RbCsv.parse('a,b\n1,2', {})"
50
+ # 基本的なパース
51
+ ruby -I lib -e "require 'rbcsv'; p RbCsv.parse('a,b\n1,2')"
83
52
  # 期待される出力: [["a", "b"], ["1", "2"]]
84
53
 
85
- # オプション付きテスト
86
- ruby -I lib -e "require 'rbcsv'; p RbCsv.parse(' a , b \n 1 , 2 ', {trim: true})"
54
+ # trim機能付きパース
55
+ ruby -I lib -e "require 'rbcsv'; p RbCsv.parse!(' a , b \n 1 , 2 ')"
87
56
  # 期待される出力: [["a", "b"], ["1", "2"]]
88
57
  ```
89
58
 
59
+ #### CSV ファイル読み込み機能
60
+
61
+ ```bash
62
+ # CSVファイル読み込み
63
+ ruby -I lib -e "require 'rbcsv'; p RbCsv.read('spec/fixtures/test.csv')"
64
+ # 期待される出力: [["name", "age", "city"], ["Alice", "25", "Tokyo"], ...]
65
+
66
+ # trim機能付きファイル読み込み
67
+ ruby -I lib -e "require 'rbcsv'; p RbCsv.read!('spec/fixtures/test_with_spaces.csv')"
68
+ # 期待される出力: [["name", "age", "city"], ["Alice", "25", "Tokyo"], ...]
69
+ ```
70
+
71
+ #### CSV ファイル書き込み機能
72
+
73
+ ```bash
74
+ # 基本的なファイル書き込み
75
+ ruby -I lib -e "
76
+ require 'rbcsv'
77
+ data = [['name', 'age', 'city'], ['Alice', '25', 'Tokyo'], ['Bob', '30', 'Osaka']]
78
+ RbCsv.write('/tmp/test_output.csv', data)
79
+ puts 'File written successfully!'
80
+ puts File.read('/tmp/test_output.csv')
81
+ "
82
+ # 期待される出力:
83
+ # File written successfully!
84
+ # name,age,city
85
+ # Alice,25,Tokyo
86
+ # Bob,30,Osaka
87
+
88
+ # 書き込み→読み込みの往復テスト
89
+ ruby -I lib -e "
90
+ require 'rbcsv'
91
+ data = [['product', 'price'], ['Apple', '100'], ['Orange', '80']]
92
+ RbCsv.write('/tmp/roundtrip.csv', data)
93
+ result = RbCsv.read('/tmp/roundtrip.csv')
94
+ puts 'Original data:'
95
+ p data
96
+ puts 'Read back data:'
97
+ p result
98
+ puts 'Match: #{data == result}'
99
+ "
100
+ # 期待される出力: Match: true
101
+
102
+ # エラーハンドリングテスト(空データ)
103
+ ruby -I lib -e "
104
+ require 'rbcsv'
105
+ begin
106
+ RbCsv.write('/tmp/empty.csv', [])
107
+ rescue => e
108
+ puts 'Error caught: #{e.message}'
109
+ end
110
+ "
111
+ # 期待される出力: Error caught: Invalid Data Error: CSV data is empty
112
+
113
+ # エラーハンドリングテスト(フィールド数不一致)
114
+ ruby -I lib -e "
115
+ require 'rbcsv'
116
+ begin
117
+ data = [['name', 'age'], ['Alice', '25', 'Tokyo']]
118
+ RbCsv.write('/tmp/mismatch.csv', data)
119
+ rescue => e
120
+ puts 'Error caught: #{e.message}'
121
+ end
122
+ "
123
+ # 期待される出力: Error caught: Invalid Data Error: Field count mismatch at line 2: expected 2 fields, got 3 fields
124
+
125
+ # ファイル上書きテスト
126
+ ruby -I lib -e "
127
+ require 'rbcsv'
128
+ # 最初のデータを書き込み
129
+ RbCsv.write('/tmp/overwrite_test.csv', [['old'], ['data']])
130
+ puts 'First write:'
131
+ puts File.read('/tmp/overwrite_test.csv')
132
+
133
+ # 新しいデータで上書き
134
+ RbCsv.write('/tmp/overwrite_test.csv', [['new', 'data'], ['updated', 'content']])
135
+ puts 'After overwrite:'
136
+ puts File.read('/tmp/overwrite_test.csv')
137
+ "
138
+ # 期待される出力: 最初にold,dataが出力され、その後new,data形式に変わる
139
+ ```
140
+
90
141
  ## ビルドプロセス
91
142
 
92
143
  ### 自動ビルド(推奨)
@@ -205,28 +256,71 @@ cd ../..
205
256
 
206
257
  ## API設計
207
258
 
208
- ### 現在のAPI(v0.1.6+)
259
+ ### 現在のAPI(v0.1.7+)
260
+
261
+ ```ruby
262
+ # 関数ベースAPI(`!`サフィックスでtrim機能分離)
263
+ RbCsv.parse(csv_string) # 通常のパース
264
+ RbCsv.parse!(csv_string) # trim機能付きパース
265
+ RbCsv.read(file_path) # 通常のファイル読み込み
266
+ RbCsv.read!(file_path) # trim機能付きファイル読み込み
267
+ RbCsv.write(file_path, data) # CSVファイル書き込み
268
+
269
+ # データ形式
270
+ data = [
271
+ ["header1", "header2", "header3"], # ヘッダー行
272
+ ["value1", "value2", "value3"], # データ行
273
+ # ...
274
+ ]
275
+ ```
276
+
277
+ ### API進化の履歴
209
278
 
279
+ #### v0.1.6以前(オプションベース)
210
280
  ```ruby
211
- # 統一されたオプションベースAPI
212
- RbCsv.parse(csv_string, options = {})
213
- RbCsv.read(file_path, options = {})
281
+ RbCsv.parse(csv_string, {trim: true})
282
+ RbCsv.read(file_path, {trim: false})
283
+ ```
214
284
 
215
- # 利用可能なオプション
216
- options = {
217
- trim: true/false # 空白文字の除去(デフォルト: false)
218
- # 将来の拡張:
219
- # headers: true/false
220
- # delimiter: ','
221
- }
285
+ #### v0.1.7+(関数ベース)
286
+ ```ruby
287
+ RbCsv.parse!(csv_string) # trim版
288
+ RbCsv.write(file_path, data) # 新機能
222
289
  ```
223
290
 
224
291
  ### 実装アーキテクチャ
225
292
 
226
- 1. **parser.rs**: CsvParseOptionsと核となるCSV解析機能
227
- 2. **ruby_api.rs**: Rubyハッシュオプションの処理とMagnus API
293
+ 1. **parser.rs**: CSV解析・書き込みコア機能、エラーハンドリング
294
+ 2. **ruby_api.rs**: Ruby API関数、Magnus バインディング
228
295
  3. **lib.rs**: Magnus初期化と関数登録
229
- 4. **error.rs**: エラーハンドリングとRuby例外の変換
296
+ 4. **error.rs**: 包括的なエラーハンドリングとRuby例外変換
297
+
298
+ ### 開発時の重要な注意点
299
+
300
+ #### Ruby拡張ライブラリの特殊性
301
+
302
+ ```bash
303
+ # ❌ 避けるべき: cargo buildは直接使用しない
304
+ # cargo build は通常のRustライブラリ用で、Ruby拡張では適切にリンクされない
305
+
306
+ # ✅ 推奨される開発フロー:
307
+ cd ext/rbcsv
308
+ cargo check # 構文チェック(リンクなし)
309
+ cargo test # Rust単体テスト
310
+ cd ../..
311
+ bundle exec rake compile # Ruby拡張ビルド
312
+ bundle exec rspec # Ruby統合テスト
313
+ ```
314
+
315
+ #### ビルドコマンドの使い分け
316
+
317
+ | コマンド | 用途 | 場所 | 備考 |
318
+ |---------|------|------|------|
319
+ | `cargo check` | 構文・型チェック | ext/rbcsv | 高速、リンクなし |
320
+ | `cargo test` | Rust単体テスト | ext/rbcsv | Rubyシンボル不要 |
321
+ | `cargo build` | **使用不可** | - | リンクエラーが発生 |
322
+ | `bundle exec rake compile` | Ruby拡張ビルド | プロジェクトルート | 本番用ビルド |
323
+ | `bundle exec rspec` | 統合テスト | プロジェクトルート | 完全な機能テスト |
230
324
 
231
325
  ## リリース手順
232
326
 
@@ -488,4 +582,4 @@ cd ../..
488
582
  - [rb_sys Documentation](https://docs.rs/rb_sys/)
489
583
  - [Ruby Extension Guide](https://docs.ruby-lang.org/en/master/extension_rdoc.html)
490
584
  - [Cargo Book](https://doc.rust-lang.org/cargo/)
491
- - [RubyGems Guides](https://guides.rubygems.org/)
585
+ - [RubyGems Guides](https://guides.rubygems.org/)
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
- # RCsv
1
+ # RbCsv
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
4
-
5
- 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/r_csv`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ A fast CSV processing library for Ruby, built with Rust for high performance. RbCsv provides simple, efficient methods for parsing CSV strings, reading CSV files, and writing CSV data to files.
6
4
 
7
5
  ## Installation
8
6
 
@@ -22,7 +20,70 @@ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
22
20
 
23
21
  ## Usage
24
22
 
25
- TODO: Write usage instructions here
23
+ ### Parsing CSV strings
24
+
25
+ ```ruby
26
+ require 'rbcsv'
27
+
28
+ # Parse CSV string
29
+ csv_data = "name,age,city\nAlice,25,Tokyo\nBob,30,Osaka"
30
+ result = RbCsv.parse(csv_data)
31
+ # => [["name", "age", "city"], ["Alice", "25", "Tokyo"], ["Bob", "30", "Osaka"]]
32
+
33
+ # Parse CSV with automatic whitespace trimming
34
+ csv_with_spaces = " name , age , city \n Alice , 25 , Tokyo "
35
+ result = RbCsv.parse!(csv_with_spaces)
36
+ # => [["name", "age", "city"], ["Alice", "25", "Tokyo"]]
37
+ ```
38
+
39
+ ### Reading CSV files
40
+
41
+ ```ruby
42
+ # Read CSV file
43
+ result = RbCsv.read("data.csv")
44
+ # => [["name", "age", "city"], ["Alice", "25", "Tokyo"], ["Bob", "30", "Osaka"]]
45
+
46
+ # Read CSV file with automatic whitespace trimming
47
+ result = RbCsv.read!("data_with_spaces.csv")
48
+ # => [["name", "age", "city"], ["Alice", "25", "Tokyo"], ["Bob", "30", "Osaka"]]
49
+ ```
50
+
51
+ ### Writing CSV files
52
+
53
+ ```ruby
54
+ # Prepare data
55
+ data = [
56
+ ["name", "age", "city"],
57
+ ["Alice", "25", "Tokyo"],
58
+ ["Bob", "30", "Osaka"]
59
+ ]
60
+
61
+ # Write CSV data to file
62
+ RbCsv.write("output.csv", data)
63
+ # Creates a file with:
64
+ # name,age,city
65
+ # Alice,25,Tokyo
66
+ # Bob,30,Osaka
67
+ ```
68
+
69
+ ### Error Handling
70
+
71
+ RbCsv provides detailed error messages for various scenarios:
72
+
73
+ ```ruby
74
+ # Empty data
75
+ RbCsv.write("output.csv", [])
76
+ # => RuntimeError: Invalid Data Error: CSV data is empty
77
+
78
+ # Inconsistent field count
79
+ data = [["name", "age"], ["Alice", "25", "Tokyo"]] # 3 fields in second row
80
+ RbCsv.write("output.csv", data)
81
+ # => RuntimeError: Invalid Data Error: Field count mismatch at line 2: expected 2 fields, got 3 fields
82
+
83
+ # File not found
84
+ RbCsv.read("nonexistent.csv")
85
+ # => RuntimeError: IO Error: File not found: nonexistent.csv
86
+ ```
26
87
 
27
88
  ## Development
28
89
 
@@ -19,6 +19,10 @@ pub enum ErrorKind {
19
19
  FieldCountMismatch,
20
20
  // 空のCSVデータ
21
21
  EmptyData,
22
+ // 書き込み権限エラー
23
+ WritePermission,
24
+ // 無効なデータエラー
25
+ InvalidData,
22
26
  // その他のエラー
23
27
  #[allow(dead_code)]
24
28
  Other,
@@ -32,6 +36,8 @@ impl fmt::Display for CsvError {
32
36
  ErrorKind::Encoding => write!(f, "Encoding Error: {}", self.message),
33
37
  ErrorKind::FieldCountMismatch => write!(f, "Field Count Mismatch: {}", self.message),
34
38
  ErrorKind::EmptyData => write!(f, "Empty Data: {}", self.message),
39
+ ErrorKind::WritePermission => write!(f, "Write Permission Error: {}", self.message),
40
+ ErrorKind::InvalidData => write!(f, "Invalid Data Error: {}", self.message),
35
41
  ErrorKind::Other => write!(f, "Error: {}", self.message),
36
42
  }
37
43
  }
@@ -70,6 +76,14 @@ impl CsvError {
70
76
  pub fn empty_data() -> Self {
71
77
  Self::new(ErrorKind::EmptyData, "CSV data is empty")
72
78
  }
79
+
80
+ pub fn write_permission(message: impl Into<String>) -> Self {
81
+ Self::new(ErrorKind::WritePermission, message)
82
+ }
83
+
84
+ pub fn invalid_data(message: impl Into<String>) -> Self {
85
+ Self::new(ErrorKind::InvalidData, message)
86
+ }
73
87
  }
74
88
 
75
89
  // csv crateのエラーからの変換
data/ext/rbcsv/src/lib.rs CHANGED
@@ -3,7 +3,7 @@ mod parser;
3
3
  mod ruby_api;
4
4
 
5
5
  use magnus::{Object, Ruby};
6
- use ruby_api::{parse, parse_trim, read, read_trim};
6
+ use ruby_api::{parse, parse_trim, read, read_trim, write};
7
7
 
8
8
  #[magnus::init]
9
9
  fn init(ruby: &Ruby) -> Result<(), magnus::Error> {
@@ -13,6 +13,7 @@ fn init(ruby: &Ruby) -> Result<(), magnus::Error> {
13
13
  module.define_singleton_method("parse!", magnus::function!(parse_trim, 1))?;
14
14
  module.define_singleton_method("read", magnus::function!(read, 1))?;
15
15
  module.define_singleton_method("read!", magnus::function!(read_trim, 1))?;
16
+ module.define_singleton_method("write", magnus::function!(write, 2))?;
16
17
 
17
18
  Ok(())
18
19
  }
@@ -4,11 +4,9 @@ use std::path::Path;
4
4
 
5
5
  /// CSV解析のオプション設定
6
6
  #[derive(Debug, Clone)]
7
+ #[allow(dead_code)]
7
8
  pub struct CsvParseOptions {
8
9
  pub trim: bool,
9
- // 将来的な拡張用
10
- // pub headers: bool,
11
- // pub delimiter: char,
12
10
  }
13
11
 
14
12
  impl Default for CsvParseOptions {
@@ -35,13 +33,11 @@ pub fn parse_csv_core(input: &str, trim_config: csv::Trim) -> Result<Vec<Vec<Str
35
33
  return Err(CsvError::empty_data());
36
34
  }
37
35
 
38
- // エスケープシーケンスを実際の文字に変換
39
- let processed = escape_sanitize(input);
40
-
36
+ // CSV crate に任せて適切なパースを行う(escape_sanitize は削除)
41
37
  let mut reader = csv::ReaderBuilder::new()
42
38
  .has_headers(false) // ヘッダーを無効にして、すべての行を読み込む
43
39
  .trim(trim_config)
44
- .from_reader(processed.as_bytes());
40
+ .from_reader(input.as_bytes());
45
41
 
46
42
  let mut records = Vec::new();
47
43
 
@@ -77,13 +73,13 @@ pub fn parse_csv_core(input: &str, trim_config: csv::Trim) -> Result<Vec<Vec<Str
77
73
  }
78
74
 
79
75
  /// オプション設定を使ったCSV解析(文字列用)
80
- pub fn parse_csv_with_options(input: &str, options: &CsvParseOptions) -> Result<Vec<Vec<String>>, CsvError> {
76
+ pub fn _parse_csv_with_options(input: &str, options: &CsvParseOptions) -> Result<Vec<Vec<String>>, CsvError> {
81
77
  let trim_config = if options.trim { csv::Trim::All } else { csv::Trim::None };
82
78
  parse_csv_core(input, trim_config)
83
79
  }
84
80
 
85
81
  /// オプション設定を使ったCSV解析(ファイル用)
86
- pub fn parse_csv_file_with_options(file_path: &str, options: &CsvParseOptions) -> Result<Vec<Vec<String>>, CsvError> {
82
+ pub fn _parse_csv_file_with_options(file_path: &str, options: &CsvParseOptions) -> Result<Vec<Vec<String>>, CsvError> {
87
83
  let trim_config = if options.trim { csv::Trim::All } else { csv::Trim::None };
88
84
  parse_csv_file(file_path, trim_config)
89
85
  }
@@ -184,4 +180,127 @@ mod tests {
184
180
  assert_eq!(records[1], vec!["Alice", "25", "Tokyo"]);
185
181
  assert_eq!(records[2], vec!["Bob", "30", "Osaka"]);
186
182
  }
187
- }
183
+
184
+ #[test]
185
+ fn test_write_csv_file_basic() {
186
+
187
+ let temp_path = "/tmp/test_write_csv.csv";
188
+ let test_data = vec![
189
+ vec!["name".to_string(), "age".to_string(), "city".to_string()],
190
+ vec!["Alice".to_string(), "25".to_string(), "Tokyo".to_string()],
191
+ vec!["Bob".to_string(), "30".to_string(), "Osaka".to_string()],
192
+ ];
193
+
194
+ // ファイルに書き込み
195
+ let result = write_csv_file(temp_path, &test_data);
196
+ assert!(result.is_ok(), "Write should succeed");
197
+
198
+ // 書き込んだファイルを読み込んで検証
199
+ let content = std::fs::read_to_string(temp_path).expect("Failed to read written file");
200
+ let expected = "name,age,city\nAlice,25,Tokyo\nBob,30,Osaka\n";
201
+ assert_eq!(content, expected);
202
+
203
+ // クリーンアップ
204
+ let _ = std::fs::remove_file(temp_path);
205
+ }
206
+
207
+ #[test]
208
+ fn test_write_csv_file_empty_data() {
209
+ let temp_path = "/tmp/test_write_empty.csv";
210
+ let empty_data: Vec<Vec<String>> = vec![];
211
+
212
+ let result = write_csv_file(temp_path, &empty_data);
213
+ assert!(result.is_err());
214
+ if let Err(e) = result {
215
+ assert!(e.to_string().contains("CSV data is empty"));
216
+ }
217
+ }
218
+
219
+ #[test]
220
+ fn test_write_csv_file_field_count_mismatch() {
221
+ let temp_path = "/tmp/test_write_mismatch.csv";
222
+ let inconsistent_data = vec![
223
+ vec!["name".to_string(), "age".to_string()],
224
+ vec!["Alice".to_string(), "25".to_string(), "Tokyo".to_string()], // 3 fields instead of 2
225
+ ];
226
+
227
+ let result = write_csv_file(temp_path, &inconsistent_data);
228
+ assert!(result.is_err());
229
+ if let Err(e) = result {
230
+ assert!(e.to_string().contains("Field count mismatch"));
231
+ }
232
+ }
233
+
234
+ #[test]
235
+ fn test_write_csv_file_permission_denied() {
236
+ // 書き込み権限のないパスをテスト(rootディレクトリ)
237
+ let result = write_csv_file("/root/test.csv", &vec![vec!["test".to_string()]]);
238
+ assert!(result.is_err());
239
+ if let Err(e) = result {
240
+ // Permission deniedまたはParent directory does not existのいずれかになる
241
+ let error_msg = e.to_string();
242
+ assert!(error_msg.contains("Permission denied") || error_msg.contains("Parent directory does not exist"));
243
+ }
244
+ }
245
+ }
246
+
247
+ /// CSVデータをファイルに書き込む
248
+ pub fn write_csv_file(file_path: &str, data: &[Vec<String>]) -> Result<(), CsvError> {
249
+ // データ検証:空配列チェック
250
+ if data.is_empty() {
251
+ return Err(CsvError::invalid_data("CSV data is empty"));
252
+ }
253
+
254
+ // データ検証:各行のフィールド数一貫性チェック
255
+ if data.len() > 1 {
256
+ let expected_len = data[0].len();
257
+ for (line_num, row) in data.iter().enumerate() {
258
+ if row.len() != expected_len {
259
+ let error_msg = format!(
260
+ "Field count mismatch at line {}: expected {} fields, got {} fields",
261
+ line_num + 1,
262
+ expected_len,
263
+ row.len()
264
+ );
265
+ return Err(CsvError::invalid_data(error_msg));
266
+ }
267
+ }
268
+ }
269
+
270
+ // ファイルパス検証:親ディレクトリの存在確認
271
+ let path = Path::new(file_path);
272
+ if let Some(parent) = path.parent() {
273
+ if !parent.exists() {
274
+ return Err(CsvError::io(format!("Parent directory does not exist: {}", parent.display())));
275
+ }
276
+ }
277
+
278
+ // CSV Writer作成とデータ書き込み
279
+ let file = match fs::File::create(path) {
280
+ Ok(file) => file,
281
+ Err(e) => {
282
+ if e.kind() == std::io::ErrorKind::PermissionDenied {
283
+ return Err(CsvError::write_permission(format!("Permission denied: {}", file_path)));
284
+ }
285
+ return Err(CsvError::io(format!("Failed to create file '{}': {}", file_path, e)));
286
+ }
287
+ };
288
+
289
+ let mut writer = csv::WriterBuilder::new()
290
+ .has_headers(false)
291
+ .from_writer(file);
292
+
293
+ // データ書き込み
294
+ for row in data {
295
+ if let Err(e) = writer.write_record(row) {
296
+ return Err(CsvError::from(e));
297
+ }
298
+ }
299
+
300
+ // ファイルフラッシュ:データの確実な書き込み保証
301
+ if let Err(e) = writer.flush() {
302
+ return Err(CsvError::io(format!("Failed to flush data to file '{}': {}", file_path, e)));
303
+ }
304
+
305
+ Ok(())
306
+ }
@@ -1,5 +1,5 @@
1
1
  use magnus::{Error as MagnusError, Ruby};
2
- use crate::parser::{parse_csv_core, parse_csv_file};
2
+ use crate::parser::{parse_csv_core, parse_csv_file, write_csv_file};
3
3
 
4
4
  /// CSV文字列をパースする(通常版)
5
5
  ///
@@ -53,10 +53,23 @@ pub fn read_trim(ruby: &Ruby, file_path: String) -> Result<Vec<Vec<String>>, Mag
53
53
  .map_err(|e| MagnusError::new(ruby.exception_runtime_error(), e.to_string()))
54
54
  }
55
55
 
56
+ /// CSVファイルに書き込む
57
+ ///
58
+ /// # Arguments
59
+ /// * `ruby` - Ruby VMの参照
60
+ /// * `file_path` - 書き込み先ファイルのパス
61
+ /// * `data` - 書き込むCSVデータ(2次元配列)
62
+ ///
63
+ /// # Returns
64
+ /// * `Result<(), MagnusError>` - 成功時は空、失敗時はエラー
65
+ pub fn write(ruby: &Ruby, file_path: String, data: Vec<Vec<String>>) -> Result<(), MagnusError> {
66
+ write_csv_file(&file_path, &data)
67
+ .map_err(|e| MagnusError::new(ruby.exception_runtime_error(), e.to_string()))
68
+ }
69
+
56
70
 
57
71
  #[cfg(test)]
58
72
  mod tests {
59
- use super::*;
60
73
 
61
74
  #[test]
62
75
  fn test_parse_basic() {
data/lib/rbcsv/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RbCsv
4
- VERSION = "0.1.7"
4
+ VERSION = "0.1.8"
5
5
  end
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # RbCsv.write() 機能のテストスクリプト
5
+ #
6
+ # 実行方法:
7
+ # ruby test_write_functionality.rb
8
+ #
9
+ # 前提条件:
10
+ # - bundle exec rake compile でライブラリがビルド済みであること
11
+
12
+ require_relative 'lib/rbcsv'
13
+ require 'fileutils'
14
+
15
+ class RbCsvWriteTest
16
+ def initialize
17
+ @test_dir = '/tmp/rbcsv_write_tests'
18
+ @success_count = 0
19
+ @total_count = 0
20
+ setup_test_directory
21
+ end
22
+
23
+ def run_all_tests
24
+ puts "=" * 60
25
+ puts "RbCsv.write() 機能テスト開始"
26
+ puts "=" * 60
27
+ puts
28
+
29
+ test_basic_write
30
+ test_roundtrip_write_read
31
+ test_file_overwrite
32
+ test_empty_data_error
33
+ test_field_count_mismatch_error
34
+ test_single_row_write
35
+ test_unicode_content
36
+ test_special_characters
37
+
38
+ puts
39
+ puts "=" * 60
40
+ puts "テスト結果: #{@success_count}/#{@total_count} 成功"
41
+ puts "=" * 60
42
+
43
+ cleanup_test_directory
44
+
45
+ if @success_count == @total_count
46
+ puts "✅ すべてのテストが成功しました!"
47
+ exit 0
48
+ else
49
+ puts "❌ 一部のテストが失敗しました"
50
+ exit 1
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def setup_test_directory
57
+ FileUtils.mkdir_p(@test_dir)
58
+ puts "テストディレクトリを作成: #{@test_dir}"
59
+ end
60
+
61
+ def cleanup_test_directory
62
+ FileUtils.rm_rf(@test_dir)
63
+ puts "テストディレクトリを削除: #{@test_dir}"
64
+ end
65
+
66
+ def run_test(name)
67
+ @total_count += 1
68
+ print "#{name}... "
69
+
70
+ begin
71
+ yield
72
+ @success_count += 1
73
+ puts "✅ 成功"
74
+ rescue => e
75
+ puts "❌ 失敗: #{e.message}"
76
+ puts " #{e.backtrace.first}"
77
+ end
78
+ end
79
+
80
+ def test_basic_write
81
+ run_test("基本的なCSV書き込み") do
82
+ file_path = File.join(@test_dir, 'basic.csv')
83
+ data = [
84
+ ['name', 'age', 'city'],
85
+ ['Alice', '25', 'Tokyo'],
86
+ ['Bob', '30', 'Osaka']
87
+ ]
88
+
89
+ RbCsv.write(file_path, data)
90
+
91
+ content = File.read(file_path)
92
+ expected = "name,age,city\nAlice,25,Tokyo\nBob,30,Osaka\n"
93
+
94
+ raise "書き込み内容が期待値と異なります" unless content == expected
95
+ end
96
+ end
97
+
98
+ def test_roundtrip_write_read
99
+ run_test("書き込み→読み込みの往復テスト") do
100
+ file_path = File.join(@test_dir, 'roundtrip.csv')
101
+ original_data = [
102
+ ['product', 'price', 'category'],
103
+ ['Apple', '100', 'Fruit'],
104
+ ['Carrot', '50', 'Vegetable']
105
+ ]
106
+
107
+ RbCsv.write(file_path, original_data)
108
+ read_data = RbCsv.read(file_path)
109
+
110
+ raise "往復テストで元データと異なります" unless original_data == read_data
111
+ end
112
+ end
113
+
114
+ def test_file_overwrite
115
+ run_test("ファイル上書きテスト") do
116
+ file_path = File.join(@test_dir, 'overwrite.csv')
117
+
118
+ # 最初のデータ
119
+ first_data = [['old'], ['data']]
120
+ RbCsv.write(file_path, first_data)
121
+ first_content = File.read(file_path)
122
+
123
+ # 上書き
124
+ second_data = [['new', 'header'], ['updated', 'content']]
125
+ RbCsv.write(file_path, second_data)
126
+ second_content = File.read(file_path)
127
+
128
+ expected_first = "old\ndata\n"
129
+ expected_second = "new,header\nupdated,content\n"
130
+
131
+ raise "最初の書き込み内容が不正" unless first_content == expected_first
132
+ raise "上書き後の内容が不正" unless second_content == expected_second
133
+ end
134
+ end
135
+
136
+ def test_empty_data_error
137
+ run_test("空データエラーテスト") do
138
+ file_path = File.join(@test_dir, 'empty.csv')
139
+
140
+ error_raised = false
141
+ begin
142
+ RbCsv.write(file_path, [])
143
+ rescue RuntimeError => e
144
+ error_raised = true
145
+ raise "エラーメッセージが期待値と異なります" unless e.message.include?("CSV data is empty")
146
+ end
147
+
148
+ raise "空データでエラーが発生しませんでした" unless error_raised
149
+ end
150
+ end
151
+
152
+ def test_field_count_mismatch_error
153
+ run_test("フィールド数不一致エラーテスト") do
154
+ file_path = File.join(@test_dir, 'mismatch.csv')
155
+ inconsistent_data = [
156
+ ['name', 'age'],
157
+ ['Alice', '25', 'Tokyo'] # 3フィールド(期待は2フィールド)
158
+ ]
159
+
160
+ error_raised = false
161
+ begin
162
+ RbCsv.write(file_path, inconsistent_data)
163
+ rescue RuntimeError => e
164
+ error_raised = true
165
+ unless e.message.include?("Field count mismatch") && e.message.include?("line 2")
166
+ raise "エラーメッセージが期待値と異なります: #{e.message}"
167
+ end
168
+ end
169
+
170
+ raise "フィールド数不一致でエラーが発生しませんでした" unless error_raised
171
+ end
172
+ end
173
+
174
+ def test_single_row_write
175
+ run_test("単一行書き込みテスト") do
176
+ file_path = File.join(@test_dir, 'single.csv')
177
+ data = [['single', 'row', 'test']]
178
+
179
+ RbCsv.write(file_path, data)
180
+ content = File.read(file_path)
181
+ expected = "single,row,test\n"
182
+
183
+ raise "単一行の書き込み内容が不正" unless content == expected
184
+ end
185
+ end
186
+
187
+ def test_unicode_content
188
+ run_test("Unicode文字テスト") do
189
+ file_path = File.join(@test_dir, 'unicode.csv')
190
+ data = [
191
+ ['名前', '年齢', '都市'],
192
+ ['田中太郎', '30', '東京'],
193
+ ['山田花子', '25', '大阪'],
194
+ ['🎉', '😀', '🌸']
195
+ ]
196
+
197
+ RbCsv.write(file_path, data)
198
+ read_data = RbCsv.read(file_path)
199
+
200
+ raise "Unicode文字の往復テストが失敗" unless data == read_data
201
+ end
202
+ end
203
+
204
+ def test_special_characters
205
+ run_test("特殊文字テスト") do
206
+ file_path = File.join(@test_dir, 'special.csv')
207
+ data = [
208
+ ['field1', 'field2', 'field3'],
209
+ ['comma,test', 'quote"test', 'newline\ntest'],
210
+ ['tab\ttest', 'backslash\\test', 'normal']
211
+ ]
212
+
213
+ RbCsv.write(file_path, data)
214
+ read_data = RbCsv.read(file_path)
215
+
216
+ raise "特殊文字の往復テストが失敗" unless data == read_data
217
+ end
218
+ end
219
+ end
220
+
221
+ # テスト実行
222
+ if __FILE__ == $0
223
+ tester = RbCsvWriteTest.new
224
+ tester.run_all_tests
225
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rbcsv
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - fujitani sora
@@ -64,6 +64,7 @@ files:
64
64
  - test.rb
65
65
  - test_fixed.rb
66
66
  - test_install.rb
67
+ - test_write_functionality.rb
67
68
  homepage: https://github.com/fs0414/rbcsv
68
69
  licenses:
69
70
  - MIT