1use std::{fmt::Display, str::FromStr};
6
7use nonempty::NonEmpty;
8
9use crate::{
10 utils::{HeaderIteratorExt, NonEmptyExt, ParseString, StrExt},
11 CodedUrl, IF,
12};
13
14pub use self::error::InvalidIf;
15
16#[derive(Clone, Debug, PartialEq)]
18pub enum If {
19 NoTagList(Box<NonEmpty<NonEmpty<Condition>>>),
20 TaggedList(Box<NonEmpty<(ResourceTag, NonEmpty<NonEmpty<Condition>>)>>),
21}
22
23impl headers::Header for If {
24 fn name() -> &'static http::HeaderName {
25 &IF
26 }
27
28 fn decode<'i, I>(values: &mut I) -> Result<Self, headers::Error>
29 where
30 Self: Sized,
31 I: Iterator<Item = &'i http::HeaderValue>,
32 {
33 Ok(values.extract_str()?.parse()?)
34 }
35
36 fn encode<E: Extend<http::HeaderValue>>(&self, values: &mut E) {
37 values.extend(std::iter::once(self.to_string().parse().unwrap()))
38 }
39}
40
41impl Display for If {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 fn fmt_condition_lists(
44 f: &mut std::fmt::Formatter<'_>,
45 lists: &NonEmpty<NonEmpty<Condition>>,
46 ) -> std::fmt::Result {
47 for (i, conditions) in lists.iter().enumerate() {
48 if i > 0 {
49 f.write_str(" ")?;
50 }
51
52 f.write_str("(")?;
53 for (j, condition) in conditions.iter().enumerate() {
54 if j > 0 {
55 f.write_str(" ")?;
56 }
57 condition.fmt(f)?;
58 }
59 f.write_str(")")?;
60 }
61 Ok(())
62 }
63
64 match self {
65 If::NoTagList(lists) => fmt_condition_lists(f, lists)?,
66 If::TaggedList(resources) => {
67 for (resource_tag, lists) in &**resources {
68 resource_tag.fmt(f)?;
69 f.write_str(" ")?;
70 fmt_condition_lists(f, lists)?
71 }
72 }
73 }
74 Ok(())
75 }
76}
77
78impl FromStr for If {
79 type Err = InvalidIf;
80
81 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
82 Self::parse(&mut s)
83 }
84}
85
86impl ParseString for If {
87 type Err = InvalidIf;
88
89 fn peek(mut s: &str) -> Result<(Self, &str), Self::Err> {
90 s = s.trim();
91
92 match ResourceTag::peek(s) {
93 Ok(_) => {
94 let resources = NonEmpty::try_collect(std::iter::from_fn(|| {
95 (!s.is_empty()).then(|| {
96 let resource_tag = ResourceTag::parse(&mut s)?;
97 s = s.trim_start();
98
99 let condition_lists = NonEmpty::try_collect(std::iter::from_fn(|| {
100 (!s.is_empty() && !s.starts_with('<')).then(|| {
101 let conditions = <NonEmpty<Condition>>::parse(&mut s)?;
102 s = s.trim_start();
103 Ok::<_, InvalidIf>(conditions)
104 })
105 }))?
106 .ok_or(InvalidIf::EmptyConditionList)?;
107
108 Ok::<_, InvalidIf>((resource_tag, condition_lists))
109 })
110 }))?
111 .ok_or(InvalidIf::EmptyResourceList)?;
112
113 Ok((If::TaggedList(Box::new(resources)), s))
114 }
115 Err(_) => {
116 let condition_lists = NonEmpty::try_collect(std::iter::from_fn(|| {
117 (!s.is_empty()).then(|| {
118 let conditions = <NonEmpty<Condition>>::parse(&mut s)?;
119 s = s.trim_start();
120 Ok::<_, InvalidIf>(conditions)
121 })
122 }))?
123 .ok_or(InvalidIf::EmptyConditionList)?;
124
125 Ok((If::NoTagList(Box::new(condition_lists)), s))
126 }
127 }
128 }
129}
130
131#[derive(Clone, Debug, PartialEq)]
133pub enum Condition {
134 StateToken { not: bool, coded_url: CodedUrl },
135 ETag { not: bool, etag: String },
136}
137
138impl Display for Condition {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 match self {
141 Condition::StateToken { not, coded_url } => {
142 if *not {
143 f.write_str("Not ")?;
144 }
145 coded_url.fmt(f)?;
146 }
147 Condition::ETag { not, etag } => {
148 if *not {
149 f.write_str("Not ")?;
150 }
151 f.write_fmt(format_args!("[{etag}]"))?;
152 }
153 }
154 Ok(())
155 }
156}
157
158impl ParseString for Condition {
159 type Err = InvalidIf;
160
161 fn peek(mut s: &str) -> Result<(Self, &str), Self::Err> {
162 let not = s.starts_with_ignore_ascii_case("NOT");
163 if not {
164 s = s[3..].trim_start();
165 }
166
167 if s.starts_with('[') {
168 s = &s[1..];
169 let Some(end) = s.find(']') else {
170 return Err(InvalidIf::ExpectedChar(']'));
171 };
172
173 Ok((
174 Condition::ETag {
175 not,
176 etag: s[..end].to_owned(),
177 },
178 &s[end + 1..],
179 ))
180 } else {
181 Ok((
182 Condition::StateToken {
183 not,
184 coded_url: CodedUrl::parse(&mut s).map_err(InvalidIf::CodedUrl)?,
185 },
186 s,
187 ))
188 }
189 }
190}
191
192impl ParseString for NonEmpty<Condition> {
193 type Err = InvalidIf;
194
195 fn peek(mut s: &str) -> Result<(Self, &str), Self::Err> {
196 if s.starts_with('(') {
197 s = s[1..].trim_start();
198 } else {
199 return Err(InvalidIf::ExpectedChar('('));
200 }
201 let conditions = NonEmpty::try_collect(std::iter::from_fn(|| {
202 (!s.starts_with(')')).then(|| {
203 let condition = Condition::parse(&mut s)?;
204 s = s.trim_start();
205 Ok::<_, InvalidIf>(condition)
206 })
207 }))?
208 .ok_or(InvalidIf::EmptyConditionList)?;
209
210 Ok((conditions, s[1..].trim_start()))
211 }
212}
213
214#[derive(Clone, Debug, PartialEq)]
216pub struct ResourceTag(pub http::Uri);
217
218impl Display for ResourceTag {
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 write!(f, "<{}>", self.0)
221 }
222}
223
224impl FromStr for ResourceTag {
225 type Err = InvalidIf;
226
227 fn from_str(mut s: &str) -> Result<Self, Self::Err> {
228 Self::parse(&mut s)
229 }
230}
231
232impl ParseString for ResourceTag {
233 type Err = InvalidIf;
234
235 fn peek(mut s: &str) -> Result<(Self, &str), Self::Err> {
236 if s.starts_with('<') {
237 s = &s[1..];
238 } else {
239 return Err(InvalidIf::ExpectedChar('<'));
240 }
241 let Some(end) = s.find('>') else {
242 return Err(InvalidIf::ExpectedChar('>'));
243 };
244
245 let uri = http::Uri::from_str(&s[..end]).map_err(InvalidIf::Uri)?;
246
247 Ok((ResourceTag(uri), &s[end + 1..]))
248 }
249}
250
251mod error {
252 use crate::InvalidCodedUrl;
253
254 #[derive(Debug)]
256 pub enum InvalidIf {
257 ExpectedChar(char),
258 EmptyConditionList,
259 EmptyResourceList,
260 CodedUrl(InvalidCodedUrl),
261 Uri(http::uri::InvalidUri),
262 }
263
264 impl std::fmt::Display for InvalidIf {
265 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266 match self {
267 Self::ExpectedChar(c) => write!(f, "expected '{c}'"),
268 Self::EmptyConditionList => f.write_str("empty condition list"),
269 Self::EmptyResourceList => f.write_str("empty resource list"),
270 Self::CodedUrl(..) => f.write_str("invalid Coded-URL"),
271 Self::Uri(..) => f.write_str("invalid URI"),
272 }
273 }
274 }
275
276 impl std::error::Error for InvalidIf {
277 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
278 match self {
279 Self::Uri(e) => Some(e),
280 _ => None,
281 }
282 }
283 }
284
285 impl From<InvalidIf> for headers::Error {
286 fn from(_: InvalidIf) -> Self {
287 headers::Error::invalid()
288 }
289 }
290}
291
292#[cfg(test)]
293#[test]
294fn test() {
295 use nonempty::nonempty;
296
297 use crate::test::test_all;
298
299 test_all([
300 (
301 r#"(<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> ["I am an ETag"]) (["I am another ETag"])"#,
303 If::NoTagList(Box::new(nonempty![
304 nonempty![
305 Condition::StateToken {
306 not: false,
307 coded_url: CodedUrl(
308 uniresid::AbsoluteUri::parse(
309 "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2"
310 )
311 .unwrap()
312 )
313 },
314 Condition::ETag {
315 not: false,
316 etag: r#""I am an ETag""#.into()
317 },
318 ],
319 nonempty![
320 Condition::ETag {
321 not: false,
322 etag: r#""I am another ETag""#.into()
323 },
324 ],
325 ])),
326 ),
327 (
328 "(Not <urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> <urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092>)",
330 If::NoTagList(Box::new(nonempty![
331 nonempty![
332 Condition::StateToken {
333 not: true,
334 coded_url: CodedUrl(
335 uniresid::AbsoluteUri::parse("urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2")
336 .unwrap()
337 )
338 },
339 Condition::StateToken {
340 not: false,
341 coded_url: CodedUrl(
342 uniresid::AbsoluteUri::parse("urn:uuid:58f202ac-22cf-11d1-b12d-002035b29092")
343 .unwrap()
344 )
345 },
346 ],
347 ]))
348 ),
349 (
350 r#"</resource1> (<urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> [W/"A weak ETag"]) (["strong ETag"])"#,
352 If::TaggedList(Box::new(nonempty![
353 (
354 ResourceTag("/resource1".parse().unwrap()) ,
355 nonempty![
356 nonempty![
357 Condition::StateToken {
358 not: false,
359 coded_url: CodedUrl(
360 uniresid::AbsoluteUri::parse(
361 "urn:uuid:181d4fae-7d8c-11d0-a765-00a0c91e6bf2"
362 )
363 .unwrap()
364 )
365 },
366 Condition::ETag {
367 not: false,
368 etag: r#"W/"A weak ETag""#.into()
369 }
370 ],
371 nonempty![Condition::ETag {
372 not: false,
373 etag: r#""strong ETag""#.into()
374 }],
375 ]
376 ),
377 ]))
378 ),
379 ]);
380}