use super::{RegularFileReadError, read_optional_regular_bytes, write_bytes};
use serde::Serialize;
use std::{io, path::Path};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CanonicalJsonStyle {
Compact,
PrettyNewline,
}
#[derive(Debug)]
pub enum CanonicalJsonEncodeError {
Serialization(serde_json::Error),
TooLarge,
}
#[derive(Debug)]
pub enum BoundedRegularFileReadError {
Read(RegularFileReadError),
TooLarge,
}
#[derive(Debug)]
pub enum ExactReplaceError {
Read(RegularFileReadError),
Write(io::Error),
}
pub fn encode_canonical_json<T: Serialize>(
document: &T,
style: CanonicalJsonStyle,
maximum_bytes: usize,
) -> Result<Vec<u8>, CanonicalJsonEncodeError> {
let mut bytes = match style {
CanonicalJsonStyle::Compact => serde_json::to_vec(document),
CanonicalJsonStyle::PrettyNewline => serde_json::to_vec_pretty(document),
}
.map_err(CanonicalJsonEncodeError::Serialization)?;
if style == CanonicalJsonStyle::PrettyNewline {
bytes.push(b'\n');
}
if bytes.len() > maximum_bytes {
return Err(CanonicalJsonEncodeError::TooLarge);
}
Ok(bytes)
}
pub fn read_optional_bounded_regular_bytes(
path: &Path,
maximum_bytes: usize,
) -> Result<Option<Vec<u8>>, BoundedRegularFileReadError> {
let bytes = read_optional_regular_bytes(path).map_err(BoundedRegularFileReadError::Read)?;
if bytes
.as_ref()
.is_some_and(|bytes| bytes.len() > maximum_bytes)
{
return Err(BoundedRegularFileReadError::TooLarge);
}
Ok(bytes)
}
pub fn replace_bytes_exact(path: &Path, expected: &[u8]) -> Result<(), ExactReplaceError> {
replace_bytes_exact_with(path, expected, write_bytes)
}
fn replace_bytes_exact_with(
path: &Path,
expected: &[u8],
replace: impl FnOnce(&Path, &[u8]) -> io::Result<()>,
) -> Result<(), ExactReplaceError> {
if let Err(source) = replace(path, expected) {
return match read_optional_regular_bytes(path) {
Ok(Some(observed)) if observed == expected => Ok(()),
Ok(_) => Err(ExactReplaceError::Write(source)),
Err(error) => Err(ExactReplaceError::Read(error)),
};
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Serialize;
use std::fs;
#[derive(Serialize)]
struct Example {
value: u8,
}
#[test]
fn canonical_styles_and_bounds_are_explicit() {
let example = Example { value: 7 };
assert_eq!(
encode_canonical_json(&example, CanonicalJsonStyle::Compact, 11)
.expect("compact document"),
br#"{"value":7}"#
);
assert_eq!(
encode_canonical_json(&example, CanonicalJsonStyle::PrettyNewline, 17)
.expect("pretty document"),
b"{\n \"value\": 7\n}\n"
);
assert!(matches!(
encode_canonical_json(&example, CanonicalJsonStyle::Compact, 10),
Err(CanonicalJsonEncodeError::TooLarge)
));
}
#[test]
fn exact_replacement_reconciles_a_lost_success_response() {
let root = crate::test_support::temp_dir("durable-document-response-loss");
let path = root.join("journal.json");
let expected = b"exact replacement";
replace_bytes_exact_with(&path, expected, |path, bytes| {
write_bytes(path, bytes)?;
Err(io::Error::other("simulated response loss"))
})
.expect("exact durable bytes reconcile the uncertain write");
assert_eq!(fs::read(&path).expect("read reconciled document"), expected);
fs::remove_dir_all(root).expect("remove document scratch");
}
}