Skip to content

Commit

Permalink
Added error handling with thiserror and anyhow
Browse files Browse the repository at this point in the history
  • Loading branch information
iByteABit256 committed Aug 3, 2024
1 parent 35f4d4c commit bd454bd
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 56 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ chrono = "0.4.23"
rand = "0.8.5"
clap = { version = "4.0.29", features = ["derive"] }
thiserror = "1.0.63"
anyhow = "1.0.86"
155 changes: 104 additions & 51 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
use chrono::{Datelike, Utc};
use rand::{distributions::Alphanumeric, prelude::Distribution};
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum MrnGeneratorError {
#[error("{0} is not a valid country code, it should be exactly two characters (e.g. 'IT')")]
CountryCodeLength(String),
#[error("{0} is not a valid procedure category")]
InvalidProcedureCategory(String),
#[error("{procedure_category}-{combination} is not a valid procedure category combination")]
InvalidProcedureCategoryCombination {
procedure_category: String,
combination: String,
},
#[error("{0} is not an alphanumeric")]
NotAlphanumeric(char),
}

/// Returns a valid MRN given a country code
pub fn generate_random_mrn(
country_code: &str,
procedure: Option<Procedure>,
declaration_office: Option<&str>,
) -> String {
) -> Result<String, MrnGeneratorError> {
use MrnGeneratorError::*;

let curr_year: String = Utc::now().year().to_string().chars().skip(2).collect();

let random_str_len = 14 - declaration_office.map_or(0, |decoffice| decoffice.len());
Expand All @@ -18,7 +36,7 @@ pub fn generate_random_mrn(
.collect();

if country_code.len() != 2 {
panic!("Country code should be 2 characters")
return Err(CountryCodeLength(country_code.to_string()));
}

let mut mrn = format!(
Expand All @@ -37,17 +55,17 @@ pub fn generate_random_mrn(
}

// Check MRN, and replace last character if invalid
let last_digit = is_mrn_valid(&mrn);
let last_digit = is_mrn_valid(&mrn)?;

if let Some(last_digit) = last_digit {
replace_last_char(&mrn, last_digit)
Ok(replace_last_char(&mrn, last_digit))
} else {
mrn
Ok(mrn)
}
}

/// Returns None if MRN is valid, and correct last character if it's invalid
pub fn is_mrn_valid(mrn: &str) -> Option<char> {
pub fn is_mrn_valid(mrn: &str) -> Result<Option<char>, MrnGeneratorError> {
let mut mrn_iter = mrn.chars();
let last_digit = mrn_iter.next_back().unwrap();

Expand All @@ -57,11 +75,13 @@ pub fn is_mrn_valid(mrn: &str) -> Option<char> {
let multiplied_sum: u32 = mrn_temp
.chars()
.zip(0..mrn_temp.len())
.map(|(c, m)| (check_character_value(c) as u32) << m)
.map(|(c, m)| (check_character_value(c).map(|value| (value as u32) << m)))
.collect::<Result<Vec<u32>, MrnGeneratorError>>()?
.iter()
.sum();

let check_digit: u8 = (multiplied_sum % 11).try_into().unwrap();
check_remainder_value(check_digit, last_digit)
Ok(check_remainder_value(check_digit, last_digit))
}

/// Procedure types
Expand Down Expand Up @@ -107,45 +127,53 @@ pub fn procecure_category_to_char(procedure: Procedure) -> char {

/// Matches a procedure category code (optionally combined with another one) and returns
/// the corresponding customs procedure
pub fn match_procedure(proctgr: &str, combined: Option<&str>) -> Procedure {
pub fn match_procedure(
proctgr: &str,
combined: Option<&str>,
) -> Result<Procedure, MrnGeneratorError> {
use MrnGeneratorError::*;

let exit_combined = ["A"];
let entry_combined = ["F"];
match proctgr {
"B1" | "B2" | "B3" | "C1" if combined.is_none() => Procedure::ExportOnly,
"B1" | "B2" | "B3" | "C1" if combined.is_none() => Ok(Procedure::ExportOnly),
"B1" | "B2" | "B3" | "C1" if combined.is_some_and(|c| exit_combined.contains(&c)) => {
Procedure::ExportAndExitSummaryDeclaration
Ok(Procedure::ExportAndExitSummaryDeclaration)
}
"A1" | "A2" => Procedure::ExitSummaryDeclarationOnly,
"A3" => Procedure::ReExportNotification,
"B4" => Procedure::DispatchOfGoodsInRelationWithSpecialFiscalTerritories,
"D1" | "D2" | "D3" if combined.is_none() => Procedure::TransitDeclarationOnly,
"A1" | "A2" => Ok(Procedure::ExitSummaryDeclarationOnly),
"A3" => Ok(Procedure::ReExportNotification),
"B4" => Ok(Procedure::DispatchOfGoodsInRelationWithSpecialFiscalTerritories),
"D1" | "D2" | "D3" if combined.is_none() => Ok(Procedure::TransitDeclarationOnly),
"D1" | "D2" | "D3" if combined.is_some_and(|c| exit_combined.contains(&c)) => {
Procedure::TransitDeclarationAndExitSummaryDeclaration
Ok(Procedure::TransitDeclarationAndExitSummaryDeclaration)
}
"D1" | "D2" | "D3" if combined.is_some_and(|c| entry_combined.contains(&c)) => {
Procedure::TransitDeclarationAndEntrySummaryDeclaration
Ok(Procedure::TransitDeclarationAndEntrySummaryDeclaration)
}
"E1" | "E2" => Procedure::ProofOfTheCustomsStatusOfUnionGoods,
"E1" | "E2" => Ok(Procedure::ProofOfTheCustomsStatusOfUnionGoods),
"H1" | "H2" | "H3" | "H4" | "H6" | "I1" if combined.is_none() => {
Procedure::ImportDeclarationOnly
Ok(Procedure::ImportDeclarationOnly)
}
"H1" | "H2" | "H3" | "H4" | "H6" | "I1"
if combined.is_some_and(|c| entry_combined.contains(&c)) =>
{
Procedure::ImportDeclarationAndEntrySummaryDeclaration
Ok(Procedure::ImportDeclarationAndEntrySummaryDeclaration)
}
"F1a" | "F1b" | "F1c" | "F1d" | "F2a" | "F2b" | "F2c" | "F2d" | "F3a" | "F3b" | "F4a"
| "F4b" | "F4c" | "F5" => Procedure::EntrySummaryDeclarationOnly,
"H5" => Procedure::IntroductionOfGoodsInRelationWithSpecialFiscalTerritories,
"G4" if combined.is_none() => Procedure::TemporaryStorageDeclaration,
| "F4b" | "F4c" | "F5" => Ok(Procedure::EntrySummaryDeclarationOnly),
"H5" => Ok(Procedure::IntroductionOfGoodsInRelationWithSpecialFiscalTerritories),
"G4" if combined.is_none() => Ok(Procedure::TemporaryStorageDeclaration),
"G4" if combined.is_some_and(|c| entry_combined.contains(&c)) => {
Procedure::TemporaryStorageDeclarationAndEntrySummaryDeclaration
Ok(Procedure::TemporaryStorageDeclarationAndEntrySummaryDeclaration)
}
_ => {
if let Some(c) = combined {
panic!("{proctgr} combined with {c} is not a valid combined procedure category.")
Err(InvalidProcedureCategoryCombination {
procedure_category: proctgr.to_string(),
combination: c.to_string(),
})
} else {
panic!("{proctgr} is not a valid procedure category.")
Err(InvalidProcedureCategory(proctgr.to_string()))
}
}
}
Expand Down Expand Up @@ -174,24 +202,24 @@ pub fn check_remainder_value(check_digit: u8, last_digit: char) -> Option<char>
}

/// Character values according to tables in ISO 6346
pub fn check_character_value(c: char) -> u8 {
pub fn check_character_value(c: char) -> Result<u8, MrnGeneratorError> {
if c.is_ascii_digit() {
return c as u8 - 48;
return Ok(c as u8 - 48);
}
if c.is_alphabetic() {
if c == 'A' {
return 10;
return Ok(10);
} else if ('B'..='K').contains(&c) {
return c as u8 - 54;
return Ok(c as u8 - 54);
} else if ('L'..='U').contains(&c) {
return c as u8 - 53;
return Ok(c as u8 - 53);
} else {
return c as u8 - 52;
return Ok(c as u8 - 52);
}
}

// Default as fallback, change to an error sometime
0
Err(MrnGeneratorError::NotAlphanumeric(c))
}

#[cfg(test)]
Expand All @@ -201,7 +229,7 @@ mod tests {

#[test]
fn generate_random_mrn_test() {
let mrn = generate_random_mrn("DK", Some(Procedure::ExportOnly), None);
let mrn = generate_random_mrn("DK", Some(Procedure::ExportOnly), None).unwrap();

let country_code: String = mrn.chars().skip(2).take(2).collect();
let actual_year: String = mrn.chars().take(2).collect();
Expand All @@ -211,25 +239,25 @@ mod tests {
assert_eq!(expected_year, actual_year);
assert_eq!('A', procedure_char);
assert_eq!("DK".to_string(), country_code);
assert_eq!(None, is_mrn_valid(&mrn));
assert_eq!(None, is_mrn_valid(&mrn).unwrap());
}

#[test]
fn generate_random_mrn_test_without_procedure() {
let mrn = generate_random_mrn("DK", None, None);
let mrn = generate_random_mrn("DK", None, None).unwrap();

let country_code: String = mrn.chars().skip(2).take(2).collect();
let actual_year: String = mrn.chars().take(2).collect();
let expected_year: String = Utc::now().year().to_string().chars().skip(2).collect();
assert_eq!(18, mrn.len());
assert_eq!(expected_year, actual_year);
assert_eq!("DK".to_string(), country_code);
assert_eq!(None, is_mrn_valid(&mrn));
assert_eq!(None, is_mrn_valid(&mrn).unwrap());
}

#[test]
fn generate_random_mrn_test_with_declaration_office() {
let mrn = generate_random_mrn("DK", None, Some("004700"));
let mrn = generate_random_mrn("DK", None, Some("004700")).unwrap();

let country_code: String = mrn.chars().skip(2).take(2).collect();
let actual_year: String = mrn.chars().take(2).collect();
Expand All @@ -239,30 +267,51 @@ mod tests {
assert_eq!(expected_year, actual_year);
assert_eq!("DK".to_string(), country_code);
assert_eq!("004700".to_string(), declaration_office);
assert_eq!(None, is_mrn_valid(&mrn));
assert_eq!(None, is_mrn_valid(&mrn).unwrap());
}

#[test]
fn is_mrn_valid_test() {
assert_eq!(None, is_mrn_valid("22ITZXBZYUTJFLJXK6"));
assert_eq!(Some('1'), is_mrn_valid("22DK1V0QQK2S6J7TU2"));
assert_eq!(None, is_mrn_valid("22ITZXBZYUTJFLJXK6").unwrap());
assert_eq!(Some('1'), is_mrn_valid("22DK1V0QQK2S6J7TU2").unwrap());
}

#[test]
fn procedure_matched_test() {
assert_eq!(Procedure::ExportOnly, match_procedure("B1", None));
assert_eq!(Procedure::ExportOnly, match_procedure("B1", None).unwrap());
assert_eq!(
Procedure::ExportAndExitSummaryDeclaration,
match_procedure("B2", Some("A"))
match_procedure("B2", Some("A")).unwrap()
);
}

#[test]
#[should_panic]
fn procedure_not_matched_test() {
match_procedure("B2", Some("B"));
match_procedure("clearly not a valid procedure 🤡", None);
match_procedure("clearly not a valid procedure 🤡", Some("F"));
use MrnGeneratorError::*;

assert_eq!(
Err(InvalidProcedureCategoryCombination {
procedure_category: "B2".to_string(),
combination: "B".to_string()
}),
match_procedure("B2", Some("B"))
);

let invalid_procedure_category = "not a valid procedure 🤡";

assert_eq!(
Err(InvalidProcedureCategory(
invalid_procedure_category.to_string()
)),
match_procedure(invalid_procedure_category, None)
);
assert_eq!(
Err(InvalidProcedureCategoryCombination {
procedure_category: invalid_procedure_category.to_string(),
combination: "F".to_string()
}),
match_procedure(invalid_procedure_category, Some("F"))
);
}

#[test]
Expand All @@ -285,9 +334,13 @@ mod tests {

#[test]
fn check_character_value_test() {
assert_eq!(3, check_character_value('3'));
assert_eq!(10, check_character_value('A'));
assert_eq!(13, check_character_value('C'));
assert_eq!(35, check_character_value('W'));
assert_eq!(3, check_character_value('3').unwrap());
assert_eq!(10, check_character_value('A').unwrap());
assert_eq!(13, check_character_value('C').unwrap());
assert_eq!(35, check_character_value('W').unwrap());
assert_eq!(
Err(MrnGeneratorError::NotAlphanumeric('🤡')),
check_character_value('🤡')
);
}
}
10 changes: 5 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
mod parser;

use std::error::Error;

use anyhow::Result;
use clap::Parser;
use mrn_generator::*;
use parser::Args;

fn main() -> Result<(), Box<dyn Error>> {
fn main() -> Result<()> {
let args = Args::parse();
let declaration_office = args.declaration_office.as_deref();
let combined = args.combined.as_deref();
let procedure = args
.procedure_category
.map(|proctg| match_procedure(&proctg, combined));
.map(|proctg| match_procedure(&proctg, combined))
.transpose()?;

for _ in 0..args.number_of_mrns {
let mrn: &str = &generate_random_mrn(&args.country_code, procedure, declaration_office);
let mrn: &str = &generate_random_mrn(&args.country_code, procedure, declaration_office)?;
println!("{mrn}");
}

Expand Down

0 comments on commit bd454bd

Please sign in to comment.