starweaver-core 0.10.0

Core abstractions for the Starweaver agent SDK
Documentation
#![allow(missing_docs, clippy::expect_used)]

use serde::{Deserialize, Serialize};
use starweaver_core::{
    VersionedRecord, VersionedRecordError, from_versioned_json, to_versioned_value,
};

const V0: &str = include_str!("fixtures/contracts/record-v0.json");
const V1: &str = include_str!("fixtures/contracts/record-v1.json");
const UNKNOWN_VERSION: &str = include_str!("fixtures/contracts/record-unknown-version.json");
const WRONG_SCHEMA: &str = include_str!("fixtures/contracts/record-wrong-schema.json");
const VERSION_ZERO: &str = include_str!("fixtures/contracts/record-version-zero.json");
const MISSING_VERSION: &str = include_str!("fixtures/contracts/record-missing-version.json");
const MISSING_PAYLOAD: &str = include_str!("fixtures/contracts/record-missing-payload.json");
const MIGRATING_V1: &str = include_str!("fixtures/contracts/migrating-record-v1.json");
const MIGRATING_V2: &str = include_str!("fixtures/contracts/migrating-record-v2.json");

#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
struct FixtureRecord {
    value: String,
}

impl VersionedRecord for FixtureRecord {
    const SCHEMA: &'static str = "starweaver.fixture.record";
    const ALLOW_BARE_V0: bool = true;
}

#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
struct MigratingRecord {
    value: String,
    generation: u32,
}

#[derive(Deserialize)]
struct MigratingRecordV1 {
    legacy_value: String,
}

impl VersionedRecord for MigratingRecord {
    const SCHEMA: &'static str = "starweaver.fixture.migrating_record";
    const VERSION: u32 = 2;

    fn decode_version(
        version: u32,
        payload: serde_json::Value,
    ) -> Result<Self, VersionedRecordError> {
        match version {
            1 => {
                let legacy = serde_json::from_value::<MigratingRecordV1>(payload)
                    .map_err(VersionedRecordError::Json)?;
                Ok(Self {
                    value: legacy.legacy_value,
                    generation: 2,
                })
            }
            2 => serde_json::from_value(payload).map_err(VersionedRecordError::Json),
            actual => Err(VersionedRecordError::UnsupportedVersion {
                schema: Self::SCHEMA,
                supported: Self::VERSION,
                actual,
            }),
        }
    }
}

#[test]
fn reads_v0_and_v1_and_always_writes_current_envelope() {
    let legacy = from_versioned_json::<FixtureRecord>(V0).expect("read v0 fixture");
    let current = from_versioned_json::<FixtureRecord>(V1).expect("read v1 fixture");
    assert_eq!(legacy, current);
    assert_eq!(
        to_versioned_value(&legacy).expect("write current envelope"),
        serde_json::from_str::<serde_json::Value>(V1).expect("parse v1 fixture")
    );
}

#[test]
fn rejects_unknown_versions_and_wrong_schemas() {
    assert!(matches!(
        from_versioned_json::<FixtureRecord>(UNKNOWN_VERSION),
        Err(VersionedRecordError::UnsupportedVersion { actual: 2, .. })
    ));
    assert!(matches!(
        from_versioned_json::<FixtureRecord>(WRONG_SCHEMA),
        Err(VersionedRecordError::WrongSchema { .. })
    ));
    assert!(matches!(
        from_versioned_json::<FixtureRecord>(VERSION_ZERO),
        Err(VersionedRecordError::UnsupportedVersion { actual: 0, .. })
    ));
    assert!(matches!(
        from_versioned_json::<FixtureRecord>(MISSING_VERSION),
        Err(VersionedRecordError::Json(_))
    ));
    assert!(matches!(
        from_versioned_json::<FixtureRecord>(MISSING_PAYLOAD),
        Err(VersionedRecordError::Json(_))
    ));
}

#[test]
fn dispatches_older_enveloped_versions_through_a_dedicated_legacy_dto() {
    let migrated = from_versioned_json::<MigratingRecord>(MIGRATING_V1)
        .expect("migrate previous enveloped version");
    let current =
        from_versioned_json::<MigratingRecord>(MIGRATING_V2).expect("read current version");
    assert_eq!(migrated, current);
    assert_eq!(
        to_versioned_value(&migrated).expect("write migrated record"),
        serde_json::from_str::<serde_json::Value>(MIGRATING_V2)
            .expect("parse current migrating fixture")
    );
}

#[test]
fn bare_v0_requires_an_explicit_record_opt_in() {
    assert!(matches!(
        from_versioned_json::<MigratingRecord>(r#"{"value":"fixture","generation":2}"#),
        Err(VersionedRecordError::BareV0Unsupported { .. })
    ));
}