sim_lib_music_serial/
event.rs1use 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 pub fn new(value: impl Into<String>) -> Result<Self, SerialPlanError> {
43 Ok(Self(validate_id_text($kind, value)?))
44 }
45
46 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
81pub type VoiceId = ObjectId;
83
84#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
86pub struct OrdinalRef {
87 pub row_id: RowInstanceId,
89 pub ordinal: usize,
91}
92
93#[derive(Clone, Debug, PartialEq, Eq)]
95pub struct StructuralLicense {
96 pub reading_id: StructuralReadingId,
98 pub rationale: String,
100}
101
102impl StructuralLicense {
103 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 pub fn new(row_id: RowInstanceId, ordinal: usize) -> Self {
122 Self { row_id, ordinal }
123 }
124}
125
126#[derive(Clone, Debug, PartialEq, Eq)]
128pub struct EventPlacement {
129 simultaneous_group: Option<SimultaneousGroupId>,
130}
131
132impl EventPlacement {
133 pub const fn independent() -> Self {
135 Self {
136 simultaneous_group: None,
137 }
138 }
139
140 pub fn simultaneous(group: SimultaneousGroupId) -> Self {
142 Self {
143 simultaneous_group: Some(group),
144 }
145 }
146
147 pub fn simultaneous_group(&self) -> Option<&SimultaneousGroupId> {
149 self.simultaneous_group.as_ref()
150 }
151}
152
153#[derive(Clone, Debug, PartialEq, Eq)]
155pub struct PlannedSerialEvent {
156 pub id: SerialEventId,
158 pub ordinals: Vec<OrdinalRef>,
160 pub role: SerialRole,
162 pub origin: SerialOrigin,
164 pub voice: VoiceId,
166 pub placement: EventPlacement,
168 pub parents: Vec<SerialEventId>,
170 pub licenses: Vec<StructuralLicense>,
172}