Skip to main content

sim_lib_music_serial/
event.rs

1//! Stable ids and immutable planned serial events.
2
3use std::fmt::{Display, Formatter};
4
5use sim_lib_music_core::ObjectId;
6
7use crate::{SerialOrigin, SerialPlanError, SerialRole};
8
9fn validate_id_text(
10    kind: &'static str,
11    value: impl Into<String>,
12) -> Result<String, SerialPlanError> {
13    let value = value.into();
14    if value.trim().is_empty() {
15        return Err(SerialPlanError::InvalidId {
16            kind,
17            value,
18            reason: "value cannot be empty",
19        });
20    }
21    if value
22        .chars()
23        .any(|ch| !(ch.is_ascii_alphanumeric() || matches!(ch, '/' | '-' | '_' | '.')))
24    {
25        return Err(SerialPlanError::InvalidId {
26            kind,
27            value,
28            reason: "value must use ASCII letters, digits, /, -, _, or .",
29        });
30    }
31    Ok(value)
32}
33
34macro_rules! stable_id {
35    ($name:ident, $kind:literal, $doc:literal) => {
36        #[doc = $doc]
37        #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
38        pub struct $name(String);
39
40        impl $name {
41            /// Creates a validated stable identifier.
42            pub fn new(value: impl Into<String>) -> Result<Self, SerialPlanError> {
43                Ok(Self(validate_id_text($kind, value)?))
44            }
45
46            /// Returns the stable wire text.
47            pub fn as_str(&self) -> &str {
48                &self.0
49            }
50        }
51
52        impl Display for $name {
53            fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
54                formatter.write_str(&self.0)
55            }
56        }
57    };
58}
59
60stable_id!(
61    RowInstanceId,
62    "row-instance",
63    "Stable identity for one row instance in a serial plan."
64);
65stable_id!(
66    SerialEventId,
67    "serial-event",
68    "Stable identity for one planned serial event."
69);
70stable_id!(
71    SimultaneousGroupId,
72    "simultaneous-group",
73    "Stable identity for one equal-onset simultaneous event group."
74);
75stable_id!(
76    StructuralReadingId,
77    "structural-reading",
78    "Stable identity for one structural reading or deployment witness."
79);
80
81/// Stable voice identity reused from the exact music-core score model.
82pub type VoiceId = ObjectId;
83
84/// One stable structural ordinal within a specific row instance.
85#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
86pub struct OrdinalRef {
87    /// Row instance owning the ordinal.
88    pub row_id: RowInstanceId,
89    /// Zero-based ordinal within that row instance.
90    pub ordinal: usize,
91}
92
93/// Inspectable structural reading that licenses one event or realized note.
94#[derive(Clone, Debug, PartialEq, Eq)]
95pub struct StructuralLicense {
96    /// Stable reading identity.
97    pub reading_id: StructuralReadingId,
98    /// Human-facing explanation of the structural reading.
99    pub rationale: String,
100}
101
102impl StructuralLicense {
103    /// Creates one structural reading license.
104    pub fn new(
105        reading_id: StructuralReadingId,
106        rationale: impl Into<String>,
107    ) -> Result<Self, SerialPlanError> {
108        let rationale = rationale.into();
109        if rationale.trim().is_empty() {
110            return Err(SerialPlanError::EmptyStructuralLicenseRationale(reading_id));
111        }
112        Ok(Self {
113            reading_id,
114            rationale,
115        })
116    }
117}
118
119impl OrdinalRef {
120    /// Creates one stable row/ordinal reference.
121    pub fn new(row_id: RowInstanceId, ordinal: usize) -> Self {
122        Self { row_id, ordinal }
123    }
124}
125
126/// Placement metadata that preserves simultaneity without inventing a chord order.
127#[derive(Clone, Debug, PartialEq, Eq)]
128pub struct EventPlacement {
129    simultaneous_group: Option<SimultaneousGroupId>,
130}
131
132impl EventPlacement {
133    /// Returns an event placement with no simultaneous chord/group membership.
134    pub const fn independent() -> Self {
135        Self {
136            simultaneous_group: None,
137        }
138    }
139
140    /// Returns an event placement belonging to one simultaneous group.
141    pub fn simultaneous(group: SimultaneousGroupId) -> Self {
142        Self {
143            simultaneous_group: Some(group),
144        }
145    }
146
147    /// Returns the optional simultaneous group id.
148    pub fn simultaneous_group(&self) -> Option<&SimultaneousGroupId> {
149        self.simultaneous_group.as_ref()
150    }
151}
152
153/// One immutable planned serial event with row provenance, voice identity, and parent evidence.
154#[derive(Clone, Debug, PartialEq, Eq)]
155pub struct PlannedSerialEvent {
156    /// Stable event identity.
157    pub id: SerialEventId,
158    /// Structural row ordinals this event realizes together.
159    pub ordinals: Vec<OrdinalRef>,
160    /// Structural, derived, ornamental, or external role.
161    pub role: SerialRole,
162    /// Role-specific origin/provenance details.
163    pub origin: SerialOrigin,
164    /// Stable voice identity for the event.
165    pub voice: VoiceId,
166    /// Simultaneous placement metadata.
167    pub placement: EventPlacement,
168    /// Explicit parent evidence.
169    pub parents: Vec<SerialEventId>,
170    /// Structural readings that license this event's note claims.
171    pub licenses: Vec<StructuralLicense>,
172}