copybook_safe_text/
lib.rs1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2use copybook_error::{Error, ErrorCode};
9
10pub type Result<T> = std::result::Result<T, Error>;
12
13#[inline]
19#[must_use = "Handle the Result or propagate the error"]
20pub fn parse_usize(s: &str, context: &str) -> Result<usize> {
21 s.parse().map_err(|_| {
22 Error::new(
23 ErrorCode::CBKP001_SYNTAX,
24 format!("Invalid numeric value '{s}' in {context}"),
25 )
26 })
27}
28
29#[inline]
35#[must_use = "Handle the Result or propagate the error"]
36pub fn parse_isize(s: &str, context: &str) -> Result<isize> {
37 s.parse().map_err(|_| {
38 Error::new(
39 ErrorCode::CBKP001_SYNTAX,
40 format!("Invalid signed numeric value '{s}' in {context}"),
41 )
42 })
43}
44
45#[inline]
51#[must_use = "Handle the Result or propagate the error"]
52pub fn safe_parse_u16(s: &str, context: &str) -> Result<u16> {
53 s.parse().map_err(|_| {
54 Error::new(
55 ErrorCode::CBKP001_SYNTAX,
56 format!("Invalid u16 value '{s}' in {context}"),
57 )
58 })
59}
60
61#[inline]
67#[must_use = "Handle the Result or propagate the error"]
68pub fn safe_string_char_at(s: &str, index: usize, context: &str) -> Result<char> {
69 s.chars().nth(index).ok_or_else(|| {
70 Error::new(
71 ErrorCode::CBKP001_SYNTAX,
72 format!(
73 "String character access out of bounds in {context}: index {index} >= length {}",
74 s.len()
75 ),
76 )
77 })
78}
79
80#[inline]
86#[must_use = "Handle the Result or propagate the error"]
87pub fn safe_write(buffer: &mut String, args: std::fmt::Arguments<'_>) -> Result<()> {
88 use std::fmt::Write;
89 buffer.write_fmt(args).map_err(|e| {
90 Error::new(
91 ErrorCode::CBKD101_INVALID_FIELD_TYPE,
92 format!("String formatting error: {e}"),
93 )
94 })
95}
96
97#[inline]
103#[must_use = "Handle the Result or propagate the error"]
104pub fn safe_write_str(buffer: &mut String, s: &str) -> Result<()> {
105 use std::fmt::Write;
106 buffer.write_str(s).map_err(|e| {
107 Error::new(
108 ErrorCode::CBKD101_INVALID_FIELD_TYPE,
109 format!("String write error: {e}"),
110 )
111 })
112}
113
114#[cfg(test)]
115#[allow(clippy::expect_used, clippy::unwrap_used)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn parse_usize_ok() {
121 assert_eq!(parse_usize("123", "test").expect("parse usize"), 123);
122 }
123
124 #[test]
125 fn parse_usize_err() {
126 assert!(matches!(
127 parse_usize("invalid", "test"),
128 Err(error) if error.code == ErrorCode::CBKP001_SYNTAX
129 ));
130 }
131
132 #[test]
133 fn parse_isize_ok() {
134 assert_eq!(parse_isize("-42", "test").expect("parse isize"), -42);
135 }
136
137 #[test]
138 fn safe_parse_u16_ok_and_err() {
139 assert_eq!(safe_parse_u16("42", "test").expect("parse u16"), 42);
140 assert!(matches!(
141 safe_parse_u16("99999", "test"),
142 Err(error) if error.code == ErrorCode::CBKP001_SYNTAX
143 ));
144 }
145
146 #[test]
147 fn safe_string_char_at_ok() {
148 assert_eq!(
149 safe_string_char_at("abc", 1, "test").expect("char index"),
150 'b'
151 );
152 }
153
154 #[test]
155 fn safe_string_char_at_err() {
156 assert!(matches!(
157 safe_string_char_at("abc", 3, "test"),
158 Err(error) if error.code == ErrorCode::CBKP001_SYNTAX
159 ));
160 }
161
162 #[test]
165 fn test_safe_write_basic() {
166 let mut buf = String::new();
167 safe_write(&mut buf, format_args!("hello {}", 42)).unwrap();
168 assert_eq!(buf, "hello 42");
169 }
170
171 #[test]
172 fn test_safe_write_empty_format() {
173 let mut buf = String::new();
174 safe_write(&mut buf, format_args!("")).unwrap();
175 assert_eq!(buf, "");
176 }
177
178 #[test]
179 fn test_safe_write_append() {
180 let mut buf = String::from("prefix:");
181 safe_write(&mut buf, format_args!("value")).unwrap();
182 assert_eq!(buf, "prefix:value");
183 }
184
185 #[test]
188 fn test_safe_write_str_basic() {
189 let mut buf = String::new();
190 safe_write_str(&mut buf, "hello").unwrap();
191 assert_eq!(buf, "hello");
192 }
193
194 #[test]
195 fn test_safe_write_str_empty() {
196 let mut buf = String::new();
197 safe_write_str(&mut buf, "").unwrap();
198 assert_eq!(buf, "");
199 }
200
201 #[test]
202 fn test_safe_write_str_append() {
203 let mut buf = String::from("first");
204 safe_write_str(&mut buf, " second").unwrap();
205 assert_eq!(buf, "first second");
206 }
207
208 #[test]
209 fn test_safe_write_str_unicode() {
210 let mut buf = String::new();
211 safe_write_str(&mut buf, "日本語").unwrap();
212 assert_eq!(buf, "日本語");
213 }
214
215 #[test]
218 fn parse_usize_zero() {
219 assert_eq!(parse_usize("0", "test").unwrap(), 0);
220 }
221
222 #[test]
223 fn parse_usize_whitespace_err() {
224 assert!(parse_usize(" 123", "test").is_err());
225 }
226
227 #[test]
228 fn parse_usize_negative_err() {
229 assert!(parse_usize("-1", "test").is_err());
230 }
231
232 #[test]
233 fn parse_isize_zero() {
234 assert_eq!(parse_isize("0", "test").unwrap(), 0);
235 }
236
237 #[test]
238 fn parse_isize_positive() {
239 assert_eq!(parse_isize("42", "test").unwrap(), 42);
240 }
241
242 #[test]
243 fn parse_isize_empty_err() {
244 assert!(parse_isize("", "test").is_err());
245 }
246
247 #[test]
248 fn safe_parse_u16_zero() {
249 assert_eq!(safe_parse_u16("0", "test").unwrap(), 0);
250 }
251
252 #[test]
253 fn safe_parse_u16_max() {
254 assert_eq!(safe_parse_u16("65535", "test").unwrap(), u16::MAX);
255 }
256
257 #[test]
258 fn safe_parse_u16_negative_err() {
259 assert!(safe_parse_u16("-1", "test").is_err());
260 }
261
262 #[test]
263 fn safe_string_char_at_empty_string() {
264 assert!(safe_string_char_at("", 0, "test").is_err());
265 }
266
267 #[test]
268 fn safe_string_char_at_first_char() {
269 assert_eq!(safe_string_char_at("x", 0, "test").unwrap(), 'x');
270 }
271
272 #[test]
273 fn safe_string_char_at_unicode() {
274 assert_eq!(safe_string_char_at("日本", 1, "test").unwrap(), '本');
275 }
276}