use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::{BTreeMap, BTreeSet, HashMap};
fn serialize_tuple_map<S>(
map: &BTreeMap<(String, String), String>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeMap;
let mut m = serializer.serialize_map(Some(map.len()))?;
for ((k1, k2), v) in map {
m.serialize_entry(&format!("{}::{}", k1, k2), v)?;
}
m.end()
}
fn deserialize_tuple_map<'de, D>(
deserializer: D,
) -> Result<BTreeMap<(String, String), String>, D::Error>
where
D: Deserializer<'de>,
{
let string_map: BTreeMap<String, String> = BTreeMap::deserialize(deserializer)?;
let mut result = BTreeMap::new();
for (key, value) in string_map {
if let Some((k1, k2)) = key.split_once("::") {
result.insert((k1.to_string(), k2.to_string()), value);
}
}
Ok(result)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RenderedComponent {
pub name: String,
#[serde(default)]
pub conditional: bool,
}
impl RenderedComponent {
pub fn unconditional(name: impl Into<String>) -> Self {
Self {
name: name.into(),
conditional: false,
}
}
pub fn conditional(name: impl Into<String>) -> Self {
Self {
name: name.into(),
conditional: true,
}
}
}
impl From<&str> for RenderedComponent {
fn from(name: &str) -> Self {
Self::unconditional(name)
}
}
impl From<String> for RenderedComponent {
fn from(name: String) -> Self {
Self {
name,
conditional: false,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ComponentSourceProfile {
pub name: String,
pub file: String,
pub rendered_elements: BTreeMap<String, u32>,
pub rendered_components: Vec<RenderedComponent>,
#[serde(
serialize_with = "serialize_tuple_map",
deserialize_with = "deserialize_tuple_map"
)]
pub aria_attributes: BTreeMap<(String, String), String>,
pub role_attributes: BTreeMap<String, String>,
#[serde(
serialize_with = "serialize_tuple_map",
deserialize_with = "deserialize_tuple_map"
)]
pub data_attributes: BTreeMap<(String, String), String>,
pub prop_defaults: BTreeMap<String, String>,
pub uses_portal: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub portal_target: Option<String>,
pub consumed_contexts: Vec<String>,
pub provided_contexts: Vec<String>,
pub is_forward_ref: bool,
pub is_memo: bool,
pub css_tokens_used: BTreeSet<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bem_block: Option<String>,
pub bem_elements: BTreeSet<String>,
pub bem_modifiers: BTreeSet<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub prop_style_bindings: BTreeMap<String, BTreeSet<String>>,
pub extends_props: Vec<String>,
pub children_slot_path: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub children_slot_detail: Vec<(String, Option<String>)>,
pub has_children_prop: bool,
pub all_props: BTreeSet<String>,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub required_props: BTreeSet<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub prop_types: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub clone_element_injections: Vec<CloneElementInjection>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub managed_attributes: Vec<ManagedAttributeBinding>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CloneElementInjection {
pub injected_props: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManagedAttributeBinding {
pub prop_name: String,
pub generator_function: String,
pub target_element: String,
pub overridden_attributes: Vec<String>,
#[serde(default)]
pub component_overrides: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceLevelChange {
pub component: String,
pub category: SourceLevelCategory,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub old_value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_value: Option<String>,
pub has_test_implications: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub test_description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub element: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub migration_from: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dependency_chain: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceLevelCategory {
DomStructure,
AriaChange,
RoleChange,
DataAttribute,
CssToken,
PropDefault,
PortalUsage,
ContextDependency,
Composition,
ForwardRef,
Memo,
RenderedComponent,
PropAttributeOverride,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CompositionTree {
pub root: String,
pub family_members: Vec<String>,
pub edges: Vec<CompositionEdge>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompositionEdge {
pub parent: String,
pub child: String,
pub relationship: ChildRelationship,
pub required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub bem_evidence: Option<String>,
#[serde(default)]
pub strength: EdgeStrength,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prop_name: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EdgeStrength {
#[default]
Allowed = 0,
Structural = 1,
Wrapper = 2,
Required = 3,
}
impl EdgeStrength {
pub fn child_requires_parent(&self) -> bool {
matches!(self, EdgeStrength::Required | EdgeStrength::Structural)
}
pub fn parent_requires_child(&self) -> bool {
matches!(self, EdgeStrength::Required | EdgeStrength::Wrapper)
}
pub fn combine(&self, other: &EdgeStrength) -> EdgeStrength {
let chp = self.child_requires_parent() || other.child_requires_parent();
let pmc = self.parent_requires_child() || other.parent_requires_child();
match (chp, pmc) {
(true, true) => EdgeStrength::Required,
(true, false) => EdgeStrength::Structural,
(false, true) => EdgeStrength::Wrapper,
(false, false) => EdgeStrength::Allowed,
}
}
pub fn collapse_chain(&self, child_edge: &EdgeStrength) -> EdgeStrength {
let chp = child_edge.child_requires_parent() && self.parent_requires_child();
let pmc = self.parent_requires_child() && child_edge.parent_requires_child();
match (chp, pmc) {
(true, true) => EdgeStrength::Required,
(true, false) => EdgeStrength::Structural,
(false, true) => EdgeStrength::Wrapper,
(false, false) => EdgeStrength::Allowed,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChildRelationship {
BemElement,
IndependentBlock,
Internal,
DirectChild,
PropPassed,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompositionChange {
pub family: String,
pub change_type: CompositionChangeType,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub before_pattern: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after_pattern: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CompositionChangeType {
NewRequiredChild {
parent: String,
new_child: String,
wraps: Vec<String>,
},
PropToChild {
parent: String,
child: String,
props: Vec<String>,
},
ChildToProp {
parent: String,
child: String,
props: Vec<String>,
},
FamilyMemberRemoved { member: String },
FamilyMemberAdded { member: String },
PropDrivenToComposition { parent: String },
CompositionToPropDriven { parent: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConformanceCheck {
pub family: String,
pub check_type: ConformanceCheckType,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub correct_example: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConformanceCheckType {
MissingIntermediate {
parent: String,
child: String,
required_intermediate: String,
},
MissingChild {
parent: String,
expected_child: String,
},
InvalidDirectChild {
parent: String,
child: String,
expected_parent: String,
},
ExclusiveWrapper {
parent: String,
allowed_children: Vec<String>,
},
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SdPipelineResult {
pub source_level_changes: Vec<SourceLevelChange>,
pub composition_trees: Vec<CompositionTree>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub old_composition_trees: Vec<CompositionTree>,
pub composition_changes: Vec<CompositionChange>,
pub conformance_checks: Vec<ConformanceCheck>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub component_packages: HashMap<String, String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub old_component_packages: HashMap<String, String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub old_component_props: HashMap<String, BTreeSet<String>>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub new_component_props: HashMap<String, BTreeSet<String>>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub old_component_prop_types: HashMap<String, BTreeMap<String, String>>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub new_component_prop_types: HashMap<String, BTreeMap<String, String>>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub new_required_props: HashMap<String, BTreeSet<String>>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub dep_repo_packages: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub removed_css_blocks: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dead_css_classes_after_swap: Vec<(String, String)>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub deprecated_replacements: Vec<DeprecatedReplacement>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub old_profiles: HashMap<String, ComponentSourceProfile>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub new_profiles: HashMap<String, ComponentSourceProfile>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum ReplacementEvidence {
#[default]
RenderingSwap,
CommitCoChange,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DeprecatedReplacement {
pub old_component: String,
pub new_component: String,
pub evidence_hosts: Vec<String>,
#[serde(default)]
pub evidence_source: ReplacementEvidence,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_combine_structural_plus_wrapper_equals_required() {
assert_eq!(
EdgeStrength::Structural.combine(&EdgeStrength::Wrapper),
EdgeStrength::Required,
);
assert_eq!(
EdgeStrength::Wrapper.combine(&EdgeStrength::Structural),
EdgeStrength::Required,
);
}
#[test]
fn test_combine_allowed_with_structural() {
assert_eq!(
EdgeStrength::Allowed.combine(&EdgeStrength::Structural),
EdgeStrength::Structural,
);
}
#[test]
fn test_combine_allowed_with_wrapper() {
assert_eq!(
EdgeStrength::Allowed.combine(&EdgeStrength::Wrapper),
EdgeStrength::Wrapper,
);
}
#[test]
fn test_combine_required_dominates() {
assert_eq!(
EdgeStrength::Required.combine(&EdgeStrength::Allowed),
EdgeStrength::Required,
);
assert_eq!(
EdgeStrength::Required.combine(&EdgeStrength::Structural),
EdgeStrength::Required,
);
assert_eq!(
EdgeStrength::Required.combine(&EdgeStrength::Wrapper),
EdgeStrength::Required,
);
}
#[test]
fn test_combine_allowed_stays_allowed() {
assert_eq!(
EdgeStrength::Allowed.combine(&EdgeStrength::Allowed),
EdgeStrength::Allowed,
);
}
#[test]
fn test_collapse_modal_chain_wrapper_then_structural() {
let outer = EdgeStrength::Wrapper;
let inner = EdgeStrength::Structural;
assert_eq!(
outer.collapse_chain(&inner),
EdgeStrength::Structural,
"Modal→ModalBody should be Structural after collapse"
);
}
#[test]
fn test_collapse_required_chain() {
let outer = EdgeStrength::Required;
let inner = EdgeStrength::Required;
assert_eq!(
outer.collapse_chain(&inner),
EdgeStrength::Required,
"Required + Required = Required"
);
}
#[test]
fn test_collapse_wrapper_chain() {
let outer = EdgeStrength::Wrapper;
let inner = EdgeStrength::Wrapper;
assert_eq!(
outer.collapse_chain(&inner),
EdgeStrength::Wrapper,
"Wrapper + Wrapper = Wrapper (parent still needs child)"
);
}
#[test]
fn test_collapse_structural_then_wrapper() {
let outer = EdgeStrength::Structural;
let inner = EdgeStrength::Wrapper;
assert_eq!(
outer.collapse_chain(&inner),
EdgeStrength::Allowed,
"Structural + Wrapper = Allowed (outer doesn't guarantee intermediate)"
);
}
#[test]
fn test_collapse_wrapper_then_allowed() {
let outer = EdgeStrength::Wrapper;
let inner = EdgeStrength::Allowed;
assert_eq!(
outer.collapse_chain(&inner),
EdgeStrength::Allowed,
"Wrapper + Allowed = Allowed (inner has no constraints)"
);
}
#[test]
fn test_collapse_allowed_kills_everything() {
let outer = EdgeStrength::Allowed;
assert_eq!(
outer.collapse_chain(&EdgeStrength::Required),
EdgeStrength::Allowed
);
assert_eq!(
outer.collapse_chain(&EdgeStrength::Structural),
EdgeStrength::Allowed
);
assert_eq!(
outer.collapse_chain(&EdgeStrength::Wrapper),
EdgeStrength::Allowed
);
}
#[test]
fn test_collapse_required_then_structural() {
let outer = EdgeStrength::Required;
let inner = EdgeStrength::Structural;
assert_eq!(
outer.collapse_chain(&inner),
EdgeStrength::Structural,
"Required + Structural = Structural (child constraint propagates, parent constraint doesn't)"
);
}
#[test]
fn test_collapse_required_then_wrapper() {
let outer = EdgeStrength::Required;
let inner = EdgeStrength::Wrapper;
assert_eq!(
outer.collapse_chain(&inner),
EdgeStrength::Wrapper,
"Required + Wrapper = Wrapper (parent needs child transitively)"
);
}
#[test]
fn test_strength_dimensions() {
assert!(!EdgeStrength::Allowed.child_requires_parent());
assert!(!EdgeStrength::Allowed.parent_requires_child());
assert!(EdgeStrength::Structural.child_requires_parent());
assert!(!EdgeStrength::Structural.parent_requires_child());
assert!(!EdgeStrength::Wrapper.child_requires_parent());
assert!(EdgeStrength::Wrapper.parent_requires_child());
assert!(EdgeStrength::Required.child_requires_parent());
assert!(EdgeStrength::Required.parent_requires_child());
}
}