aamva_parser_rs/
lib.rs

1// src/lib.rs
2
3pub use clap::Parser;
4use serde::Serialize;
5pub use serde_json;
6pub use serde_yaml;
7use std::collections::HashMap;
8
9#[derive(Serialize)]
10pub enum Gender {
11    MALE = 1,
12    FEMALE = 2,
13    UNSPECIFIED = 9,
14}
15
16#[derive(clap::ValueEnum, Debug, Clone)]
17pub enum OutputFormat {
18    Json,
19    Yaml,
20}
21
22#[derive(Serialize)]
23pub struct ParsedData {
24    pub vehicle_class: String,
25    pub driving_privileges: String,
26    pub additional_privileges: String,
27    pub expiration_date: String,
28    pub last_name: String,
29    pub first_name: String,
30    pub middle_name: String,
31    pub issue_date: String,
32    pub date_of_birth: String,
33    pub gender: Gender,
34    pub eye_color: String,
35    pub height: String,
36    pub street: String,
37    pub city: String,
38    pub state: String,
39    pub postal_code: String,
40}
41
42impl ParsedData {
43    pub fn from_raw_data(raw_data: &str) -> Self {
44        let mut parsed_data = ParsedData {
45            vehicle_class: String::new(),
46            driving_privileges: String::new(),
47            additional_privileges: String::new(),
48            expiration_date: String::new(),
49            last_name: String::new(),
50            first_name: String::new(),
51            middle_name: String::new(),
52            issue_date: String::new(),
53            date_of_birth: String::new(),
54            gender: Gender::UNSPECIFIED,
55            eye_color: String::new(),
56            height: String::new(),
57            street: String::new(),
58            city: String::new(),
59            state: String::new(),
60            postal_code: String::new(),
61        };
62
63        let mut data_map = HashMap::new();
64
65        // Split the raw data into lines
66        for line in raw_data.lines() {
67            if line.len() >= 3 {
68                let key = line[..3].to_string();
69                let value = line[3..].trim().to_string(); // Get the value after the key
70
71                data_map.insert(key, value);
72            }
73        }
74
75        // Map values from data_map to parsed_data fields
76        if let Some(value) = data_map.get("DCA") {
77            parsed_data.vehicle_class = value.to_string();
78        }
79        if let Some(value) = data_map.get("DCB") {
80            parsed_data.driving_privileges = value.to_string();
81        }
82        if let Some(value) = data_map.get("DCD") {
83            parsed_data.additional_privileges = value.to_string();
84        }
85        if let Some(value) = data_map.get("DBA") {
86            parsed_data.expiration_date = Self::standardize_date(value);
87        }
88        if let Some(value) = data_map.get("DCS") {
89            parsed_data.last_name = value.to_string();
90        }
91        if let Some(value) = data_map.get("DAC") {
92            parsed_data.first_name = value.to_string();
93        }
94        if let Some(value) = data_map.get("DAD") {
95            parsed_data.middle_name = value.to_string();
96        }
97        if let Some(value) = data_map.get("DBD") {
98            parsed_data.issue_date = Self::standardize_date(value);
99        }
100        if let Some(value) = data_map.get("DBB") {
101            parsed_data.date_of_birth = Self::standardize_date(value);
102        }
103        if let Some(value) = data_map.get("DBC") {
104            parsed_data.gender = match value.parse::<u8>().unwrap_or(9) {
105                1 => Gender::MALE,
106                2 => Gender::FEMALE,
107                _ => Gender::UNSPECIFIED,
108            };
109        }
110        if let Some(value) = data_map.get("DAY") {
111            parsed_data.eye_color = value.to_string();
112        }
113        if let Some(value) = data_map.get("DAU") {
114            parsed_data.height = Self::convert_height(value);
115        }
116        if let Some(value) = data_map.get("DAG") {
117            parsed_data.street = value.to_string();
118        }
119        if let Some(value) = data_map.get("DAI") {
120            parsed_data.city = value.to_string();
121        }
122        if let Some(value) = data_map.get("DAJ") {
123            parsed_data.state = value.to_string();
124        }
125        if let Some(value) = data_map.get("DAK") {
126            parsed_data.postal_code = value.to_string();
127        }
128
129        parsed_data
130    }
131
132    fn standardize_date(date_str: &str) -> String {
133        if date_str.len() == 8 {
134            // MMDDCCYY format (US)
135            if let Ok(month) = date_str[..2].parse::<u32>() {
136                if let Ok(day) = date_str[2..4].parse::<u32>() {
137                    let year_str = &date_str[4..8];
138                    let year = match year_str.parse::<u32>() {
139                        Ok(y) => y,
140                        Err(_) => return "Invalid Date".to_string(),
141                    };
142
143                    // Convert to ISO format (YYYY-MM-DD)
144                    return format!("{:04}-{:02}-{:02}", year, month, day);
145                }
146            }
147        } else if date_str.len() == 8 {
148            // CCYYMMDD format (Canada)
149            let year = &date_str[..4];
150            let month = &date_str[4..6];
151            let day = &date_str[6..8];
152
153            // Validate and convert to ISO format (YYYY-MM-DD)
154            return format!("{}-{}-{}", year, month, day);
155        }
156
157        "Invalid Date".to_string() // Fallback for invalid formats
158    }
159
160    fn convert_height(height_str: &str) -> String {
161        if height_str.ends_with("CM") {
162            if let Ok(cm) = height_str[..height_str.len() - 2].trim().parse::<f64>() {
163                let inches = cm / 2.54; // Convert cm to inches
164                return format!("{:.2}", inches); // Return only the number
165            }
166        } else if height_str.ends_with("IN") {
167            if let Ok(inches) = height_str[..height_str.len() - 2].trim().parse::<f64>() {
168                return format!("{:.2}", inches); // Return only the number
169            }
170        }
171        "Invalid Height".to_string() // Fallback for invalid heights
172    }
173}
174
175#[derive(Parser, Debug)]
176pub struct CommandLineArguments {
177    #[arg(short, long, value_name = "FILE")]
178    /// Input file (defaults to stdin if not provided)
179    pub file: Option<String>,
180
181    #[arg(short = 'o', long, value_name = "FORMAT", default_value = "json")]
182    /// Output format (defaults to json)
183    pub format: OutputFormat,
184}