aztec_decoder/lib.rs
1/******************************************************************************
2 *
3 * Dekoder kodow AZTEC 2D z dowodow rejestracyjnych interfejs Web API
4 *
5 * Wersja : AZTecDecoder v2.0
6 * Jezyk : Rust
7 * Zaleznosci : reqwest, serde_json, thiserror, tokio
8 * Autor : Bartosz Wójcik (support@pelock.com)
9 * Strona domowa : https://www.dekoderaztec.pl | https://www.pelock.com
10 *
11 *****************************************************************************/
12
13//! # aztec-decoder
14//!
15//! Biblioteka programistyczna pozwalająca na dekodowanie danych z dowodów
16//! rejestracyjnych pojazdów samochodowych zapisanych w formie kodu AZTEC 2D.
17//!
18//! ## Szybki start
19//!
20//! ```rust,no_run
21//! use aztec_decoder::AZTecDecoder;
22//!
23//! #[tokio::main]
24//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
25//! let decoder = AZTecDecoder::new("ABCD-ABCD-ABCD-ABCD");
26//!
27//! let result = decoder.decode_image_from_file("zdjecie-dowodu.jpg").await?;
28//!
29//! if result["Status"] == true {
30//! println!("{}", serde_json::to_string_pretty(&result)?);
31//! }
32//!
33//! Ok(())
34//! }
35//! ```
36
37use std::path::Path;
38
39use reqwest::multipart;
40use serde_json::Value;
41use thiserror::Error;
42
43const API_URL: &str = "https://www.pelock.com/api/aztec-decoder/v1";
44
45/// Błędy zwracane przez [`AZTecDecoder`].
46#[derive(Debug, Error)]
47pub enum AZTecError {
48 /// Klucz API jest pusty.
49 #[error("brak klucza API")]
50 EmptyApiKey,
51
52 /// Nie udało się odczytać pliku.
53 #[error("błąd odczytu pliku: {0}")]
54 FileRead(#[from] std::io::Error),
55
56 /// Błąd komunikacji z serwerem Web API.
57 #[error("błąd żądania HTTP: {0}")]
58 Request(#[from] reqwest::Error),
59
60 /// Serwer zwrócił odpowiedź, której nie można sparsować jako JSON.
61 #[error("nieprawidłowa odpowiedź JSON: {0}")]
62 InvalidJson(#[from] serde_json::Error),
63}
64
65/// Klient dekodera kodów AZTEC 2D z dowodów rejestracyjnych (Web API).
66///
67/// Komunikuje się z usługą Web API pod adresem
68/// `https://www.pelock.com/api/aztec-decoder/v1`.
69///
70/// # Przykład
71///
72/// ```rust,no_run
73/// use aztec_decoder::AZTecDecoder;
74///
75/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
76/// let decoder = AZTecDecoder::new("ABCD-ABCD-ABCD-ABCD");
77/// let result = decoder.decode_text("ggMAANtYAAJD...").await?;
78/// println!("{}", serde_json::to_string_pretty(&result)?);
79/// # Ok(())
80/// # }
81/// ```
82pub struct AZTecDecoder {
83 api_key: String,
84 client: reqwest::Client,
85}
86
87impl AZTecDecoder {
88 /// Tworzy nową instancję dekodera.
89 ///
90 /// # Argumenty
91 ///
92 /// * `api_key` – klucz do usługi Web API
93 pub fn new(api_key: impl Into<String>) -> Self {
94 Self {
95 api_key: api_key.into(),
96 client: reqwest::Client::new(),
97 }
98 }
99
100 /// Dekoduje zaszyfrowaną wartość tekstową do wyjściowej struktury JSON.
101 ///
102 /// Wysyła polecenie `decode-text` wraz z podanym tekstem (np. odczytanym
103 /// skanerem w formacie Base64) do serwera Web API.
104 ///
105 /// # Argumenty
106 ///
107 /// * `text` – odczytana wartość kodu AZTEC 2D w formie ASCII
108 ///
109 /// # Błędy
110 ///
111 /// Zwraca [`AZTecError`] w przypadku pustego klucza API, błędu sieciowego
112 /// lub nieprawidłowej odpowiedzi JSON.
113 pub async fn decode_text(&self, text: &str) -> Result<Value, AZTecError> {
114 let form = multipart::Form::new()
115 .text("key", self.api_key.clone())
116 .text("command", "decode-text")
117 .text("text", text.to_owned());
118
119 self.post_request(form).await
120 }
121
122 /// Dekoduje zaszyfrowaną wartość tekstową ze wskazanego pliku do
123 /// wyjściowej struktury JSON.
124 ///
125 /// Odczytuje zawartość pliku jako UTF-8 i przekazuje ją do
126 /// [`decode_text`](Self::decode_text).
127 ///
128 /// # Argumenty
129 ///
130 /// * `path` – ścieżka do pliku z odczytaną wartością kodu AZTEC 2D
131 ///
132 /// # Błędy
133 ///
134 /// Zwraca [`AZTecError::FileRead`] jeśli plik nie istnieje lub nie można
135 /// go odczytać, oraz pozostałe warianty [`AZTecError`] w przypadku błędów
136 /// komunikacji z API.
137 pub async fn decode_text_from_file(
138 &self,
139 path: impl AsRef<Path>,
140 ) -> Result<Value, AZTecError> {
141 let data = tokio::fs::read_to_string(path).await?;
142 self.decode_text(&data).await
143 }
144
145 /// Dekoduje zaszyfrowaną wartość zakodowaną w obrazku PNG lub JPG/JPEG
146 /// do wyjściowej struktury JSON.
147 ///
148 /// Wysyła plik graficzny jako formularz multipart z poleceniem
149 /// `decode-image` do serwera Web API.
150 ///
151 /// # Argumenty
152 ///
153 /// * `path` – ścieżka do obrazka z kodem AZTEC 2D
154 ///
155 /// # Błędy
156 ///
157 /// Zwraca [`AZTecError::FileRead`] jeśli plik nie istnieje lub nie można
158 /// go odczytać, oraz pozostałe warianty [`AZTecError`] w przypadku błędów
159 /// komunikacji z API.
160 pub async fn decode_image_from_file(
161 &self,
162 path: impl AsRef<Path>,
163 ) -> Result<Value, AZTecError> {
164 if self.api_key.is_empty() {
165 return Err(AZTecError::EmptyApiKey);
166 }
167
168 let path = path.as_ref();
169 let file_bytes = tokio::fs::read(path).await?;
170
171 let file_name = path
172 .file_name()
173 .map(|n| n.to_string_lossy().into_owned())
174 .unwrap_or_default();
175
176 let file_part = multipart::Part::bytes(file_bytes).file_name(file_name);
177
178 let form = multipart::Form::new()
179 .text("key", self.api_key.clone())
180 .text("command", "decode-image")
181 .part("image", file_part);
182
183 self.post_request(form).await
184 }
185
186 /// Wysyła żądanie POST (multipart) do serwera Web API i zwraca
187 /// odpowiedź jako [`serde_json::Value`].
188 async fn post_request(&self, form: multipart::Form) -> Result<Value, AZTecError> {
189 if self.api_key.is_empty() {
190 return Err(AZTecError::EmptyApiKey);
191 }
192
193 let response = self
194 .client
195 .post(API_URL)
196 .multipart(form)
197 .send()
198 .await?;
199
200 let text = response.text().await?;
201 let json: Value = serde_json::from_str(&text)?;
202
203 Ok(json)
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn new_decoder_stores_api_key() {
213 let decoder = AZTecDecoder::new("test-key");
214 assert_eq!(decoder.api_key, "test-key");
215 }
216
217 #[tokio::test]
218 async fn empty_api_key_returns_error() {
219 let decoder = AZTecDecoder::new("");
220 let result = decoder.decode_text("test").await;
221 assert!(matches!(result, Err(AZTecError::EmptyApiKey)));
222 }
223
224 #[tokio::test]
225 async fn missing_file_returns_error() {
226 let decoder = AZTecDecoder::new("test-key");
227 let result = decoder
228 .decode_text_from_file("nieistniejacy-plik.txt")
229 .await;
230 assert!(matches!(result, Err(AZTecError::FileRead(_))));
231 }
232
233 #[tokio::test]
234 async fn missing_image_returns_error() {
235 let decoder = AZTecDecoder::new("test-key");
236 let result = decoder
237 .decode_image_from_file("nieistniejacy-obrazek.png")
238 .await;
239 assert!(matches!(result, Err(AZTecError::FileRead(_))));
240 }
241}