gled-protocol 2.31.2

What gled's user interfaces and its engine send each other
Documentation
//! Assets survive the wire, and a change sends only what changed.

use std::{fs, path::Path, str::FromStr, sync::Arc};

use gled_model::{
    asset::{
        Asset, AssetTrait, animation::Animation, curve::Curve, midi_controller::MidiController,
        output_device::OutputDevice, palette::Palette, project::Project, scene::Scene,
    },
    asset_id::AssetId,
    collection::Collections,
};
use gled_protocol::{
    assets::{AnyAsset, AnyAssetId, AssetChanges, all_assets, collections_from},
    codec::{decode, encode},
    message::ServerMessage,
};
use uuid::Uuid;

fn load<T: AssetTrait>(collections: &mut Collections) {
    load_from::<T>(
        &Path::new(env!("CARGO_MANIFEST_DIR")).join("../gled-model/tests/fixtures"),
        collections,
    );
}

fn load_from<T: AssetTrait>(root: &Path, collections: &mut Collections) {
    let Ok(entries) = fs::read_dir(root.join(T::DIR_NAME)) else {
        return;
    };
    for entry in entries {
        let path = entry.expect("entry").path();
        if path.extension().and_then(|ext| ext.to_str()) != Some("json") {
            continue;
        }
        let stem = path
            .file_stem()
            .and_then(|stem| stem.to_str())
            .expect("name");
        let id = AssetId::<T>::from_uuid(Uuid::from_str(stem).expect("uuid name"));
        // Assets that no longer load are the JSON tests' business.
        if let Ok(asset) = Asset::<T>::read(fs::read(&path).expect("readable").as_slice(), id) {
            collections.get_mut::<T>().insert_shared(asset);
        }
    }
}

fn fixtures() -> Collections {
    let mut collections = Collections::default();
    load::<Animation>(&mut collections);
    load::<Curve>(&mut collections);
    load::<MidiController>(&mut collections);
    load::<OutputDevice>(&mut collections);
    load::<Palette>(&mut collections);
    load::<Project>(&mut collections);
    load::<Scene>(&mut collections);
    collections
}

#[test]
fn every_asset_survives_the_wire() {
    let collections = fixtures();
    let count = all_assets(&collections).len();
    let message = ServerMessage::Assets(all_assets(&collections));
    let bytes = encode(&message).expect("encodes");
    println!("{count} fixture assets: {} bytes", bytes.len());
    let ServerMessage::Assets(assets) = decode(&bytes).expect("decodes") else {
        panic!("not an Assets message");
    };
    assert_eq!(assets.len(), count);
    let rebuilt = collections_from(assets);
    assert!(AssetChanges::between(&collections, &rebuilt).is_empty());
}

#[test]
fn changes_hold_only_what_changed() {
    let old = fixtures();
    let mut new = old.clone();

    let curve = Asset::all(&new).into_iter().next().expect("a curve");
    let mut edited: Asset<Curve> = Arc::unwrap_or_clone(curve);
    edited.data.linked = !edited.data.linked;
    new.get_mut::<Curve>().set_asset(edited.clone());

    let palette = Asset::<Palette>::all(&new)
        .into_iter()
        .next()
        .expect("a palette");
    new.get_mut::<Palette>().delete_asset(palette.id);

    let added = Asset::<Palette>::new(vec!["Added".to_owned()]);
    new.get_mut::<Palette>().set_asset(added.clone());

    let changes = AssetChanges::between(&old, &new);
    assert_eq!(changes.changed.len(), 2);
    assert!(changes.changed.contains(&AnyAsset::Curve(Arc::new(edited))));
    assert!(
        changes
            .changed
            .contains(&AnyAsset::Palette(Arc::new(added)))
    );
    assert_eq!(changes.deleted, vec![AnyAssetId::Palette(palette.id)]);

    // Through the wire and onto a replica of the old collections.
    let changes: AssetChanges = decode(&encode(&changes).expect("encodes")).expect("decodes");
    let mut replica = old.clone();
    changes.apply_to(&mut replica);
    assert!(AssetChanges::between(&replica, &new).is_empty());
}

#[test]
fn equal_copies_are_unchanged() {
    // A reload from disk makes new shared values with the same content.
    let old = fixtures();
    let reloaded = fixtures();
    assert!(AssetChanges::between(&old, &reloaded).is_empty());
}

#[test]
#[ignore = "needs the local asset repository"]
fn the_local_repository_survives_the_wire() {
    let root = std::env::var_os("GLED_ASSETS")
        .map(std::path::PathBuf::from)
        .or_else(|| {
            std::env::var_os("HOME").map(|home| Path::new(&home).join(".local/share/gled2"))
        })
        .expect("an asset repository");
    let mut collections = Collections::default();
    load_from::<Animation>(&root, &mut collections);
    load_from::<Curve>(&root, &mut collections);
    load_from::<MidiController>(&root, &mut collections);
    load_from::<OutputDevice>(&root, &mut collections);
    load_from::<Palette>(&root, &mut collections);
    load_from::<Project>(&root, &mut collections);
    load_from::<Scene>(&root, &mut collections);

    let assets = all_assets(&collections);
    let count = assets.len();
    let bytes = encode(&ServerMessage::Assets(assets)).expect("encodes");
    println!("{count} assets: {} bytes", bytes.len());
    let ServerMessage::Assets(assets) = decode(&bytes).expect("decodes") else {
        panic!("not an Assets message");
    };
    let rebuilt = collections_from(assets);
    let changes = AssetChanges::between(&collections, &rebuilt);
    assert!(
        changes.is_empty(),
        "changed on the wire: {:?}",
        changes.changed.iter().map(AnyAsset::id).collect::<Vec<_>>()
    );
}