Skip to main content

use_spatial_reference/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7fn non_empty_text(value: impl AsRef<str>) -> Result<String, SpatialReferenceTextError> {
8    let trimmed = value.as_ref().trim();
9
10    if trimmed.is_empty() {
11        Err(SpatialReferenceTextError::Empty)
12    } else {
13        Ok(trimmed.to_string())
14    }
15}
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum SpatialReferenceTextError {
19    Empty,
20}
21
22impl fmt::Display for SpatialReferenceTextError {
23    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            Self::Empty => formatter.write_str("spatial reference text cannot be empty"),
26        }
27    }
28}
29
30impl Error for SpatialReferenceTextError {}
31
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub enum SpatialReferenceValueError {
34    ZeroEpsgCode,
35}
36
37impl fmt::Display for SpatialReferenceValueError {
38    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            Self::ZeroEpsgCode => formatter.write_str("EPSG code must be greater than zero"),
41        }
42    }
43}
44
45impl Error for SpatialReferenceValueError {}
46
47#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
48pub struct SpatialReferenceId(String);
49
50impl SpatialReferenceId {
51    /// Creates a spatial reference identifier from non-empty text.
52    ///
53    /// # Errors
54    ///
55    /// Returns [`SpatialReferenceTextError::Empty`] when the trimmed value is empty.
56    pub fn new(value: impl AsRef<str>) -> Result<Self, SpatialReferenceTextError> {
57        non_empty_text(value).map(Self)
58    }
59
60    #[must_use]
61    pub fn from_epsg(code: EpsgCode) -> Self {
62        Self(code.to_string())
63    }
64
65    #[must_use]
66    pub fn as_str(&self) -> &str {
67        &self.0
68    }
69
70    #[must_use]
71    pub fn into_string(self) -> String {
72        self.0
73    }
74}
75
76impl AsRef<str> for SpatialReferenceId {
77    fn as_ref(&self) -> &str {
78        self.as_str()
79    }
80}
81
82impl fmt::Display for SpatialReferenceId {
83    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84        formatter.write_str(self.as_str())
85    }
86}
87
88impl FromStr for SpatialReferenceId {
89    type Err = SpatialReferenceTextError;
90
91    fn from_str(value: &str) -> Result<Self, Self::Err> {
92        Self::new(value)
93    }
94}
95
96#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
97pub struct EpsgCode(u32);
98
99impl EpsgCode {
100    /// Creates an EPSG code from a non-zero integer.
101    ///
102    /// # Errors
103    ///
104    /// Returns [`SpatialReferenceValueError::ZeroEpsgCode`] when `value` is zero.
105    pub const fn new(value: u32) -> Result<Self, SpatialReferenceValueError> {
106        if value == 0 {
107            return Err(SpatialReferenceValueError::ZeroEpsgCode);
108        }
109
110        Ok(Self(value))
111    }
112
113    #[must_use]
114    pub const fn get(self) -> u32 {
115        self.0
116    }
117}
118
119impl fmt::Display for EpsgCode {
120    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
121        write!(formatter, "EPSG:{}", self.get())
122    }
123}
124
125#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
126pub struct SpatialReferenceSystem {
127    identifier: SpatialReferenceId,
128}
129
130impl SpatialReferenceSystem {
131    #[must_use]
132    pub const fn new(identifier: SpatialReferenceId) -> Self {
133        Self { identifier }
134    }
135
136    #[must_use]
137    pub const fn identifier(&self) -> &SpatialReferenceId {
138        &self.identifier
139    }
140}
141
142impl fmt::Display for SpatialReferenceSystem {
143    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
144        self.identifier.fmt(formatter)
145    }
146}
147
148#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
149pub struct CoordinateReferenceSystem {
150    identifier: SpatialReferenceId,
151}
152
153impl CoordinateReferenceSystem {
154    #[must_use]
155    pub const fn new(identifier: SpatialReferenceId) -> Self {
156        Self { identifier }
157    }
158
159    #[must_use]
160    pub fn from_epsg(code: EpsgCode) -> Self {
161        Self::new(SpatialReferenceId::from_epsg(code))
162    }
163
164    #[must_use]
165    pub fn from_system(system: SpatialReferenceSystem) -> Self {
166        Self::new(system.identifier)
167    }
168
169    #[must_use]
170    pub const fn identifier(&self) -> &SpatialReferenceId {
171        &self.identifier
172    }
173}
174
175impl fmt::Display for CoordinateReferenceSystem {
176    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
177        self.identifier.fmt(formatter)
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::{
184        CoordinateReferenceSystem, EpsgCode, SpatialReferenceId, SpatialReferenceTextError,
185        SpatialReferenceValueError,
186    };
187
188    #[test]
189    fn valid_epsg_code() -> Result<(), SpatialReferenceValueError> {
190        let epsg = EpsgCode::new(3857)?;
191
192        assert_eq!(epsg.get(), 3857);
193        Ok(())
194    }
195
196    #[test]
197    fn zero_epsg_code_rejected() {
198        assert_eq!(
199            EpsgCode::new(0),
200            Err(SpatialReferenceValueError::ZeroEpsgCode)
201        );
202    }
203
204    #[test]
205    fn spatial_reference_id_construction() -> Result<(), SpatialReferenceTextError> {
206        let identifier = SpatialReferenceId::new("WGS84")?;
207
208        assert_eq!(identifier.as_str(), "WGS84");
209        Ok(())
210    }
211
212    #[test]
213    fn empty_spatial_reference_id_rejected() {
214        assert_eq!(
215            SpatialReferenceId::new("   "),
216            Err(SpatialReferenceTextError::Empty)
217        );
218    }
219
220    #[test]
221    fn crs_identifier_display() -> Result<(), SpatialReferenceValueError> {
222        let crs = CoordinateReferenceSystem::from_epsg(EpsgCode::new(4326)?);
223
224        assert_eq!(crs.to_string(), "EPSG:4326");
225        Ok(())
226    }
227}