1use serde::{Deserialize, Serialize, de::DeserializeOwned};
2use std::fmt;
3
4pub mod account_transfer;
5pub mod account_transfer_result;
6mod fixed;
7pub mod general_transfer;
8pub mod payment_notice;
9pub mod payroll_transfer;
10pub mod transfer_account_inquiry;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
13#[serde(untagged)]
14pub enum ParsedFile {
15 GeneralTransfer(general_transfer::File),
16 PayrollTransfer(payroll_transfer::File),
17 AccountTransfer(account_transfer::File),
18 AccountTransferResult(account_transfer_result::File),
19 TransferAccountInquiry(transfer_account_inquiry::File),
20 PaymentNotice(payment_notice::File),
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum FileType {
25 Auto,
26 GeneralTransfer,
27 PayrollTransfer,
28 AccountTransfer,
29 AccountTransferResult,
30 TransferAccountInquiry,
31 PaymentNotice,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum CodeDivision {
36 #[default]
37 Jis,
38 Ebcdic,
39}
40
41impl CodeDivision {
42 pub const fn as_u8(self) -> u8 {
43 match self {
44 Self::Jis => 0,
45 Self::Ebcdic => 1,
46 }
47 }
48
49 pub const fn from_u8(value: u8) -> Option<Self> {
50 match value {
51 0 => Some(Self::Jis),
52 1 => Some(Self::Ebcdic),
53 _ => None,
54 }
55 }
56}
57
58impl Serialize for CodeDivision {
59 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
60 where
61 S: serde::Serializer,
62 {
63 serializer.serialize_u8(self.as_u8())
64 }
65}
66
67impl<'de> Deserialize<'de> for CodeDivision {
68 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
69 where
70 D: serde::Deserializer<'de>,
71 {
72 struct Visitor;
73
74 impl serde::de::Visitor<'_> for Visitor {
75 type Value = CodeDivision;
76
77 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78 formatter.write_str("0 or 1")
79 }
80
81 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
82 where
83 E: serde::de::Error,
84 {
85 let value = u8::try_from(value).map_err(E::custom)?;
86 CodeDivision::from_u8(value)
87 .ok_or_else(|| E::custom(format!("invalid code division {value}")))
88 }
89
90 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
91 where
92 E: serde::de::Error,
93 {
94 match value {
95 "0" => Ok(CodeDivision::Jis),
96 "1" => Ok(CodeDivision::Ebcdic),
97 other => Err(E::custom(format!("invalid code division {other:?}"))),
98 }
99 }
100 }
101
102 deserializer.deserialize_any(Visitor)
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum Encoding {
108 Ascii,
109 Jis,
110 Ebcdic,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum LineEnding {
115 None,
116 Lf,
117 Crlf,
118}
119
120impl LineEnding {
121 pub(crate) const fn as_bytes(self) -> &'static [u8] {
122 match self {
123 Self::None => b"",
124 Self::Lf => b"\n",
125 Self::Crlf => b"\r\n",
126 }
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub struct OutputFormat {
132 pub encoding: Encoding,
133 pub line_ending: LineEnding,
134 pub eof: bool,
135}
136
137impl OutputFormat {
138 pub const fn canonical() -> Self {
139 Self {
140 encoding: Encoding::Jis,
141 line_ending: LineEnding::None,
142 eof: false,
143 }
144 }
145
146 pub const fn readable() -> Self {
147 Self {
148 encoding: Encoding::Jis,
149 line_ending: LineEnding::Lf,
150 eof: false,
151 }
152 }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub enum Error {
157 UnsupportedEncoding(Encoding),
158 AmbiguousInput(String),
159 InvalidInput(String),
160 InvalidField {
161 record: &'static str,
162 field: &'static str,
163 message: String,
164 },
165 Validation(String),
166 Serde(String),
167}
168
169impl core::fmt::Display for Error {
170 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
171 match self {
172 Self::UnsupportedEncoding(encoding) => {
173 write!(f, "unsupported encoding: {encoding:?}")
174 }
175 Self::AmbiguousInput(message) => f.write_str(message),
176 Self::InvalidInput(message) => f.write_str(message),
177 Self::InvalidField {
178 record,
179 field,
180 message,
181 } => {
182 write!(f, "{record}.{field}: {message}")
183 }
184 Self::Validation(message) => f.write_str(message),
185 Self::Serde(message) => f.write_str(message),
186 }
187 }
188}
189
190impl std::error::Error for Error {}
191
192impl From<serde_json::Error> for Error {
193 fn from(error: serde_json::Error) -> Self {
194 Self::Serde(error.to_string())
195 }
196}
197
198pub fn from_bytes<T>(input: &[u8]) -> Result<T, Error>
199where
200 T: DeserializeOwned,
201{
202 from_bytes_as(input, FileType::Auto)
203}
204
205pub fn from_bytes_as<T>(input: &[u8], file_type: FileType) -> Result<T, Error>
206where
207 T: DeserializeOwned,
208{
209 let file = parse_as(input, file_type)?;
210 let value = serde_json::to_value(file)?;
211 Ok(serde_json::from_value(value)?)
212}
213
214pub fn parse(input: &[u8]) -> Result<ParsedFile, Error> {
215 parse_as(input, FileType::Auto)
216}
217
218pub fn parse_as(input: &[u8], file_type: FileType) -> Result<ParsedFile, Error> {
219 match file_type {
220 FileType::Auto => parse_auto(input),
221 FileType::GeneralTransfer => parse_general_transfer(input).map(ParsedFile::GeneralTransfer),
222 FileType::PayrollTransfer => parse_payroll_transfer(input).map(ParsedFile::PayrollTransfer),
223 FileType::AccountTransfer => parse_account_transfer(input).map(ParsedFile::AccountTransfer),
224 FileType::AccountTransferResult => {
225 parse_account_transfer_result(input).map(ParsedFile::AccountTransferResult)
226 }
227 FileType::TransferAccountInquiry => {
228 parse_transfer_account_inquiry(input).map(ParsedFile::TransferAccountInquiry)
229 }
230 FileType::PaymentNotice => parse_payment_notice(input).map(ParsedFile::PaymentNotice),
231 }
232}
233
234pub fn parse_general_transfer(input: &[u8]) -> Result<general_transfer::File, Error> {
235 general_transfer::parse(input)
236}
237
238pub fn parse_payroll_transfer(input: &[u8]) -> Result<payroll_transfer::File, Error> {
239 payroll_transfer::parse(input)
240}
241
242pub fn parse_account_transfer(input: &[u8]) -> Result<account_transfer::File, Error> {
243 account_transfer::parse(input)
244}
245
246pub fn parse_account_transfer_result(input: &[u8]) -> Result<account_transfer_result::File, Error> {
247 account_transfer_result::parse(input)
248}
249
250pub fn parse_transfer_account_inquiry(
251 input: &[u8],
252) -> Result<transfer_account_inquiry::File, Error> {
253 transfer_account_inquiry::parse(input)
254}
255
256pub fn parse_payment_notice(input: &[u8]) -> Result<payment_notice::File, Error> {
257 payment_notice::parse(input)
258}
259
260fn parse_auto(input: &[u8]) -> Result<ParsedFile, Error> {
261 let mut matches = Vec::new();
262 let mut errors = Vec::new();
263
264 match general_transfer::parse(input) {
265 Ok(file) => matches.push(("general transfer", ParsedFile::GeneralTransfer(file))),
266 Err(error) => errors.push(("general transfer", error)),
267 }
268 match payroll_transfer::parse(input) {
269 Ok(file) => matches.push(("payroll transfer", ParsedFile::PayrollTransfer(file))),
270 Err(error) => errors.push(("payroll transfer", error)),
271 }
272 match account_transfer::parse(input) {
273 Ok(file) => matches.push((
274 "account transfer request",
275 ParsedFile::AccountTransfer(file),
276 )),
277 Err(error) => errors.push(("account transfer request", error)),
278 }
279 match account_transfer_result::parse(input) {
280 Ok(file) => matches.push((
281 "account transfer result",
282 ParsedFile::AccountTransferResult(file),
283 )),
284 Err(error) => errors.push(("account transfer result", error)),
285 }
286 match transfer_account_inquiry::parse(input) {
287 Ok(file) => matches.push((
288 "transfer account inquiry",
289 ParsedFile::TransferAccountInquiry(file),
290 )),
291 Err(error) => errors.push(("transfer account inquiry", error)),
292 }
293 match payment_notice::parse(input) {
294 Ok(file) => matches.push(("payment notice", ParsedFile::PaymentNotice(file))),
295 Err(error) => errors.push(("payment notice", error)),
296 }
297
298 match matches.len() {
299 1 => Ok(matches.pop().expect("one match").1),
300 0 => {
301 let summary = errors
302 .into_iter()
303 .map(|(name, error)| format!("{name}: {error}"))
304 .collect::<Vec<_>>()
305 .join("; ");
306 Err(Error::InvalidInput(format!(
307 "unsupported zengin file: {summary}"
308 )))
309 }
310 _ => {
311 let names = matches
312 .into_iter()
313 .map(|(name, _)| name)
314 .collect::<Vec<_>>()
315 .join(", ");
316 Err(Error::AmbiguousInput(format!(
317 "input is valid as both or more supported file types ({names}); pass an explicit file type"
318 )))
319 }
320 }
321}
322
323pub fn to_bytes<T>(value: &T, format: OutputFormat) -> Result<Vec<u8>, Error>
324where
325 T: Serialize,
326{
327 to_bytes_as(value, FileType::Auto, format)
328}
329
330pub fn to_bytes_as<T>(
331 value: &T,
332 file_type: FileType,
333 format: OutputFormat,
334) -> Result<Vec<u8>, Error>
335where
336 T: Serialize,
337{
338 let value = serde_json::to_value(value)?;
339 match file_type {
340 FileType::Auto => write_auto_value(&value, format),
341 FileType::GeneralTransfer => {
342 write_value_as::<general_transfer::File>(&value, format, general_transfer::write)
343 }
344 FileType::PayrollTransfer => {
345 write_value_as::<payroll_transfer::File>(&value, format, payroll_transfer::write)
346 }
347 FileType::AccountTransfer => {
348 write_value_as::<account_transfer::File>(&value, format, account_transfer::write)
349 }
350 FileType::AccountTransferResult => write_value_as::<account_transfer_result::File>(
351 &value,
352 format,
353 account_transfer_result::write,
354 ),
355 FileType::TransferAccountInquiry => write_value_as::<transfer_account_inquiry::File>(
356 &value,
357 format,
358 transfer_account_inquiry::write,
359 ),
360 FileType::PaymentNotice => {
361 write_value_as::<payment_notice::File>(&value, format, payment_notice::write)
362 }
363 }
364}
365
366fn write_value_as<T>(
367 value: &serde_json::Value,
368 format: OutputFormat,
369 write: fn(&T, OutputFormat) -> Result<Vec<u8>, Error>,
370) -> Result<Vec<u8>, Error>
371where
372 T: DeserializeOwned,
373{
374 let file = serde_json::from_value(value.clone())?;
375 write(&file, format)
376}
377
378fn write_auto_value(value: &serde_json::Value, format: OutputFormat) -> Result<Vec<u8>, Error> {
379 let mut matches = Vec::new();
380
381 if let Ok(file) = serde_json::from_value::<general_transfer::File>(value.clone()) {
382 matches.push(("general transfer", general_transfer::write(&file, format)?));
383 }
384 if let Ok(file) = serde_json::from_value::<payroll_transfer::File>(value.clone()) {
385 matches.push(("payroll transfer", payroll_transfer::write(&file, format)?));
386 }
387 if let Ok(file) = serde_json::from_value::<account_transfer::File>(value.clone()) {
388 matches.push((
389 "account transfer request",
390 account_transfer::write(&file, format)?,
391 ));
392 }
393 if let Ok(file) = serde_json::from_value::<account_transfer_result::File>(value.clone()) {
394 matches.push((
395 "account transfer result",
396 account_transfer_result::write(&file, format)?,
397 ));
398 }
399 if let Ok(file) = serde_json::from_value::<transfer_account_inquiry::File>(value.clone()) {
400 matches.push((
401 "transfer account inquiry",
402 transfer_account_inquiry::write(&file, format)?,
403 ));
404 }
405 if let Ok(file) = serde_json::from_value::<payment_notice::File>(value.clone()) {
406 matches.push(("payment notice", payment_notice::write(&file, format)?));
407 }
408
409 match matches.len() {
410 1 => Ok(matches.pop().expect("one match").1),
411 0 => Err(Error::InvalidInput(
412 "unsupported zengin output value; pass a supported file type".to_string(),
413 )),
414 _ => {
415 let names = matches
416 .into_iter()
417 .map(|(name, _)| name)
418 .collect::<Vec<_>>()
419 .join(", ");
420 Err(Error::AmbiguousInput(format!(
421 "value can be written as multiple supported file types ({names}); pass an explicit file type"
422 )))
423 }
424 }
425}
426
427#[cfg(doctest)]
428mod readme_doctests {
429 doc_comment::doctest!("../../../README.md");
430}
431
432#[cfg(test)]
433mod tests {
434 use super::{
435 CodeDivision, Encoding, Error, FileType, LineEnding, OutputFormat,
436 account_transfer::Detail, account_transfer::End, account_transfer::File,
437 account_transfer::Header, account_transfer::Trailer, from_bytes_as, parse_account_transfer,
438 to_bytes,
439 };
440
441 fn sample_file() -> File {
442 File {
443 header: Header {
444 kind_code: 91,
445 code_division: CodeDivision::Jis,
446 collector_code: "1234567890".to_string(),
447 collection_date: "0430".to_string(),
448 collector_name: "ACME COLLECT".to_string(),
449 bank_code: "0001".to_string(),
450 bank_name: "BANK ALPHA".to_string(),
451 branch_code: "123".to_string(),
452 branch_name: "MAIN BRANCH".to_string(),
453 account_type: 1,
454 account_number: "7654321".to_string(),
455 },
456 details: vec![Detail {
457 bank_code: "0005".to_string(),
458 bank_name: "BANK BETA".to_string(),
459 branch_code: "001".to_string(),
460 branch_name: "WEST".to_string(),
461 account_type: 1,
462 account_number: "1234567".to_string(),
463 payer_name: "TARO YAMADA".to_string(),
464 amount: 1200,
465 new_code: "0".to_string(),
466 customer_number: "00000000001234567890".to_string(),
467 }],
468 trailer: Trailer {
469 record_count: 1,
470 total_amount: 1200,
471 },
472 end: End,
473 }
474 }
475
476 fn sample_jis_file() -> File {
477 File {
478 header: Header {
479 kind_code: 91,
480 code_division: CodeDivision::Jis,
481 collector_code: "1234567890".to_string(),
482 collection_date: "0430".to_string(),
483 collector_name: "テストシュウキン".to_string(),
484 bank_code: "0001".to_string(),
485 bank_name: "テストギンコウ".to_string(),
486 branch_code: "123".to_string(),
487 branch_name: "ホンテン".to_string(),
488 account_type: 1,
489 account_number: "7654321".to_string(),
490 },
491 details: vec![Detail {
492 bank_code: "0005".to_string(),
493 bank_name: "テストギンコウ".to_string(),
494 branch_code: "001".to_string(),
495 branch_name: "シテン".to_string(),
496 account_type: 1,
497 account_number: "1234567".to_string(),
498 payer_name: "ヤマダタロウ".to_string(),
499 amount: 1200,
500 new_code: "0".to_string(),
501 customer_number: "00000000001234567890".to_string(),
502 }],
503 trailer: Trailer {
504 record_count: 1,
505 total_amount: 1200,
506 },
507 end: End,
508 }
509 }
510
511 #[test]
512 fn roundtrips_readable_format() {
513 let file = sample_file();
514 let encoded = to_bytes(&file, OutputFormat::readable()).unwrap();
515
516 assert!(encoded.contains(&b'\n'));
517
518 let decoded = parse_account_transfer(&encoded).unwrap();
519 assert_eq!(decoded, file);
520 }
521
522 #[test]
523 fn canonical_format_has_no_line_breaks_or_eof() {
524 let encoded = to_bytes(&sample_file(), OutputFormat::canonical()).unwrap();
525
526 assert!(!encoded.contains(&b'\n'));
527 assert!(!encoded.contains(&b'\r'));
528 assert_ne!(encoded.last(), Some(&0x1a));
529 }
530
531 #[test]
532 fn roundtrips_crlf_with_eof() {
533 let encoded = to_bytes(
534 &sample_file(),
535 OutputFormat {
536 encoding: Encoding::Ascii,
537 line_ending: LineEnding::Crlf,
538 eof: true,
539 },
540 )
541 .unwrap();
542
543 assert!(encoded.windows(2).any(|window| window == b"\r\n"));
544 assert_eq!(encoded.last(), Some(&0x1a));
545
546 let decoded = parse_account_transfer(&encoded).unwrap();
547 assert_eq!(decoded, sample_file());
548 }
549
550 #[test]
551 fn roundtrips_jis_halfwidth_text_as_unicode() {
552 let file = sample_jis_file();
553 let encoded = to_bytes(&file, OutputFormat::readable()).unwrap();
554
555 assert!(encoded.iter().any(|byte| *byte >= 0xA1));
556
557 let decoded = parse_account_transfer(&encoded).unwrap();
558 assert_eq!(decoded, file);
559 assert_eq!(decoded.header.collector_name, "テストシュウキン");
560 assert_eq!(decoded.details[0].payer_name, "ヤマダタロウ");
561 }
562
563 #[test]
564 fn ascii_output_rejects_jis_text() {
565 let error = to_bytes(
566 &sample_jis_file(),
567 OutputFormat {
568 encoding: Encoding::Ascii,
569 line_ending: LineEnding::Lf,
570 eof: false,
571 },
572 )
573 .unwrap_err();
574
575 assert!(error.to_string().contains("must be encodable as ASCII"));
576 }
577
578 #[test]
579 fn rejects_trailer_mismatch_on_write() {
580 let mut file = sample_file();
581 file.trailer.total_amount = 9999;
582
583 let error = to_bytes(&file, OutputFormat::canonical()).unwrap_err();
584 assert!(error.to_string().contains("trailer total_amount"));
585 }
586
587 #[test]
588 fn auto_parse_rejects_ambiguous_files() {
589 let encoded = to_bytes(&sample_file(), OutputFormat::readable()).unwrap();
590
591 let error = super::parse(&encoded).unwrap_err();
592 assert!(matches!(error, Error::AmbiguousInput(_)));
593 }
594
595 #[test]
596 fn explicit_from_bytes_as_parses_ambiguous_files() {
597 let encoded = to_bytes(&sample_file(), OutputFormat::readable()).unwrap();
598
599 let decoded: File = from_bytes_as(&encoded, FileType::AccountTransfer).unwrap();
600 assert_eq!(decoded, sample_file());
601 }
602
603 #[test]
604 fn auto_parse_handles_malformed_inputs_without_panicking() {
605 for len in 0..=512 {
606 let input = (0..len)
607 .map(|index| ((index * 37 + len) % 256) as u8)
608 .collect::<Vec<_>>();
609
610 let _ = super::parse(&input);
611 }
612 }
613}