Skip to main content

icydb_core/db/
response.rs

1use crate::{
2    error::{ErrorClass, ErrorOrigin, InternalError},
3    prelude::*,
4    view::View,
5};
6use thiserror::Error as ThisError;
7
8///
9/// Row
10///
11
12pub type Row<E> = (Key, E);
13
14///
15/// ResponseError
16/// Errors related to interpreting a materialized response.
17///
18
19#[derive(Debug, ThisError)]
20pub enum ResponseError {
21    #[error("expected exactly one row, found 0 (entity {entity})")]
22    NotFound { entity: &'static str },
23
24    #[error("expected exactly one row, found {count} (entity {entity})")]
25    NotUnique { entity: &'static str, count: u32 },
26}
27
28impl ResponseError {
29    pub(crate) const fn class(&self) -> ErrorClass {
30        match self {
31            Self::NotFound { .. } => ErrorClass::NotFound,
32            Self::NotUnique { .. } => ErrorClass::Conflict,
33        }
34    }
35}
36
37impl From<ResponseError> for InternalError {
38    fn from(err: ResponseError) -> Self {
39        Self::new(err.class(), ErrorOrigin::Response, err.to_string())
40    }
41}
42
43///
44/// Response
45/// Materialized query result: ordered `(Key, Entity)` pairs.
46///
47/// Fully materialized executor output. Pagination, ordering, and
48/// filtering are expressed at the intent layer.
49///
50#[derive(Debug)]
51pub struct Response<E: EntityKind>(pub Vec<Row<E>>);
52
53impl<E: EntityKind> Response<E> {
54    // ------------------------------------------------------------------
55    // Introspection
56    // ------------------------------------------------------------------
57
58    #[must_use]
59    #[allow(clippy::cast_possible_truncation)]
60    pub const fn count(&self) -> u32 {
61        self.0.len() as u32
62    }
63
64    #[must_use]
65    pub const fn is_empty(&self) -> bool {
66        self.0.is_empty()
67    }
68
69    // ------------------------------------------------------------------
70    // Cardinality enforcement
71    // ------------------------------------------------------------------
72
73    pub fn require_one(&self) -> Result<(), InternalError> {
74        match self.count() {
75            1 => Ok(()),
76            0 => Err(ResponseError::NotFound { entity: E::PATH }.into()),
77            n => Err(ResponseError::NotUnique {
78                entity: E::PATH,
79                count: n,
80            }
81            .into()),
82        }
83    }
84
85    pub fn require_some(&self) -> Result<(), InternalError> {
86        if self.is_empty() {
87            Err(ResponseError::NotFound { entity: E::PATH }.into())
88        } else {
89            Ok(())
90        }
91    }
92
93    // ------------------------------------------------------------------
94    // Rows
95    // ------------------------------------------------------------------
96
97    pub fn row(self) -> Result<Row<E>, InternalError> {
98        self.require_one()?;
99        Ok(self.0.into_iter().next().unwrap())
100    }
101
102    #[allow(clippy::cast_possible_truncation)]
103    pub fn try_row(self) -> Result<Option<Row<E>>, InternalError> {
104        match self.0.len() {
105            0 => Ok(None),
106            1 => Ok(Some(self.0.into_iter().next().unwrap())),
107            n => Err(ResponseError::NotUnique {
108                entity: E::PATH,
109                count: n as u32,
110            }
111            .into()),
112        }
113    }
114
115    #[must_use]
116    pub fn rows(self) -> Vec<Row<E>> {
117        self.0
118    }
119
120    // ------------------------------------------------------------------
121    // Entities (primary ergonomic surface)
122    // ------------------------------------------------------------------
123
124    pub fn entity(self) -> Result<E, InternalError> {
125        self.row().map(|(_, e)| e)
126    }
127
128    pub fn try_entity(self) -> Result<Option<E>, InternalError> {
129        Ok(self.try_row()?.map(|(_, e)| e))
130    }
131
132    #[must_use]
133    pub fn entities(self) -> Vec<E> {
134        self.0.into_iter().map(|(_, e)| e).collect()
135    }
136
137    // ------------------------------------------------------------------
138    // Keys
139    // ------------------------------------------------------------------
140
141    #[must_use]
142    pub fn key(&self) -> Option<Key> {
143        self.0.first().map(|(k, _)| *k)
144    }
145
146    pub fn key_strict(self) -> Result<Key, InternalError> {
147        self.row().map(|(k, _)| k)
148    }
149
150    pub fn try_key(self) -> Result<Option<Key>, InternalError> {
151        Ok(self.try_row()?.map(|(k, _)| k))
152    }
153
154    #[must_use]
155    pub fn keys(&self) -> Vec<Key> {
156        self.0.iter().map(|(k, _)| *k).collect()
157    }
158
159    #[must_use]
160    pub fn contains_key(&self, key: &Key) -> bool {
161        self.0.iter().any(|(k, _)| k == key)
162    }
163
164    // ------------------------------------------------------------------
165    // Views
166    // ------------------------------------------------------------------
167
168    /// Require exactly one result and return it as a view.
169    pub fn view(&self) -> Result<View<E>, InternalError> {
170        self.require_one()?;
171        let view = self
172            .0
173            .first()
174            .map(|(_, entity)| entity.to_view())
175            .expect("require_one ensures one row");
176
177        Ok(view)
178    }
179
180    /// Return zero or one result as a view.
181    #[allow(clippy::cast_possible_truncation)]
182    pub fn view_opt(&self) -> Result<Option<View<E>>, InternalError> {
183        match self.0.len() {
184            0 => Ok(None),
185            1 => Ok(self.0.first().map(|(_, entity)| entity.to_view())),
186            n => Err(ResponseError::NotUnique {
187                entity: E::PATH,
188                count: n as u32,
189            }
190            .into()),
191        }
192    }
193
194    /// Return zero or one result as a view.
195    pub fn try_view(&self) -> Result<Option<View<E>>, InternalError> {
196        self.view_opt()
197    }
198
199    /// Return all results as views.
200    #[must_use]
201    pub fn views(&self) -> Vec<View<E>> {
202        self.0.iter().map(|(_, entity)| entity.to_view()).collect()
203    }
204
205    // ------------------------------------------------------------------
206    // Non-strict access (explicitly unsafe)
207    // ------------------------------------------------------------------
208
209    #[must_use]
210    pub fn first(self) -> Option<Row<E>> {
211        self.0.into_iter().next()
212    }
213
214    #[must_use]
215    pub fn first_entity(self) -> Option<E> {
216        self.first().map(|(_, e)| e)
217    }
218
219    #[must_use]
220    pub fn first_pk(self) -> Option<E::PrimaryKey> {
221        self.first_entity().map(|e| e.primary_key())
222    }
223}
224
225impl<E: EntityKind> IntoIterator for Response<E> {
226    type Item = Row<E>;
227    type IntoIter = std::vec::IntoIter<Self::Item>;
228
229    fn into_iter(self) -> Self::IntoIter {
230        self.0.into_iter()
231    }
232}
233
234///
235/// ResponseExt
236/// Ergonomic helpers for `Result<Response<E>, InternalError>`.
237///
238/// This trait exists solely to avoid repetitive `?` when
239/// working with executor results. It intentionally exposes
240/// a *minimal* surface.
241///
242pub trait ResponseExt<E: EntityKind> {
243    // --- entities (primary use case) ---
244
245    fn entities(self) -> Result<Vec<E>, InternalError>;
246    fn entity(self) -> Result<E, InternalError>;
247    fn try_entity(self) -> Result<Option<E>, InternalError>;
248
249    // --- introspection ---
250
251    fn count(self) -> Result<u32, InternalError>;
252}
253
254impl<E: EntityKind> ResponseExt<E> for Result<Response<E>, InternalError> {
255    fn entities(self) -> Result<Vec<E>, InternalError> {
256        Ok(self?.entities())
257    }
258
259    fn entity(self) -> Result<E, InternalError> {
260        self?.entity()
261    }
262
263    fn try_entity(self) -> Result<Option<E>, InternalError> {
264        self?.try_entity()
265    }
266
267    fn count(self) -> Result<u32, InternalError> {
268        Ok(self?.count())
269    }
270}