use std::mem;
use super::stage::Stage;
use crate::{pcp, sdf, tf};
pub type StageSinkId = sdf::sink::Id<dyn StageSink>;
pub trait StageSink {
fn after_commit(&self, stage: &Stage, change: &CommittedChange<'_>) {
let _ = (stage, change);
}
fn before_commit(&self, stage: &Stage, change: &PendingChange<'_>) {
let _ = (stage, change);
}
fn edit_target_changed(&self, stage: &Stage) {
let _ = stage;
}
fn layer_muting_changed(&self, stage: &Stage, layer: &str, muted: bool, resynced: &[sdf::Path]) {
let _ = (stage, layer, muted, resynced);
}
fn load_rules_changed(&self, stage: &Stage, resynced: &[sdf::Path]) {
let _ = (stage, resynced);
}
}
#[derive(Debug, Clone)]
pub enum Provenance {
LocalStack,
EditTarget(pcp::MapFunction),
DirectLayerEdit,
}
impl Provenance {
pub fn mapping(&self) -> Option<&pcp::MapFunction> {
match self {
Provenance::EditTarget(m) => Some(m),
Provenance::LocalStack | Provenance::DirectLayerEdit => None,
}
}
}
pub struct CommittedChange<'a> {
pub resynced: &'a [sdf::Path],
pub changed_info_only: &'a [sdf::Path],
pub asset_paths_resynced: &'a [sdf::Path],
pub layer_identifier: &'a str,
pub change_list: &'a sdf::ChangeList,
pub layer_changes: &'a [(String, sdf::ChangeList)],
pub provenance: &'a Provenance,
pub generation: u64,
}
impl CommittedChange<'_> {
pub fn changed_fields<'a>(&'a self, path: &sdf::Path) -> impl Iterator<Item = &'a tf::Token> + use<'a> {
let key = self
.provenance
.mapping()
.map_or_else(|| Some(path.clone()), |m| m.map_target_to_source(path));
self.change_list
.entries()
.iter()
.find(|(p, _)| Some(p) == key.as_ref())
.into_iter()
.flat_map(|(_, entry)| entry.info_changed())
}
}
pub struct PendingChange<'a> {
pub layer_identifier: &'a str,
pub base: &'a dyn sdf::AbstractData,
pub change_list: &'a sdf::ChangeList,
pub mapping: Option<&'a pcp::MapFunction>,
pub generation: u64,
}
impl<F: Fn(&Stage, &CommittedChange<'_>)> StageSink for F {
fn after_commit(&self, stage: &Stage, change: &CommittedChange<'_>) {
self(stage, change);
}
}
pub(super) struct Payload {
stage: NamespacedPaths,
authored: NamespacedPaths,
stage_info: Vec<sdf::Path>,
authored_info: Vec<sdf::Path>,
changed_info_only: Vec<sdf::Path>,
asset_paths_resynced: Vec<sdf::Path>,
change_list: sdf::ChangeList,
layer_changes: Vec<(String, sdf::ChangeList)>,
}
#[derive(Default)]
struct NamespacedPaths {
resynced: Vec<sdf::Path>,
subtree: Vec<sdf::Path>,
}
impl NamespacedPaths {
fn normalize(&mut self) {
self.resynced.sort();
self.resynced.dedup();
keep_ancestors(&mut self.subtree);
self.resynced.retain(|p| !is_covered_below(&self.subtree, p));
}
fn reports(&self, path: &sdf::Path) -> bool {
is_covered(&self.subtree, path) || self.resynced.binary_search(path).is_ok()
}
fn absorb(&mut self, other: &mut Self) {
if other.resynced.is_empty() {
return;
}
self.resynced.append(&mut other.resynced);
self.resynced.sort();
self.resynced.dedup();
}
}
impl Payload {
pub(super) fn new(
changes: &pcp::Changes,
scratch: &sdf::ChangeList,
layer_changes: Vec<(String, sdf::ChangeList)>,
provenance: &Provenance,
) -> Self {
let to_stage = |path: &sdf::Path| match provenance {
Provenance::EditTarget(m) => m.map_source_to_target(path),
Provenance::LocalStack => Some(path.strip_all_variant_selections()),
Provenance::DirectLayerEdit => Some(path.clone()),
};
let mut stage = NamespacedPaths {
resynced: changes.cache.stage_resynced_paths().cloned().collect(),
subtree: changes.cache.stage_subtree_paths().cloned().collect(),
};
let mut authored = NamespacedPaths {
resynced: changes.cache.authored_resynced_paths().filter_map(&to_stage).collect(),
subtree: changes.cache.authored_subtree_paths().filter_map(&to_stage).collect(),
};
let info_is_authored = matches!(provenance, Provenance::DirectLayerEdit);
if !info_is_authored {
stage.resynced.append(&mut authored.resynced);
stage.subtree.append(&mut authored.subtree);
}
let mut stage_info: Vec<sdf::Path> = changes.cache.reported.stage_info.iter().cloned().collect();
let mut authored_info: Vec<sdf::Path> = changes.cache.reported.authored_info.iter().cloned().collect();
if !info_is_authored {
stage_info.extend(authored_info.drain(..).filter_map(|path| to_stage(&path)));
stage_info.sort();
stage_info.dedup();
}
Self {
stage,
authored,
stage_info,
authored_info,
changed_info_only: Vec::new(),
asset_paths_resynced: Vec::new(),
change_list: scratch.clone(),
layer_changes,
}
}
pub(super) fn finish(&mut self, outcome: pcp::ApplyOutcome) {
let pcp::ApplyOutcome {
resynced: mut stage_resynced,
resynced_prims,
mut asset_paths_resynced,
} = outcome;
self.stage.resynced.extend(stage_resynced.iter().cloned());
self.stage.resynced.extend(resynced_prims);
self.stage.subtree.append(&mut stage_resynced);
self.stage.normalize();
self.authored.normalize();
self.stage_info.retain(|p| !self.stage.reports(p));
self.authored_info.retain(|p| !self.authored.reports(p));
keep_ancestors(&mut asset_paths_resynced);
asset_paths_resynced.retain(|p| !is_covered(&self.stage.subtree, p));
self.asset_paths_resynced = asset_paths_resynced;
let mut authored = mem::take(&mut self.authored);
self.stage.absorb(&mut authored);
self.changed_info_only = mem::take(&mut self.stage_info);
self.changed_info_only.append(&mut self.authored_info);
self.changed_info_only.sort();
self.changed_info_only.dedup();
}
pub(super) fn committed_change<'a>(
&'a self,
layer_identifier: &'a str,
provenance: &'a Provenance,
generation: u64,
) -> CommittedChange<'a> {
CommittedChange {
resynced: &self.stage.resynced,
changed_info_only: &self.changed_info_only,
asset_paths_resynced: &self.asset_paths_resynced,
layer_identifier,
change_list: &self.change_list,
layer_changes: &self.layer_changes,
provenance,
generation,
}
}
}
pub(super) fn keep_ancestors(paths: &mut Vec<sdf::Path>) {
paths.sort();
let mut kept = 0;
for i in 0..paths.len() {
if !is_covered(&paths[..kept], &paths[i]) {
paths.swap(kept, i);
kept += 1;
}
}
paths.truncate(kept);
}
fn is_covered(covering: &[sdf::Path], path: &sdf::Path) -> bool {
covering.iter().any(|prefix| path.has_prefix(prefix))
}
fn is_covered_below(covering: &[sdf::Path], path: &sdf::Path) -> bool {
covering.iter().any(|prefix| prefix != path && path.has_prefix(prefix))
}
#[cfg(test)]
mod tests {
use super::*;
fn p(s: &str) -> sdf::Path {
sdf::Path::new(s).expect("valid path")
}
fn paths(paths: &[&str]) -> Vec<sdf::Path> {
paths.iter().map(|s| p(s)).collect()
}
fn shaped(
provenance: Provenance,
stage: &[&str],
authored: &[&str],
info: &[&str],
outcome: pcp::ApplyOutcome,
) -> Payload {
let mut changes = pcp::Changes::reporting();
for path in stage {
changes.cache.did_change_significantly.insert(p(path));
}
for path in authored {
changes.cache.authored_significant.insert(p(path));
}
for path in info {
changes.cache.reported.authored_info.insert(p(path));
}
let mut payload = Payload::new(&changes, &sdf::ChangeList::new(), Vec::new(), &provenance);
payload.finish(outcome);
payload
}
#[test]
fn resync_subsumes_subtree() {
let payload = shaped(
Provenance::LocalStack,
&["/A", "/A/B"],
&[],
&["/A/B", "/A.x", "/B.y"],
pcp::ApplyOutcome::default(),
);
assert_eq!(payload.stage.resynced, paths(&["/A"]));
assert_eq!(payload.changed_info_only, paths(&["/B.y"]));
}
#[test]
fn asset_paths_yield_resync() {
let outcome = pcp::ApplyOutcome {
resynced_prims: Vec::new(),
resynced: Vec::new(),
asset_paths_resynced: paths(&["/A/B", "/B", "/B/C"]),
};
let payload = shaped(Provenance::LocalStack, &["/A"], &[], &[], outcome);
assert_eq!(payload.asset_paths_resynced, paths(&["/B"]));
let outcome = pcp::ApplyOutcome {
resynced_prims: Vec::new(),
resynced: paths(&["/"]),
asset_paths_resynced: paths(&["/A", "/B"]),
};
let payload = shaped(Provenance::LocalStack, &[], &[], &[], outcome);
assert!(payload.asset_paths_resynced.is_empty());
}
#[test]
fn direct_edit_keeps_namespaces() {
let outcome = pcp::ApplyOutcome {
resynced_prims: Vec::new(),
resynced: paths(&["/A"]),
asset_paths_resynced: paths(&["/A/B", "/C"]),
};
let payload = shaped(
Provenance::DirectLayerEdit,
&["/A"],
&["/S"],
&["/A", "/A.x", "/S/T", "/D.y"],
outcome,
);
assert_eq!(
payload.changed_info_only,
paths(&["/A", "/A.x", "/D.y"]),
"only the authored resync covers a layer-namespace path"
);
assert_eq!(payload.asset_paths_resynced, paths(&["/C"]));
assert_eq!(
payload.stage.resynced,
paths(&["/A", "/S"]),
"both namespaces are reported"
);
}
#[test]
fn edit_target_maps_authored() {
let mapping = pcp::MapFunction::new(vec![(p("/Ref"), p("/Model"))]);
let payload = shaped(
Provenance::EditTarget(mapping),
&["/Other"],
&["/Ref/Child", "/Elsewhere"],
&[],
pcp::ApplyOutcome::default(),
);
assert_eq!(payload.stage.resynced, paths(&["/Model/Child", "/Other"]));
}
}