Skip to main content

icydb_core/db/response/
mod.rs

1mod write;
2
3use crate::{prelude::*, traits::AsView, types::Id};
4use thiserror::Error as ThisError;
5
6// re-exports
7pub use write::*;
8
9///
10/// Row
11///
12
13pub type Row<E> = (Id<E>, E);
14
15///
16/// ResponseError
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    const fn not_found<E: EntityKind>() -> Self {
30        Self::NotFound { entity: E::PATH }
31    }
32
33    const fn not_unique<E: EntityKind>(count: u32) -> Self {
34        Self::NotUnique {
35            entity: E::PATH,
36            count,
37        }
38    }
39}
40
41///
42/// Response
43///
44/// Materialized query result: ordered `(Id, Entity)` pairs.
45/// IDs are returned for correlation, reporting, and lookup; they are public values and do not imply
46/// authorization, ownership, or row existence outside this response payload.
47///
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    #[expect(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 const fn require_one(&self) -> Result<(), ResponseError> {
73        match self.count() {
74            1 => Ok(()),
75            0 => Err(ResponseError::not_found::<E>()),
76            n => Err(ResponseError::not_unique::<E>(n)),
77        }
78    }
79
80    pub const fn require_some(&self) -> Result<(), ResponseError> {
81        if self.is_empty() {
82            Err(ResponseError::not_found::<E>())
83        } else {
84            Ok(())
85        }
86    }
87
88    // ------------------------------------------------------------------
89    // Rows
90    // ------------------------------------------------------------------
91
92    #[expect(clippy::cast_possible_truncation)]
93    pub fn try_row(self) -> Result<Option<Row<E>>, ResponseError> {
94        match self.0.len() {
95            0 => Ok(None),
96            1 => Ok(Some(self.0.into_iter().next().unwrap())),
97            n => Err(ResponseError::not_unique::<E>(n as u32)),
98        }
99    }
100
101    pub fn row(self) -> Result<Row<E>, ResponseError> {
102        self.try_row()?.ok_or_else(ResponseError::not_found::<E>)
103    }
104
105    #[must_use]
106    pub fn rows(self) -> Vec<Row<E>> {
107        self.0
108    }
109
110    // ------------------------------------------------------------------
111    // Entities
112    // ------------------------------------------------------------------
113
114    pub fn try_entity(self) -> Result<Option<E>, ResponseError> {
115        Ok(self.try_row()?.map(|(_, e)| e))
116    }
117
118    pub fn entity(self) -> Result<E, ResponseError> {
119        self.row().map(|(_, e)| e)
120    }
121
122    #[must_use]
123    pub fn entities(self) -> Vec<E> {
124        self.0.into_iter().map(|(_, e)| e).collect()
125    }
126
127    // ------------------------------------------------------------------
128    // Ids (identity-level)
129    // ------------------------------------------------------------------
130
131    /// Return the first row identifier, if present.
132    ///
133    /// This identifier is a public value for correlation, reporting, and lookup only.
134    #[must_use]
135    pub fn id(&self) -> Option<Id<E>> {
136        self.0.first().map(|(id, _)| *id)
137    }
138
139    /// Require exactly one row and return its identifier.
140    pub fn require_id(self) -> Result<Id<E>, ResponseError> {
141        self.row().map(|(id, _)| id)
142    }
143
144    /// Return all row identifiers in response order for correlation/reporting/lookup.
145    #[must_use]
146    pub fn ids(&self) -> Vec<Id<E>> {
147        self.0.iter().map(|(id, _)| *id).collect()
148    }
149
150    /// Check whether the response contains the provided identifier.
151    pub fn contains_id(&self, id: &Id<E>) -> bool {
152        self.0.iter().any(|(k, _)| k == id)
153    }
154
155    // ------------------------------------------------------------------
156    // Views
157    // ------------------------------------------------------------------
158
159    pub fn view(&self) -> Result<<E as AsView>::ViewType, ResponseError> {
160        self.require_one()?;
161        Ok(self.0[0].1.as_view())
162    }
163
164    pub fn view_opt(&self) -> Result<Option<<E as AsView>::ViewType>, ResponseError> {
165        match self.count() {
166            0 => Ok(None),
167            1 => Ok(Some(self.0[0].1.as_view())),
168            n => Err(ResponseError::not_unique::<E>(n)),
169        }
170    }
171
172    #[must_use]
173    pub fn views(&self) -> Vec<<E as AsView>::ViewType> {
174        self.0.iter().map(|(_, e)| e.as_view()).collect()
175    }
176}
177
178impl<E: EntityKind> IntoIterator for Response<E> {
179    type Item = Row<E>;
180    type IntoIter = std::vec::IntoIter<Self::Item>;
181
182    fn into_iter(self) -> Self::IntoIter {
183        self.0.into_iter()
184    }
185}