Skip to main content

radio_code_calculator/
model.rs

1//! RadioModel class used to calculate the radio code for specified car radio/navigation
2//!
3//! Usage:
4//!
5//! ```ignore
6//! // create Radio Code Calculator API class instance (we are using our activation key)
7//! let my_radio_code_calculator = RadioCodeCalculator::new(Some("ABCD-ABCD-ABCD-ABCD".into()));
8//!
9//! // validate the serial number (offline) before sending the Web API request
10//! let error = radio_model.validate(serial, extra);
11//!
12//! ...
13//!
14//! // generate radio code (using Web API)
15//! my_radio_code_calculator.calc(&RadioModels::FORD_M_SERIES, "123456").await?;
16//!
17//!     println!("Radio code is {}", result["code"]);
18//! ```
19
20use std::collections::HashMap;
21use std::sync::LazyLock;
22
23use regex::Regex;
24use serde_json::Value;
25
26use crate::error::RadioErrors;
27
28/// RadioModel class used to calculate the radio code for specified car radio/navigation
29#[derive(Debug, Clone)]
30pub struct RadioModel {
31    /// @var string A single radio model with its parameters
32    pub name: String,
33
34    /// @var int Required, valid length of the radio serial/seed number
35    pub serial_max_len: usize,
36
37    /// @var array PCRE compatible regex patterns for the radio serial/seed number
38    _serial_regex_patterns: HashMap<String, String>,
39
40    /// @var int Length of the optional param for radio code generation
41    pub extra_max_len: usize,
42
43    /// @var array|null PCRE compatible regex patterns for the optional radio serial/seed number
44    _extra_regex_patterns: Option<HashMap<String, String>>,
45
46    /// @var string Default programming language used to determine the format of regular expression formats
47    pub default_programming_language: String,
48}
49
50impl RadioModel {
51    /// In JS you cannot pass the extra parameters for the RegExp in a single string
52    /// This function splits the provided rule into Reg Exp rule & extra params (like case insensitive flag)
53    ///
54    /// @return RegExp regular expression
55    pub fn regex_string_to_rule(regex_string: &str) -> Result<Regex, regex::Error> {
56        let s = regex_string.trim();
57        if !s.starts_with('/') {
58            return Regex::new(s);
59        }
60        let inner = &s[1..];
61        let Some(last_slash) = inner.rfind('/') else {
62            return Regex::new(s);
63        };
64        let (pattern_src, flags) = inner.split_at(last_slash);
65        let flags = &flags[1..];
66        let mut b = regex::RegexBuilder::new(pattern_src);
67        if flags.contains('i') {
68            b.case_insensitive(true);
69        }
70        b.build()
71    }
72
73    /// Return the regex pattern for the current programming language only
74    ///
75    /// @return RegExp|string PCRE compatible regular expression or an empty string ""
76    pub fn serial_regex_pattern(&self) -> String {
77        self._serial_regex_patterns
78            .get(&self.default_programming_language)
79            .cloned()
80            .unwrap_or_default()
81    }
82
83    /// Extra field (if defined) regex pattern for the current programming language only or null
84    ///
85    /// @return RegExp|null PCRE compatible regular expression or null if not required
86    pub fn extra_regex_pattern(&self) -> Option<String> {
87        let map = self._extra_regex_patterns.as_ref()?;
88        map.get(&self.default_programming_language).cloned()
89    }
90
91    /// Initialize RadioModel class with the radio model name, serial & extra fields max. length and regex pattern
92    ///
93    /// @param string name Radio model name
94    /// @param int serial_max_len Max. serial length
95    /// @param string|array serial_regex_pattern Serial number single regex pattern or a dictionary
96    /// @param int extra_max_len Max. extra field length
97    /// @param string|array|null extra_regex_pattern: Extra field single regex pattern or a dictionary
98    pub fn new(
99        name: impl Into<String>,
100        serial_max_len: usize,
101        serial_regex_pattern: Value,
102        extra_max_len: usize,
103        extra_regex_pattern: Option<Value>,
104    ) -> Self {
105        let name = name.into();
106        // create an empty dict to prevent Python re-using previous dict from previous object (!)
107        let mut _serial_regex_patterns: HashMap<String, String> = HashMap::new();
108
109        let default_programming_language = "js".to_string();
110
111        // store the regex pattern under the key for the default programming language (compatibility)
112        match &serial_regex_pattern {
113            Value::String(s) => {
114                _serial_regex_patterns.insert(default_programming_language.clone(), s.clone());
115            }
116            Value::Object(map) => {
117                for (k, v) in map {
118                    if let Some(s) = v.as_str() {
119                        _serial_regex_patterns.insert(k.clone(), s.to_string());
120                    }
121                }
122            }
123            _ => {}
124        }
125
126        // initialize extra field
127        let mut _extra_regex_patterns: Option<HashMap<String, String>> = None;
128
129        if extra_max_len != 0 {
130            let mut map = HashMap::new();
131            match extra_regex_pattern {
132                Some(Value::String(s)) => {
133                    map.insert(default_programming_language.clone(), s);
134                }
135                Some(Value::Object(o)) => {
136                    for (k, v) in o {
137                        if let Some(s) = v.as_str() {
138                            map.insert(k.clone(), s.to_string());
139                        }
140                    }
141                }
142                _ => {}
143            }
144            _extra_regex_patterns = Some(map);
145        }
146
147        Self {
148            name,
149            serial_max_len,
150            _serial_regex_patterns,
151            extra_max_len,
152            _extra_regex_patterns,
153            default_programming_language,
154        }
155    }
156
157    /// Convenience for static presets using a single JS-style slash regex string.
158    pub fn with_slash_patterns(
159        name: impl Into<String>,
160        serial_max_len: usize,
161        serial_regex: &str,
162        extra_max_len: usize,
163        extra_regex: Option<&str>,
164    ) -> Self {
165        Self::new(
166            name,
167            serial_max_len,
168            Value::String(serial_regex.to_string()),
169            extra_max_len,
170            extra_regex.map(|s| Value::String(s.to_string())),
171        )
172    }
173
174    /// Validate radio serial number and extra data (if provided), check their lenghts and regex patterns
175    ///
176    /// @param string serial Radio serial number
177    /// @param string|null extra: Extra data (optional)
178    /// @return int one of the RadioErrors values
179    pub fn validate(&self, serial: &str, extra: Option<&str>) -> i32 {
180        if serial.len() != self.serial_max_len {
181            return RadioErrors::INVALID_SERIAL_LENGTH;
182        }
183
184        let serial_pat = self.serial_regex_pattern();
185        if serial_pat.is_empty() {
186            return RadioErrors::INVALID_SERIAL_PATTERN;
187        }
188        let Ok(re) = Self::regex_string_to_rule(&serial_pat) else {
189            return RadioErrors::INVALID_SERIAL_PATTERN;
190        };
191        if !re.is_match(serial) {
192            return RadioErrors::INVALID_SERIAL_PATTERN;
193        }
194
195        if let Some(ex) = extra {
196            if !ex.is_empty() {
197                if ex.len() != self.extra_max_len {
198                    return RadioErrors::INVALID_EXTRA_LENGTH;
199                }
200                let Some(extra_pat) = self.extra_regex_pattern() else {
201                    return RadioErrors::INVALID_EXTRA_PATTERN;
202                };
203                let Ok(re_extra) = Self::regex_string_to_rule(&extra_pat) else {
204                    return RadioErrors::INVALID_EXTRA_PATTERN;
205                };
206                if !re_extra.is_match(ex) {
207                    return RadioErrors::INVALID_EXTRA_PATTERN;
208                }
209            }
210        }
211
212        RadioErrors::SUCCESS
213    }
214}
215
216/// Supported radio models with the validation parameters (max. lengths & regex patterns)
217///
218/// This helper class can be used to quickly perform offline validation of the radio
219/// serial/seed codes before its send to the WebApi.
220///
221/// Usage:
222///
223/// let radioModel = RadioModels::FORD_M_SERIES
224#[allow(non_snake_case)]
225pub mod RadioModels {
226    use super::{LazyLock, RadioModel};
227
228    macro_rules! lazy_model {
229        ($name:ident, $slug:literal, $len:expr, $pat:literal) => {
230            /// Supported radio model preset (lazy-initialized).
231            pub static $name: LazyLock<RadioModel> =
232                LazyLock::new(|| RadioModel::with_slash_patterns($slug, $len, $pat, 0, None));
233        };
234    }
235
236    lazy_model!(RENAULT_DACIA, "renault-dacia", 4, "/^([A-Z]{1}[0-9]{3})$/");
237    lazy_model!(
238        CHRYSLER_PANASONIC_TM9,
239        "chrysler-panasonic-tm9",
240        4,
241        "/^([0-9]{4})$/"
242    );
243    lazy_model!(
244        CHRYSLER_DODGE_VP,
245        "chrysler-dodge-vp",
246        4,
247        "/^([a-zA-Z0-9]{4})$/"
248    );
249    lazy_model!(FORD_M_SERIES, "ford-m-series", 6, "/^([0-9]{6})$/");
250    lazy_model!(FORD_V_SERIES, "ford-v-series", 6, "/^([0-9]{6})$/");
251    lazy_model!(FORD_TRAVELPILOT, "ford-travelpilot", 7, "/^([0-9]{7})$/");
252    lazy_model!(
253        FIAT_STILO_BRAVO_VISTEON,
254        "fiat-stilo-bravo-visteon",
255        6,
256        "/^([a-zA-Z0-9]{6})$/"
257    );
258    lazy_model!(FIAT_DAIICHI, "fiat-daiichi", 4, "/^([0-9]{4})$/");
259    lazy_model!(FIAT_VP, "fiat-vp", 4, "/^([0-9]{4})$/");
260    lazy_model!(TOYOTA_ERC, "toyota-erc", 16, "/^([a-zA-Z0-9]{16})$/");
261    lazy_model!(
262        JEEP_CHEROKEE,
263        "jeep-cherokee",
264        14,
265        "/^([a-zA-Z0-9]{10}[0-9]{4})$/"
266    );
267    lazy_model!(
268        NISSAN_GLOVE_BOX,
269        "nissan-glove-box",
270        12,
271        "/^([a-zA-Z0-9]{12})$/"
272    );
273    lazy_model!(ECLIPSE_ESN, "eclipse-esn", 6, "/^([a-zA-Z0-9]{6})$/");
274    lazy_model!(JAGUAR_ALPINE, "jaguar-alpine", 5, "/^([0-9]{5})$/");
275}