• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zbraniecki / icu4x / 9357137046

03 Jun 2024 08:51PM UTC coverage: 75.121% (-1.1%) from 76.254%
9357137046

push

github

web-flow
Switch locid Value to use Subtag (#4941)

This is part of #1833 switching Value API to use Subtag.

61 of 71 new or added lines in 11 files covered. (85.92%)

3224 existing lines in 178 files now uncovered.

52958 of 70497 relevant lines covered (75.12%)

572757.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.52
/utils/pattern/src/multi_named.rs
1
// This file is part of ICU4X. For terms of use, please see the file
×
2
// called LICENSE at the top level of the ICU4X source tree
3
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4

5
//! Code for the [`MultiNamedPlaceholder`] pattern backend.
6

7
#[cfg(feature = "alloc")]
8
use alloc::{borrow::Cow, collections::BTreeMap, str::FromStr, string::String};
9
use core::fmt;
10
#[cfg(feature = "litemap")]
11
use litemap::LiteMap;
12
use writeable::Writeable;
13

14
use crate::common::*;
15
use crate::Error;
16

17
/// A string wrapper for the [`MultiNamedPlaceholder`] pattern backend.
18
///
19
/// # Examples
20
///
21
/// ```
22
/// use core::cmp::Ordering;
23
/// use core::str::FromStr;
24
/// use icu_pattern::MultiNamedPlaceholderKey;
25
/// use icu_pattern::MultiNamedPlaceholderPattern;
26
/// use icu_pattern::PatternItem;
27
///
28
/// // Parse the string syntax and check the resulting data store:
29
/// let pattern = MultiNamedPlaceholderPattern::from_str(
30
///     "Hello, {person0} and {person1}!",
31
/// )
32
/// .unwrap();
33
///
34
/// assert_eq!(
35
///     pattern.iter().cmp(
36
///         [
37
///             PatternItem::Literal("Hello, "),
38
///             PatternItem::Placeholder(MultiNamedPlaceholderKey(
39
///                 "person0".into()
40
///             )),
41
///             PatternItem::Literal(" and "),
42
///             PatternItem::Placeholder(MultiNamedPlaceholderKey(
43
///                 "person1".into()
44
///             )),
45
///             PatternItem::Literal("!")
46
///         ]
47
///         .into_iter()
48
///     ),
49
///     Ordering::Equal
50
/// );
51
/// ```
52
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
4✔
53
#[repr(transparent)]
54
#[allow(clippy::exhaustive_structs)] // transparent newtype
55
pub struct MultiNamedPlaceholderKey<'a>(pub &'a str);
2✔
56

57
/// Cowable version of [`MultiNamedPlaceholderKey`], used during construction.
58
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
×
59
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
×
60
#[repr(transparent)]
61
#[allow(clippy::exhaustive_structs)] // transparent newtype
62
#[cfg(feature = "alloc")]
63
pub struct MultiNamedPlaceholderKeyCow<'a>(pub Cow<'a, str>);
×
64

65
#[cfg(feature = "alloc")]
66
impl<'a> FromStr for MultiNamedPlaceholderKeyCow<'a> {
67
    type Err = Error;
68
    fn from_str(s: &str) -> Result<Self, Self::Err> {
96✔
69
        // Can't borrow the str here unfortunately
70
        Ok(MultiNamedPlaceholderKeyCow(Cow::Owned(String::from(s))))
96✔
71
    }
96✔
72
}
73

74
#[derive(Debug, Clone, PartialEq, Eq)]
2✔
75
#[non_exhaustive]
76
pub struct MissingNamedPlaceholderError<'a> {
77
    pub name: &'a str,
1✔
78
}
79

80
impl<'a> Writeable for MissingNamedPlaceholderError<'a> {
81
    fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result {
×
82
        sink.write_char('{')?;
×
83
        sink.write_str(self.name)?;
×
84
        sink.write_char('}')?;
×
85
        Ok(())
×
86
    }
×
87
}
88

89
#[cfg(feature = "alloc")]
90
impl<'k, K, W> PlaceholderValueProvider<MultiNamedPlaceholderKey<'k>> for BTreeMap<K, W>
91
where
92
    K: Ord + core::borrow::Borrow<str>,
93
    W: Writeable,
94
{
95
    type Error = MissingNamedPlaceholderError<'k>;
96
    type W<'a> = Result<&'a W, Self::Error> where W: 'a, Self: 'a;
97
    const LITERAL_PART: writeable::Part = crate::PATTERN_LITERAL_PART;
98
    #[inline]
99
    fn value_for<'a>(
100
        &'a self,
101
        key: MultiNamedPlaceholderKey<'k>,
102
    ) -> (Self::W<'a>, writeable::Part) {
103
        let writeable = match self.get(key.0) {
104
            Some(value) => Ok(value),
105
            None => Err(MissingNamedPlaceholderError { name: key.0 }),
106
        };
107
        (writeable, crate::PATTERN_PLACEHOLDER_PART)
108
    }
109
}
110

111
#[cfg(feature = "litemap")]
112
impl<'k, K, W, S> PlaceholderValueProvider<MultiNamedPlaceholderKey<'k>> for LiteMap<K, W, S>
113
where
114
    K: Ord + core::borrow::Borrow<str>,
115
    W: Writeable,
116
    S: litemap::store::Store<K, W>,
117
{
118
    type Error = MissingNamedPlaceholderError<'k>;
119
    type W<'a> = Result<&'a W, Self::Error> where W: 'a, Self: 'a;
120
    const LITERAL_PART: writeable::Part = crate::PATTERN_LITERAL_PART;
121
    #[inline]
122
    fn value_for<'a>(
123
        &'a self,
124
        key: MultiNamedPlaceholderKey<'k>,
125
    ) -> (Self::W<'a>, writeable::Part) {
126
        let writeable = match self.get(key.0) {
127
            Some(value) => Ok(value),
128
            None => Err(MissingNamedPlaceholderError { name: key.0 }),
129
        };
130
        (writeable, crate::PATTERN_PLACEHOLDER_PART)
131
    }
132
}
133

134
/// Backend for patterns containing zero or more named placeholders.
135
///
136
/// This empty type is not constructible.
137
///
138
/// # Placeholder Keys
139
///
140
/// The placeholder is [`MultiNamedPlaceholderKey`].
141
///
142
/// In [`Pattern::interpolate()`], pass a map-like structure. Missing keys will be replaced
143
/// with the Unicode replacement character U+FFFD.
144
///
145
/// # Encoding Details
146
///
147
/// The literals and placeholders are stored in context. A placeholder is encoded as a name length
148
/// in octal code points followed by the placeholder name.
149
///
150
/// For example, consider the pattern: "Hello, {user} and {someone_else}!"
151
///
152
/// The encoding for this would be:
153
///
154
/// ```txt
155
/// Hello, \x00\x04user and \x01\x04someone_else!
156
/// ```
157
///
158
/// where `\x00\x04` and `\x01\x04` are a big-endian octal number representing the lengths of
159
/// their respective placeholder names.
160
///
161
/// Consequences of this encoding:
162
///
163
/// 1. The maximum placeholder name length is 64 bytes
164
/// 2. Code points in the range `\x00` through `\x07` are reserved for the placeholder name
165
///
166
/// # Examples
167
///
168
/// Parsing and comparing the pattern store:
169
///
170
/// ```
171
/// use core::str::FromStr;
172
/// use icu_pattern::MultiNamedPlaceholder;
173
/// use icu_pattern::Pattern;
174
///
175
/// // Parse the string syntax and check the resulting data store:
176
/// let store = Pattern::<MultiNamedPlaceholder, _>::from_str(
177
///     "Hello, {user} and {someone_else}!",
178
/// )
179
/// .unwrap()
180
/// .take_store();
181
///
182
/// assert_eq!("Hello, \x00\x04user and \x01\x04someone_else!", store);
183
/// ```
184
///
185
/// Example patterns supported by this backend:
186
///
187
/// ```
188
/// use core::str::FromStr;
189
/// use icu_pattern::MultiNamedPlaceholder;
190
/// use icu_pattern::Pattern;
191
/// use std::collections::BTreeMap;
192
///
193
/// let placeholder_value_map: BTreeMap<&str, &str> = [
194
///     ("num", "5"),
195
///     ("letter", "X"),
196
///     ("", "empty"),
197
///     ("unused", "unused"),
198
/// ]
199
/// .into_iter()
200
/// .collect();
201
///
202
/// // Single placeholder:
203
/// assert_eq!(
204
///     Pattern::<MultiNamedPlaceholder, _>::from_str("{num} days ago")
205
///         .unwrap()
206
///         .try_interpolate_to_string(&placeholder_value_map)
207
///         .unwrap(),
208
///     "5 days ago",
209
/// );
210
///
211
/// // No placeholder (note, the placeholder value is never accessed):
212
/// assert_eq!(
213
///     Pattern::<MultiNamedPlaceholder, _>::from_str("yesterday")
214
///         .unwrap()
215
///         .try_interpolate_to_string(&placeholder_value_map)
216
///         .unwrap(),
217
///     "yesterday",
218
/// );
219
///
220
/// // No literals, only placeholders:
221
/// assert_eq!(
222
///     Pattern::<MultiNamedPlaceholder, _>::from_str("{letter}{num}{}")
223
///         .unwrap()
224
///         .try_interpolate_to_string(&placeholder_value_map)
225
///         .unwrap(),
226
///     "X5empty",
227
/// );
228
/// ```
229
///
230
/// Use [`LiteMap`] for alloc-free formatting:
231
///
232
/// ```
233
/// use core::str::FromStr;
234
/// use icu_pattern::MultiNamedPlaceholderPattern;
235
/// use litemap::LiteMap;
236
/// use writeable::TryWriteable;
237
///
238
/// static placeholder_value_map: LiteMap<&str, usize, &[(&str, usize)]> =
239
///     LiteMap::from_sorted_store_unchecked(&[("seven", 11)]);
240
///
241
/// // Note: String allocates, but this could be a non-allocating sink
242
/// let mut sink = String::new();
243
///
244
/// MultiNamedPlaceholderPattern::from_str("{seven}")
245
///     .unwrap()
246
///     .try_interpolate(&placeholder_value_map)
247
///     .try_write_to(&mut sink)
248
///     .unwrap()
249
///     .unwrap();
250
///
251
/// assert_eq!(sink, "11");
252
/// ```
253
///
254
/// Missing placeholder values cause an error result to be returned. However,
255
/// based on the design of [`TryWriteable`], the error can be discarded to get
256
/// a best-effort interpolation with potential replacement characters.
257
///
258
/// ```should_panic
259
/// use core::str::FromStr;
260
/// use icu_pattern::MultiNamedPlaceholder;
261
/// use icu_pattern::Pattern;
262
/// use std::collections::BTreeMap;
263
///
264
/// let placeholder_value_map: BTreeMap<&str, &str> =
265
///     [("num", "5"), ("letter", "X")].into_iter().collect();
266
///
267
/// Pattern::<MultiNamedPlaceholder, _>::from_str("Your name is {your_name}")
268
///     .unwrap()
269
///     .try_interpolate_to_string(&placeholder_value_map)
270
///     .unwrap();
271
/// ```
272
///
273
/// Recover the best-effort lossy string by directly using [`Pattern::try_interpolate()`]:
274
///
275
/// ```
276
/// use core::str::FromStr;
277
/// use icu_pattern::MissingNamedPlaceholderError;
278
/// use icu_pattern::MultiNamedPlaceholder;
279
/// use icu_pattern::Pattern;
280
/// use std::borrow::Cow;
281
/// use std::collections::BTreeMap;
282
/// use writeable::TryWriteable;
283
///
284
/// let placeholder_value_map: BTreeMap<&str, &str> =
285
///     [("num", "5"), ("letter", "X")].into_iter().collect();
286
///
287
/// let pattern = Pattern::<MultiNamedPlaceholder, _>::from_str(
288
///     "Your name is {your_name}",
289
/// )
290
/// .unwrap();
291
///
292
/// let mut buffer = String::new();
293
/// let result = pattern
294
///     .try_interpolate(&placeholder_value_map)
295
///     .try_write_to(&mut buffer)
296
///     .expect("infallible write to String");
297
///
298
/// assert!(matches!(result, Err(MissingNamedPlaceholderError { .. })));
299
/// assert_eq!(result.unwrap_err().name, "your_name");
300
/// assert_eq!(buffer, "Your name is {your_name}");
301
/// ```
302
///
303
/// [`Pattern::interpolate()`]: crate::Pattern::interpolate
304
/// [`Pattern::try_interpolate()`]: crate::Pattern::try_interpolate
305
/// [`TryWriteable`]: writeable::TryWriteable
306
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
×
307
#[allow(clippy::exhaustive_enums)] // Empty Enum
308
pub enum MultiNamedPlaceholder {}
309

310
impl crate::private::Sealed for MultiNamedPlaceholder {}
311

312
impl PatternBackend for MultiNamedPlaceholder {
313
    type PlaceholderKey<'a> = MultiNamedPlaceholderKey<'a>;
314
    #[cfg(feature = "alloc")]
315
    type PlaceholderKeyCow<'a> = MultiNamedPlaceholderKeyCow<'a>;
316
    type Error<'a> = MissingNamedPlaceholderError<'a>;
317
    type Store = str;
318
    type Iter<'a> = MultiNamedPlaceholderPatternIterator<'a>;
319

320
    fn validate_store(store: &Self::Store) -> Result<(), Error> {
33✔
321
        let mut iter = MultiNamedPlaceholderPatternIterator::new(store);
33✔
322
        while iter
195✔
323
            .try_next()
324
            .map_err(|e| match e {
4✔
325
                MultiNamedPlaceholderError::InvalidStore => Error::InvalidPattern,
4✔
326
                MultiNamedPlaceholderError::Unreachable => {
327
                    debug_assert!(false, "unreachable");
×
UNCOV
328
                    Error::InvalidPattern
×
329
                }
330
            })?
8✔
331
            .is_some()
332
        {}
333
        Ok(())
29✔
334
    }
33✔
335

336
    fn iter_items(store: &Self::Store) -> Self::Iter<'_> {
32✔
337
        MultiNamedPlaceholderPatternIterator::new(store)
32✔
338
    }
32✔
339

340
    #[cfg(feature = "alloc")]
341
    fn try_from_items<
24✔
342
        'cow,
343
        'ph,
344
        I: Iterator<Item = Result<PatternItemCow<'cow, Self::PlaceholderKeyCow<'ph>>, Error>>,
345
    >(
346
        items: I,
347
    ) -> Result<String, Error> {
348
        let mut string = String::new();
268✔
349
        for item in items {
175✔
350
            match item? {
143✔
351
                PatternItemCow::Literal(s) if s.contains(|x| (x as usize) <= 0x07) => {
140✔
352
                    // TODO: Should this be a different error type?
353
                    return Err(Error::InvalidPattern);
4✔
354
                }
4✔
355
                PatternItemCow::Literal(s) => string.push_str(&s),
57✔
356
                PatternItemCow::Placeholder(ph_key) => {
91✔
357
                    let name_length = ph_key.0.len();
91✔
358
                    if name_length >= 64 {
81✔
359
                        return Err(Error::InvalidPlaceholder);
1✔
360
                    }
361
                    let lead = (name_length >> 3) as u8;
82✔
362
                    let trail = (name_length & 0x7) as u8;
82✔
363
                    string.push(char::from(lead));
80✔
364
                    string.push(char::from(trail));
80✔
365
                    string.push_str(&ph_key.0);
79✔
366
                }
83✔
367
            }
368
        }
31✔
369
        Ok(string)
20✔
370
    }
24✔
371
}
372

373
#[derive(Debug)]
×
374
pub struct MultiNamedPlaceholderPatternIterator<'a> {
375
    store: &'a str,
×
376
}
377

378
// Note: we don't implement ExactSizeIterator since we don't store that metadata in MultiNamed.
379

380
impl<'a> Iterator for MultiNamedPlaceholderPatternIterator<'a> {
381
    type Item = PatternItem<'a, MultiNamedPlaceholderKey<'a>>;
382
    fn next(&mut self) -> Option<Self::Item> {
212✔
383
        match self.try_next() {
212✔
384
            Ok(next) => next,
212✔
385
            Err(MultiNamedPlaceholderError::InvalidStore) => {
386
                debug_assert!(
×
387
                    false,
388
                    "invalid store with {} bytes remaining",
389
                    self.store.len()
×
390
                );
UNCOV
391
                None
×
392
            }
393
            Err(MultiNamedPlaceholderError::Unreachable) => {
394
                debug_assert!(false, "unreachable");
×
UNCOV
395
                None
×
396
            }
397
        }
398
    }
212✔
399
}
400

401
enum MultiNamedPlaceholderError {
402
    InvalidStore,
403
    Unreachable,
404
}
405

406
impl<'a> MultiNamedPlaceholderPatternIterator<'a> {
407
    fn new(store: &'a str) -> Self {
65✔
408
        Self { store }
65✔
409
    }
65✔
410

411
    fn try_next(
409✔
412
        &mut self,
413
    ) -> Result<Option<PatternItem<'a, MultiNamedPlaceholderKey<'a>>>, MultiNamedPlaceholderError>
414
    {
415
        match self.store.find(|x| (x as usize) <= 0x07) {
1,099✔
416
            Some(0) => {
417
                // Placeholder
418
                let Some(&[lead, trail]) = self.store.as_bytes().get(0..2) else {
200✔
419
                    return Err(MultiNamedPlaceholderError::InvalidStore);
2✔
420
                };
421
                debug_assert!(lead <= 7);
198✔
422
                if trail > 7 {
198✔
423
                    return Err(MultiNamedPlaceholderError::InvalidStore);
×
424
                }
425
                let placeholder_len = (lead << 3) + trail;
198✔
426
                let boundary = (placeholder_len as usize) + 2;
198✔
427
                // TODO: use .split_at_checked() when available:
428
                // https://github.com/rust-lang/rust/issues/119128
429
                let Some(placeholder_name) = self.store.get(2..boundary) else {
198✔
430
                    return Err(MultiNamedPlaceholderError::InvalidStore);
2✔
431
                };
432
                let Some(remainder) = self.store.get(boundary..) else {
196✔
433
                    debug_assert!(false, "should be a perfect slice");
×
UNCOV
434
                    return Err(MultiNamedPlaceholderError::Unreachable);
×
435
                };
436
                self.store = remainder;
196✔
437
                Ok(Some(PatternItem::Placeholder(MultiNamedPlaceholderKey(
196✔
438
                    placeholder_name,
439
                ))))
440
            }
196✔
441
            Some(i) => {
135✔
442
                // Literal
443
                // TODO: use .split_at_checked() when available:
444
                // https://github.com/rust-lang/rust/issues/119128
445
                let Some(literal) = self.store.get(..i) else {
135✔
446
                    debug_assert!(false, "should be a perfect slice");
×
UNCOV
447
                    return Err(MultiNamedPlaceholderError::Unreachable);
×
448
                };
449
                let Some(remainder) = self.store.get(i..) else {
135✔
450
                    debug_assert!(false, "should be a perfect slice");
×
UNCOV
451
                    return Err(MultiNamedPlaceholderError::Unreachable);
×
452
                };
453
                self.store = remainder;
135✔
454
                Ok(Some(PatternItem::Literal(literal)))
135✔
455
            }
135✔
456
            None if self.store.is_empty() => {
74✔
457
                // End of string
458
                Ok(None)
61✔
459
            }
460
            None => {
461
                // Closing literal
462
                let literal = self.store;
13✔
463
                self.store = "";
13✔
464
                Ok(Some(PatternItem::Literal(literal)))
13✔
465
            }
13✔
466
        }
467
    }
409✔
468
}
469

470
#[cfg(test)]
471
mod tests {
472
    use crate::MultiNamedPlaceholderPattern;
473
    use core::str::FromStr;
474

475
    #[test]
476
    fn test_invalid() {
2✔
477
        let long_str = "0123456789".repeat(1000000);
1✔
478
        let strings = [
1✔
479
            "{",    // invalid syntax
480
            "{@}",  // placeholder name too long
481
            "\x00", // invalid character
482
            "\x07", // invalid character
483
        ];
484
        for string in strings {
1✔
485
            let string = string.replace('@', &long_str);
4✔
486
            assert!(
4✔
487
                MultiNamedPlaceholderPattern::from_str(&string).is_err(),
4✔
488
                "{string:?}"
489
            );
490
        }
4✔
491
        let stores = [
17✔
492
            "\x00",      // too short
493
            "\x02",      // too short
494
            "\x00\x02",  // no placeholder name
495
            "\x00\x02a", // placeholder name too short
496
        ];
497
        for store in stores {
1✔
498
            let store = store.replace('@', &long_str);
4✔
499
            assert!(
4✔
500
                MultiNamedPlaceholderPattern::try_from_store(&store).is_err(),
4✔
501
                "{store:?}"
502
            );
503
        }
4✔
504
    }
8✔
505
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc