use serde::{Deserialize, Serialize};
use crate::{
Engine, EngineError, FtsPropertyPathMode, FtsPropertyPathSpec, FtsPropertySchemaRecord,
};
use fathomdb_engine::{FtsProfile, ProjectionImpact, VecProfile};
#[derive(Clone, Copy, Debug, Deserialize, Serialize, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PyPropertyPathMode {
Scalar,
Recursive,
}
impl From<PyPropertyPathMode> for FtsPropertyPathMode {
fn from(value: PyPropertyPathMode) -> Self {
match value {
PyPropertyPathMode::Scalar => Self::Scalar,
PyPropertyPathMode::Recursive => Self::Recursive,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct PyPropertyPathSpec {
pub path: String,
pub mode: PyPropertyPathMode,
#[serde(default)]
pub weight: Option<f32>,
}
impl From<PyPropertyPathSpec> for FtsPropertyPathSpec {
fn from(value: PyPropertyPathSpec) -> Self {
let base = match value.mode {
PyPropertyPathMode::Recursive => FtsPropertyPathSpec::recursive(value.path),
PyPropertyPathMode::Scalar => FtsPropertyPathSpec::scalar(value.path),
};
match value.weight {
Some(w) => base.with_weight(w),
None => base,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct PyRegisterFtsPropertySchemaRequest {
pub kind: String,
pub entries: Vec<PyPropertyPathSpec>,
#[serde(default = "default_separator")]
pub separator: String,
#[serde(default)]
pub exclude_paths: Vec<String>,
}
fn default_separator() -> String {
" ".to_owned()
}
#[derive(Debug)]
pub enum AdminFfiError {
Parse(serde_json::Error),
Engine(EngineError),
Serialize(serde_json::Error),
}
impl std::fmt::Display for AdminFfiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Parse(e) => write!(f, "admin request JSON parse error: {e}"),
Self::Engine(e) => write!(f, "admin operation error: {e}"),
Self::Serialize(e) => write!(f, "admin response serialize error: {e}"),
}
}
}
impl std::error::Error for AdminFfiError {}
pub fn register_fts_property_schema_with_entries_json(
engine: &Engine,
request_json: &str,
) -> Result<String, AdminFfiError> {
let request: PyRegisterFtsPropertySchemaRequest =
serde_json::from_str(request_json).map_err(AdminFfiError::Parse)?;
let entries: Vec<FtsPropertyPathSpec> = request.entries.into_iter().map(Into::into).collect();
let record: FtsPropertySchemaRecord = engine
.register_fts_property_schema_with_entries(
&request.kind,
&entries,
Some(request.separator.as_str()),
&request.exclude_paths,
)
.map_err(AdminFfiError::Engine)?;
serde_json::to_string(&record).map_err(AdminFfiError::Serialize)
}
#[derive(Debug, Deserialize)]
struct SetFtsProfileRequest {
kind: String,
tokenizer: String,
}
pub fn set_fts_profile_json(engine: &Engine, request_json: &str) -> Result<String, AdminFfiError> {
let request: SetFtsProfileRequest =
serde_json::from_str(request_json).map_err(AdminFfiError::Parse)?;
let profile: FtsProfile = engine
.admin()
.service()
.set_fts_profile(&request.kind, &request.tokenizer)
.map_err(AdminFfiError::Engine)?;
serde_json::to_string(&profile).map_err(AdminFfiError::Serialize)
}
pub fn get_fts_profile_json(engine: &Engine, kind: &str) -> Result<String, AdminFfiError> {
let profile: Option<FtsProfile> = engine
.admin()
.service()
.get_fts_profile(kind)
.map_err(AdminFfiError::Engine)?;
serde_json::to_string(&profile).map_err(AdminFfiError::Serialize)
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct SetVecProfileRequest {
model_identity: String,
#[serde(default)]
model_version: Option<String>,
dimensions: u32,
#[serde(default)]
normalization_policy: Option<String>,
}
pub fn set_vec_profile_json(engine: &Engine, request_json: &str) -> Result<String, AdminFfiError> {
let _validated: SetVecProfileRequest =
serde_json::from_str(request_json).map_err(AdminFfiError::Parse)?;
let profile: VecProfile = engine
.admin()
.service()
.set_vec_profile(request_json)
.map_err(AdminFfiError::Engine)?;
serde_json::to_string(&profile).map_err(AdminFfiError::Serialize)
}
pub fn get_vec_profile_json(engine: &Engine, kind: &str) -> Result<String, AdminFfiError> {
let profile: Option<VecProfile> = engine
.admin()
.service()
.get_vec_profile(kind)
.map_err(AdminFfiError::Engine)?;
serde_json::to_string(&profile).map_err(AdminFfiError::Serialize)
}
pub fn preview_projection_impact_json(
engine: &Engine,
kind: &str,
facet: &str,
) -> Result<String, AdminFfiError> {
let impact: ProjectionImpact = engine
.admin()
.service()
.preview_projection_impact(kind, facet)
.map_err(AdminFfiError::Engine)?;
serde_json::to_string(&impact).map_err(AdminFfiError::Serialize)
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::{PyPropertyPathMode, PyPropertyPathSpec, PyRegisterFtsPropertySchemaRequest};
use crate::FtsPropertyPathSpec;
#[test]
fn property_path_mode_snake_case_wire_form() {
let json = serde_json::to_string(&PyPropertyPathMode::Scalar).expect("serialize");
assert_eq!(json, "\"scalar\"");
let json = serde_json::to_string(&PyPropertyPathMode::Recursive).expect("serialize");
assert_eq!(json, "\"recursive\"");
}
#[test]
fn property_path_spec_roundtrip() {
let spec = PyPropertyPathSpec {
path: "$.payload".to_owned(),
mode: PyPropertyPathMode::Recursive,
weight: None,
};
let json = serde_json::to_string(&spec).expect("serialize");
let parsed: PyPropertyPathSpec = serde_json::from_str(&json).expect("deserialize");
assert_eq!(spec, parsed);
}
#[test]
fn register_request_defaults_separator_and_exclude_paths() {
let request: PyRegisterFtsPropertySchemaRequest =
serde_json::from_str(r#"{"kind":"K","entries":[{"path":"$.title","mode":"scalar"}]}"#)
.expect("parse");
assert_eq!(request.kind, "K");
assert_eq!(request.separator, " ");
assert!(request.exclude_paths.is_empty());
assert_eq!(request.entries.len(), 1);
}
#[test]
fn weight_round_trips_through_py_property_path_spec() {
let json = r#"{"path": "$.title", "mode": "scalar", "weight": 10.0}"#;
let spec: PyPropertyPathSpec = serde_json::from_str(json).expect("deserialize");
assert_eq!(spec.weight, Some(10.0_f32));
let fts_spec: FtsPropertyPathSpec = spec.into();
let _ = fts_spec; }
#[test]
fn weight_absent_defaults_to_none() {
let json = r#"{"path": "$.body", "mode": "scalar"}"#;
let spec: PyPropertyPathSpec = serde_json::from_str(json).expect("deserialize");
assert_eq!(spec.weight, None);
}
#[test]
fn register_request_roundtrip_recursive_entry() {
let request = PyRegisterFtsPropertySchemaRequest {
kind: "KnowledgeItem".to_owned(),
entries: vec![
PyPropertyPathSpec {
path: "$.title".to_owned(),
mode: PyPropertyPathMode::Scalar,
weight: None,
},
PyPropertyPathSpec {
path: "$.payload".to_owned(),
mode: PyPropertyPathMode::Recursive,
weight: None,
},
],
separator: " ".to_owned(),
exclude_paths: vec!["$.payload.ignored".to_owned()],
};
let json = serde_json::to_string(&request).expect("serialize");
let parsed: PyRegisterFtsPropertySchemaRequest =
serde_json::from_str(&json).expect("deserialize");
assert_eq!(request, parsed);
}
}