gled-protocol 2.30.1

What gled's user interfaces and its engine send each other
Documentation
//! A project patch 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};

use gled_model::{
    asset::{Asset, project::Project, scene::grid::GridLocation},
    asset_id::AssetId,
};
use gled_protocol::{
    codec::encode,
    project_update::{ProjectPatch, 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")
}

/// Patches `old` into `new`, checks the result and returns the patch.
fn patch(old: &Project, new: &Project) -> ProjectPatch {
    let update = ProjectUpdate::between(Some(old), Some(new));
    let mut replica = Some(old.clone());
    update.clone().apply_to(&mut replica);
    assert_eq!(replica.as_ref(), Some(new));
    match update {
        ProjectUpdate::Patched(patch) => patch,
        other => panic!("expected a patch, 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 patch = patch(&old, &new);
    assert_eq!(patch.scenes.len(), 1);
    assert!(patch.svg.is_none());
    assert!(patch.rest.is_none());

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

#[test]
fn removed_scenes_svg_and_settings_are_patched() {
    let old = fixture_project();
    let mut new = old.clone();
    let location = first_location(&new);
    new.scenes_instances_grid.remove(&location);
    new.svg = None;
    new.main_dimmer = 0.25;

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

#[test]
fn unchanged_and_replaced_projects() {
    let project = fixture_project();
    assert_eq!(
        ProjectUpdate::between(Some(&project), Some(&project)),
        ProjectUpdate::Unchanged
    );
    let mut replica = Some(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, Some(project));
}