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