Skip to main content

backhopper_core/model/
snapshot_diff.rs

1// Copyright (C) 2026 Michael S. Klishin and Contributors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// See LICENSE-APACHE and LICENSE-MIT for details.
4
5//! Wire payloads of the `snapshots diff` verbs.
6//!
7//! Modeled here so the CLI emits and the driver parses one definition.
8//! The name-bearing fields carry the same serde-transparent newtypes the
9//! rest of core uses, so the serialized form is unchanged and a consumer
10//! stops re-validating identifiers it round-trips. The composite
11//! (`fun_arity`, `type_arity`), path (`header`), and value fields stay
12//! `String`: no name newtype fits them.
13
14use serde::{Deserialize, Serialize};
15
16use crate::model::names::{ModuleName, ProjectName, SeriesName, TagName};
17
18/// API delta between two tags of one project, oriented `from -> to`:
19/// `*_added` are present at `to` and absent at `from`; `*_removed` are
20/// present at `from` and absent at `to`.
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct DiffPayload {
23    pub project: ProjectName,
24    /// The older tag: the baseline of the delta.
25    pub from: TagName,
26    /// The newer tag: the target of the delta.
27    pub to: TagName,
28    pub modules_added: Vec<ModuleName>,
29    pub modules_removed: Vec<ModuleName>,
30    pub exports_added: Vec<QualifiedFunArity>,
31    pub exports_removed: Vec<QualifiedFunArity>,
32    pub types_added: Vec<QualifiedTypeArity>,
33    pub types_removed: Vec<QualifiedTypeArity>,
34    pub callbacks_added: Vec<QualifiedFunArity>,
35    pub callbacks_removed: Vec<QualifiedFunArity>,
36    pub headers_added: Vec<String>,
37    pub headers_removed: Vec<String>,
38    pub records_added: Vec<QualifiedRecord>,
39    pub records_removed: Vec<QualifiedRecord>,
40    #[serde(default, skip_serializing_if = "Vec::is_empty")]
41    pub versioned_machine_version_changes: Vec<VersionedMachineVersionChange>,
42    #[serde(default, skip_serializing_if = "Vec::is_empty")]
43    pub wire_constant_changes: Vec<WireConstantChange>,
44}
45
46impl DiffPayload {
47    /// True when the diff records no API change on any tracked axis.
48    #[must_use]
49    pub fn is_empty(&self) -> bool {
50        self.modules_added.is_empty()
51            && self.modules_removed.is_empty()
52            && self.exports_added.is_empty()
53            && self.exports_removed.is_empty()
54            && self.types_added.is_empty()
55            && self.types_removed.is_empty()
56            && self.callbacks_added.is_empty()
57            && self.callbacks_removed.is_empty()
58            && self.headers_added.is_empty()
59            && self.headers_removed.is_empty()
60            && self.records_added.is_empty()
61            && self.records_removed.is_empty()
62            && self.versioned_machine_version_changes.is_empty()
63            && self.wire_constant_changes.is_empty()
64    }
65
66    /// Count of removed symbols: the breaking side of an API delta.
67    #[must_use]
68    pub fn breaking_removal_count(&self) -> usize {
69        self.modules_removed.len()
70            + self.exports_removed.len()
71            + self.callbacks_removed.len()
72            + self.types_removed.len()
73            + self.records_removed.len()
74            + self.headers_removed.len()
75    }
76}
77
78/// A versioned-machine version that was added, removed, or moved.
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
80#[serde(rename_all = "snake_case", tag = "kind")]
81pub enum VersionedMachineVersionChange {
82    Missing {
83        module: ModuleName,
84        side: String,
85    },
86    Drift {
87        module: ModuleName,
88        from: Option<u64>,
89        to: Option<u64>,
90    },
91}
92
93/// A wire-constant macro that was added, removed, or moved.
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
95#[serde(rename_all = "snake_case", tag = "kind")]
96pub enum WireConstantChange {
97    Missing {
98        module: ModuleName,
99        side: String,
100        macros: Vec<String>,
101    },
102    Drift {
103        module: ModuleName,
104        macro_name: String,
105        from: String,
106        to: String,
107    },
108}
109
110/// A function or callback named `module:fun/arity`.
111#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
112pub struct QualifiedFunArity {
113    pub module: ModuleName,
114    pub fun_arity: String,
115}
116
117/// A type named `module:type/arity`.
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119pub struct QualifiedTypeArity {
120    pub module: ModuleName,
121    pub type_arity: String,
122}
123
124/// A record declared in a header.
125#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
126pub struct QualifiedRecord {
127    pub header: String,
128    pub record: String,
129}
130
131/// Per-project diffs between the pins of two series, each oriented
132/// `from_series -> to_series` (see [`DiffPayload`] for the per-project
133/// `added`/`removed` orientation).
134#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
135pub struct CrossSeriesDiffPayload {
136    /// The baseline series.
137    pub from_series: SeriesName,
138    /// The target series.
139    pub to_series: SeriesName,
140    /// One entry per project that differs between the two series.
141    pub projects: Vec<DiffPayload>,
142}