gled-protocol 2.33.0

What gled's user interfaces and its engine send each other
Documentation
//! A project diff turns the old project into the new one and stays small
//! for the edits an interface makes all the time.

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

use gled_model::{
    asset::{Asset, project::Project, scene::grid::GridLocation},
    asset_id::AssetId,
    patching::{
        Patch,
        fixture::{Fixture, LayoutPosition},
    },
};
use gled_protocol::{
    codec::encode,
    project_update::{ProjectDiff, ProjectUpdate},
};

fn fixture_project() -> Project {
    let path = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../gled-model/tests/fixtures/projects/0bb3f5cd-f47f-4453-bd89-8bc53d9e1eb3.json");
    let text = fs::read(&path).expect("fixture project is readable");
    Asset::<Project>::read(text.as_slice(), AssetId::default())
        .expect("fixture project loads")
        .data
        .clone()
}

fn first_location(project: &Project) -> GridLocation {
    *project
        .scenes_instances_grid
        .keys()
        .min_by_key(|location| (location.row, location.col))
        .expect("the fixture has scenes")
}

/// Turns `old` into `new`, checks the result and returns the diff.
fn diff(old: &Project, new: &Project) -> ProjectDiff {
    let update = ProjectUpdate::between(Some(old), Some(new));
    let mut replica = Some(Arc::new(old.clone()));
    update.clone().apply_to(&mut replica);
    assert_eq!(replica.as_deref(), Some(new));
    match update {
        ProjectUpdate::Changed(diff) => diff,
        other => panic!("expected a diff, got {other:?}"),
    }
}

#[test]
fn a_scene_edit_sends_only_that_scene() {
    let old = fixture_project();
    let mut new = old.clone();
    let location = first_location(&new);
    if let Some(scene) = new.scenes_instances_grid.get_mut(&location) {
        scene.active = !scene.active;
    }

    let diff = diff(&old, &new);
    assert_eq!(diff.scenes.len(), 1);
    assert!(diff.patch.is_none());
    assert!(diff.rest.is_none());

    let whole = encode(&new).expect("encodes").len();
    let changed = encode(&diff).expect("encodes").len();
    println!("whole project {whole} bytes, scene edit {changed} bytes");
    assert!(changed * 5 < whole);
}

#[test]
fn removed_scenes_the_patch_and_settings_are_in_the_diff() {
    let old = fixture_project();
    let mut new = old.clone();
    let location = first_location(&new);
    new.scenes_instances_grid.remove(&location);
    new.patch
        .add_fixture(Fixture::new("bar"), LayoutPosition::default());
    new.main_dimmer = 0.25;

    let diff = diff(&old, &new);
    assert_eq!(diff.scenes, vec![(location, None)]);
    assert_eq!(diff.patch.as_deref(), Some(&new.patch));
    let rest = diff.rest.expect("the dimmer changed");
    assert!(rest.scenes_instances_grid.is_empty());
    assert_eq!(rest.patch, Patch::default());
}

#[test]
fn unchanged_and_replaced_projects() {
    let project = fixture_project();
    assert_eq!(
        ProjectUpdate::between(Some(&project), Some(&project)),
        ProjectUpdate::Unchanged
    );
    let mut replica = Some(Arc::new(project.clone()));
    ProjectUpdate::between(Some(&project), None).apply_to(&mut replica);
    assert_eq!(replica, None);
    ProjectUpdate::between(None, Some(&project)).apply_to(&mut replica);
    assert_eq!(replica.as_deref(), Some(&project));
}