neshan_rs/
lib.rs

1use reqwest::header;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5/// Neshan client based on its api documentation.
6/// <https://platform.neshan.org/api/getting-started>
7pub struct Client {
8    client: reqwest::Client,
9}
10
11#[derive(Serialize, Deserialize)]
12pub struct Error {
13    code: i32,
14    message: String,
15}
16
17impl fmt::Display for Error {
18    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
19        write!(f, "{}", self.message)
20    }
21}
22
23impl std::error::Error for Error {}
24
25impl fmt::Debug for Error {
26    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
27        write!(
28            f,
29            "AppError {{ code: {}, message: {} }}",
30            self.code, self.message
31        )
32    }
33}
34
35pub struct Point {
36    pub longitude: f64,
37    pub latitude: f64,
38}
39
40pub enum Type {
41    Car,
42    Motorcycle,
43}
44
45#[derive(Debug, Serialize, Deserialize)]
46pub struct Routes {
47    pub routes: Vec<Route>,
48}
49
50#[derive(Debug, Serialize, Deserialize)]
51pub struct Route {
52    pub legs: Vec<Leg>,
53}
54
55#[derive(Debug, Serialize, Deserialize)]
56pub struct Leg {
57    pub summary: String,
58    pub duration: Duration,
59    pub distance: Distance,
60}
61
62#[derive(Debug, Serialize, Deserialize)]
63pub struct PostalAddress {
64    pub formatted_address: String,
65    pub route_name: String,
66    pub neighbourhood: Option<String>,
67    pub city: String,
68    pub state: String,
69    pub place: Option<String>,
70    pub municipality_zone: Option<String>,
71    pub in_traffic_zone: bool,
72    pub in_odd_even_zone: bool,
73}
74
75/// distance from origin to destination in persian text form and meter.
76#[derive(Debug, Serialize, Deserialize)]
77pub struct Distance {
78    pub value: f64,
79    pub text: String,
80}
81
82/// distance from origin to destination in persian text form and seconds.
83#[derive(Debug, Serialize, Deserialize)]
84pub struct Duration {
85    pub value: f64,
86    pub text: String,
87}
88
89impl fmt::Display for Type {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        match self {
92            Type::Car => f.write_str("car"),
93            Type::Motorcycle => f.write_str("motorcycle"),
94        }
95    }
96}
97
98impl Client {
99    /// create client for communicating with neshan.
100    pub fn new(api_key: &str) -> Client {
101        let mut headers = header::HeaderMap::new();
102        headers.insert("Api-Key", header::HeaderValue::from_str(api_key).unwrap());
103
104        let client = reqwest::Client::builder()
105            .user_agent("neshan-rs")
106            .default_headers(headers)
107            .build()
108            .unwrap();
109
110        Client { client }
111    }
112
113    /// route finds route(s) from origin to destination.
114    ///
115    /// avoid_traffic_zone finds route(s) that doesn't cross the traffic zone.
116    /// avoid_odd_even_zone finds route(s) that doesn's cross the odd_even_zone.
117    /// alternative_paths returns alternative routes besides the primary route.
118    pub async fn route(
119        &self,
120        vehicle: Type,
121        origin: Point,
122        destination: Point,
123        avoid_traffic_zone: bool,
124        avoid_odd_even_zone: bool,
125        alternative_paths: bool,
126    ) -> Result<Routes, Box<dyn std::error::Error>> {
127        let res = self
128            .client
129            .get("https://api.neshan.org/v3/direction")
130            .query(&[
131                ("type", vehicle.to_string()),
132                (
133                    "origin",
134                    format!("{},{}", origin.latitude, origin.longitude),
135                ),
136                (
137                    "destination",
138                    format!("{},{}", destination.latitude, destination.longitude),
139                ),
140                ("avoid_traffic_zone", avoid_traffic_zone.to_string()),
141                ("avoid_odd_event_zone", avoid_odd_even_zone.to_string()),
142                ("alternative", alternative_paths.to_string()),
143            ])
144            .send()
145            .await?;
146
147        if !res.status().is_success() {
148            let err = res.json::<Error>().await?;
149
150            return Err(Box::new(err));
151        }
152
153        let routes = res.json::<Routes>().await?;
154
155        Ok(routes)
156    }
157
158    /// find postal address for the given point.
159    /// https://platform.neshan.org/api/reverse-geocoding
160    pub async fn reverse_geocode(
161        &self,
162        point: Point,
163    ) -> Result<PostalAddress, Box<dyn std::error::Error>> {
164        let res = self
165            .client
166            .get("https://api.neshan.org/v2/reverse")
167            .query(&[
168                ("lat", point.latitude.to_string()),
169                ("lng", point.longitude.to_string()),
170            ])
171            .send()
172            .await?;
173
174        if !res.status().is_success() {
175            let err = res.json::<Error>().await?;
176
177            return Err(Box::new(err));
178        }
179
180        let postal_address = res.json::<PostalAddress>().await?;
181
182        Ok(postal_address)
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    #[tokio::test]
189    async fn routes() {
190        let api_key = std::env::var("NESHAN_RS_API_KEY").unwrap();
191
192        let client = super::Client::new(&api_key);
193        let routes = client
194            .route(
195                super::Type::Car,
196                super::Point {
197                    latitude: 35.731984409609694,
198                    longitude: 51.392684661470156,
199                },
200                super::Point {
201                    latitude: 35.723680037006304,
202                    longitude: 50.953103738230396,
203                },
204                true,
205                true,
206                false,
207            )
208            .await
209            .unwrap();
210
211        println!("{:?}", routes);
212    }
213
214    #[tokio::test]
215    async fn reverse_geocode() {
216        let api_key = std::env::var("NESHAN_RS_API_KEY").unwrap();
217
218        let client = super::Client::new(&api_key);
219        let postal_address = client
220            .reverse_geocode(super::Point {
221                latitude: 35.731984409609694,
222                longitude: 51.392684661470156,
223            })
224            .await
225            .unwrap();
226
227        assert_eq!(postal_address.neighbourhood.as_ref().unwrap(), "قزل قلعه");
228        assert_eq!(postal_address.municipality_zone.as_ref().unwrap(), "6");
229        assert_eq!(postal_address.city, "تهران");
230
231        println!("{:?}", postal_address);
232    }
233}