recoord/lib.rs
1#![forbid(unsafe_code)]
2#![deny(
3 missing_docs,
4 clippy::missing_docs_in_private_items
5)]
6
7//! Recoord is a create for handling work with coordinates
8//!
9//! All corrdinates are always converted to the latitude and longitude float format
10
11use std::{
12 fmt,
13 fmt::{Display, Formatter},
14};
15/// A wrapper around different coordinate formats
16pub mod formats;
17
18/// A wrapper around differend resolvers for Coordinates
19pub mod resolvers;
20
21#[cfg(feature = "serde")]
22use serde::{Deserialize, Serialize};
23
24#[cfg(feature = "format_any")]
25use std::str::FromStr;
26
27#[cfg(any(feature = "format_dd", feature = "format_dms", feature = "resolve_osm"))]
28use std::num::ParseFloatError;
29
30use thiserror::Error;
31
32/// The base coordinate struct.
33/// It stores the location as latitude, longitude floats
34///
35#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
36#[derive(Debug, Clone, PartialEq)]
37pub struct Coordinate {
38 /// Longitude of the coordinate (-90 - 90)
39 pub lat: f64,
40 /// Latitude of the coordinate (-180 - 180)
41 pub lng: f64,
42}
43
44impl Coordinate {
45 /// Create a new coordinate with longitude and latitude
46 ///
47 /// ```
48 /// /// Normal Coordinate creation
49 /// # use recoord::Coordinate;
50 /// let manual = Coordinate { lat: 10., lng: 20. };
51 /// let coordinate = Coordinate::new(10., 20.);
52 /// assert_eq!(coordinate, manual)
53 /// ```
54 pub fn new(lat: f64, lng: f64) -> Self {
55 Self { lat, lng }
56 }
57}
58
59impl Display for Coordinate {
60 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
61 write!(f, "{},{}", self.lat, self.lng)
62 }
63}
64
65/// Error when handling coordinates
66#[derive(Debug, Error)]
67pub enum CoordinateError {
68 /// No parser available - enable them via features
69 #[error("No parser available - enable them via features")]
70 MissingParser,
71 /// Value can't be converted into a coordinate
72 #[error("Value can't be converted into a coordinate")]
73 InvalidValue,
74 /// String passed into from_str was malformed
75 #[cfg(any(
76 feature = "format_dd",
77 feature = "format_dms",
78 feature = "format_geohash"
79 ))]
80 #[error("String passed into from_str was malformed")]
81 Malformed,
82 /// String passed into from_str contained invalid floats
83 #[cfg(any(feature = "format_dd", feature = "format_dms", feature = "resolve_osm"))]
84 #[error("String passed into from_str contained invalid floats")]
85 ParseFloatError(#[from] ParseFloatError),
86 /// Location not resolvable
87 #[cfg(feature = "resolve_osm")]
88 #[error("Location not resolvable")]
89 Unresolveable,
90 /// There was a problem connecting to the API
91 #[cfg(feature = "resolve_osm")]
92 #[error("There was a problem connecting to the API")]
93 ReqwestError(#[from] reqwest::Error),
94}
95
96impl TryFrom<(f64, f64)> for Coordinate {
97 type Error = CoordinateError;
98 /// Try to convert a tuple of coordinates into a Coordinate struct
99 ///
100 /// ```
101 /// /// Parsing works
102 /// # use recoord::Coordinate;
103 /// let from = Coordinate::try_from((10., 20.));
104 /// assert_eq!(Coordinate { lat: 10.0, lng: 20.0}, from.unwrap());
105 /// ```
106 ///
107 /// ```
108 /// /// Detect invalid values
109 /// # use recoord::{Coordinate, CoordinateError};
110 /// let from = Coordinate::try_from((100., 20.));
111 /// assert!(from.is_err());
112 /// ```
113 fn try_from(tupl_coord: (f64, f64)) -> Result<Self, Self::Error> {
114 match tupl_coord {
115 (lat, lng) if (-90.0..=90.0).contains(&lat) && (-180.0..=180.0).contains(&lng) => {
116 Ok(Self { lat, lng })
117 }
118 _ => Err(CoordinateError::InvalidValue),
119 }
120 }
121}
122
123#[cfg(feature = "format_any")]
124impl FromStr for Coordinate {
125 type Err = CoordinateError;
126
127 fn from_str(str_coords: &str) -> Result<Self, Self::Err> {
128 let mut result: Result<Coordinate, CoordinateError> = Err(CoordinateError::MissingParser);
129
130 #[cfg(feature = "format_dd")]
131 {
132 result = result
133 .or_else(|_| formats::dd::DDCoordinate::from_str(str_coords).map(Coordinate::from));
134 }
135 #[cfg(feature = "format_dms")]
136 {
137 result = result.or_else(|_| {
138 formats::dms::DMSCoordinate::from_str(str_coords).map(Coordinate::from)
139 });
140 }
141 #[cfg(feature = "format_geohash")]
142 {
143 result = result
144 .or_else(|_| formats::geohash::Geohash::from_str(str_coords).map(Coordinate::from));
145 }
146
147 result
148 }
149}
150
151// /// Resolver for strings to Coordinates - this should be used for more expensive (and async) resolving
152// pub trait Resolver {
153// /// Resolve a &str to a Coordinate
154// fn resolve(s: &str) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Coordinate, CoordinateError>> + Send + '_>>;
155// }
156
157// #[cfg(test)]
158// mod tests {
159// #[cfg(feature = "format_dd")]
160// #[test]
161// fn format_dd_integer() {
162// use crate::Coordinate;
163
164// let expected = Coordinate { lat: 10., lng: 20. };
165// let real = Coordinate::format_dd("10,20").unwrap();
166// assert_eq!(expected, real);
167// }
168// #[cfg(feature = "format_dd")]
169// #[test]
170// fn format_dd_float() {
171// use crate::Coordinate;
172
173// let expected = Coordinate { lat: 10., lng: 20. };
174// let real = Coordinate::format_dd("10.0,20.0").unwrap();
175// assert_eq!(expected, real);
176// }
177// #[cfg(feature = "format_dd")]
178// #[test]
179// fn format_dd_invalid() {
180// use crate::{Coordinate, CoordinateError};
181
182// match Coordinate::format_dd("Asd,20.0") {
183// Err(CoordinateError::Malformed) => {}
184// Err(_) => panic!("Wrong Error"),
185// Ok(_) => panic!("Should've failed"),
186// }
187// }
188// }