Skip to main content

wdl_core/file/
location.rs

1//! Locations.
2//!
3//! ## [`Position`]
4//!
5//! A [`Position`] is a row and a column within a file. [`Positions`](Position)
6//! are the foundation of many of the broader [`Location`] types.
7//!
8//! ## [`Location`]
9//!
10//! A [`Location`] refers to coordinate within a file where an element
11//! originated (or lack thereof). [`Locations`](Location) can be one of the
12//! following:
13//!
14//! * [`Location::Unplaced`], meaning the entity associated with the location
15//!   did not originate from any location within a file. This is generally
16//!   useful when you'd like to represent the location of an element generated
17//!   by code rather than parsed from a file.
18//! * [`Location::Position`], meaning an entity originated at a single position
19//!   within a file.
20//! * [`Location::Span`], meaning an entity is represented by a range between a
21//!   start and end position within a file.
22//!
23//! Within `wdl-core`, [`Locations`](Location) are generally used in conjunction
24//! with the [`Located<E>`] type.
25//!
26//! ## [`Located<E>`]
27//!
28//! This module introduces [`Located<E>`]—a wrapper type that pairs entities
29//! (`E`) with a [`Location`]. The [`Located`] type provides direct access to
30//! the `E` value via dereferencing and exposes the associated [`Location`]
31//! through the [`Located::location()`] method. Notably, trait implementations
32//! (excluding [`Clone`]) focus solely on the inner `E` value, meaning
33//! operations like comparison, hashing, and ordering do not consider the
34//! [`Location`]. This ensures that the type is generally treated as the inner
35//! `E` while also providing the context of the [`Location`] when desired.
36
37mod located;
38pub mod position;
39
40pub use located::Located;
41pub use position::Position;
42use serde::Deserialize;
43use serde::Serialize;
44
45/// An error related to a [`Location`].
46#[derive(Debug)]
47pub enum Error {
48    /// A position error.
49    Position(position::Error),
50}
51
52impl std::fmt::Display for Error {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self {
55            Error::Position(err) => write!(f, "position error: {err}"),
56        }
57    }
58}
59
60impl std::error::Error for Error {}
61
62/// A 1-based location.
63///
64/// See the [module documentation](crate::file::Location) for more information.
65#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
66pub enum Location {
67    /// No location.
68    ///
69    /// This is generally the case when an element was programmatically
70    /// generated instead of parsed from an existing document.
71    Unplaced,
72
73    /// A single position.
74    Position(Position),
75
76    /// Spanning from a start location to an end location (inclusive).
77    Span {
78        /// The start position.
79        start: Position,
80
81        /// The end position (inclusive).
82        end: Position,
83    },
84}
85
86impl Location {
87    /// Gets the byte range for the [`Location`].
88    ///
89    /// # Examples
90    ///
91    /// ```
92    /// use std::num::NonZeroUsize;
93    ///
94    /// use wdl_core::file::location::Position;
95    /// use wdl_core::file::Location;
96    ///
97    /// let location = Location::Unplaced;
98    /// assert!(location.byte_range().is_none());
99    ///
100    /// let location = Location::Position(Position::new(
101    ///     NonZeroUsize::try_from(1).unwrap(),
102    ///     NonZeroUsize::try_from(1).unwrap(),
103    ///     0,
104    /// ));
105    /// assert_eq!(location.byte_range(), Some(0..0));
106    ///
107    /// let location = Location::Span {
108    ///     start: Position::new(
109    ///         NonZeroUsize::try_from(1).unwrap(),
110    ///         NonZeroUsize::try_from(1).unwrap(),
111    ///         0,
112    ///     ),
113    ///     end: Position::new(
114    ///         NonZeroUsize::try_from(3).unwrap(),
115    ///         NonZeroUsize::try_from(4).unwrap(),
116    ///         6,
117    ///     ),
118    /// };
119    /// assert_eq!(location.byte_range(), Some(0..6));
120    /// ```
121    pub fn byte_range(&self) -> Option<std::ops::Range<usize>> {
122        match self {
123            Location::Unplaced => None,
124            Location::Position(position) => Some(position.byte_no()..position.byte_no()),
125            Location::Span { start, end } => Some(start.byte_no()..end.byte_no()),
126        }
127    }
128
129    /// Converts a [`Location`] to a [`String`] (if it can be converted).
130    ///
131    /// Notably, this method conflicts with and does not implement
132    /// [`std::string::ToString`]. This was an intentional decision, as that
133    /// trait assumes that the struct may _always_ be able to be converted into
134    /// a [`String`].
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// use std::num::NonZeroUsize;
140    ///
141    /// use wdl_core::file::location::Position;
142    /// use wdl_core::file::Location;
143    ///
144    /// assert_eq!(Location::Unplaced.to_string(), None);
145    /// assert_eq!(
146    ///     Location::Position(Position::new(
147    ///         NonZeroUsize::try_from(1).unwrap(),
148    ///         NonZeroUsize::try_from(2).unwrap(),
149    ///         1
150    ///     ))
151    ///     .to_string(),
152    ///     Some(String::from("1:2"))
153    /// );
154    /// assert_eq!(
155    ///     Location::Span {
156    ///         start: Position::new(
157    ///             NonZeroUsize::try_from(1).unwrap(),
158    ///             NonZeroUsize::try_from(2).unwrap(),
159    ///             1
160    ///         ),
161    ///         end: Position::new(
162    ///             NonZeroUsize::try_from(3).unwrap(),
163    ///             NonZeroUsize::try_from(4).unwrap(),
164    ///             6
165    ///         )
166    ///     }
167    ///     .to_string(),
168    ///     Some(String::from("1:2-3:4"))
169    /// );
170    /// ```
171    pub fn to_string(&self) -> Option<String> {
172        match self {
173            Location::Unplaced => None,
174            Location::Position(position) => Some(format!("{}", position)),
175            Location::Span { start, end } => Some(format!("{}-{}", start, end)),
176        }
177    }
178}
179
180impl TryFrom<pest::Span<'_>> for Location {
181    type Error = Error;
182
183    fn try_from(span: pest::Span<'_>) -> Result<Self, Self::Error> {
184        let start = Position::try_from(span.start_pos()).map_err(Error::Position)?;
185        let end = Position::try_from(span.end_pos()).map_err(Error::Position)?;
186
187        Ok(Location::Span { start, end })
188    }
189}
190
191impl std::fmt::Display for Location {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        match self {
194            Location::Unplaced => write!(f, ""),
195            Location::Position(position) => write!(f, "{}", position),
196            Location::Span { start, end } => write!(f, "{}-{}", start, end),
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use std::num::NonZeroUsize;
204
205    use super::*;
206
207    #[test]
208    fn display_file() {
209        assert_eq!(Location::Unplaced.to_string(), None);
210    }
211
212    #[test]
213    fn display_position() {
214        let result = Location::Position(Position::new(
215            NonZeroUsize::try_from(1).unwrap(),
216            NonZeroUsize::try_from(1).unwrap(),
217            0,
218        ))
219        .to_string();
220        assert_eq!(result, Some(String::from("1:1")));
221    }
222
223    #[test]
224    fn display_span() {
225        let result = Location::Span {
226            start: Position::new(
227                NonZeroUsize::try_from(1).unwrap(),
228                NonZeroUsize::try_from(1).unwrap(),
229                0,
230            ),
231            end: Position::new(
232                NonZeroUsize::try_from(5).unwrap(),
233                NonZeroUsize::try_from(5).unwrap(),
234                24,
235            ),
236        }
237        .to_string();
238        assert_eq!(result, Some(String::from("1:1-5:5")));
239    }
240}