liminal-protocol 0.14.0

Shared participant-lifecycle protocol types for liminal
Documentation
use super::{
    AttachAttemptToken, AttachEnvelope, ClientRequest, CodecError, DetachAttemptToken,
    DetachEnvelope, EnrollmentEnvelope, EnrollmentToken, Generation, LeaveAttemptToken,
    LeaveEnvelope, ParticipantFrame, ReceiverDirection, RecordAdmissionAttemptToken,
    RecordAdmissionEnvelope, RecordAdmissionFaultClass, RecordAdmissionProtocolFault, ServerValue,
    SettlementAnswer, SettlementOutcome, SettlementQuery, WriteOutcomeUnknown, decode, encode,
    encoded_len,
};
use alloc::{boxed::Box, vec, vec::Vec};

const FIXTURE: &str = include_str!("../../../../docs/wire/settlement-frames.json");
type TestResult = Result<(), CodecError>;
type Frames = Vec<(&'static str, ParticipantFrame)>;

#[test]
fn settlement_frames_match_the_committed_rust_encoder_bytes()
-> Result<(), Box<dyn core::error::Error>> {
    let rows = strings("frame_hex")?;
    let names = strings("name")?;
    let frames = frames()?;
    assert_eq!(rows.len(), frames.len());
    assert_eq!(names.len(), frames.len());
    for ((hex, name), (expected_name, frame)) in rows.into_iter().zip(names).zip(frames) {
        assert_eq!(name, expected_name);
        let expected = hex_bytes(hex)?;
        pin(&frame, &expected)
            .map_err(|error| alloc::format!("CodecError for {name}: {error:?}"))?;
    }
    Ok(())
}

fn pin(frame: &ParticipantFrame, expected: &[u8]) -> TestResult {
    let mut encoded = vec![0; encoded_len(frame)?];
    assert_eq!(encode(frame, &mut encoded)?, encoded.len());
    assert_eq!(encoded.as_slice(), expected);
    let receiver = if matches!(frame, ParticipantFrame::ClientRequest(_)) {
        ReceiverDirection::Server
    } else {
        ReceiverDirection::Client
    };
    assert_eq!(decode(expected, receiver)?, *frame);
    Ok(())
}

fn strings(field: &str) -> Result<Vec<&'static str>, Box<dyn core::error::Error>> {
    let prefix = alloc::format!("\"{field}\": \"");
    FIXTURE
        .lines()
        .filter_map(|line| line.trim().strip_prefix(prefix.as_str()))
        .map(|s| {
            let s = s.strip_suffix(',').unwrap_or(s);
            let s = s.strip_suffix('"').ok_or("unclosed fixture string")?;
            if s.contains(['\\', '"']) {
                return Err("fixture fields must be literal unescaped bytes".into());
            }
            Ok(s)
        })
        .collect()
}

fn hex_bytes(hex: &str) -> Result<Vec<u8>, Box<dyn core::error::Error>> {
    if !hex.is_ascii() || !hex.len().is_multiple_of(2) {
        return Err("fixture hex must be even ASCII".into());
    }
    (0..hex.len())
        .step_by(2)
        .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).map_err(Into::into))
        .collect()
}

fn frames() -> Result<Frames, Box<dyn core::error::Error>> {
    let generation = Generation::new(3).ok_or("fixture generation is zero")?;
    let token = RecordAdmissionAttemptToken::new([0x22; 16]);
    let request = RecordAdmissionEnvelope {
        conversation_id: 2102,
        participant_id: 7,
        capability_generation: generation,
        record_admission_attempt_token: token,
    };
    let mut out = vec![(
        "SettlementQuery",
        ParticipantFrame::ClientRequest(ClientRequest::SettlementQuery(SettlementQuery {
            conversation_id: 2102,
            participant_id: 7,
            capability_generation: generation,
            record_admission_attempt_token: token,
            payload_fingerprint: [0x33; 32],
        })),
    )];
    out.extend(fault_frames(&request));
    out.extend(unknown_frames(generation, request));
    out.extend(answer_frames(token));
    Ok(out)
}

fn fault_frames(request: &RecordAdmissionEnvelope) -> Frames {
    let mut out = Vec::new();
    for (name, class) in [
        (
            "RecordAdmissionProtocolFault.Projection",
            RecordAdmissionFaultClass::Projection,
        ),
        (
            "RecordAdmissionProtocolFault.Order",
            RecordAdmissionFaultClass::Order,
        ),
        (
            "RecordAdmissionProtocolFault.Sequence",
            RecordAdmissionFaultClass::Sequence,
        ),
        (
            "RecordAdmissionProtocolFault.RequiredCapacity",
            RecordAdmissionFaultClass::RequiredCapacity,
        ),
        (
            "RecordAdmissionProtocolFault.RefusalInvariant",
            RecordAdmissionFaultClass::RefusalInvariant,
        ),
    ] {
        out.push((
            name,
            ParticipantFrame::ServerValue(ServerValue::RecordAdmissionProtocolFault(
                RecordAdmissionProtocolFault {
                    request: request.clone(),
                    class,
                },
            )),
        ));
    }
    out
}

fn unknown_frames(generation: Generation, request: RecordAdmissionEnvelope) -> Frames {
    let mut out = Vec::new();
    for (name, value) in [
        (
            "WriteOutcomeUnknown.Enrollment",
            WriteOutcomeUnknown::Enrollment {
                request: EnrollmentEnvelope {
                    conversation_id: 2102,
                    enrollment_token: EnrollmentToken::new([0x22; 16]),
                },
            },
        ),
        (
            "WriteOutcomeUnknown.CredentialAttach",
            WriteOutcomeUnknown::CredentialAttach {
                request: AttachEnvelope {
                    conversation_id: 2102,
                    participant_id: 7,
                    capability_generation: generation,
                    attach_attempt_token: AttachAttemptToken::new([0x22; 16]),
                    accept_marker_delivery_seq: None,
                },
            },
        ),
        (
            "WriteOutcomeUnknown.Detach",
            WriteOutcomeUnknown::Detach {
                request: DetachEnvelope {
                    conversation_id: 2102,
                    participant_id: 7,
                    capability_generation: generation,
                    detach_attempt_token: DetachAttemptToken::new([0x22; 16]),
                },
            },
        ),
        (
            "WriteOutcomeUnknown.Leave",
            WriteOutcomeUnknown::Leave {
                request: LeaveEnvelope {
                    conversation_id: 2102,
                    participant_id: 7,
                    capability_generation: generation,
                    leave_attempt_token: LeaveAttemptToken::new([0x22; 16]),
                },
            },
        ),
        (
            "WriteOutcomeUnknown.RecordAdmission",
            WriteOutcomeUnknown::RecordAdmission { request },
        ),
    ] {
        out.push((
            name,
            ParticipantFrame::ServerValue(ServerValue::WriteOutcomeUnknown(value)),
        ));
    }
    out
}

fn answer_frames(token: RecordAdmissionAttemptToken) -> Frames {
    let mut out = Vec::new();
    for (name, outcome) in [
        (
            "SettlementAnswer.Committed",
            SettlementOutcome::Committed { delivery_seq: 12 },
        ),
        ("SettlementAnswer.Withdrawn", SettlementOutcome::Withdrawn),
        ("SettlementAnswer.Expired", SettlementOutcome::Expired),
        (
            "SettlementAnswer.NotCommitted",
            SettlementOutcome::NotCommitted,
        ),
        ("SettlementAnswer.Unknown", SettlementOutcome::Unknown),
    ] {
        out.push((
            name,
            ParticipantFrame::ServerValue(ServerValue::SettlementAnswer(SettlementAnswer {
                conversation_id: 2102,
                participant_id: 7,
                record_admission_attempt_token: token,
                outcome,
            })),
        ));
    }
    out
}