Skip to main content

rsynko_memory/
term.rs

1//! The reified media terms: the carrier one reference interpreter chooses for every sort.
2
3use alux_sdk::IterTraversableExt;
4use derive_more::From;
5use derive_new::new;
6use rsynko_media::*;
7use std::collections::BTreeMap;
8
9/// Denotes a metadata value without closing the set of extractor-defined fields.
10#[derive(Clone, Debug, Default, PartialEq, From)]
11pub enum InfoValue {
12    /// Denotes unavailable metadata.
13    #[default]
14    Null,
15    /// Denotes a boolean observation.
16    Bool(bool),
17    /// Denotes an integral numeric observation.
18    Integer(i64),
19    /// Denotes a non-integral numeric observation.
20    Float(f64),
21    /// Denotes textual metadata.
22    String(String),
23    /// Denotes an ordered metadata sequence.
24    List(Vec<InfoValue>),
25    /// Denotes a metadata record.
26    Record(InfoRecord),
27}
28
29impl InfoValue {
30    /// Observes the value as text exactly when it denotes text.
31    #[must_use]
32    pub fn as_text(&self) -> Option<&str> {
33        match self {
34            Self::String(value) => Some(value),
35            _ => None,
36        }
37    }
38
39    /// Observes the value as a stated truth exactly when it denotes one.
40    #[must_use]
41    pub fn as_bool(&self) -> Option<bool> {
42        match self {
43            Self::Bool(value) => Some(*value),
44            _ => None,
45        }
46    }
47
48    /// Observes the value as a number exactly when it denotes one.
49    ///
50    /// Integral observations are widened, which is exact for every magnitude a format states and
51    /// approximate beyond it. Comparison is the only thing this crate does with the result.
52    #[must_use]
53    #[expect(clippy::cast_precision_loss, reason = "format observations are compared, not accumulated")]
54    pub fn as_number(&self) -> Option<f64> {
55        match self {
56            Self::Integer(value) => Some(*value as f64),
57            Self::Float(value) => Some(*value),
58            _ => None,
59        }
60    }
61}
62
63/// Denotes an open metadata record.
64///
65/// Keys are canonicalized by lexical order. Field order is intentionally not part of metadata
66/// equality.
67#[derive(Clone, Debug, Default, PartialEq)]
68pub struct InfoRecord(BTreeMap<String, InfoValue>);
69
70impl InfoRecord {
71    /// Inserts a field and returns the previously denoted value, if any.
72    pub fn insert(&mut self, key: impl Into<String>, value: impl Into<InfoValue>) -> Option<InfoValue> {
73        self.0.insert(key.into(), value.into())
74    }
75
76    /// Observes one field without exposing the record representation.
77    #[must_use]
78    pub fn get(&self, key: &str) -> Option<&InfoValue> {
79        self.0.get(key)
80    }
81
82    /// Observes whether the record contains no fields.
83    #[must_use]
84    pub fn is_empty(&self) -> bool {
85        self.0.is_empty()
86    }
87
88    /// Observes the number of fields in the record.
89    #[must_use]
90    pub fn len(&self) -> usize {
91        self.0.len()
92    }
93}
94
95impl From<&str> for InfoValue {
96    fn from(value: &str) -> Self {
97        Self::String(value.to_owned())
98    }
99}
100
101/// Denotes one alternative representation of a media item.
102///
103/// A format is an identity together with an open record of what an extractor observed about it. It
104/// fixes no schema: where its bytes rest, what streams it carries, and every quantity selection
105/// compares are observations, not fields. An extractor states what it saw, and an interpreter that
106/// locates bytes by other means simply states other observations.
107#[derive(Clone, Debug, PartialEq, new)]
108pub struct Format {
109    /// Identifies the format within one media description.
110    pub id: String,
111    /// Preserves everything the extractor observed about the format.
112    pub metadata: InfoRecord,
113}
114
115impl Format {
116    /// Observes one named value.
117    #[must_use]
118    pub fn observe(&self, key: &str) -> Option<&InfoValue> {
119        self.metadata.get(key)
120    }
121
122    /// Observes one named value as text.
123    #[must_use]
124    pub fn text(&self, key: &str) -> Option<&str> {
125        self.observe(key).and_then(InfoValue::as_text)
126    }
127
128    /// Observes one named value as a number.
129    #[must_use]
130    pub fn number(&self, key: &str) -> Option<f64> {
131        self.observe(key).and_then(InfoValue::as_number)
132    }
133
134    /// Observes one named truth, where an unstated observation denotes falsity.
135    #[must_use]
136    pub fn flag(&self, key: &str) -> bool {
137        self.observe(key).and_then(InfoValue::as_bool).unwrap_or(false)
138    }
139
140    /// Observes the container or filename extension when stated.
141    #[must_use]
142    pub fn extension(&self) -> Option<&str> {
143        self.text(FORMAT_EXTENSION)
144    }
145
146    /// Observes whether the format carries audio.
147    #[must_use]
148    pub fn has_audio(&self) -> bool {
149        self.flag(FORMAT_HAS_AUDIO)
150    }
151
152    /// Observes whether the format carries video.
153    #[must_use]
154    pub fn has_video(&self) -> bool {
155        self.flag(FORMAT_HAS_VIDEO)
156    }
157
158    /// Observes where the format's bytes rest when the extractor stated it.
159    #[must_use]
160    pub fn source(&self) -> Option<&str> {
161        self.text(FORMAT_SOURCE)
162    }
163}
164
165/// Denotes one artifact owned by the processing pipeline.
166#[derive(Clone, Debug, PartialEq, new)]
167pub struct Artifact {
168    /// Identifies the artifact independently of where it currently rests.
169    pub id: String,
170    /// Classifies the artifact's role.
171    pub kind: ArtifactKind,
172    /// Preserves artifact-specific observations.
173    pub metadata: InfoRecord,
174}
175
176impl Artifact {
177    /// Observes where the artifact currently rests when that has been stated.
178    #[must_use]
179    pub fn location(&self) -> Option<&str> {
180        self.metadata.get(ARTIFACT_LOCATION).and_then(InfoValue::as_text)
181    }
182}
183
184/// Identifies an extractor independently of its implementation type.
185#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
186pub struct ExtractorKey(pub String);
187
188impl ExtractorKey {
189    /// Constructs an extractor key.
190    #[must_use]
191    pub fn new(value: impl Into<String>) -> Self {
192        Self(value.into())
193    }
194}
195
196/// Denotes one fully described media item.
197#[derive(Clone, Debug, PartialEq, new)]
198pub struct Media {
199    /// Identifies the media within its extractor.
200    pub id: String,
201    /// Preserves all extracted metadata, including extractor-specific fields.
202    pub metadata: InfoRecord,
203    /// Lists directly retrievable formats in extractor order.
204    pub formats: Vec<Format>,
205}
206
207/// Denotes a URL whose meaning requires another extraction.
208#[derive(Clone, Debug, PartialEq, Eq, new)]
209pub struct UrlReference {
210    /// Names the referenced URL.
211    pub url: String,
212    /// Selects a specific extractor when present.
213    pub extractor: Option<ExtractorKey>,
214    /// Preserves whether outer metadata overlays the referenced result.
215    pub transparent: bool,
216}
217
218/// Denotes an ordered collection of extraction results.
219#[derive(Clone, Debug, PartialEq, new)]
220pub struct Collection {
221    /// Identifies the collection when known.
222    pub id: Option<String>,
223    /// Classifies collection composition.
224    pub kind: CollectionKind,
225    /// Preserves collection metadata.
226    pub metadata: InfoRecord,
227    /// Preserves entry order.
228    pub entries: Vec<Extraction>,
229}
230
231/// Denotes one step of extraction meaning.
232#[derive(Clone, Debug, PartialEq)]
233pub enum Extraction {
234    /// Denotes a fully described media item.
235    Media(Media),
236    /// Denotes a URL requiring another extraction step.
237    Url(UrlReference),
238    /// Denotes an ordered collection of further results.
239    Collection(Collection),
240}
241
242/// Denotes a predicate over what one format states about itself.
243///
244/// Every primitive except identity ranges over observations, so a predicate can speak about any
245/// field an extractor recorded rather than a fixed set of columns.
246#[derive(Clone, Debug, PartialEq)]
247pub enum FormatPredicate {
248    /// Accepts every format.
249    Any,
250    /// Accepts one format identifier.
251    Id(String),
252    /// Accepts formats stating the named observation.
253    Observed(String),
254    /// Accepts formats whose named observation is the stated text.
255    Text {
256        /// Names the observation.
257        key: String,
258        /// States the text the observation must equal.
259        value: String,
260    },
261    /// Accepts formats whose named observation is the stated truth.
262    Flag {
263        /// Names the observation.
264        key: String,
265        /// States the truth the observation must equal.
266        value: bool,
267    },
268    /// Accepts formats whose named observation compares as stated.
269    Number {
270        /// Names the observation.
271        key: String,
272        /// States how the observation is compared.
273        comparison: FormatComparison,
274        /// States the number compared against.
275        value: f64,
276    },
277    /// Accepts formats satisfying both predicates.
278    All(Box<FormatPredicate>, Box<FormatPredicate>),
279    /// Accepts formats rejected by the child predicate.
280    Not(Box<FormatPredicate>),
281}
282
283/// Denotes a first-order format-selection program.
284#[derive(Clone, Debug, PartialEq)]
285pub enum FormatSelection {
286    /// Chooses the most preferred matching format.
287    Best(FormatPredicate),
288    /// Chooses the least preferred matching format.
289    Worst(FormatPredicate),
290    /// Combines every child selection in declaration order.
291    Merge(Vec<FormatSelection>),
292    /// Chooses the first successful child selection.
293    Fallback(Vec<FormatSelection>),
294}
295
296/// Interprets one reified predicate against one format.
297///
298/// An unstated observation satisfies no predicate that speaks about it, so absence is rejection
299/// rather than failure.
300#[must_use]
301pub fn predicate_accepts(predicate: &FormatPredicate, format: &Format) -> bool {
302    match predicate {
303        FormatPredicate::Any => true,
304        FormatPredicate::Id(id) => format.id == *id,
305        FormatPredicate::Observed(key) => format.observe(key).is_some(),
306        FormatPredicate::Text { key, value } => format.text(key) == Some(value.as_str()),
307        FormatPredicate::Flag { key, value } => format.flag(key) == *value,
308        FormatPredicate::Number { key, comparison, value } => {
309            format.number(key).is_some_and(|observed| match comparison {
310                FormatComparison::AtMost => observed <= *value,
311                FormatComparison::Exactly => (observed - *value).abs() < f64::EPSILON,
312                FormatComparison::AtLeast => observed >= *value,
313            })
314        }
315        FormatPredicate::All(left, right) => predicate_accepts(left, format) && predicate_accepts(right, format),
316        FormatPredicate::Not(child) => !predicate_accepts(child, format),
317    }
318}
319
320/// Interprets one reified selection program against a preference-ordered catalog.
321///
322/// This is the canonical meaning of the reified syntax; an interpreter that chose those sorts
323/// states `FormatSelectionApplyAlg` by delegating here.
324#[must_use]
325pub fn interpret_selection<'a, This>(
326    alg: &This,
327    formats: &'a [Format],
328    selection: &FormatSelection,
329) -> Option<Vec<&'a Format>>
330where
331    This: FormatPredicateMatchAlg<Predicate = FormatPredicate, Format = Format>,
332{
333    match selection {
334        FormatSelection::Best(predicate) => {
335            formats.iter().rev().find(|format| alg.format_matches(predicate, format)).map(|format| vec![format])
336        }
337        FormatSelection::Worst(predicate) => {
338            formats.iter().find(|format| alg.format_matches(predicate, format)).map(|format| vec![format])
339        }
340        FormatSelection::Merge(selections) => {
341            selections.iter().traverse_iter(|child| interpret_selection(alg, formats, child).ok_or(())).ok()
342        }
343        FormatSelection::Fallback(selections) => {
344            selections.iter().find_map(|child| interpret_selection(alg, formats, child))
345        }
346    }
347}
348
349/// Identifies a processor independently of its implementation type.
350#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
351pub struct ProcessorId(pub String);
352
353impl ProcessorId {
354    /// Constructs a processor identity.
355    #[must_use]
356    pub fn new(value: impl Into<String>) -> Self {
357        Self(value.into())
358    }
359}
360
361/// Denotes application of one processor at one stage.
362#[derive(Clone, Debug, PartialEq, Eq)]
363pub struct ProcessingStep {
364    /// Names the stage selecting this step.
365    pub stage: ProcessingStage,
366    /// Names the processor to apply.
367    pub processor: ProcessorId,
368}
369
370impl ProcessingStep {
371    /// Constructs a processing step.
372    #[must_use]
373    pub fn new(stage: ProcessingStage, processor: impl Into<String>) -> Self {
374        Self { stage, processor: ProcessorId::new(processor) }
375    }
376}
377
378/// Denotes an ordered sequence of artifact transformations.
379#[derive(Clone, Debug, Default, PartialEq, Eq)]
380pub struct ProcessingProgram(Vec<ProcessingStep>);
381
382impl ProcessingProgram {
383    /// Constructs a program from steps in declaration order.
384    #[must_use]
385    pub fn from_steps(steps: impl IntoIterator<Item = ProcessingStep>) -> Self {
386        Self(steps.into_iter().collect())
387    }
388
389    /// Appends one step to the program.
390    pub fn push(&mut self, step: ProcessingStep) {
391        self.0.push(step);
392    }
393
394    /// Observes steps in declaration order.
395    pub fn steps(&self) -> impl Iterator<Item = &ProcessingStep> {
396        self.0.iter()
397    }
398
399    /// Denotes sequential composition of two programs.
400    #[must_use]
401    pub fn then(mut self, next: Self) -> Self {
402        self.0.extend(next.0);
403        self
404    }
405}