use std::collections::HashMap;
use std::hash::BuildHasher;
use panproto_gat::Theory;
use panproto_schema::{EdgeRule, Protocol, Schema, SchemaBuilder};
use crate::emit::{IndentWriter, children_by_edge, find_roots};
use crate::error::ProtocolError;
use crate::theories;
#[must_use]
pub fn protocol() -> Protocol {
Protocol {
name: "redis".into(),
schema_theory: "ThRedisSchema".into(),
instance_theory: "ThRedisInstance".into(),
edge_rules: edge_rules(),
obj_kinds: vec![
"index".into(),
"field".into(),
"text".into(),
"tag".into(),
"numeric".into(),
"geo".into(),
"vector".into(),
],
constraint_sorts: vec![],
has_order: true,
nominal_identity: true,
..Protocol::default()
}
}
pub fn register_theories<S: BuildHasher>(registry: &mut HashMap<String, Theory, S>) {
theories::register_simple_graph_flat(registry, "ThRedisSchema", "ThRedisInstance");
}
pub fn parse_redis_schema(input: &str) -> Result<Schema, ProtocolError> {
let proto = protocol();
let mut builder = SchemaBuilder::new(&proto);
for line in input.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
if trimmed.starts_with("FT.CREATE") {
builder = parse_ft_create(builder, trimmed)?;
}
}
let schema = builder.build()?;
Ok(schema)
}
fn parse_ft_create(mut builder: SchemaBuilder, line: &str) -> Result<SchemaBuilder, ProtocolError> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 2 {
return Err(ProtocolError::Parse("invalid FT.CREATE".into()));
}
let index_name = parts[1];
builder = builder.vertex(index_name, "index", None)?;
let schema_idx = parts.iter().position(|p| p.eq_ignore_ascii_case("SCHEMA"));
if let Some(idx) = schema_idx {
let mut i = idx + 1;
while i < parts.len() {
let field_name = parts[i];
let field_type = parts.get(i + 1).copied().unwrap_or("TEXT");
let field_id = format!("{index_name}.{field_name}");
let kind = redis_type_to_kind(field_type);
builder = builder.vertex(&field_id, kind, None)?;
builder = builder.edge(index_name, &field_id, "prop", Some(field_name))?;
i += 2;
}
}
Ok(builder)
}
fn redis_type_to_kind(type_str: &str) -> &'static str {
match type_str.to_uppercase().as_str() {
"TEXT" => "text",
"TAG" => "tag",
"NUMERIC" => "numeric",
"GEO" => "geo",
"VECTOR" => "vector",
_ => "field",
}
}
fn kind_to_redis_type(kind: &str) -> &'static str {
match kind {
"text" => "TEXT",
"tag" => "TAG",
"numeric" => "NUMERIC",
"geo" => "GEO",
"vector" => "VECTOR",
_ => "TEXT",
}
}
pub fn emit_redis_schema(schema: &Schema) -> Result<String, ProtocolError> {
let structural = &["prop"];
let roots = find_roots(schema, structural);
let mut w = IndentWriter::new(" ");
for root in &roots {
if root.kind != "index" {
continue;
}
let fields = children_by_edge(schema, &root.id, "prop");
let field_strs: Vec<String> = fields
.iter()
.map(|(edge, child)| {
let name = edge.name.as_deref().unwrap_or(&child.id);
let type_str = kind_to_redis_type(&child.kind);
format!("{name} {type_str}")
})
.collect();
w.line(&format!(
"FT.CREATE {} ON HASH SCHEMA {}",
root.id,
field_strs.join(" ")
));
}
Ok(w.finish())
}
fn edge_rules() -> Vec<EdgeRule> {
vec![EdgeRule {
edge_kind: "prop".into(),
src_kinds: vec!["index".into()],
tgt_kinds: vec![
"text".into(),
"tag".into(),
"numeric".into(),
"geo".into(),
"vector".into(),
"field".into(),
],
}]
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn protocol_def() {
let p = protocol();
assert_eq!(p.name, "redis");
}
#[test]
fn register_theories_works() {
let mut registry = HashMap::new();
register_theories(&mut registry);
assert!(registry.contains_key("ThRedisSchema"));
assert!(registry.contains_key("ThRedisInstance"));
}
#[test]
fn parse_and_emit() {
let input = "FT.CREATE myidx ON HASH SCHEMA title TEXT name TAG age NUMERIC\n";
let schema = parse_redis_schema(input).expect("should parse");
assert!(schema.has_vertex("myidx"));
assert!(schema.has_vertex("myidx.title"));
assert!(schema.has_vertex("myidx.name"));
assert!(schema.has_vertex("myidx.age"));
let emitted = emit_redis_schema(&schema).expect("should emit");
assert!(emitted.contains("FT.CREATE myidx"));
assert!(emitted.contains("title TEXT"));
}
#[test]
fn roundtrip() {
let input = "FT.CREATE idx ON HASH SCHEMA f1 TEXT f2 NUMERIC\n";
let s1 = parse_redis_schema(input).expect("parse");
let emitted = emit_redis_schema(&s1).expect("emit");
let s2 = parse_redis_schema(&emitted).expect("re-parse");
assert_eq!(s1.vertex_count(), s2.vertex_count());
}
}