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