1mod accessors;
4#[cfg(test)]
5#[allow(clippy::unwrap_used, clippy::expect_used)]
6mod envelope_tests;
7
8use serde::{Deserialize, Serialize};
9
10use crate::constants::DEFAULT_LIMIT;
11use crate::error::{ErrorCode, TalonError, TalonResult};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
17#[serde(try_from = "u16", into = "u16")]
18pub struct PositiveCount(u16);
19
20impl Default for PositiveCount {
21 fn default() -> Self {
22 Self(DEFAULT_LIMIT)
23 }
24}
25
26impl PositiveCount {
27 pub fn new(value: u16, field: &'static str) -> TalonResult<Self> {
33 if value == 0 {
34 return Err(TalonError::InvalidInput {
35 field,
36 message: "must be greater than zero".to_string(),
37 });
38 }
39 Ok(Self(value))
40 }
41
42 #[must_use]
44 pub const fn get(self) -> u16 {
45 self.0
46 }
47
48 pub(crate) const fn from_const(value: u16) -> Self {
50 Self(value)
51 }
52}
53
54impl TryFrom<u16> for PositiveCount {
55 type Error = TalonError;
56
57 fn try_from(value: u16) -> Result<Self, Self::Error> {
58 Self::new(value, "count")
59 }
60}
61
62impl From<PositiveCount> for u16 {
63 fn from(value: PositiveCount) -> Self {
64 value.0
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
72#[serde(transparent)]
73pub struct VaultPath(String);
74
75impl VaultPath {
76 pub fn parse(value: impl Into<String>) -> TalonResult<Self> {
82 let value = value.into();
83 if value.trim().is_empty() {
84 return Err(TalonError::InvalidInput {
85 field: "path",
86 message: "must not be empty".to_string(),
87 });
88 }
89 Ok(Self(value))
90 }
91
92 #[must_use]
94 pub fn as_str(&self) -> &str {
95 &self.0
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
101#[serde(transparent)]
102pub struct ContainerPath(String);
103
104impl ContainerPath {
105 pub fn parse(value: impl Into<String>) -> TalonResult<Self> {
111 let value = value.into();
112 if value.trim().is_empty() {
113 return Err(TalonError::InvalidInput {
114 field: "path",
115 message: "must not be empty".to_string(),
116 });
117 }
118 Ok(Self(value))
119 }
120
121 #[must_use]
123 pub fn root() -> Self {
124 Self("/".to_string())
125 }
126
127 #[must_use]
129 pub fn as_str(&self) -> &str {
130 &self.0
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(rename_all = "camelCase")]
139pub struct ResponseMeta {
140 pub duration_ms: u64,
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub result_count: Option<u32>,
145 #[serde(default)]
147 pub warnings: Vec<String>,
148 #[serde(skip_serializing_if = "Option::is_none")]
150 pub scope_set: Option<Vec<String>>,
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub since: Option<String>,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub struct ErrorEnvelope {
160 pub code: ErrorCode,
162 pub message: String,
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub detail: Option<serde_json::Value>,
167}
168
169use crate::indexing::{InspectResponse, StatusResponse, SyncResponse};
172use crate::query::{ChangesResponse, MetaResponse, ReadResponse, RecallResponse, RelatedResponse};
173use crate::search::SearchResponse;
174
175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
181#[serde(tag = "action", rename_all = "kebab-case")]
182pub enum TalonResponseData {
183 Search(SearchResponse),
185 Ask(crate::query::AskResponse),
187 Read(ReadResponse),
189 Sync(SyncResponse),
191 Status(StatusResponse),
193 Related(RelatedResponse),
195 Meta(MetaResponse),
197 Changes(ChangesResponse),
199 Inspect(InspectResponse),
201 Recall(RecallResponse),
203}
204
205#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
213pub struct TalonEnvelope {
214 pub action: String,
216 pub version: String,
218 pub ok: bool,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub data: Option<TalonResponseData>,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub meta: Option<ResponseMeta>,
226 #[serde(skip_serializing_if = "Option::is_none")]
228 pub error: Option<ErrorEnvelope>,
229}
230
231impl TalonEnvelope {
232 #[must_use]
234 pub fn ok(action: &'static str, data: TalonResponseData, meta: ResponseMeta) -> Self {
235 Self {
236 action: action.to_string(),
237 version: env!("CARGO_PKG_VERSION").to_string(),
238 ok: true,
239 data: Some(data),
240 meta: Some(meta),
241 error: None,
242 }
243 }
244
245 #[must_use]
247 pub fn err(action: &str, error: ErrorEnvelope) -> Self {
248 Self {
249 action: action.to_string(),
250 version: env!("CARGO_PKG_VERSION").to_string(),
251 ok: false,
252 data: None,
253 meta: None,
254 error: Some(error),
255 }
256 }
257
258 #[must_use]
260 pub const fn data(&self) -> Option<&TalonResponseData> {
261 self.data.as_ref()
262 }
263
264 #[must_use]
266 #[allow(clippy::missing_const_for_fn)]
267 pub fn data_mut(&mut self) -> Option<&mut TalonResponseData> {
268 self.data.as_mut()
269 }
270
271 #[must_use]
273 #[allow(clippy::missing_const_for_fn)]
274 pub fn into_data(self) -> Option<TalonResponseData> {
275 self.data
276 }
277
278 #[must_use]
280 #[allow(clippy::missing_const_for_fn)]
281 pub fn as_response(&self) -> Option<&dyn TalonResponseTrait> {
282 self.data.as_ref().map(|d| d as &dyn TalonResponseTrait)
283 }
284}
285
286pub trait TalonResponseTrait {
291 fn action(&self) -> &str;
293}
294
295use crate::indexing::{InspectInput, StatusInput, SyncInput};
298use crate::query::{ChangesInput, MetaInput, ReadInput, RecallInput, RelatedInput};
299use crate::search::SearchInput;
300
301#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
303#[serde(tag = "action", rename_all = "kebab-case")]
304pub enum TalonInput {
305 Search(SearchInput),
307 Read(ReadInput),
309 Sync(SyncInput),
311 Status(StatusInput),
313 Related(RelatedInput),
315 Meta(MetaInput),
317 Changes(ChangesInput),
319 Inspect(InspectInput),
321 Recall(RecallInput),
323}