#![allow(missing_docs)]
use brain_db_sdk::wire::types::{
StatementGetRequest, StatementKindWire, StatementListRequest, StatementObjectWire,
StatementValueWire, StatementView,
};
use serde::{Deserialize, Serialize};
use super::{parse_uuid, uuid_string};
const DEFAULT_LIST_LIMIT: u32 = 100;
const MAX_LIST_LIMIT: u32 = 1000;
const fn default_follow_supersession() -> bool {
true
}
fn statement_kind_str(kind: StatementKindWire) -> String {
match kind {
StatementKindWire::Fact => "fact".to_owned(),
StatementKindWire::Preference => "preference".to_owned(),
StatementKindWire::Event => "event".to_owned(),
StatementKindWire::Attribute => "attribute".to_owned(),
StatementKindWire::Relation => "relation".to_owned(),
StatementKindWire::Directive => "directive".to_owned(),
StatementKindWire::Custom(_) => "custom".to_owned(),
}
}
fn kind_list_filter_byte(raw: &str) -> Result<u8, String> {
Ok(match raw.trim().to_ascii_lowercase().as_str() {
"fact" => 1,
"preference" => 2,
"event" => 3,
"attribute" => 4,
"relation" => 5,
"directive" => 6,
other => {
return Err(format!(
"kind must be one of: fact, preference, event, attribute, relation, directive (got `{other}`)"
));
}
})
}
#[derive(Debug, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum StatementValueDto {
Text { value: String },
Integer { value: i64 },
Float { value: f64 },
Bool { value: bool },
UnixNanos { value: u64 },
Blob { value: Vec<u8> },
}
impl From<StatementValueWire> for StatementValueDto {
fn from(v: StatementValueWire) -> Self {
match v {
StatementValueWire::Text(s) => Self::Text { value: s },
StatementValueWire::Integer(i) => Self::Integer { value: i },
StatementValueWire::Float(f) => Self::Float { value: f },
StatementValueWire::Bool(b) => Self::Bool { value: b },
StatementValueWire::UnixNanos(n) => Self::UnixNanos { value: n },
StatementValueWire::Blob(b) => Self::Blob { value: b },
}
}
}
#[derive(Debug, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum StatementObjectDto {
Entity { id: String },
Value { value: StatementValueDto },
Memory { id: String },
Statement { id: String },
}
impl From<StatementObjectWire> for StatementObjectDto {
fn from(o: StatementObjectWire) -> Self {
match o {
StatementObjectWire::EntityRef(id) => Self::Entity {
id: uuid_string(&id),
},
StatementObjectWire::Value(v) => Self::Value {
value: StatementValueDto::from(v),
},
StatementObjectWire::MemoryRef(id) => Self::Memory {
id: uuid_string(&id),
},
StatementObjectWire::StatementRef(id) => Self::Statement {
id: uuid_string(&id),
},
}
}
}
#[derive(Debug, Serialize)]
pub struct StatementDetailDto {
pub statement_id: String,
pub kind: String,
pub subject: String,
pub predicate: String,
pub object: StatementObjectDto,
pub confidence: f32,
pub event_at_unix_nanos: u64,
pub valid_from_unix_nanos: u64,
pub valid_to_unix_nanos: u64,
pub tombstoned: bool,
}
impl From<StatementView> for StatementDetailDto {
fn from(v: StatementView) -> Self {
Self {
statement_id: uuid_string(&v.statement_id),
kind: statement_kind_str(v.kind),
subject: uuid_string(&v.subject),
predicate: v.predicate,
object: StatementObjectDto::from(v.object),
confidence: v.confidence,
event_at_unix_nanos: v.event_at_unix_nanos,
valid_from_unix_nanos: v.valid_from_unix_nanos,
valid_to_unix_nanos: v.valid_to_unix_nanos,
tombstoned: v.tombstoned,
}
}
}
#[derive(Debug, Deserialize)]
pub struct GetStatementQuery {
#[serde(default = "default_follow_supersession")]
pub follow_supersession: bool,
}
pub fn get_request_from(
id: &str,
query: &GetStatementQuery,
) -> Result<StatementGetRequest, String> {
Ok(StatementGetRequest {
statement_id: parse_uuid(id)?,
follow_supersession: query.follow_supersession,
act_as: None,
})
}
#[derive(Debug, Deserialize)]
pub struct ListStatementsQuery {
#[serde(default)]
pub subject: Option<String>,
#[serde(default)]
pub predicate: String,
#[serde(default)]
pub kind: Option<String>,
#[serde(default)]
pub min_confidence: f32,
#[serde(default = "default_follow_supersession")]
pub only_current: bool,
#[serde(default)]
pub include_tombstoned: bool,
pub limit: Option<u32>,
}
impl ListStatementsQuery {
pub fn to_request(&self) -> Result<StatementListRequest, String> {
let subject = match &self.subject {
Some(s) if !s.trim().is_empty() => parse_uuid(s)?,
_ => [0u8; 16],
};
let kind = match &self.kind {
Some(k) if !k.trim().is_empty() => kind_list_filter_byte(k)?,
_ => 0,
};
let limit = self
.limit
.unwrap_or(DEFAULT_LIST_LIMIT)
.clamp(1, MAX_LIST_LIMIT);
Ok(StatementListRequest {
subject,
predicate: self.predicate.clone(),
kind,
min_confidence: self.min_confidence,
time_range_start_unix_nanos: 0,
time_range_end_unix_nanos: 0,
only_current: self.only_current,
include_tombstoned: self.include_tombstoned,
limit,
cursor: Vec::new(),
act_as: None,
})
}
}
#[derive(Debug, Serialize)]
pub struct ListStatementsResponseDto {
pub statements: Vec<StatementDetailDto>,
pub count: usize,
}
impl ListStatementsResponseDto {
pub fn from_views(views: Vec<StatementView>) -> Self {
let statements: Vec<StatementDetailDto> =
views.into_iter().map(StatementDetailDto::from).collect();
let count = statements.len();
Self { statements, count }
}
}
#[cfg(test)]
mod statement_tests {
use super::*;
#[test]
fn list_query_maps_filters_and_sentinels() {
let none = ListStatementsQuery {
subject: None,
predicate: String::new(),
kind: None,
min_confidence: 0.0,
only_current: true,
include_tombstoned: false,
limit: None,
};
let req = none.to_request().unwrap();
assert_eq!(req.subject, [0u8; 16]);
assert_eq!(req.kind, 0);
assert_eq!(req.limit, DEFAULT_LIST_LIMIT);
assert!(req.only_current);
let with = ListStatementsQuery {
subject: Some("00000000-0000-0000-0000-000000000007".into()),
predicate: "brain:likes".into(),
kind: Some("preference".into()),
min_confidence: 0.5,
only_current: false,
include_tombstoned: true,
limit: Some(9999),
};
let req = with.to_request().unwrap();
assert_eq!(req.subject[15], 7);
assert_eq!(req.kind, 2); assert_eq!(req.limit, MAX_LIST_LIMIT);
assert!(!req.only_current);
assert!(req.include_tombstoned);
}
#[test]
fn list_query_rejects_bad_kind_and_subject() {
assert!(
ListStatementsQuery {
subject: None,
predicate: String::new(),
kind: Some("nonsense".into()),
min_confidence: 0.0,
only_current: true,
include_tombstoned: false,
limit: None,
}
.to_request()
.is_err()
);
assert!(
ListStatementsQuery {
subject: Some("nope".into()),
predicate: String::new(),
kind: None,
min_confidence: 0.0,
only_current: true,
include_tombstoned: false,
limit: None,
}
.to_request()
.is_err()
);
}
#[test]
fn get_request_defaults_follow_supersession() {
let q = GetStatementQuery {
follow_supersession: true,
};
let req = get_request_from("00000000-0000-0000-0000-000000000001", &q).unwrap();
assert!(req.follow_supersession);
assert_eq!(req.statement_id[15], 1);
assert!(get_request_from("bad", &q).is_err());
}
#[test]
fn object_value_serializes_tagged() {
let entity = StatementObjectDto::from(StatementObjectWire::EntityRef([0x01u8; 16]));
let json = serde_json::to_value(&entity).unwrap();
assert_eq!(json["kind"], "entity");
assert!(json["id"].as_str().unwrap().contains('-'));
let value =
StatementObjectDto::from(StatementObjectWire::Value(StatementValueWire::Integer(42)));
let json = serde_json::to_value(&value).unwrap();
assert_eq!(json["kind"], "value");
assert_eq!(json["value"]["type"], "integer");
assert_eq!(json["value"]["value"], 42);
}
}