wdl_ast/v1/task/common/container/value/
uri.rs1use std::ops::Deref;
5use std::str::FromStr;
6
7use wdl_grammar::SyntaxNode;
8
9use crate::AstNode;
10use crate::TreeNode;
11use crate::v1::LiteralString;
12
13pub const ANY_CONTAINER_VALUE: &str = "*";
16
17const DEFAULT_PROTOCOL: &str = "docker";
19
20const PROTOCOL_SEPARATOR: &str = "://";
22
23const TAG_SEPARATOR: &str = ":";
26
27const SHA256_TOKEN: &str = "@sha256:";
29
30#[derive(Debug)]
32pub enum Error {
33 EmptyTag,
35
36 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
54type Result<T> = std::result::Result<T, Error>;
56
57#[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#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct Location {
78 offset_within_parent: usize,
80
81 value: String,
83
84 image_end: usize,
87
88 tag_start: Option<usize>,
91
92 immutable: bool,
94}
95
96impl Location {
97 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 pub fn offset_within_parent(&self) -> usize {
136 self.offset_within_parent
137 }
138
139 pub fn image(&self) -> &str {
141 &self.value[..self.image_end]
142 }
143
144 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 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#[derive(Clone, Debug, Eq, PartialEq)]
169pub struct Entry {
170 protocol: Option<Protocol>,
172
173 location: Location,
175}
176
177impl Entry {
178 pub fn protocol(&self) -> Option<&Protocol> {
180 self.protocol.as_ref()
181 }
182
183 pub fn location(&self) -> &Location {
185 &self.location
186 }
187
188 pub fn image(&self) -> &str {
190 self.location.image()
191 }
192
193 pub fn tag(&self) -> Option<&str> {
195 self.location.tag()
196 }
197
198 pub fn immutable(&self) -> bool {
200 self.location.immutable()
201 }
202}
203
204#[derive(Clone, Debug, Eq, PartialEq)]
206pub enum Kind {
207 Any,
209
210 Entry(Entry),
212}
213
214impl Kind {
215 pub fn is_any(&self) -> bool {
217 matches!(self, Kind::Any)
218 }
219
220 pub fn is_entry(&self) -> bool {
222 matches!(self, Kind::Entry(_))
223 }
224
225 pub fn as_entry(&self) -> Option<&Entry> {
231 match self {
232 Kind::Entry(entry) => Some(entry),
233 _ => None,
234 }
235 }
236
237 pub fn into_entry(self) -> Option<Entry> {
242 match self {
243 Kind::Entry(entry) => Some(entry),
244 _ => None,
245 }
246 }
247
248 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#[derive(Clone, Debug, Eq, PartialEq)]
287pub struct Uri<N: TreeNode = SyntaxNode> {
288 kind: Kind,
290
291 literal_string: LiteralString<N>,
293}
294
295impl<N: TreeNode> Uri<N> {
296 pub fn kind(&self) -> &Kind {
298 &self.kind
299 }
300
301 pub fn into_kind(self) -> Kind {
303 self.kind
304 }
305
306 pub fn literal_string(&self) -> &LiteralString<N> {
308 &self.literal_string
309 }
310
311 pub fn into_literal_string(self) -> LiteralString<N> {
313 self.literal_string
314 }
315
316 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}