wdl_ast/v1/task/common/container/value/
uri.rs

1//! URIs for the `container` item within the `runtime` and `requirements`
2//! blocks.
3
4use std::ops::Deref;
5use std::str::FromStr;
6
7use wdl_grammar::SyntaxNode;
8
9use crate::AstNode;
10use crate::TreeNode;
11use crate::v1::LiteralString;
12
13/// The value of the key that signifies _any_ POSIX-compliant operating
14/// environment may be used.
15pub const ANY_CONTAINER_VALUE: &str = "*";
16
17/// The default protocol for a container URI.
18const DEFAULT_PROTOCOL: &str = "docker";
19
20/// The separator for the protocol section within a container URI.
21const PROTOCOL_SEPARATOR: &str = "://";
22
23/// The separator within the location that splits the image identifier from the
24/// tag.
25const TAG_SEPARATOR: &str = ":";
26
27/// The token that specifies whether an image points to an immutable sha256 tag.
28const SHA256_TOKEN: &str = "@sha256:";
29
30/// An error related to a [`Uri`].
31#[derive(Debug)]
32pub enum Error {
33    /// An empty tag was encountered.
34    EmptyTag,
35
36    /// Attempted to create a [`Uri`] from an interpolated, literal string.
37    Interpolated(String),
38}
39
40impl std::fmt::Display for Error {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        match self {
43            Error::EmptyTag => write!(f, "tag of a container URI cannot be empty"),
44            Error::Interpolated(s) => write!(
45                f,
46                "cannot create a uri from an interpolated string literal: {s}",
47            ),
48        }
49    }
50}
51
52impl std::error::Error for Error {}
53
54/// A [`Result`](std::result::Result) with an [`Error`].
55type Result<T> = std::result::Result<T, Error>;
56
57/// The protocol portion of the container URI.
58#[derive(Clone, Debug, Eq, PartialEq)]
59pub struct Protocol(String);
60
61impl std::ops::Deref for Protocol {
62    type Target = String;
63
64    fn deref(&self) -> &Self::Target {
65        &self.0
66    }
67}
68
69impl Default for Protocol {
70    fn default() -> Self {
71        Self(String::from(DEFAULT_PROTOCOL))
72    }
73}
74
75/// The location portion of the container URI.
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct Location {
78    /// The textual offset of the location within the parent [`Uri`].
79    offset_within_parent: usize,
80
81    /// The entire location.
82    value: String,
83
84    /// The offset at which the image portion of the location ends within the
85    /// value.
86    image_end: usize,
87
88    /// The offset at which the tag portion of the location starts within the
89    /// value.
90    tag_start: Option<usize>,
91
92    /// Whether or not the location is immutable.
93    immutable: bool,
94}
95
96impl Location {
97    /// Attempts to create a new [`Location`].
98    ///
99    /// # Errors
100    ///
101    /// * If the tag is empty, an [`Error::EmptyTag`] will be returned.
102    pub fn try_new(value: String, offset_within_parent: usize) -> Result<Self> {
103        let immutable = value.contains(SHA256_TOKEN);
104
105        let tag_start = value
106            .find(TAG_SEPARATOR)
107            .map(|offset| offset + TAG_SEPARATOR.len())
108            .map(|offset| {
109                if value[offset..].is_empty() {
110                    Err(Error::EmptyTag)
111                } else {
112                    Ok(offset)
113                }
114            })
115            .transpose()?;
116
117        let image_end = if let Some(sha_offset) = value.find(SHA256_TOKEN) {
118            sha_offset
119        } else if let Some(tag_start) = tag_start {
120            tag_start - TAG_SEPARATOR.len()
121        } else {
122            value.len()
123        };
124
125        Ok(Self {
126            offset_within_parent,
127            value,
128            image_end,
129            tag_start,
130            immutable,
131        })
132    }
133
134    /// Gets the textual offset of the location within the parent [`Uri`].
135    pub fn offset_within_parent(&self) -> usize {
136        self.offset_within_parent
137    }
138
139    /// Gets the image portion of the location.
140    pub fn image(&self) -> &str {
141        &self.value[..self.image_end]
142    }
143
144    /// Gets the tag portion of the location (if it exists).
145    pub fn tag(&self) -> Option<&str> {
146        if let Some(offset) = self.tag_start {
147            Some(&self.value[offset..])
148        } else {
149            None
150        }
151    }
152
153    /// Gets whether the location is immutable.
154    pub fn immutable(&self) -> bool {
155        self.immutable
156    }
157}
158
159impl std::ops::Deref for Location {
160    type Target = String;
161
162    fn deref(&self) -> &Self::Target {
163        &self.value
164    }
165}
166
167/// An individual URI entry.
168#[derive(Clone, Debug, Eq, PartialEq)]
169pub struct Entry {
170    /// The protocol.
171    protocol: Option<Protocol>,
172
173    /// The location.
174    location: Location,
175}
176
177impl Entry {
178    /// Gets a reference to the protocol.
179    pub fn protocol(&self) -> Option<&Protocol> {
180        self.protocol.as_ref()
181    }
182
183    /// Gets a reference to the location.
184    pub fn location(&self) -> &Location {
185        &self.location
186    }
187
188    /// Gets the image name.
189    pub fn image(&self) -> &str {
190        self.location.image()
191    }
192
193    /// Gets the tag (if it exists).
194    pub fn tag(&self) -> Option<&str> {
195        self.location.tag()
196    }
197
198    /// Gets whether the [`Entry`] is immutable.
199    pub fn immutable(&self) -> bool {
200        self.location.immutable()
201    }
202}
203
204/// A kind of container URI as defined by the WDL specification.
205#[derive(Clone, Debug, Eq, PartialEq)]
206pub enum Kind {
207    /// Any POSIX-compliant operating environment the executor wishes.
208    Any,
209
210    /// A container URI entry.
211    Entry(Entry),
212}
213
214impl Kind {
215    /// Returns whether this kind is a [`Kind::Any`].
216    pub fn is_any(&self) -> bool {
217        matches!(self, Kind::Any)
218    }
219
220    /// Returns whether this kind is a [`Kind::Entry`].
221    pub fn is_entry(&self) -> bool {
222        matches!(self, Kind::Entry(_))
223    }
224
225    /// Attempts to return a reference to the inner [`Entry`].
226    ///
227    /// - If the value is an [`Kind::Entry`], a reference to the inner [`Entry`]
228    ///   is returned.
229    /// - Else, [`None`] is returned.
230    pub fn as_entry(&self) -> Option<&Entry> {
231        match self {
232            Kind::Entry(entry) => Some(entry),
233            _ => None,
234        }
235    }
236
237    /// Consumes `self` and attempts to return the inner [`Entry`].
238    ///
239    /// - If the value is a [`Kind::Entry`], the inner [`Entry`] is returned.
240    /// - Else, [`None`] is returned.
241    pub fn into_entry(self) -> Option<Entry> {
242        match self {
243            Kind::Entry(entry) => Some(entry),
244            _ => None,
245        }
246    }
247
248    /// Consumes `self` and attempts to return the inner [`Entry`].
249    ///
250    /// # Panics
251    ///
252    /// Panics if the kind is not a [`Kind::Entry`].
253    pub fn unwrap_entry(self) -> Entry {
254        self.into_entry().expect("uri kind is not an entry")
255    }
256}
257
258impl FromStr for Kind {
259    type Err = Error;
260
261    fn from_str(text: &str) -> Result<Self> {
262        if text == ANY_CONTAINER_VALUE {
263            return Ok(Kind::Any);
264        }
265
266        let (protocol, location_offset, location) = match text.find(PROTOCOL_SEPARATOR) {
267            Some(offset) => {
268                let location_offset = offset + PROTOCOL_SEPARATOR.len();
269                (
270                    Some(&text[..offset]),
271                    location_offset,
272                    &text[location_offset..],
273                )
274            }
275            None => (None, 0, text),
276        };
277
278        let protocol = protocol.map(|s| Protocol(String::from(s)));
279        let location = Location::try_new(String::from(location), location_offset)?;
280
281        Ok(Kind::Entry(Entry { protocol, location }))
282    }
283}
284
285/// A container URI as defined by the WDL specification.
286#[derive(Clone, Debug, Eq, PartialEq)]
287pub struct Uri<N: TreeNode = SyntaxNode> {
288    /// The kind of the container URI.
289    kind: Kind,
290
291    /// The literal string backing this container URI.
292    literal_string: LiteralString<N>,
293}
294
295impl<N: TreeNode> Uri<N> {
296    /// Gets the kind of the [`Uri`].
297    pub fn kind(&self) -> &Kind {
298        &self.kind
299    }
300
301    /// Consumes `self` and returns the kind of the [`Uri`].
302    pub fn into_kind(self) -> Kind {
303        self.kind
304    }
305
306    /// Gets the backing literal string of the [`Uri`].
307    pub fn literal_string(&self) -> &LiteralString<N> {
308        &self.literal_string
309    }
310
311    /// Consumes `self` and returns the literal string backing the [`Uri`].
312    pub fn into_literal_string(self) -> LiteralString<N> {
313        self.literal_string
314    }
315
316    /// Consumes `self` and returns the parts of the [`Uri`].
317    pub fn into_parts(self) -> (Kind, LiteralString<N>) {
318        (self.kind, self.literal_string)
319    }
320}
321
322impl<N: TreeNode> Deref for Uri<N> {
323    type Target = Kind;
324
325    fn deref(&self) -> &Self::Target {
326        &self.kind
327    }
328}
329
330impl<N: TreeNode> TryFrom<LiteralString<N>> for Uri<N> {
331    type Error = Error;
332
333    fn try_from(literal_string: LiteralString<N>) -> Result<Self> {
334        let kind = literal_string
335            .text()
336            .ok_or_else(|| Error::Interpolated(literal_string.inner().text().to_string()))?
337            .text()
338            .parse::<Kind>()?;
339
340        Ok(Uri {
341            kind,
342            literal_string,
343        })
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn any_uri_kind() {
353        let kind = "*".parse::<Kind>().expect("kind to parse");
354        assert!(kind.is_any());
355    }
356
357    #[test]
358    fn standard_uri_kind() {
359        let entry = "ubuntu:latest"
360            .parse::<Kind>()
361            .expect("kind to parse")
362            .unwrap_entry();
363
364        assert!(entry.protocol().is_none());
365        assert_eq!(entry.location().as_str(), "ubuntu:latest");
366        assert_eq!(entry.location().image(), "ubuntu");
367        assert_eq!(entry.location().tag().unwrap(), "latest");
368        assert!(!entry.location().immutable());
369    }
370
371    #[test]
372    fn standard_uri_kind_with_protocol() {
373        let entry = "docker://ubuntu:latest"
374            .parse::<Kind>()
375            .expect("uri to parse")
376            .unwrap_entry();
377
378        assert_eq!(entry.protocol().unwrap().as_str(), "docker");
379        assert_eq!(entry.location().as_str(), "ubuntu:latest");
380        assert_eq!(entry.location().image(), "ubuntu");
381        assert_eq!(entry.location().tag().unwrap(), "latest");
382        assert!(!entry.location().immutable());
383    }
384
385    #[test]
386    fn standard_uri_kind_with_protocol_and_immutable_tag() {
387        let entry = "docker://ubuntu@sha256:abcd1234"
388            .parse::<Kind>()
389            .expect("uri to parse")
390            .into_entry()
391            .expect("uri to be an entry");
392        assert_eq!(entry.protocol().unwrap().as_str(), "docker");
393        assert_eq!(entry.location().as_str(), "ubuntu@sha256:abcd1234");
394        assert_eq!(entry.location().image(), "ubuntu");
395        assert_eq!(entry.location().tag().unwrap(), "abcd1234");
396        assert!(entry.location().immutable());
397    }
398
399    #[test]
400    fn standard_uri_kind_with_protocol_without_tag() {
401        let entry = "docker://ubuntu"
402            .parse::<Kind>()
403            .expect("uri to parse")
404            .unwrap_entry();
405
406        assert_eq!(entry.protocol().unwrap().as_str(), "docker");
407        assert_eq!(entry.location().as_str(), "ubuntu");
408        assert_eq!(entry.location().image(), "ubuntu");
409        assert!(entry.location().tag().is_none());
410        assert!(!entry.location().immutable());
411    }
412
413    #[test]
414    fn empty_tag() {
415        let err = "docker://ubuntu:".parse::<Kind>().unwrap_err();
416        assert!(matches!(err, Error::EmptyTag));
417
418        let err = "ubuntu:".parse::<Kind>().unwrap_err();
419        assert!(matches!(err, Error::EmptyTag));
420    }
421}