printobserver-types 0.10.2

Shared domain types for printobserver.
Documentation
//! What a consumer reads off these types, beyond their wire forms.
//!
//! The accessors here are how a consumer reads a value without destructuring
//! it, so each is held to agreeing with the data it reads: a reported value's
//! flag is what its range says, and an identifier or an instant is the one it
//! was built from. The domains' own accessors — an action's kind, an actor's
//! class, the adjustable a string names — are held the same way where those
//! types are declared.

use std::str::FromStr as _;

use chrono::{DateTime, TimeZone as _, Utc};
use printobserver_types::contract::Sample;
use printobserver_types::{
    EventKind, EventRecord, FileName, FileNameRefusal, PrintId, Range, RawBytes, Reported,
    Timestamp,
};

/// A range to flag against, in the shape the printer port declares its own.
const FACTOR_RANGE: Range = Range::new(0.1, 10.0);

/// A minted identifier is a version 7 UUID, displayed in the one spelling.
#[test]
fn a_minted_identifier_is_a_version_seven_uuid() {
    let minted = PrintId::new();
    assert_eq!(minted.as_uuid().get_version_num(), 7);
    let displayed = minted.to_string();
    assert_eq!(displayed.parse::<PrintId>(), Ok(minted));
    assert_eq!(PrintId::default().as_uuid().get_version_num(), 7);
    assert_ne!(
        PrintId::new(),
        PrintId::new(),
        "two mintings are the same identifier"
    );
}

/// A refused identifier says which type refused it and why.
#[test]
fn a_refused_identifier_says_which_type_refused_it() {
    let error = PrintId::from_str("nonsense").expect_err("the spelling is refused");
    assert_eq!(error.type_name(), "PrintId");
    assert!(error.detail().contains("nonsense"), "{}", error.detail());
    assert!(error.to_string().contains("PrintId"));

    for refused in [
        "0191f0a0-0000-4000-8000-000000000001",
        "0191F0A0-0000-7000-8000-000000000001",
        "{0191f0a0-0000-7000-8000-000000000001}",
        "0191f0a0000070008000000000000001",
    ] {
        assert!(
            PrintId::from_str(refused).is_err(),
            "{refused} was accepted"
        );
    }
}

/// An instant is the UTC instant it was built from, however it was built.
#[test]
fn an_instant_is_the_utc_instant_it_was_built_from() {
    let epoch = Timestamp::from_unix_seconds(0).expect("the epoch is representable");
    assert_eq!(epoch.to_string(), "1970-01-01T00:00:00Z");
    assert_eq!(
        *epoch.as_utc(),
        DateTime::<Utc>::from_timestamp(0, 0).expect("the epoch")
    );

    let from_chrono: Timestamp = Utc.with_ymd_and_hms(2026, 3, 1, 12, 0, 0).unwrap().into();
    assert_eq!(from_chrono, Timestamp::sample_full());

    assert!(Timestamp::now() > epoch, "now is not after the epoch");
    assert!(
        Timestamp::from_unix_seconds(i64::MAX).is_err(),
        "an unrepresentable instant parsed"
    );
}

/// The latest instant is the last one written as a string that reads back:
/// one nanosecond later is written as nothing an instant parses from.
#[test]
fn the_latest_instant_is_the_last_one_that_reads_back() {
    let latest = Timestamp::latest();
    assert_eq!(latest.to_string(), "9999-12-31T23:59:59.999999999Z");
    assert_eq!(Timestamp::from_str(&latest.to_string()), Ok(latest));

    let beyond: Timestamp = (*latest.as_utc() + chrono::TimeDelta::nanoseconds(1)).into();
    assert!(
        Timestamp::from_str(&beyond.to_string()).is_err(),
        "{beyond} read back, so a later instant than the latest is writable"
    );
    assert!(
        beyond.to_string() < latest.to_string(),
        "{beyond} orders after {latest} as text"
    );
}

/// A refused instant says what was wrong with it.
#[test]
fn a_refused_instant_says_what_was_wrong() {
    let error = Timestamp::from_str("the first of March").expect_err("the string is refused");
    assert!(
        error.detail().contains("the first of March"),
        "{}",
        error.detail()
    );
    assert!(error.to_string().contains("RFC 3339"));
    assert!(
        Timestamp::from_str("2026-03-01T12:00:00").is_err(),
        "an offsetless instant parsed"
    );
}

/// Whether two values are the same one bit for bit.
///
/// "Carried through as reported" is an exact claim, so this is an exact
/// comparison rather than a tolerance: a value that came back near enough is a
/// value this type coerced.
fn identical(left: f64, right: f64) -> bool {
    left.to_bits() == right.to_bits()
}

/// A reported value carries what it was given, and its range says the rest.
#[test]
fn a_reported_value_carries_what_it_was_given() {
    let inside = Reported::new(1.0, FACTOR_RANGE);
    assert!(
        identical(inside.value(), 1.0),
        "{} is not the value it was given",
        inside.value()
    );
    assert!(!inside.out_of_range());
    assert_eq!(inside.to_string(), "1");

    let outside = Reported::new(40.0, FACTOR_RANGE);
    assert!(
        identical(outside.value(), 40.0),
        "{} is not the value it was given",
        outside.value()
    );
    assert!(outside.out_of_range());
    assert_eq!(outside.to_string(), "40 (out of range)");

    let range = Range::new(0.0, 1.0);
    assert!(range.contains(0.0) && range.contains(1.0) && range.contains(0.5));
    assert!(!range.contains(-0.1) && !range.contains(1.1) && !range.contains(f64::NAN));
}

/// Raw bytes are held exactly as received, and displayed as base64.
#[test]
fn raw_bytes_are_held_exactly_as_received() {
    let held = RawBytes::new(b"{\"event\":{}}".to_vec());
    assert_eq!(held.as_slice(), b"{\"event\":{}}");
    assert_eq!(held.to_string(), "eyJldmVudCI6e319");
    assert!(RawBytes::default().as_slice().is_empty());
    assert!(
        serde_json::from_value::<RawBytes>(serde_json::json!("not base64!")).is_err(),
        "a string that is no base64 was read as bytes"
    );
}

/// A file name is the characters it was given, and says why one is refused.
#[test]
fn a_file_name_is_the_characters_it_was_given() {
    let name = FileName::new("benchy.gcode").expect("a name carrying no forbidden property");
    assert_eq!(name.as_str(), "benchy.gcode");
    assert_eq!(name.to_string(), "benchy.gcode");
    assert_eq!(FileName::from_str("benchy.gcode"), Ok(name));

    let error = FileName::new("../etc/passwd").expect_err("a parent-directory segment");
    assert_eq!(error.refusal(), FileNameRefusal::Separator);
    assert!(error.to_string().contains("separator"), "{error}");
    for refusal in FileNameRefusal::ALL {
        assert!(!refusal.detail().is_empty());
        assert_eq!(refusal.to_string(), refusal.detail());
    }
}

/// The envelope's sample is under a kind of its own.
#[test]
fn the_envelope_sample_is_under_a_kind_of_its_own() {
    assert_eq!(EventRecord::sample_full().kind(), &EventKind::sample_full());
    assert_eq!(EventKind::sample_full().as_str(), "sample_event");
}