radio_code_calculator/
client.rs1use std::collections::HashMap;
17
18use reqwest::Client;
19use serde_json::{json, Value};
20
21use crate::error::{RadioCodeCalculatorError, RadioErrors};
22use crate::model::RadioModel;
23
24#[derive(Debug, Clone)]
26pub struct InfoResult {
27 pub value: Value,
28 pub radio_model: RadioModel,
29}
30
31#[derive(Debug, Clone)]
33pub struct ListResult {
34 pub value: Value,
35 pub radio_models: Vec<RadioModel>,
36}
37
38pub 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
67pub struct RadioCodeCalculator {
69 pub api_url: String,
71
72 _api_key: Option<String>,
74
75 client: Client,
76}
77
78impl RadioCodeCalculator {
79 pub const DEFAULT_API_URL: &'static str = "https://www.pelock.com/api/radio-code-calculator/v1";
81
82 pub fn new(api_key: Option<String>) -> Self {
86 Self::with_client(api_key, Client::new())
87 }
88
89 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 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 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 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 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 pub async fn post_request(
194 &self,
195 params_array: HashMap<String, String>,
196 ) -> Result<Value, RadioCodeCalculatorError> {
197 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}