rbcsv 0.1.6 → 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 +4 -4
- data/.rust-analyzer.json +8 -0
- data/CHANGELOG.md +44 -0
- data/Cargo.lock +123 -45
- data/DEVELOPMENT.md +467 -129
- data/README.md +66 -5
- data/ext/rbcsv/Cargo.toml +6 -2
- data/ext/rbcsv/src/error.rs +101 -0
- data/ext/rbcsv/src/lib.rs +14 -143
- data/ext/rbcsv/src/parser.rs +306 -0
- data/ext/rbcsv/src/ruby_api.rs +111 -0
- data/lib/rbcsv/version.rb +1 -1
- data/test_write_functionality.rb +225 -0
- metadata +6 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
use magnus::{Error as MagnusError, Ruby};
|
|
2
|
+
use crate::parser::{parse_csv_core, parse_csv_file, write_csv_file};
|
|
3
|
+
|
|
4
|
+
/// CSV文字列をパースする(通常版)
|
|
5
|
+
///
|
|
6
|
+
/// # Arguments
|
|
7
|
+
/// * `ruby` - Ruby VMの参照
|
|
8
|
+
/// * `s` - パースするCSV文字列
|
|
9
|
+
///
|
|
10
|
+
/// # Returns
|
|
11
|
+
/// * `Result<Vec<Vec<String>>, MagnusError>` - パース結果またはエラー
|
|
12
|
+
pub fn parse(ruby: &Ruby, s: String) -> Result<Vec<Vec<String>>, MagnusError> {
|
|
13
|
+
parse_csv_core(&s, csv::Trim::None)
|
|
14
|
+
.map_err(|e| MagnusError::new(ruby.exception_runtime_error(), e.to_string()))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// CSV文字列をパースする(trim版)
|
|
18
|
+
///
|
|
19
|
+
/// # Arguments
|
|
20
|
+
/// * `ruby` - Ruby VMの参照
|
|
21
|
+
/// * `s` - パースするCSV文字列
|
|
22
|
+
///
|
|
23
|
+
/// # Returns
|
|
24
|
+
/// * `Result<Vec<Vec<String>>, MagnusError>` - パース結果またはエラー
|
|
25
|
+
pub fn parse_trim(ruby: &Ruby, s: String) -> Result<Vec<Vec<String>>, MagnusError> {
|
|
26
|
+
parse_csv_core(&s, csv::Trim::All)
|
|
27
|
+
.map_err(|e| MagnusError::new(ruby.exception_runtime_error(), e.to_string()))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// CSVファイルを読み込む(通常版)
|
|
31
|
+
///
|
|
32
|
+
/// # Arguments
|
|
33
|
+
/// * `ruby` - Ruby VMの参照
|
|
34
|
+
/// * `file_path` - 読み込むCSVファイルのパス
|
|
35
|
+
///
|
|
36
|
+
/// # Returns
|
|
37
|
+
/// * `Result<Vec<Vec<String>>, MagnusError>` - パース結果またはエラー
|
|
38
|
+
pub fn read(ruby: &Ruby, file_path: String) -> Result<Vec<Vec<String>>, MagnusError> {
|
|
39
|
+
parse_csv_file(&file_path, csv::Trim::None)
|
|
40
|
+
.map_err(|e| MagnusError::new(ruby.exception_runtime_error(), e.to_string()))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// CSVファイルを読み込む(trim版)
|
|
44
|
+
///
|
|
45
|
+
/// # Arguments
|
|
46
|
+
/// * `ruby` - Ruby VMの参照
|
|
47
|
+
/// * `file_path` - 読み込むCSVファイルのパス
|
|
48
|
+
///
|
|
49
|
+
/// # Returns
|
|
50
|
+
/// * `Result<Vec<Vec<String>>, MagnusError>` - パース結果またはエラー
|
|
51
|
+
pub fn read_trim(ruby: &Ruby, file_path: String) -> Result<Vec<Vec<String>>, MagnusError> {
|
|
52
|
+
parse_csv_file(&file_path, csv::Trim::All)
|
|
53
|
+
.map_err(|e| MagnusError::new(ruby.exception_runtime_error(), e.to_string()))
|
|
54
|
+
}
|
|
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
|
+
|
|
70
|
+
|
|
71
|
+
#[cfg(test)]
|
|
72
|
+
mod tests {
|
|
73
|
+
|
|
74
|
+
#[test]
|
|
75
|
+
fn test_parse_basic() {
|
|
76
|
+
let csv_data = "a,b,c\n1,2,3";
|
|
77
|
+
let result = crate::parser::parse_csv_core(csv_data, csv::Trim::None);
|
|
78
|
+
|
|
79
|
+
assert!(result.is_ok());
|
|
80
|
+
let records = result.unwrap();
|
|
81
|
+
assert_eq!(records.len(), 2);
|
|
82
|
+
assert_eq!(records[0], vec!["a", "b", "c"]);
|
|
83
|
+
assert_eq!(records[1], vec!["1", "2", "3"]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn test_parse_with_trim_enabled() {
|
|
88
|
+
let csv_data = " a , b , c \n 1 , 2 , 3 ";
|
|
89
|
+
let result = crate::parser::parse_csv_core(csv_data, csv::Trim::All);
|
|
90
|
+
|
|
91
|
+
assert!(result.is_ok());
|
|
92
|
+
let records = result.unwrap();
|
|
93
|
+
assert_eq!(records[0], vec!["a", "b", "c"]);
|
|
94
|
+
assert_eq!(records[1], vec!["1", "2", "3"]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#[test]
|
|
98
|
+
fn test_parse_with_trim_disabled() {
|
|
99
|
+
let csv_data = " a , b , c \n 1 , 2 , 3 ";
|
|
100
|
+
let result = crate::parser::parse_csv_core(csv_data, csv::Trim::None);
|
|
101
|
+
|
|
102
|
+
assert!(result.is_ok());
|
|
103
|
+
let records = result.unwrap();
|
|
104
|
+
assert_eq!(records[0], vec![" a ", " b ", " c "]);
|
|
105
|
+
assert_eq!(records[1], vec![" 1 ", " 2 ", " 3 "]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Note: Ruby API functions that return MagnusError cannot be tested
|
|
109
|
+
// in unit tests because they require a Ruby VM context.
|
|
110
|
+
// File reading functionality is tested in the parser module.
|
|
111
|
+
}
|
data/lib/rbcsv/version.rb
CHANGED
|
@@ -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.
|
|
4
|
+
version: 0.1.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- fujitani sora
|
|
@@ -33,6 +33,7 @@ extensions:
|
|
|
33
33
|
extra_rdoc_files: []
|
|
34
34
|
files:
|
|
35
35
|
- ".ruby-version"
|
|
36
|
+
- ".rust-analyzer.json"
|
|
36
37
|
- ".serena/.gitignore"
|
|
37
38
|
- ".serena/memories/code_style_conventions.md"
|
|
38
39
|
- ".serena/memories/project_overview.md"
|
|
@@ -50,7 +51,10 @@ files:
|
|
|
50
51
|
- benchmark.rb
|
|
51
52
|
- ext/rbcsv/Cargo.toml
|
|
52
53
|
- ext/rbcsv/extconf.rb
|
|
54
|
+
- ext/rbcsv/src/error.rs
|
|
53
55
|
- ext/rbcsv/src/lib.rs
|
|
56
|
+
- ext/rbcsv/src/parser.rs
|
|
57
|
+
- ext/rbcsv/src/ruby_api.rs
|
|
54
58
|
- lib/rbcsv.rb
|
|
55
59
|
- lib/rbcsv/version.rb
|
|
56
60
|
- output_comparison.rb
|
|
@@ -60,6 +64,7 @@ files:
|
|
|
60
64
|
- test.rb
|
|
61
65
|
- test_fixed.rb
|
|
62
66
|
- test_install.rb
|
|
67
|
+
- test_write_functionality.rb
|
|
63
68
|
homepage: https://github.com/fs0414/rbcsv
|
|
64
69
|
licenses:
|
|
65
70
|
- MIT
|