winget_types/locale/
release_notes.rs1use 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 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 #[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}