Skip to main content

aqc_text_engine_core/requirement/
model.rs

1//! Text byte-stream requirement model.
2
3#![allow(
4    clippy::module_name_repetitions,
5    reason = "Public text-engine values use the TextFile prefix."
6)]
7
8use core::any::Any;
9use core::cmp::Ordering;
10use core::fmt;
11
12use aqc_file_engine_core::{
13    ConflictEntry, EngineRequirement, FileItemRequirement, ItemAssertionInput, ItemRequirements,
14    RequiredItemResolution, ResolvedItemRequirements, ResolvedRequirement, ScalarAssertion,
15    ScalarValue,
16};
17
18#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
19/// Expected byte contents for a whole text file or contained item.
20pub struct TextFileContents(Vec<u8>);
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23/// Validation errors for text requirement values.
24pub enum TextFileValueError {
25    /// A required text value was empty.
26    Empty { field: &'static str },
27}
28
29#[derive(Debug, Clone, Default)]
30/// Requirements for a generic text byte stream.
31pub struct TextFileRequirements {
32    /// Optional exact file contents assertion.
33    pub exact_contents: Option<ScalarAssertion<TextFileContents>>,
34    /// Byte sequences that must appear when exact file contents are not required.
35    pub contents: ItemRequirements<TextFileContents>,
36}
37
38#[derive(Debug, Clone, Default)]
39/// Resolved requirements for a generic text byte stream.
40pub struct ResolvedTextFileRequirements {
41    /// Resolved exact file contents assertion.
42    pub exact_contents: Option<
43        ResolvedRequirement<ScalarAssertion<TextFileContents>, ScalarAssertion<TextFileContents>>,
44    >,
45    /// Resolved contained byte assertions.
46    pub contents: ResolvedItemRequirements<TextFileContents>,
47}
48
49impl EngineRequirement for TextFileRequirements {
50    fn engine_id(&self) -> &'static str {
51        crate::ENGINE_ID
52    }
53
54    fn as_any(&self) -> &dyn Any {
55        self
56    }
57}
58
59impl TextFileContents {
60    /// Build non-empty text file contents.
61    ///
62    /// # Errors
63    ///
64    /// Returns [`TextFileValueError::Empty`] when `value` is empty.
65    pub fn new(value: impl Into<Vec<u8>>) -> Result<Self, TextFileValueError> {
66        let value = value.into();
67        if value.is_empty() {
68            return Err(TextFileValueError::Empty { field: "contents" });
69        }
70        Ok(Self(value))
71    }
72
73    #[must_use]
74    pub fn as_bytes(&self) -> &[u8] {
75        &self.0
76    }
77}
78
79impl fmt::Display for TextFileValueError {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match self {
82            Self::Empty { field } => write!(f, "{field} must not be empty"),
83        }
84    }
85}
86
87impl fmt::Display for TextFileContents {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        f.write_str("contents")
90    }
91}
92
93impl ScalarValue for TextFileContents {
94    fn render(&self) -> String {
95        format!("{} bytes", self.0.len())
96    }
97
98    fn compare_for_order(&self, _other: &Self) -> Option<Ordering> {
99        None
100    }
101}
102
103impl FileItemRequirement for TextFileContents {
104    type Identity = Self;
105
106    fn merge_identity(&self) -> Self::Identity {
107        self.clone()
108    }
109
110    fn compose_item(
111        key: &str,
112        items: Vec<ItemAssertionInput<Self>>,
113        conflicts: &mut Vec<ConflictEntry>,
114    ) -> Option<RequiredItemResolution<Self>> {
115        aqc_file_engine_core::compose_item_by(key, items, Clone::clone, conflicts)
116    }
117}