1#![allow(missing_docs)]
16
17use brain_db_sdk::wire::types::{
18 StatementGetRequest, StatementKindWire, StatementListRequest, StatementObjectWire,
19 StatementValueWire, StatementView,
20};
21use serde::{Deserialize, Serialize};
22
23use super::{parse_uuid, uuid_string};
24
25const DEFAULT_LIST_LIMIT: u32 = 100;
27const MAX_LIST_LIMIT: u32 = 1000;
29
30const fn default_follow_supersession() -> bool {
33 true
34}
35
36fn statement_kind_str(kind: StatementKindWire) -> String {
38 match kind {
39 StatementKindWire::Fact => "fact".to_owned(),
40 StatementKindWire::Preference => "preference".to_owned(),
41 StatementKindWire::Event => "event".to_owned(),
42 StatementKindWire::Attribute => "attribute".to_owned(),
43 StatementKindWire::Relation => "relation".to_owned(),
44 StatementKindWire::Directive => "directive".to_owned(),
45 StatementKindWire::Custom(_) => "custom".to_owned(),
46 }
47}
48
49fn kind_list_filter_byte(raw: &str) -> Result<u8, String> {
52 Ok(match raw.trim().to_ascii_lowercase().as_str() {
53 "fact" => 1,
54 "preference" => 2,
55 "event" => 3,
56 "attribute" => 4,
57 "relation" => 5,
58 "directive" => 6,
59 other => {
60 return Err(format!(
61 "kind must be one of: fact, preference, event, attribute, relation, directive (got `{other}`)"
62 ));
63 }
64 })
65}
66
67#[derive(Debug, Serialize)]
69#[serde(tag = "type", rename_all = "snake_case")]
70pub enum StatementValueDto {
71 Text { value: String },
73 Integer { value: i64 },
75 Float { value: f64 },
77 Bool { value: bool },
79 UnixNanos { value: u64 },
81 Blob { value: Vec<u8> },
83}
84
85impl From<StatementValueWire> for StatementValueDto {
86 fn from(v: StatementValueWire) -> Self {
87 match v {
88 StatementValueWire::Text(s) => Self::Text { value: s },
89 StatementValueWire::Integer(i) => Self::Integer { value: i },
90 StatementValueWire::Float(f) => Self::Float { value: f },
91 StatementValueWire::Bool(b) => Self::Bool { value: b },
92 StatementValueWire::UnixNanos(n) => Self::UnixNanos { value: n },
93 StatementValueWire::Blob(b) => Self::Blob { value: b },
94 }
95 }
96}
97
98#[derive(Debug, Serialize)]
100#[serde(tag = "kind", rename_all = "snake_case")]
101pub enum StatementObjectDto {
102 Entity { id: String },
104 Value { value: StatementValueDto },
106 Memory { id: String },
108 Statement { id: String },
110}
111
112impl From<StatementObjectWire> for StatementObjectDto {
113 fn from(o: StatementObjectWire) -> Self {
114 match o {
115 StatementObjectWire::EntityRef(id) => Self::Entity {
116 id: uuid_string(&id),
117 },
118 StatementObjectWire::Value(v) => Self::Value {
119 value: StatementValueDto::from(v),
120 },
121 StatementObjectWire::MemoryRef(id) => Self::Memory {
122 id: uuid_string(&id),
123 },
124 StatementObjectWire::StatementRef(id) => Self::Statement {
125 id: uuid_string(&id),
126 },
127 }
128 }
129}
130
131#[derive(Debug, Serialize)]
133pub struct StatementDetailDto {
134 pub statement_id: String,
136 pub kind: String,
138 pub subject: String,
140 pub predicate: String,
142 pub object: StatementObjectDto,
144 pub confidence: f32,
146 pub event_at_unix_nanos: u64,
148 pub valid_from_unix_nanos: u64,
150 pub valid_to_unix_nanos: u64,
152 pub tombstoned: bool,
154}
155
156impl From<StatementView> for StatementDetailDto {
157 fn from(v: StatementView) -> Self {
158 Self {
159 statement_id: uuid_string(&v.statement_id),
160 kind: statement_kind_str(v.kind),
161 subject: uuid_string(&v.subject),
162 predicate: v.predicate,
163 object: StatementObjectDto::from(v.object),
164 confidence: v.confidence,
165 event_at_unix_nanos: v.event_at_unix_nanos,
166 valid_from_unix_nanos: v.valid_from_unix_nanos,
167 valid_to_unix_nanos: v.valid_to_unix_nanos,
168 tombstoned: v.tombstoned,
169 }
170 }
171}
172
173#[derive(Debug, Deserialize)]
175pub struct GetStatementQuery {
176 #[serde(default = "default_follow_supersession")]
178 pub follow_supersession: bool,
179}
180
181pub fn get_request_from(
183 id: &str,
184 query: &GetStatementQuery,
185) -> Result<StatementGetRequest, String> {
186 Ok(StatementGetRequest {
187 statement_id: parse_uuid(id)?,
188 follow_supersession: query.follow_supersession,
189 act_as: None,
190 })
191}
192
193#[derive(Debug, Deserialize)]
195pub struct ListStatementsQuery {
196 #[serde(default)]
198 pub subject: Option<String>,
199 #[serde(default)]
201 pub predicate: String,
202 #[serde(default)]
204 pub kind: Option<String>,
205 #[serde(default)]
207 pub min_confidence: f32,
208 #[serde(default = "default_follow_supersession")]
210 pub only_current: bool,
211 #[serde(default)]
213 pub include_tombstoned: bool,
214 pub limit: Option<u32>,
216}
217
218impl ListStatementsQuery {
219 pub fn to_request(&self) -> Result<StatementListRequest, String> {
222 let subject = match &self.subject {
223 Some(s) if !s.trim().is_empty() => parse_uuid(s)?,
224 _ => [0u8; 16],
225 };
226 let kind = match &self.kind {
227 Some(k) if !k.trim().is_empty() => kind_list_filter_byte(k)?,
228 _ => 0,
229 };
230 let limit = self
231 .limit
232 .unwrap_or(DEFAULT_LIST_LIMIT)
233 .clamp(1, MAX_LIST_LIMIT);
234 Ok(StatementListRequest {
235 subject,
236 predicate: self.predicate.clone(),
237 kind,
238 min_confidence: self.min_confidence,
239 time_range_start_unix_nanos: 0,
240 time_range_end_unix_nanos: 0,
241 only_current: self.only_current,
242 include_tombstoned: self.include_tombstoned,
243 limit,
244 cursor: Vec::new(),
245 act_as: None,
246 })
247 }
248}
249
250#[derive(Debug, Serialize)]
252pub struct ListStatementsResponseDto {
253 pub statements: Vec<StatementDetailDto>,
255 pub count: usize,
257}
258
259impl ListStatementsResponseDto {
260 pub fn from_views(views: Vec<StatementView>) -> Self {
262 let statements: Vec<StatementDetailDto> =
263 views.into_iter().map(StatementDetailDto::from).collect();
264 let count = statements.len();
265 Self { statements, count }
266 }
267}
268
269#[cfg(test)]
270mod statement_tests {
271 use super::*;
272
273 #[test]
274 fn list_query_maps_filters_and_sentinels() {
275 let none = ListStatementsQuery {
276 subject: None,
277 predicate: String::new(),
278 kind: None,
279 min_confidence: 0.0,
280 only_current: true,
281 include_tombstoned: false,
282 limit: None,
283 };
284 let req = none.to_request().unwrap();
285 assert_eq!(req.subject, [0u8; 16]);
286 assert_eq!(req.kind, 0);
287 assert_eq!(req.limit, DEFAULT_LIST_LIMIT);
288 assert!(req.only_current);
289
290 let with = ListStatementsQuery {
291 subject: Some("00000000-0000-0000-0000-000000000007".into()),
292 predicate: "brain:likes".into(),
293 kind: Some("preference".into()),
294 min_confidence: 0.5,
295 only_current: false,
296 include_tombstoned: true,
297 limit: Some(9999),
298 };
299 let req = with.to_request().unwrap();
300 assert_eq!(req.subject[15], 7);
301 assert_eq!(req.kind, 2); assert_eq!(req.limit, MAX_LIST_LIMIT);
303 assert!(!req.only_current);
304 assert!(req.include_tombstoned);
305 }
306
307 #[test]
308 fn list_query_rejects_bad_kind_and_subject() {
309 assert!(
310 ListStatementsQuery {
311 subject: None,
312 predicate: String::new(),
313 kind: Some("nonsense".into()),
314 min_confidence: 0.0,
315 only_current: true,
316 include_tombstoned: false,
317 limit: None,
318 }
319 .to_request()
320 .is_err()
321 );
322 assert!(
323 ListStatementsQuery {
324 subject: Some("nope".into()),
325 predicate: String::new(),
326 kind: None,
327 min_confidence: 0.0,
328 only_current: true,
329 include_tombstoned: false,
330 limit: None,
331 }
332 .to_request()
333 .is_err()
334 );
335 }
336
337 #[test]
338 fn get_request_defaults_follow_supersession() {
339 let q = GetStatementQuery {
340 follow_supersession: true,
341 };
342 let req = get_request_from("00000000-0000-0000-0000-000000000001", &q).unwrap();
343 assert!(req.follow_supersession);
344 assert_eq!(req.statement_id[15], 1);
345 assert!(get_request_from("bad", &q).is_err());
346 }
347
348 #[test]
349 fn object_value_serializes_tagged() {
350 let entity = StatementObjectDto::from(StatementObjectWire::EntityRef([0x01u8; 16]));
351 let json = serde_json::to_value(&entity).unwrap();
352 assert_eq!(json["kind"], "entity");
353 assert!(json["id"].as_str().unwrap().contains('-'));
354
355 let value =
356 StatementObjectDto::from(StatementObjectWire::Value(StatementValueWire::Integer(42)));
357 let json = serde_json::to_value(&value).unwrap();
358 assert_eq!(json["kind"], "value");
359 assert_eq!(json["value"]["type"], "integer");
360 assert_eq!(json["value"]["value"], 42);
361 }
362}