dhl_api/
lib.rs

1use anyhow::Result;
2use regex::Regex;
3use serde::Deserialize;
4
5const DHL_JSON_REGEXP: &str = r#".*initialState: JSON\.parse\((.+")\).*"#;
6
7/// Parcel history events (where did you come from, where did you go?)
8#[derive(Deserialize, Debug)]
9pub struct DHLPackageItemHistoryEvent {
10    /// Date
11    #[serde(rename = "datum")]
12    pub date: String,
13    /// Status text
14    #[serde(rename = "status")]
15    pub status: String,
16    /// If it was returned (?)
17    #[serde(rename = "ruecksendung")]
18    pub return_shipment: bool,
19    /// Location where this happened
20    #[serde(rename = "ort")]
21    pub location: Option<String>,
22}
23
24/// Parcel history
25#[derive(Deserialize, Debug)]
26pub struct DHLPackageItemHistory {
27    /// The parcel may or may not have made a few stops already, this Vec may contain them.
28    #[serde(rename = "events")]
29    pub events: Option<Vec<DHLPackageItemHistoryEvent>>,
30    /// Current state of the parcel
31    #[serde(rename = "aktuellerStatus")]
32    pub current_status: Option<String>,
33    /// Number of steps there are (or should be?) - might as well use `events.len()` 🤷‍♀️
34    #[serde(rename = "fortschritt")]
35    pub steps: u64,
36}
37
38/// Information why a parcel tracking code may not have been found.
39/// This is only set when no tracking information was found (yet?).
40#[derive(Deserialize, Debug)]
41pub struct DHLPackageNotFoundInfo {
42    /// If `true`, no data is available for this tracking code.
43    #[serde(rename = "keineDatenVerfuegbar")]
44    pub no_data_available: bool,
45    /// If `true`, this is probably not a DHL tracking code.
46    #[serde(rename = "keineDhlPaketSendung")]
47    pub not_a_dhl_package: bool,
48}
49
50/// Parcel item details
51#[derive(Deserialize, Debug)]
52pub struct DHLPackageItemDetails {
53    /// History of the parcel
54    #[serde(rename = "sendungsverlauf")]
55    pub history: DHLPackageItemHistory,
56    /// Destination country of the parcel
57    #[serde(rename = "zielland")]
58    pub destination_country: Option<String>,
59}
60
61/// Parcel item elements
62#[derive(Deserialize, Debug)]
63pub struct DHLPackageItem {
64    /// Tracking code, just for keeping track.
65    pub id: String,
66
67    /// (unsure what this means, if you know let me know ❤️)
68    #[serde(rename = "hasCompleteDetails")]
69    pub has_complete_details: bool,
70
71    /// Details for this parcel
72    #[serde(rename = "sendungsdetails")]
73    pub item_details: DHLPackageItemDetails,
74
75    /// If no parcel was found, this variable is **not `None`** and may contains reasons as to why.
76    /// Otherwise it should be `None`.
77    #[serde(rename = "sendungNichtGefunden")]
78    pub package_not_found: Option<DHLPackageNotFoundInfo>,
79}
80
81/// Root element, contains a vector with parcel items
82#[derive(Deserialize, Debug)]
83pub struct DHLPackageStatus {
84    /// The items returned for the query.
85    #[serde(rename = "sendungen")]
86    pub items: Vec<DHLPackageItem>,
87}
88
89fn find_and_derez_json(body: &str) -> Result<DHLPackageStatus, anyhow::Error> {
90    let rex = Regex::new(DHL_JSON_REGEXP).unwrap();
91
92    let caps = rex.captures(&body).unwrap();
93
94    let json_escaped = &caps[1];
95    let json = json_escaped.trim()[1..json_escaped.len() - 1].replace(r#"\""#, "\"");
96
97    //println!("{}", json);
98
99    let r: DHLPackageStatus = serde_json::from_str(&json)?;
100    Ok(r)
101}
102
103/// Returns a DHLPackageStatus struct. You should check if it actually has any data with: `DHLPackageStatus.items[0].package_not_found.has_some()`  
104/// `DHLPackageItem.package_not_found` will only be set if no package was found for that tracking code.
105///
106/// # Arguments
107///
108/// * `package_id` - Tracking code as `&str` of the parcel you wish to query, usually a number but sometimes contains letters.
109///
110/// # Examples
111///
112/// ``` ignore
113/// use dhl_api::get_dhl_package_status;
114/// let status = get_dhl_package_status("123456789").await?;
115/// for item in status.items {
116///     if item.package_not_found.is_some() {
117///         // This item was not found
118///
119///         let why_not_found = item.package_not_found.unwrap();
120///         // if why_not_found.no_data_available { ...
121///         // if why_not_found.not_a_dhl_package { ...
122///
123///         continue;
124///     }
125///
126///     let tracking_code = &item.id;
127///
128///     if item.has_complete_details {
129///         let details = &item.item_details;
130///
131///         for event in &details.history.events.unwrap() {
132///             // Do whatever you need 🦈
133///         }
134///     }
135/// }
136/// ```
137pub async fn get_dhl_package_status(package_id: &str) -> Result<DHLPackageStatus, anyhow::Error> {
138    let my_url = format!("https://www.dhl.de/int-verfolgen/?lang=en&domain=de&lang=en&domain=de&lang=en&domain=de&lang=en&domain=de&piececode={}", package_id);
139    let body = reqwest::get(&my_url).await?.text().await?;
140
141    find_and_derez_json(&body)
142}
143
144/// Try to parse a HTML body and look for the shipping information, usually you need not to call this
145/// unless you wish to use your own HTTP client or backend URL or whatever other wizardry you're into.
146/// 
147/// # Arguments
148/// 
149/// `htmlbody` - HTML code to scan, check out the sourcecode to find the regexp and what it expects.
150pub fn get_dhl_package_from_html(htmlbody: &str) -> Result<DHLPackageStatus, anyhow::Error> {
151    find_and_derez_json(&htmlbody)
152}