Skip to main content

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}