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}