copybook_safe_index/
lib.rs1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2use copybook_error::{Error, ErrorCode};
10
11pub type Result<T> = std::result::Result<T, Error>;
13
14#[inline]
20#[must_use = "Handle the Result or propagate the error"]
21pub fn safe_divide(numerator: usize, denominator: usize, context: &str) -> Result<usize> {
22 if denominator == 0 {
23 return Err(Error::new(
24 ErrorCode::CBKP001_SYNTAX,
25 format!("Division by zero in {context}"),
26 ));
27 }
28 Ok(numerator / denominator)
29}
30
31#[inline]
37#[must_use = "Handle the Result or propagate the error"]
38pub fn safe_slice_get<T>(slice: &[T], index: usize, context: &str) -> Result<T>
39where
40 T: Copy,
41{
42 if index < slice.len() {
43 Ok(slice[index])
44 } else {
45 Err(Error::new(
46 ErrorCode::CBKP001_SYNTAX,
47 format!(
48 "Slice bounds violation in {context}: index {index} >= length {}",
49 slice.len()
50 ),
51 ))
52 }
53}
54
55#[cfg(test)]
56#[allow(clippy::expect_used, clippy::unwrap_used)]
57mod tests {
58 use super::*;
59 use proptest::prelude::*;
60
61 #[test]
62 fn safe_divide_ok() {
63 assert_eq!(safe_divide(10, 2, "test").expect("divide"), 5);
64 }
65
66 #[test]
67 fn safe_divide_by_zero_is_error() {
68 assert!(matches!(
69 safe_divide(10, 0, "test"),
70 Err(error) if error.code == ErrorCode::CBKP001_SYNTAX
71 ));
72 }
73
74 #[test]
75 fn safe_slice_get_ok() {
76 let data = [1u8, 2u8, 3u8];
77 assert_eq!(safe_slice_get(&data, 1, "test").expect("index"), 2u8);
78 }
79
80 #[test]
81 fn safe_slice_get_out_of_range_is_error() {
82 let data = [1u8, 2u8, 3u8];
83 assert!(matches!(
84 safe_slice_get(&data, 99, "test"),
85 Err(error) if error.code == ErrorCode::CBKP001_SYNTAX
86 ));
87 }
88
89 #[test]
92 fn safe_divide_zero_numerator() {
93 assert_eq!(safe_divide(0, 5, "test").expect("0/5"), 0);
94 }
95
96 #[test]
97 fn safe_divide_same_values() {
98 assert_eq!(safe_divide(7, 7, "test").expect("7/7"), 1);
99 }
100
101 #[test]
102 fn safe_divide_integer_truncation() {
103 assert_eq!(safe_divide(7, 2, "test").expect("7/2"), 3);
104 }
105
106 #[test]
107 fn safe_divide_large_values() {
108 assert_eq!(
109 safe_divide(usize::MAX, 1, "test").expect("max/1"),
110 usize::MAX
111 );
112 }
113
114 #[test]
115 fn safe_divide_error_message_contains_context() {
116 let err = safe_divide(1, 0, "my-context").unwrap_err();
117 assert!(
118 err.message.contains("my-context"),
119 "Error message should contain context"
120 );
121 }
122
123 #[test]
126 fn safe_slice_get_empty_slice() {
127 let data: &[u8] = &[];
128 assert!(safe_slice_get(data, 0, "test").is_err());
129 }
130
131 #[test]
132 fn safe_slice_get_first_element() {
133 let data = [42u8];
134 assert_eq!(safe_slice_get(&data, 0, "test").expect("first"), 42);
135 }
136
137 #[test]
138 fn safe_slice_get_last_element() {
139 let data = [1u8, 2, 3, 4, 5];
140 assert_eq!(safe_slice_get(&data, 4, "test").expect("last"), 5);
141 }
142
143 #[test]
144 fn safe_slice_get_exactly_out_of_bounds() {
145 let data = [1u8, 2, 3];
146 assert!(safe_slice_get(&data, 3, "test").is_err());
147 }
148
149 #[test]
150 fn safe_slice_get_with_i32_type() {
151 let data = [10i32, 20, 30];
152 assert_eq!(safe_slice_get(&data, 2, "test").expect("i32"), 30);
153 }
154
155 proptest! {
156 #[test]
157 fn safe_divide_round_trip(
158 numerator in 0usize..1_000_000usize,
159 denominator in 1usize..1000usize,
160 ) {
161 prop_assert_eq!(
162 safe_divide(numerator, denominator, "prop").expect("safe divide"),
163 numerator / denominator
164 );
165 }
166
167 #[test]
168 fn safe_slice_get_round_trip(values in prop::collection::vec(0u8..=255u8, 1..200), index in 0usize..220usize) {
169 let normalized_index = index % (values.len() + 1);
170 let result = safe_slice_get(&values, normalized_index, "prop");
171
172 if normalized_index < values.len() {
173 prop_assert_eq!(result.expect("index in bounds"), values[normalized_index]);
174 } else {
175 prop_assert!(result.is_err());
176 }
177 }
178 }
179}