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