use serde::Serialize;
use thiserror::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LimitMeasurement {
measured: usize,
allowed: usize,
}
impl LimitMeasurement {
#[must_use]
pub const fn measured(self) -> usize {
self.measured
}
#[must_use]
pub const fn allowed(self) -> usize {
self.allowed
}
}
#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
#[error("measured {measured} exceeds allowed {allowed}")]
pub struct LimitExceeded {
measured: usize,
allowed: usize,
}
impl LimitExceeded {
#[must_use]
pub const fn measured(self) -> usize {
self.measured
}
#[must_use]
pub const fn allowed(self) -> usize {
self.allowed
}
}
#[derive(Debug, Error)]
pub enum CompactJsonLimitError {
#[error("compact JSON encoding failed")]
Encoding(#[from] serde_json::Error),
#[error(transparent)]
Limit(#[from] LimitExceeded),
}
#[must_use]
pub fn trimmed_unicode_scalar_count(value: &str) -> usize {
value.trim().chars().count()
}
pub fn compact_json_utf8_bytes(value: &impl Serialize) -> Result<usize, serde_json::Error> {
serde_json::to_vec(value).map(|encoded| encoded.len())
}
#[must_use]
pub const fn byte_length(value: &[u8]) -> usize {
value.len()
}
#[must_use]
pub const fn collection_length<Element>(value: &[Element]) -> usize {
value.len()
}
pub const fn check_measurement(
measured: usize,
allowed: usize,
) -> Result<LimitMeasurement, LimitExceeded> {
if measured > allowed {
return Err(LimitExceeded { measured, allowed });
}
Ok(LimitMeasurement { measured, allowed })
}
pub fn check_trimmed_unicode_scalars(
value: &str,
allowed: usize,
) -> Result<LimitMeasurement, LimitExceeded> {
check_measurement(trimmed_unicode_scalar_count(value), allowed)
}
pub fn check_compact_json_utf8_bytes(
value: &impl Serialize,
allowed: usize,
) -> Result<LimitMeasurement, CompactJsonLimitError> {
let measured = compact_json_utf8_bytes(value)?;
check_measurement(measured, allowed).map_err(Into::into)
}
pub const fn check_bytes(value: &[u8], allowed: usize) -> Result<LimitMeasurement, LimitExceeded> {
check_measurement(byte_length(value), allowed)
}
pub const fn check_collection<Element>(
value: &[Element],
allowed: usize,
) -> Result<LimitMeasurement, LimitExceeded> {
check_measurement(collection_length(value), allowed)
}
#[cfg(test)]
mod tests {
use serde::Deserialize;
use serde_json::{Value, json};
use super::*;
#[derive(Debug, Deserialize)]
struct FixtureCorpus {
version: u8,
text: Vec<TextFixture>,
json: Vec<JsonFixture>,
bytes: Vec<BytesFixture>,
collections: Vec<CollectionFixture>,
}
#[derive(Debug, Deserialize)]
struct TextFixture {
name: String,
value: String,
trimmed_unicode_scalars: usize,
}
#[derive(Debug, Deserialize)]
struct JsonFixture {
name: String,
value: Value,
compact: String,
utf8_bytes: usize,
}
#[derive(Debug, Deserialize)]
struct BytesFixture {
name: String,
value: Vec<u8>,
bytes: usize,
}
#[derive(Debug, Deserialize)]
struct CollectionFixture {
name: String,
value: Vec<Value>,
elements: usize,
}
fn fixtures() -> FixtureCorpus {
serde_json::from_str(include_str!(
"../../../../fixtures/limits/resource-budget-measurements-v1.json"
))
.expect("resource-budget fixture should parse")
}
#[test]
fn counts_ascii_composed_decomposed_and_emoji_scalars() {
assert_eq!(trimmed_unicode_scalar_count(" plain "), 5);
assert_eq!(trimmed_unicode_scalar_count("\u{e9}"), 1);
assert_eq!(trimmed_unicode_scalar_count("e\u{301}"), 2);
assert_eq!(
trimmed_unicode_scalar_count(
"\u{1f469}\u{200d}\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}"
),
7
);
}
#[test]
fn measures_compact_objects_arrays_escapes_and_multibyte_strings() {
for fixture in fixtures().json {
let encoded = serde_json::to_string(&fixture.value).expect("fixture should encode");
assert_eq!(encoded, fixture.compact, "{}", fixture.name);
assert_eq!(
compact_json_utf8_bytes(&fixture.value).expect("fixture should encode"),
fixture.utf8_bytes,
"{}",
fixture.name
);
}
}
#[test]
fn byte_and_collection_checks_report_both_sides_of_the_boundary() {
let bytes = [1_u8, 2, 3];
let values = ["a", "b", "c"];
assert_eq!(
check_bytes(&bytes, 3)
.expect("boundary should pass")
.measured(),
3
);
assert_eq!(
check_collection(&values, 3)
.expect("boundary should pass")
.allowed(),
3
);
assert_eq!(
check_bytes(&bytes, 2),
Err(LimitExceeded {
measured: 3,
allowed: 2,
})
);
assert_eq!(
check_collection(&values, 2),
Err(LimitExceeded {
measured: 3,
allowed: 2,
})
);
}
#[test]
fn specialized_checks_return_measurement_and_allowed_values() {
assert_eq!(
check_trimmed_unicode_scalars(" e\u{301} ", 2),
Ok(LimitMeasurement {
measured: 2,
allowed: 2,
})
);
let error = check_compact_json_utf8_bytes(&json!({"value": "\u{e9}"}), 13)
.expect_err("fourteen-byte document should fail");
assert!(matches!(
error,
CompactJsonLimitError::Limit(LimitExceeded {
measured: 14,
allowed: 13,
})
));
}
#[test]
fn shared_fixture_matches_all_rust_measurements() {
let fixtures = fixtures();
assert_eq!(fixtures.version, 1);
assert!(!fixtures.text.is_empty());
assert!(!fixtures.json.is_empty());
assert!(!fixtures.bytes.is_empty());
assert!(!fixtures.collections.is_empty());
for fixture in fixtures.text {
assert_eq!(
trimmed_unicode_scalar_count(&fixture.value),
fixture.trimmed_unicode_scalars,
"{}",
fixture.name
);
}
for fixture in fixtures.bytes {
assert_eq!(
byte_length(&fixture.value),
fixture.bytes,
"{}",
fixture.name
);
}
for fixture in fixtures.collections {
assert_eq!(
collection_length(&fixture.value),
fixture.elements,
"{}",
fixture.name
);
}
}
}