Skip to main content

radio_code_calculator/
client.rs

1//! Radio Code Calculator API module
2//!
3//! Usage:
4//!
5//! ```ignore
6//! my_radio_code_calculator = RadioCodeCalculator::new(Some("YOUR-WEB-API-KEY".into()));
7//!
8//! // generate radio code (using Web API)
9//! my_radio_code_calculator.calc(&RadioModels::FORD_M_SERIES, "123456").await?;
10//!
11//!     println!("Radio code is {}", result["code"]);
12//!
13//! }
14//! ```
15
16use std::collections::HashMap;
17
18use reqwest::Client;
19use serde_json::{json, Value};
20
21use crate::error::{RadioCodeCalculatorError, RadioErrors};
22use crate::model::RadioModel;
23
24/// Return type for [`RadioCodeCalculator::info`].
25#[derive(Debug, Clone)]
26pub struct InfoResult {
27    pub value: Value,
28    pub radio_model: RadioModel,
29}
30
31/// Return type for [`RadioCodeCalculator::list`].
32#[derive(Debug, Clone)]
33pub struct ListResult {
34    pub value: Value,
35    pub radio_models: Vec<RadioModel>,
36}
37
38/// Trait for values accepted as a radio model identifier ([`RadioModel`] or model name string).
39pub trait AsRadioModelName {
40    fn radio_model_name(&self) -> &str;
41}
42
43impl AsRadioModelName for RadioModel {
44    fn radio_model_name(&self) -> &str {
45        &self.name
46    }
47}
48
49impl AsRadioModelName for &'_ RadioModel {
50    fn radio_model_name(&self) -> &str {
51        &self.name
52    }
53}
54
55impl AsRadioModelName for str {
56    fn radio_model_name(&self) -> &str {
57        self
58    }
59}
60
61impl AsRadioModelName for String {
62    fn radio_model_name(&self) -> &str {
63        self.as_str()
64    }
65}
66
67/// Radio Code Calculator API module
68pub struct RadioCodeCalculator {
69    /// @var string default Radio Code Calculator API WebApi endpoint
70    pub api_url: String,
71
72    /// @var string|null WebApi key for the service
73    _api_key: Option<String>,
74
75    client: Client,
76}
77
78impl RadioCodeCalculator {
79    /// Default Radio Code Calculator API WebApi endpoint
80    pub const DEFAULT_API_URL: &'static str = "https://www.pelock.com/api/radio-code-calculator/v1";
81
82    /// Initialize Radio Code Calculator API class
83    ///
84    /// @param string|null api_key Activation key for the service (it cannot be empty!)
85    pub fn new(api_key: Option<String>) -> Self {
86        Self::with_client(api_key, Client::new())
87    }
88
89    /// Construct with a custom [`reqwest::Client`] (timeouts, proxies, etc.).
90    pub fn with_client(api_key: Option<String>, client: Client) -> Self {
91        Self {
92            api_url: Self::DEFAULT_API_URL.to_string(),
93            _api_key: api_key,
94            client,
95        }
96    }
97
98    /// Login to the service and get the information about the current license limits
99    ///
100    /// @return RadioCodeCalculator A list with an error code, and an optional dictionary with the raw results (or null on error)
101    pub async fn login(&self) -> Result<Value, RadioCodeCalculatorError> {
102        let mut params = HashMap::new();
103        params.insert("command".to_string(), "login".to_string());
104        self.post_request(params).await
105    }
106
107    /// Calculate the radio code for the selected radio model
108    ///
109    /// @param RadioModel|string radio_model Radio model either as a RadioModel class or a string
110    /// @param string radio_serial_number Radio serial number / pre code
111    /// @param string radio_extra_data Optional extra data (for example - a supplier code) to generate the radio code
112    /// @return array A list with an error code, and an optional dictionary with the raw results (or null)
113    pub async fn calc<R: AsRadioModelName + ?Sized>(
114        &self,
115        radio_model: &R,
116        radio_serial_number: &str,
117        radio_extra_data: &str,
118    ) -> Result<Value, RadioCodeCalculatorError> {
119        let mut params = HashMap::new();
120        params.insert("command".to_string(), "calc".to_string());
121        params.insert(
122            "radio_model".to_string(),
123            radio_model.radio_model_name().to_string(),
124        );
125        params.insert("serial".to_string(), radio_serial_number.to_string());
126        params.insert("extra".to_string(), radio_extra_data.to_string());
127        self.post_request(params).await
128    }
129
130    /// Get the information about the given radio calculator and its parameters (name, max. len & regex pattern)
131    ///
132    /// @param RadioModel|string radio_model Radio model either as a RadioModel class or a string
133    /// @return array A list with an error code, and an optional RadioModel create from the return values (or null)
134    pub async fn info<R: AsRadioModelName + ?Sized>(
135        &self,
136        radio_model: &R,
137    ) -> Result<InfoResult, RadioCodeCalculatorError> {
138        let mut params = HashMap::new();
139        params.insert("command".to_string(), "info".to_string());
140        let name = radio_model.radio_model_name().to_string();
141        params.insert("radio_model".to_string(), name.clone());
142
143        let mut value = self.post_request(params).await?;
144        let model = radio_model_from_response(&name, &value).ok_or_else(|| {
145            RadioCodeCalculatorError::Transport(
146                "missing radio model fields in info response".into(),
147            )
148        })?;
149        value["radioModel"] = json_model_summary(&model);
150        Ok(InfoResult {
151            value,
152            radio_model: model,
153        })
154    }
155
156    /// List all the supported radio calculators and their parameters (name, max. len & regex pattern)
157    ///
158    /// @return array A list with an error code, and an optional list of supported RadioModels (or null)
159    pub async fn list(&self) -> Result<ListResult, RadioCodeCalculatorError> {
160        let mut params = HashMap::new();
161        params.insert("command".to_string(), "list".to_string());
162
163        let mut value = self.post_request(params).await?;
164        let supported = value
165            .get("supportedRadioModels")
166            .and_then(|v| v.as_object())
167            .cloned()
168            .unwrap_or_default();
169
170        let mut radio_models = Vec::new();
171        for (radio_model_name, obj) in supported {
172            if let Some(model) = radio_model_from_response(&radio_model_name, &obj) {
173                radio_models.push(model);
174            }
175        }
176
177        value["radioModels"] = json!(radio_models
178            .iter()
179            .map(json_model_summary)
180            .collect::<Vec<_>>());
181
182        Ok(ListResult {
183            value,
184            radio_models,
185        })
186    }
187
188    /// Send a POST request to the server & returns a Promise
189    ///
190    /// @param {Array} params_array params_array An array with the parameters
191    /// @param {decodedCallback} callback_ Funkcja callback wywolywana po zdekodowaniu danych
192    /// @returns {Promise} An array with the POST request results (or default error)
193    pub async fn post_request(
194        &self,
195        params_array: HashMap<String, String>,
196    ) -> Result<Value, RadioCodeCalculatorError> {
197        // default error -> only returned by the SDK
198        // return error if the activation key is not set (no demo version)
199        let Some(key) = &self._api_key else {
200            return Err(RadioCodeCalculatorError::InvalidLicense);
201        };
202
203        let mut form = reqwest::multipart::Form::new();
204        form = form.text("key", key.clone());
205
206        for (param, val) in params_array {
207            form = form.text(param, val);
208        }
209
210        let response = self
211            .client
212            .post(&self.api_url)
213            .multipart(form)
214            .send()
215            .await
216            .map_err(|e| RadioCodeCalculatorError::Transport(e.to_string()))?;
217
218        let value: Value = response
219            .json()
220            .await
221            .map_err(|e| RadioCodeCalculatorError::Transport(e.to_string()))?;
222
223        let err = value
224            .get("error")
225            .and_then(|e| e.as_i64())
226            .unwrap_or(RadioErrors::ERROR_CONNECTION as i64);
227
228        if err == RadioErrors::SUCCESS as i64 {
229            Ok(value)
230        } else {
231            Err(RadioCodeCalculatorError::ApiError(value))
232        }
233    }
234}
235
236fn radio_model_from_response(name: &str, v: &Value) -> Option<RadioModel> {
237    let serial_max_len = v.get("serialMaxLen")?.as_u64()? as usize;
238    let serial_regex = v.get("serialRegexPattern")?.clone();
239    let extra_max_len = v.get("extraMaxLen").and_then(|x| x.as_u64()).unwrap_or(0) as usize;
240    let extra_regex = if extra_max_len == 0 {
241        None
242    } else {
243        v.get("extraRegexPattern").cloned()
244    };
245    Some(RadioModel::new(
246        name,
247        serial_max_len,
248        serial_regex,
249        extra_max_len,
250        extra_regex,
251    ))
252}
253
254fn json_model_summary(m: &RadioModel) -> Value {
255    json!({
256        "name": m.name,
257        "serialMaxLen": m.serial_max_len,
258        "extraMaxLen": m.extra_max_len,
259        "serialRegexPattern": m.serial_regex_pattern(),
260        "extraRegexPattern": m.extra_regex_pattern(),
261    })
262}