rbcsv 0.1.7 → 0.2.0

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.
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../../lib/rbcsv'
5
+
6
+ puts "=== RbCsv 型認識機能テスト ==="
7
+ puts
8
+
9
+ # テストデータ
10
+ csv_data = <<~CSV
11
+ name,age,score,rating
12
+ Alice,25,85.5,A
13
+ Bob,30,92,B+
14
+ Charlie,0,100.0,S
15
+ CSV
16
+
17
+ puts "元のCSVデータ:"
18
+ puts csv_data
19
+ puts
20
+
21
+ # 通常のparseテスト(すべて文字列)
22
+ puts "1. RbCsv.parse (すべて文字列):"
23
+ result = RbCsv.parse(csv_data)
24
+ result.each_with_index do |row, i|
25
+ puts "Row #{i}: #{row.inspect}"
26
+ if i > 0 # ヘッダー以外
27
+ puts " age (#{row[1].class}): #{row[1]}"
28
+ puts " score (#{row[2].class}): #{row[2]}"
29
+ end
30
+ end
31
+ puts
32
+
33
+ # 型認識parseテスト
34
+ puts "2. RbCsv.parse_typed (数値は数値型):"
35
+ result_typed = RbCsv.parse_typed(csv_data)
36
+ result_typed.each_with_index do |row, i|
37
+ puts "Row #{i}: #{row.inspect}"
38
+ if i > 0 # ヘッダー以外
39
+ puts " age (#{row[1].class}): #{row[1]}"
40
+ puts " score (#{row[2].class}): #{row[2]}"
41
+ puts " 計算可能: age * 2 = #{row[1] * 2}"
42
+ end
43
+ end
44
+ puts
45
+
46
+ # エッジケースのテスト
47
+ edge_case_csv = <<~CSV
48
+ type,value
49
+ integer,123
50
+ negative,-456
51
+ float,45.6
52
+ scientific,1.23e-4
53
+ empty,
54
+ text,hello world
55
+ mixed,123abc
56
+ CSV
57
+
58
+ puts "3. エッジケーステスト:"
59
+ puts "CSVデータ:"
60
+ puts edge_case_csv
61
+ puts
62
+
63
+ puts "RbCsv.parse_typed の結果:"
64
+ result_edge = RbCsv.parse_typed(edge_case_csv)
65
+ result_edge.each_with_index do |row, i|
66
+ if i > 0 # ヘッダー以外
67
+ value = row[1]
68
+ type_name = value.class.name
69
+ puts "#{row[0]}: #{value.inspect} (#{type_name})"
70
+ end
71
+ end
72
+ puts
73
+
74
+ # trim版のテスト
75
+ csv_with_spaces = " name , age , score \n Alice , 25 , 85.5 "
76
+
77
+ puts "4. RbCsv.parse_typed! (trim + 型認識):"
78
+ puts "CSVデータ(空白付き): #{csv_with_spaces.inspect}"
79
+ result_trim = RbCsv.parse_typed!(csv_with_spaces)
80
+ result_trim.each do |row|
81
+ puts "Row: #{row.inspect}"
82
+ end
83
+ puts
84
+
85
+ # ファイル書き込み→型認識読み込みテスト
86
+ test_file = '/tmp/test_typed.csv'
87
+ write_data = [
88
+ ['product', 'price', 'quantity', 'in_stock'],
89
+ ['Apple', '100', '50', 'true'],
90
+ ['Orange', '80.5', '30', 'false'],
91
+ ['Banana', '60.25', '0', 'yes']
92
+ ]
93
+
94
+ puts "5. ファイル書き込み→型認識読み込みテスト:"
95
+ RbCsv.write(test_file, write_data)
96
+ puts "書き込み完了: #{test_file}"
97
+
98
+ read_typed = RbCsv.read_typed(test_file)
99
+ puts "RbCsv.read_typed の結果:"
100
+ read_typed.each_with_index do |row, i|
101
+ puts "Row #{i}: #{row.inspect}"
102
+ if i > 0
103
+ puts " price (#{row[1].class}): #{row[1]}"
104
+ puts " quantity (#{row[2].class}): #{row[2]}"
105
+ end
106
+ end
107
+ puts
108
+
109
+ puts "=== テスト完了 ==="
@@ -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
data/ext/rbcsv/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rbcsv"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  edition = "2021"
5
5
  authors = ["fujitani sora <fujitanisora0414@gmail.com>"]
6
6
  license = "MIT"
@@ -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,9 +76,17 @@ 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
- // csv crateのエラーからの変換
89
+ // csv crate error to CsvError conversion
76
90
  impl From<csv::Error> for CsvError {
77
91
  fn from(err: csv::Error) -> Self {
78
92
  match err.kind() {
@@ -84,4 +98,4 @@ impl From<csv::Error> for CsvError {
84
98
  _ => CsvError::parse(err.to_string()),
85
99
  }
86
100
  }
87
- }
101
+ }
data/ext/rbcsv/src/lib.rs CHANGED
@@ -1,9 +1,10 @@
1
1
  mod error;
2
2
  mod parser;
3
3
  mod ruby_api;
4
+ mod value;
4
5
 
5
6
  use magnus::{Object, Ruby};
6
- use ruby_api::{parse, parse_trim, read, read_trim};
7
+ use ruby_api::{parse, parse_trim, read, read_trim, write, parse_typed, parse_typed_trim, read_typed, read_typed_trim};
7
8
 
8
9
  #[magnus::init]
9
10
  fn init(ruby: &Ruby) -> Result<(), magnus::Error> {
@@ -13,6 +14,13 @@ fn init(ruby: &Ruby) -> Result<(), magnus::Error> {
13
14
  module.define_singleton_method("parse!", magnus::function!(parse_trim, 1))?;
14
15
  module.define_singleton_method("read", magnus::function!(read, 1))?;
15
16
  module.define_singleton_method("read!", magnus::function!(read_trim, 1))?;
17
+ module.define_singleton_method("write", magnus::function!(write, 2))?;
18
+
19
+ // typed variants
20
+ module.define_singleton_method("parse_typed", magnus::function!(parse_typed, 1))?;
21
+ module.define_singleton_method("parse_typed!", magnus::function!(parse_typed_trim, 1))?;
22
+ module.define_singleton_method("read_typed", magnus::function!(read_typed, 1))?;
23
+ module.define_singleton_method("read_typed!", magnus::function!(read_typed_trim, 1))?;
16
24
 
17
25
  Ok(())
18
26
  }