Skip to main content

icydb_core/db/
response.rs

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