1use serde::Serialize;
4use thiserror::Error;
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub struct LimitMeasurement {
9 measured: usize,
10 allowed: usize,
11}
12
13impl LimitMeasurement {
14 #[must_use]
16 pub const fn measured(self) -> usize {
17 self.measured
18 }
19
20 #[must_use]
22 pub const fn allowed(self) -> usize {
23 self.allowed
24 }
25}
26
27#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
29#[error("measured {measured} exceeds allowed {allowed}")]
30pub struct LimitExceeded {
31 measured: usize,
32 allowed: usize,
33}
34
35impl LimitExceeded {
36 #[must_use]
38 pub const fn measured(self) -> usize {
39 self.measured
40 }
41
42 #[must_use]
44 pub const fn allowed(self) -> usize {
45 self.allowed
46 }
47}
48
49#[derive(Debug, Error)]
51pub enum CompactJsonLimitError {
52 #[error("compact JSON encoding failed")]
54 Encoding(#[from] serde_json::Error),
55 #[error(transparent)]
57 Limit(#[from] LimitExceeded),
58}
59
60#[must_use]
62pub fn trimmed_unicode_scalar_count(value: &str) -> usize {
63 value.trim().chars().count()
64}
65
66pub fn compact_json_utf8_bytes(value: &impl Serialize) -> Result<usize, serde_json::Error> {
68 serde_json::to_vec(value).map(|encoded| encoded.len())
69}
70
71#[must_use]
73pub const fn byte_length(value: &[u8]) -> usize {
74 value.len()
75}
76
77#[must_use]
79pub const fn collection_length<Element>(value: &[Element]) -> usize {
80 value.len()
81}
82
83pub const fn check_measurement(
85 measured: usize,
86 allowed: usize,
87) -> Result<LimitMeasurement, LimitExceeded> {
88 if measured > allowed {
89 return Err(LimitExceeded { measured, allowed });
90 }
91 Ok(LimitMeasurement { measured, allowed })
92}
93
94pub fn check_trimmed_unicode_scalars(
96 value: &str,
97 allowed: usize,
98) -> Result<LimitMeasurement, LimitExceeded> {
99 check_measurement(trimmed_unicode_scalar_count(value), allowed)
100}
101
102pub fn check_compact_json_utf8_bytes(
104 value: &impl Serialize,
105 allowed: usize,
106) -> Result<LimitMeasurement, CompactJsonLimitError> {
107 let measured = compact_json_utf8_bytes(value)?;
108 check_measurement(measured, allowed).map_err(Into::into)
109}
110
111pub const fn check_bytes(value: &[u8], allowed: usize) -> Result<LimitMeasurement, LimitExceeded> {
113 check_measurement(byte_length(value), allowed)
114}
115
116pub const fn check_collection<Element>(
118 value: &[Element],
119 allowed: usize,
120) -> Result<LimitMeasurement, LimitExceeded> {
121 check_measurement(collection_length(value), allowed)
122}
123
124#[cfg(test)]
125mod tests {
126 use serde::Deserialize;
127 use serde_json::{Value, json};
128
129 use super::*;
130
131 #[derive(Debug, Deserialize)]
132 struct FixtureCorpus {
133 version: u8,
134 text: Vec<TextFixture>,
135 json: Vec<JsonFixture>,
136 bytes: Vec<BytesFixture>,
137 collections: Vec<CollectionFixture>,
138 }
139
140 #[derive(Debug, Deserialize)]
141 struct TextFixture {
142 name: String,
143 value: String,
144 trimmed_unicode_scalars: usize,
145 }
146
147 #[derive(Debug, Deserialize)]
148 struct JsonFixture {
149 name: String,
150 value: Value,
151 compact: String,
152 utf8_bytes: usize,
153 }
154
155 #[derive(Debug, Deserialize)]
156 struct BytesFixture {
157 name: String,
158 value: Vec<u8>,
159 bytes: usize,
160 }
161
162 #[derive(Debug, Deserialize)]
163 struct CollectionFixture {
164 name: String,
165 value: Vec<Value>,
166 elements: usize,
167 }
168
169 fn fixtures() -> FixtureCorpus {
170 serde_json::from_str(include_str!(
171 "../../../../fixtures/limits/resource-budget-measurements-v1.json"
172 ))
173 .expect("resource-budget fixture should parse")
174 }
175
176 #[test]
177 fn counts_ascii_composed_decomposed_and_emoji_scalars() {
178 assert_eq!(trimmed_unicode_scalar_count(" plain "), 5);
179 assert_eq!(trimmed_unicode_scalar_count("\u{e9}"), 1);
180 assert_eq!(trimmed_unicode_scalar_count("e\u{301}"), 2);
181 assert_eq!(
182 trimmed_unicode_scalar_count(
183 "\u{1f469}\u{200d}\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}"
184 ),
185 7
186 );
187 }
188
189 #[test]
190 fn measures_compact_objects_arrays_escapes_and_multibyte_strings() {
191 for fixture in fixtures().json {
192 let encoded = serde_json::to_string(&fixture.value).expect("fixture should encode");
193 assert_eq!(encoded, fixture.compact, "{}", fixture.name);
194 assert_eq!(
195 compact_json_utf8_bytes(&fixture.value).expect("fixture should encode"),
196 fixture.utf8_bytes,
197 "{}",
198 fixture.name
199 );
200 }
201 }
202
203 #[test]
204 fn byte_and_collection_checks_report_both_sides_of_the_boundary() {
205 let bytes = [1_u8, 2, 3];
206 let values = ["a", "b", "c"];
207
208 assert_eq!(
209 check_bytes(&bytes, 3)
210 .expect("boundary should pass")
211 .measured(),
212 3
213 );
214 assert_eq!(
215 check_collection(&values, 3)
216 .expect("boundary should pass")
217 .allowed(),
218 3
219 );
220 assert_eq!(
221 check_bytes(&bytes, 2),
222 Err(LimitExceeded {
223 measured: 3,
224 allowed: 2,
225 })
226 );
227 assert_eq!(
228 check_collection(&values, 2),
229 Err(LimitExceeded {
230 measured: 3,
231 allowed: 2,
232 })
233 );
234 }
235
236 #[test]
237 fn specialized_checks_return_measurement_and_allowed_values() {
238 assert_eq!(
239 check_trimmed_unicode_scalars(" e\u{301} ", 2),
240 Ok(LimitMeasurement {
241 measured: 2,
242 allowed: 2,
243 })
244 );
245 let error = check_compact_json_utf8_bytes(&json!({"value": "\u{e9}"}), 13)
246 .expect_err("fourteen-byte document should fail");
247 assert!(matches!(
248 error,
249 CompactJsonLimitError::Limit(LimitExceeded {
250 measured: 14,
251 allowed: 13,
252 })
253 ));
254 }
255
256 #[test]
257 fn shared_fixture_matches_all_rust_measurements() {
258 let fixtures = fixtures();
259 assert_eq!(fixtures.version, 1);
260 assert!(!fixtures.text.is_empty());
261 assert!(!fixtures.json.is_empty());
262 assert!(!fixtures.bytes.is_empty());
263 assert!(!fixtures.collections.is_empty());
264
265 for fixture in fixtures.text {
266 assert_eq!(
267 trimmed_unicode_scalar_count(&fixture.value),
268 fixture.trimmed_unicode_scalars,
269 "{}",
270 fixture.name
271 );
272 }
273 for fixture in fixtures.bytes {
274 assert_eq!(
275 byte_length(&fixture.value),
276 fixture.bytes,
277 "{}",
278 fixture.name
279 );
280 }
281 for fixture in fixtures.collections {
282 assert_eq!(
283 collection_length(&fixture.value),
284 fixture.elements,
285 "{}",
286 fixture.name
287 );
288 }
289 }
290}