winget_types/locale/
release_notes.rs

1use alloc::{borrow::Cow, string::String};
2use core::{fmt, str::FromStr};
3
4use thiserror::Error;
5
6#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8#[cfg_attr(feature = "serde", serde(try_from = "String"))]
9#[repr(transparent)]
10pub struct ReleaseNotes(String);
11
12#[derive(Error, Debug, Eq, PartialEq)]
13#[error("Release notes cannot be empty")]
14pub struct ReleaseNotesError;
15
16impl ReleaseNotes {
17    pub const MAX_CHAR_LENGTH: usize = 10_000;
18
19    /// Creates a new `ReleaseNotes` from any type that implements `AsRef<str>`.
20    ///
21    /// Release notes greater than 10,000 characters will be truncated to the first line where the
22    /// total number of characters of that line and all previous lines are less than or equal to
23    /// 10,000 characters.
24    ///
25    /// # Errors
26    ///
27    /// Returns an `Err` if the release notes are empty.
28    pub fn new<T: AsRef<str>>(release_notes: T) -> Result<Self, ReleaseNotesError> {
29        let result =
30            truncate_with_lines::<{ Self::MAX_CHAR_LENGTH }>(release_notes.as_ref().trim());
31        if result.is_empty() {
32            Err(ReleaseNotesError)
33        } else {
34            Ok(Self(result.into_owned()))
35        }
36    }
37
38    /// Creates a new `ReleaseNotes` from any type that implements `<Into<String>>` without checking
39    /// whether it is empty.
40    ///
41    /// # Safety
42    ///
43    /// The value must not be empty.
44    #[must_use]
45    #[inline]
46    pub unsafe fn new_unchecked<T: Into<String>>(release_notes: T) -> Self {
47        Self(release_notes.into())
48    }
49
50    #[must_use]
51    #[inline]
52    pub const fn as_str(&self) -> &str {
53        self.0.as_str()
54    }
55}
56
57impl AsRef<str> for ReleaseNotes {
58    #[inline]
59    fn as_ref(&self) -> &str {
60        self.as_str()
61    }
62}
63
64impl fmt::Display for ReleaseNotes {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        self.0.fmt(f)
67    }
68}
69
70impl FromStr for ReleaseNotes {
71    type Err = ReleaseNotesError;
72
73    fn from_str(s: &str) -> Result<Self, Self::Err> {
74        Self::new(s)
75    }
76}
77
78impl TryFrom<String> for ReleaseNotes {
79    type Error = ReleaseNotesError;
80
81    #[inline]
82    fn try_from(value: String) -> Result<Self, Self::Error> {
83        Self::new(value)
84    }
85}
86
87fn truncate_with_lines<const N: usize>(value: &str) -> Cow<'_, str> {
88    if value.chars().count() <= N {
89        return Cow::Borrowed(value);
90    }
91
92    let mut result = String::new();
93    let mut current_size = 0;
94
95    for (index, line) in value.lines().enumerate() {
96        let prospective_size = current_size + line.chars().count() + "\n".len();
97        if prospective_size > N {
98            break;
99        }
100        if index != 0 {
101            result.push('\n');
102        }
103        result.push_str(line);
104        current_size = prospective_size;
105    }
106
107    Cow::Owned(result)
108}
109
110#[cfg(test)]
111mod tests {
112    use alloc::string::String;
113
114    use super::truncate_with_lines;
115
116    #[test]
117    fn test_truncate_to_lines() {
118        use core::fmt::Write;
119
120        const CHAR_LIMIT: usize = 100;
121
122        let mut buffer = String::new();
123        let mut line_count = 0;
124        while buffer.chars().count() <= CHAR_LIMIT {
125            line_count += 1;
126            writeln!(buffer, "Line {line_count}").unwrap();
127        }
128        let formatted = truncate_with_lines::<CHAR_LIMIT>(&buffer);
129        let formatted_char_count = formatted.chars().count();
130        assert!(formatted_char_count < buffer.chars().count());
131        assert_eq!(formatted.trim().chars().count(), formatted_char_count);
132    }
133}