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.
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
 
data/ext/rbcsv/Cargo.toml CHANGED
@@ -11,5 +11,9 @@ crate-type = ["cdylib"]
11
11
 
12
12
  [dependencies]
13
13
  csv = "1.3.1"
14
- magnus = { version = "0.6.2" }
15
- rb-sys = { version = "0.9", features = ["link-ruby"] }
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,101 @@
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
+ WritePermission,
24
+ // 無効なデータエラー
25
+ InvalidData,
26
+ // その他のエラー
27
+ #[allow(dead_code)]
28
+ Other,
29
+ }
30
+
31
+ impl fmt::Display for CsvError {
32
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
33
+ match self.kind {
34
+ ErrorKind::Io => write!(f, "IO Error: {}", self.message),
35
+ ErrorKind::Parse => write!(f, "Parse Error: {}", self.message),
36
+ ErrorKind::Encoding => write!(f, "Encoding Error: {}", self.message),
37
+ ErrorKind::FieldCountMismatch => write!(f, "Field Count Mismatch: {}", self.message),
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),
41
+ ErrorKind::Other => write!(f, "Error: {}", self.message),
42
+ }
43
+ }
44
+ }
45
+
46
+ impl StdError for CsvError {}
47
+
48
+ impl CsvError {
49
+ pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
50
+ CsvError {
51
+ message: message.into(),
52
+ kind,
53
+ }
54
+ }
55
+
56
+ pub fn io(message: impl Into<String>) -> Self {
57
+ Self::new(ErrorKind::Io, message)
58
+ }
59
+
60
+ pub fn parse(message: impl Into<String>) -> Self {
61
+ Self::new(ErrorKind::Parse, message)
62
+ }
63
+
64
+ pub fn encoding(message: impl Into<String>) -> Self {
65
+ Self::new(ErrorKind::Encoding, message)
66
+ }
67
+
68
+ #[allow(dead_code)]
69
+ pub fn field_count_mismatch(expected: usize, actual: usize) -> Self {
70
+ Self::new(
71
+ ErrorKind::FieldCountMismatch,
72
+ format!("Expected {} fields, but got {}", expected, actual),
73
+ )
74
+ }
75
+
76
+ pub fn empty_data() -> Self {
77
+ Self::new(ErrorKind::EmptyData, "CSV data is empty")
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
+ }
87
+ }
88
+
89
+ // csv crateのエラーからの変換
90
+ impl From<csv::Error> for CsvError {
91
+ fn from(err: csv::Error) -> Self {
92
+ match err.kind() {
93
+ csv::ErrorKind::Io(_) => CsvError::io(err.to_string()),
94
+ csv::ErrorKind::Utf8 { .. } => CsvError::encoding(err.to_string()),
95
+ csv::ErrorKind::UnequalLengths { .. } => {
96
+ CsvError::new(ErrorKind::FieldCountMismatch, err.to_string())
97
+ }
98
+ _ => CsvError::parse(err.to_string()),
99
+ }
100
+ }
101
+ }
data/ext/rbcsv/src/lib.rs CHANGED
@@ -1,149 +1,20 @@
1
- use core::fmt;
1
+ mod error;
2
+ mod parser;
3
+ mod ruby_api;
2
4
 
3
- use std::error::Error as StdError;
4
-
5
- use magnus::{Error as MagnusError, Object};
6
-
7
- #[derive(Debug)]
8
- struct CustomError {
9
- message: String,
10
- kind: ErrorKind,
11
- }
12
-
13
- #[derive(Debug)]
14
- enum ErrorKind {
15
- Io,
16
- _Parse,
17
- _Network,
18
- _Other,
19
- }
20
-
21
- impl fmt::Display for CustomError {
22
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
23
- write!(f, "{:?}: {}", self.kind, self.message)
24
- }
25
- }
26
-
27
- impl StdError for CustomError {}
28
-
29
- impl CustomError {
30
- fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
31
- CustomError {
32
- message: message.into(),
33
- kind,
34
- }
35
- }
36
- }
37
-
38
- type Error = CustomError;
39
-
40
- fn escape_sanitize(s: String) -> String {
41
- s.replace("\\n", "\n")
42
- .replace("\\r", "\r")
43
- .replace("\\t", "\t")
44
- .replace("\\\"", "\"")
45
- .replace("\\\\", "\\")
46
- }
47
-
48
- fn parse(s: String) -> Result<Vec<Vec<String>>, MagnusError> {
49
- parse_with_options(s, false) // デフォルトはトリムなし
50
- .map_err(|e| MagnusError::new(magnus::exception::runtime_error(), e.to_string()))
51
- }
52
-
53
- fn parse_with_trim(s: String, trim_whitespace: bool) -> Result<Vec<Vec<String>>, MagnusError> {
54
- parse_with_options(s, trim_whitespace)
55
- .map_err(|e| MagnusError::new(magnus::exception::runtime_error(), e.to_string()))
56
- }
57
-
58
- fn parse_with_options(s: String, trim_whitespace: bool) -> Result<Vec<Vec<String>>, Error> {
59
- // エスケープシーケンスを実際の文字に変換
60
- let processed = escape_sanitize(s);
61
-
62
- let mut reader = csv::ReaderBuilder::new()
63
- .has_headers(false) // ヘッダーを無効にして、すべての行 を読み込む
64
- .trim(if trim_whitespace { csv::Trim::All } else { csv::Trim::None })
65
- .from_reader(processed.as_bytes());
66
-
67
- let mut records = Vec::new();
68
-
69
- for result in reader.records() {
70
- match result {
71
- Ok(record) => {
72
- let row: Vec<String> = record.iter().map(|field| field.to_string()).collect();
73
- println!("row {:?}", row);
74
- records.push(row);
75
- }
76
- Err(_e) => {
77
- return Err(Error::new(ErrorKind::Io, "File not found"));
78
- }
79
- }
80
- }
81
-
82
- Ok(records)
83
- }
5
+ use magnus::{Object, Ruby};
6
+ use ruby_api::{parse, parse_trim, read, read_trim, write};
84
7
 
85
8
  #[magnus::init]
86
- fn init() {
87
- let module = magnus::define_module("RbCsv").expect("Failed to define module");
88
- module.define_singleton_method("parse", magnus::function!(parse, 1)).expect("Failed to define parse method");
89
- module.define_singleton_method("parse_with_trim", magnus::function!(parse_with_trim, 2)).expect("Failed to define parse_with_trim method");
90
- }
91
-
92
- #[cfg(test)]
93
- mod tests {
94
-
95
- use super::*;
96
-
97
- #[test]
98
- fn test_parse_simple() {
99
- let csv_data = "a,b\n1,2";
100
- let result = parse(csv_data.to_string());
101
-
102
- assert!(result.is_ok());
9
+ fn init(ruby: &Ruby) -> Result<(), magnus::Error> {
10
+ let module = ruby.define_module("RbCsv")?;
103
11
 
104
- let records = result.unwrap();
105
- assert_eq!(records.len(), 2);
106
- assert_eq!(records[0], vec!["a", "b"]);
107
- assert_eq!(records[1], vec!["1", "2"]);
108
- }
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))?;
16
+ module.define_singleton_method("write", magnus::function!(write, 2))?;
109
17
 
110
- #[test]
111
- /// - シングルクォートの文字列のようなエスケープされた改行をテスト
112
- fn test_parse_with_escaped_newline() {
113
- let csv_data = "a,b\\n1,2";
114
- let result = parse(csv_data.to_string());
115
-
116
- assert!(result.is_ok());
117
-
118
- let records = result.unwrap();
119
- assert_eq!(records.len(), 2);
120
- assert_eq!(records[0], vec!["a", "b"]);
121
- assert_eq!(records[1], vec!["1", "2"]);
122
- }
123
-
124
- #[test]
125
- fn test_parse_with_trim() {
126
- let csv_data = " a , b \n 1 , 2 ";
127
- let result = parse_with_trim(csv_data.to_string(), true);
128
-
129
- assert!(result.is_ok());
130
-
131
- let records = result.unwrap();
132
- assert_eq!(records.len(), 2);
133
- assert_eq!(records[0], vec!["a", "b"]);
134
- assert_eq!(records[1], vec!["1", "2"]);
135
- }
136
-
137
- #[test]
138
- fn test_parse_without_trim() {
139
- let csv_data = " a , b \n 1 , 2 ";
140
- let result = parse_with_trim(csv_data.to_string(), false);
141
-
142
- assert!(result.is_ok());
143
-
144
- let records = result.unwrap();
145
- assert_eq!(records.len(), 2);
146
- assert_eq!(records[0], vec![" a ", " b "]);
147
- assert_eq!(records[1], vec![" 1 ", " 2 "]);
148
- }
18
+ Ok(())
149
19
  }
20
+
@@ -0,0 +1,306 @@
1
+ use crate::error::{CsvError, ErrorKind};
2
+ use std::fs;
3
+ use std::path::Path;
4
+
5
+ /// CSV解析のオプション設定
6
+ #[derive(Debug, Clone)]
7
+ #[allow(dead_code)]
8
+ pub struct CsvParseOptions {
9
+ pub trim: bool,
10
+ }
11
+
12
+ impl Default for CsvParseOptions {
13
+ fn default() -> Self {
14
+ Self {
15
+ trim: false,
16
+ }
17
+ }
18
+ }
19
+
20
+ /// エスケープシーケンスを実際の文字に変換
21
+ pub fn escape_sanitize(s: &str) -> String {
22
+ s.replace("\\n", "\n")
23
+ .replace("\\r", "\r")
24
+ .replace("\\t", "\t")
25
+ .replace("\\\"", "\"")
26
+ .replace("\\\\", "\\")
27
+ }
28
+
29
+ /// 基本的なCSVパース処理
30
+ pub fn parse_csv_core(input: &str, trim_config: csv::Trim) -> Result<Vec<Vec<String>>, CsvError> {
31
+ // 空のデータチェック
32
+ if input.trim().is_empty() {
33
+ return Err(CsvError::empty_data());
34
+ }
35
+
36
+ // CSV crate に任せて適切なパースを行う(escape_sanitize は削除)
37
+ let mut reader = csv::ReaderBuilder::new()
38
+ .has_headers(false) // ヘッダーを無効にして、すべての行を読み込む
39
+ .trim(trim_config)
40
+ .from_reader(input.as_bytes());
41
+
42
+ let mut records = Vec::new();
43
+
44
+ for (line_num, result) in reader.records().enumerate() {
45
+ match result {
46
+ Ok(record) => {
47
+ let row: Vec<String> = record.iter().map(|field| field.to_string()).collect();
48
+ records.push(row);
49
+ }
50
+ Err(e) => {
51
+ // フィールド数不一致エラーを詳細化
52
+ if let csv::ErrorKind::UnequalLengths { expected_len, len, .. } = e.kind() {
53
+ let error_msg = format!(
54
+ "Field count mismatch at line {}: expected {} fields, got {} fields",
55
+ line_num + 1,
56
+ expected_len,
57
+ len
58
+ );
59
+ return Err(CsvError::new(ErrorKind::FieldCountMismatch, error_msg));
60
+ }
61
+
62
+ // その他のcsvエラーを自動変換
63
+ return Err(CsvError::from(e));
64
+ }
65
+ }
66
+ }
67
+
68
+ if records.is_empty() {
69
+ return Err(CsvError::empty_data());
70
+ }
71
+
72
+ Ok(records)
73
+ }
74
+
75
+ /// オプション設定を使ったCSV解析(文字列用)
76
+ pub fn _parse_csv_with_options(input: &str, options: &CsvParseOptions) -> Result<Vec<Vec<String>>, CsvError> {
77
+ let trim_config = if options.trim { csv::Trim::All } else { csv::Trim::None };
78
+ parse_csv_core(input, trim_config)
79
+ }
80
+
81
+ /// オプション設定を使ったCSV解析(ファイル用)
82
+ pub fn _parse_csv_file_with_options(file_path: &str, options: &CsvParseOptions) -> Result<Vec<Vec<String>>, CsvError> {
83
+ let trim_config = if options.trim { csv::Trim::All } else { csv::Trim::None };
84
+ parse_csv_file(file_path, trim_config)
85
+ }
86
+
87
+ /// ファイルからCSVを読み込んでパースする
88
+ pub fn parse_csv_file(file_path: &str, trim_config: csv::Trim) -> Result<Vec<Vec<String>>, CsvError> {
89
+ // ファイルパスの検証
90
+ let path = Path::new(file_path);
91
+ if !path.exists() {
92
+ return Err(CsvError::io(format!("File not found: {}", file_path)));
93
+ }
94
+
95
+ if !path.is_file() {
96
+ return Err(CsvError::io(format!("Path is not a file: {}", file_path)));
97
+ }
98
+
99
+ // ファイル読み込み
100
+ let content = match fs::read_to_string(path) {
101
+ Ok(content) => content,
102
+ Err(e) => {
103
+ return Err(CsvError::io(format!("Failed to read file '{}': {}", file_path, e)));
104
+ }
105
+ };
106
+
107
+ // CSVパース
108
+ parse_csv_core(&content, trim_config)
109
+ }
110
+
111
+ #[cfg(test)]
112
+ mod tests {
113
+ use super::*;
114
+
115
+ #[test]
116
+ fn test_escape_sanitize() {
117
+ let input = "Hello\\nWorld\\t\\\"Test\\\"\\\\End";
118
+ let expected = "Hello\nWorld\t\"Test\"\\End";
119
+ assert_eq!(escape_sanitize(input), expected);
120
+ }
121
+
122
+ #[test]
123
+ fn test_parse_csv_core_basic() {
124
+ let csv_data = "a,b,c\n1,2,3";
125
+ let result = parse_csv_core(csv_data, csv::Trim::None);
126
+
127
+ assert!(result.is_ok());
128
+ let records = result.unwrap();
129
+ assert_eq!(records.len(), 2);
130
+ assert_eq!(records[0], vec!["a", "b", "c"]);
131
+ assert_eq!(records[1], vec!["1", "2", "3"]);
132
+ }
133
+
134
+ #[test]
135
+ fn test_parse_csv_file_not_found() {
136
+ let result = parse_csv_file("non_existent_file.csv", csv::Trim::None);
137
+
138
+ assert!(result.is_err());
139
+ if let Err(e) = result {
140
+ assert!(e.to_string().contains("File not found"));
141
+ }
142
+ }
143
+
144
+ #[test]
145
+ fn test_parse_csv_file_directory() {
146
+ // ディレクトリを指定した場合のテスト
147
+ let result = parse_csv_file(".", csv::Trim::None);
148
+
149
+ assert!(result.is_err());
150
+ if let Err(e) = result {
151
+ assert!(e.to_string().contains("Path is not a file"));
152
+ }
153
+ }
154
+
155
+ #[test]
156
+ fn test_parse_csv_file_with_temp_file() {
157
+ use std::io::Write;
158
+ use std::fs::File;
159
+
160
+ // 一時ファイルを作成
161
+ let temp_path = "/tmp/test_csv_file.csv";
162
+ let csv_content = "name,age,city\nAlice,25,Tokyo\nBob,30,Osaka";
163
+
164
+ {
165
+ let mut file = File::create(temp_path).expect("Failed to create temp file");
166
+ file.write_all(csv_content.as_bytes()).expect("Failed to write to temp file");
167
+ }
168
+
169
+ // ファイルからCSVを読み込み
170
+ let result = parse_csv_file(temp_path, csv::Trim::None);
171
+
172
+ // クリーンアップ
173
+ let _ = std::fs::remove_file(temp_path);
174
+
175
+ // 結果を検証
176
+ assert!(result.is_ok());
177
+ let records = result.unwrap();
178
+ assert_eq!(records.len(), 3);
179
+ assert_eq!(records[0], vec!["name", "age", "city"]);
180
+ assert_eq!(records[1], vec!["Alice", "25", "Tokyo"]);
181
+ assert_eq!(records[2], vec!["Bob", "30", "Osaka"]);
182
+ }
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
+ }