#![forbid(unsafe_code)]
use obj::{Document, DynamicSchema, IndexKind, Schema};
use obj_core::codec::{decode, encode, DOC_HEADER_SIZE};
use obj_core::pager::checksum::crc32c;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct AccountV1 {
owner: String,
balance_cents: u64,
}
impl Schema for AccountV1 {
fn schema() -> DynamicSchema {
DynamicSchema::map([
("owner", DynamicSchema::String),
("balance_cents", DynamicSchema::U64),
])
}
}
#[allow(clippy::struct_field_names)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, obj::Document)]
#[obj(version = 2, collection = "accounts")]
#[obj(history(v1 = AccountV1))]
#[obj(index = ("owner", "opened_at"))]
struct Account {
#[obj(index = unique)]
owner: String,
#[obj(index)]
region: u32,
#[obj(index = each)]
tags: Vec<String>,
opened_at: u64,
balance_cents: u64,
}
fn sample() -> Account {
Account {
owner: "ada".to_owned(),
region: 7,
tags: vec!["gold".to_owned(), "beta".to_owned()],
opened_at: 1_700_000_000,
balance_cents: 4_242,
}
}
const SAMPLE_COLLECTION_ID: u32 = 3;
#[test]
fn collection_and_version_constants_are_pinned() {
assert_eq!(<Account as Document>::COLLECTION, "accounts");
assert_eq!(<Account as Document>::VERSION, 2);
}
#[test]
fn indexes_list_is_pinned_exactly() {
let specs = <Account as Document>::indexes();
assert_eq!(specs.len(), 4, "owner(unique) + region + tags + composite");
assert_eq!(specs[0].name, "owner");
assert_eq!(specs[0].kind, IndexKind::Unique);
assert_eq!(specs[0].key_paths, vec!["owner".to_owned()]);
assert_eq!(specs[1].name, "region");
assert_eq!(specs[1].kind, IndexKind::Standard);
assert_eq!(specs[1].key_paths, vec!["region".to_owned()]);
assert_eq!(specs[2].name, "tags");
assert_eq!(specs[2].kind, IndexKind::Each);
assert_eq!(specs[2].key_paths, vec!["tags".to_owned()]);
assert_eq!(specs[3].name, "owner__opened_at");
assert_eq!(specs[3].kind, IndexKind::Composite);
assert_eq!(
specs[3].key_paths,
vec!["owner".to_owned(), "opened_at".to_owned()],
);
}
#[test]
fn historical_schemas_registry_is_pinned() {
let history = <Account as Document>::historical_schemas();
assert_eq!(history.len(), 1, "one historical version registered");
assert_eq!(history[0].0, 1, "v1 entry");
assert_eq!(history[0].1, AccountV1::schema());
}
#[test]
fn current_schema_matches_field_layout() {
let schema = <Account as Schema>::schema();
assert_eq!(
schema,
DynamicSchema::map([
("owner", DynamicSchema::String),
("region", DynamicSchema::U64),
("tags", DynamicSchema::seq(DynamicSchema::String)),
("opened_at", DynamicSchema::U64),
("balance_cents", DynamicSchema::U64),
]),
);
}
#[test]
fn encoded_record_round_trips() {
let value = sample();
let bytes = encode(&value, SAMPLE_COLLECTION_ID).expect("encode");
let back: Account = decode(&bytes, SAMPLE_COLLECTION_ID).expect("decode");
assert_eq!(back, value, "decode(encode(x)) must equal x");
}
#[test]
fn encoded_record_header_fields_are_pinned() {
let value = sample();
let bytes = encode(&value, SAMPLE_COLLECTION_ID).expect("encode");
assert!(bytes.len() > DOC_HEADER_SIZE);
let collection_id = u32::from_le_bytes(bytes[0..4].try_into().expect("4"));
let type_version = u32::from_le_bytes(bytes[4..8].try_into().expect("4"));
let payload_len = u32::from_le_bytes(bytes[8..12].try_into().expect("4"));
let payload_crc32c = u32::from_le_bytes(bytes[12..16].try_into().expect("4"));
let payload = &bytes[DOC_HEADER_SIZE..];
assert_eq!(collection_id, SAMPLE_COLLECTION_ID);
assert_eq!(type_version, 2, "header pins Account::VERSION = 2");
assert_eq!(payload_len as usize, payload.len());
assert_eq!(payload_crc32c, crc32c(payload), "header CRC pins payload");
}
#[test]
fn encoded_payload_bytes_are_pinned() {
let value = sample();
let bytes = encode(&value, SAMPLE_COLLECTION_ID).expect("encode");
let payload = &bytes[DOC_HEADER_SIZE..];
assert_eq!(
payload, EXPECTED_PAYLOAD,
"encoded payload diverged from the frozen golden bytes",
);
}
const EXPECTED_PAYLOAD: &[u8] = &[
3, 97, 100, 97, 7, 2, 4, 103, 111, 108, 100, 4, 98, 101, 116, 97, 128, 226, 207, 170, 6, 146,
33,
];