use super::fields::{
SchemaGeoField, SchemaGeoShapeField, SchemaNumericField, SchemaTagField, SchemaTextField,
};
use crate::{RedisWrite, ToRedisArgs};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum FieldDefinition {
Text(SchemaTextField),
Numeric(SchemaNumericField),
Geo(SchemaGeoField),
Tag(SchemaTagField),
GeoShape(SchemaGeoShapeField),
}
impl ToRedisArgs for FieldDefinition {
fn write_redis_args<W>(&self, out: &mut W)
where
W: ?Sized + RedisWrite,
{
match self {
Self::Text(tf) => tf.write_redis_args(out),
Self::Numeric(nf) => nf.write_redis_args(out),
Self::Geo(gf) => gf.write_redis_args(out),
Self::Tag(tf) => tf.write_redis_args(out),
Self::GeoShape(gs) => gs.write_redis_args(out),
}
}
}
impl From<SchemaTextField> for FieldDefinition {
fn from(field: SchemaTextField) -> Self {
Self::Text(field)
}
}
impl From<SchemaNumericField> for FieldDefinition {
fn from(field: SchemaNumericField) -> Self {
Self::Numeric(field)
}
}
impl From<SchemaGeoField> for FieldDefinition {
fn from(field: SchemaGeoField) -> Self {
Self::Geo(field)
}
}
impl From<SchemaTagField> for FieldDefinition {
fn from(field: SchemaTagField) -> Self {
Self::Tag(field)
}
}
impl From<SchemaGeoShapeField> for FieldDefinition {
fn from(field: SchemaGeoShapeField) -> Self {
Self::GeoShape(field)
}
}
#[must_use = "Schema has no effect unless passed to a command"]
#[derive(Debug, Clone)]
pub struct SearchSchema {
fields: Vec<(String, FieldDefinition)>,
}
impl SearchSchema {
pub fn new<K: Into<String>, V: Into<FieldDefinition>>(key: K, value: V) -> Self {
Self {
fields: vec![(key.into(), value.into())],
}
}
pub fn insert<K: Into<String>, V: Into<FieldDefinition>>(mut self, key: K, value: V) -> Self {
self.fields.push((key.into(), value.into()));
self
}
}
impl ToRedisArgs for SearchSchema {
fn write_redis_args<W>(&self, out: &mut W)
where
W: ?Sized + RedisWrite,
{
for (key, field) in &self.fields {
key.write_redis_args(out);
field.write_redis_args(out);
}
}
}
#[macro_export]
macro_rules! schema {
($first_key:expr => $first_value:expr $(, $key:expr => $value:expr)* $(,)?) => {{
$crate::search::SearchSchema::new($first_key, $first_value)
$(
.insert($key, $value)
)*
}};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::search::FtCreateCommand;
static INDEX_NAME: &str = "index";
static TEXT_FIELD_NAME: &str = "title";
static NUMERIC_FIELD_NAME: &str = "price";
static TAG_FIELD_NAME: &str = "condition";
#[test]
fn test_multiple_fields() {
let schema = SearchSchema::new(TEXT_FIELD_NAME, SchemaTextField::new().weight(2.0))
.insert(NUMERIC_FIELD_NAME, SchemaNumericField::new())
.insert(TAG_FIELD_NAME, SchemaTagField::new().separator(','));
let ft_create = FtCreateCommand::new(INDEX_NAME, schema);
assert_eq!(
ft_create.into_args(),
"FT.CREATE index SCHEMA title TEXT WEIGHT 2.0 price NUMERIC condition TAG SEPARATOR ,"
);
}
#[test]
fn test_macro_and_builder_produce_the_same_schema() {
let from_macro = FtCreateCommand::new(
INDEX_NAME,
schema! {
TEXT_FIELD_NAME => SchemaTextField::new().weight(2.0),
"subtitle" => SchemaTextField::new(),
},
);
let from_builder = FtCreateCommand::new(
INDEX_NAME,
SearchSchema::new(TEXT_FIELD_NAME, SchemaTextField::new().weight(2.0))
.insert("subtitle", SchemaTextField::new()),
);
assert_eq!(from_macro.into_args(), from_builder.into_args());
}
#[test]
fn test_macro_accepts_a_single_field_without_a_trailing_comma() {
let ft_create = FtCreateCommand::new(
INDEX_NAME,
schema! {
TEXT_FIELD_NAME => SchemaTextField::new()
},
);
assert_eq!(ft_create.into_args(), "FT.CREATE index SCHEMA title TEXT");
}
#[test]
fn test_macro_accepts_a_single_field_with_a_trailing_comma() {
let ft_create = FtCreateCommand::new(
INDEX_NAME,
schema! {
TEXT_FIELD_NAME => SchemaTextField::new(),
},
);
assert_eq!(ft_create.into_args(), "FT.CREATE index SCHEMA title TEXT");
}
#[test]
fn test_the_same_field_name_can_be_inserted_twice() {
let ft_create = FtCreateCommand::new(
INDEX_NAME,
schema! {
"sku" => SchemaTextField::new().alias("sku_text"),
"sku" => SchemaTextField::new().alias("sku_other"),
},
);
assert_eq!(
ft_create.into_args(),
"FT.CREATE index SCHEMA sku AS sku_text TEXT sku AS sku_other TEXT"
);
}
}