1use std::collections::{BTreeMap, BTreeSet};
4use std::fmt::{self, Display, Formatter};
5
6use crate::v1::index_field::FieldType;
7use crate::v1::{
8 IndexField, IndexFieldCapability, IndexFieldCardinality, IndexOrderDirection, TextAnalyzer,
9 TypedJsonIndexSpec,
10};
11
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct TypedJsonValidationError(&'static str);
14
15impl Display for TypedJsonValidationError {
16 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
17 formatter.write_str(self.0)
18 }
19}
20
21impl std::error::Error for TypedJsonValidationError {}
22
23fn invalid(message: &'static str) -> TypedJsonValidationError {
24 TypedJsonValidationError(message)
25}
26
27pub fn validate_typed_json_specification(
31 specification: &TypedJsonIndexSpec,
32) -> Result<(), TypedJsonValidationError> {
33 if specification.fields.is_empty() {
34 return Err(invalid("typed JSON index needs at least one field"));
35 }
36 let mut fields = BTreeMap::new();
37 for field in &specification.fields {
38 validate_field(field)?;
39 if fields.insert(field.name.as_str(), field).is_some() {
40 return Err(invalid("typed JSON field names must be unique"));
41 }
42 }
43
44 let mut ordered = BTreeSet::new();
45 for order in &specification.physical_order {
46 require_text(&order.field, "physical-order field is required")?;
47 if !ordered.insert(order.field.as_str()) {
48 return Err(invalid("physical-order field names must be unique"));
49 }
50 let field = fields.get(order.field.as_str()).ok_or_else(|| {
51 invalid("physical order names a field outside the typed JSON definition")
52 })?;
53 if field.cardinality != IndexFieldCardinality::Single as i32 {
54 return Err(invalid(
55 "physical order requires single-valued typed JSON fields",
56 ));
57 }
58 if !field
59 .capabilities
60 .contains(&(IndexFieldCapability::Order as i32))
61 {
62 return Err(invalid(
63 "physical order requires the typed JSON field to declare ORDER",
64 ));
65 }
66 IndexOrderDirection::try_from(order.direction)
67 .map_err(|_| invalid("physical order direction is unknown"))?;
68 }
69 Ok(())
70}
71
72fn validate_field(field: &IndexField) -> Result<(), TypedJsonValidationError> {
73 require_text(&field.name, "index field name is required")?;
74 if !field.json_pointer.is_empty()
75 && (!field.json_pointer.starts_with('/') || field.json_pointer.contains('\0'))
76 {
77 return Err(invalid("JSON pointer must be empty or begin with '/'"));
78 }
79 let cardinality = IndexFieldCardinality::try_from(field.cardinality)
80 .map_err(|_| invalid("typed JSON field cardinality is unknown"))?;
81 let field_type = field
82 .field_type
83 .as_ref()
84 .ok_or_else(|| invalid("typed JSON field type is required"))?;
85 if let FieldType::Text(text) = field_type {
86 TextAnalyzer::try_from(text.analyzer)
87 .map_err(|_| invalid("typed JSON text analyzer is unknown"))?;
88 }
89 if field.capabilities.is_empty() {
90 return Err(invalid("typed JSON field needs at least one capability"));
91 }
92 let mut capabilities = BTreeSet::new();
93 for encoded in &field.capabilities {
94 let capability = IndexFieldCapability::try_from(*encoded)
95 .map_err(|_| invalid("typed JSON field capability is unknown"))?;
96 if !capabilities.insert(capability) {
97 return Err(invalid("typed JSON field capabilities must be unique"));
98 }
99 if !capability_allowed(field_type, capability) {
100 return Err(invalid(
101 "typed JSON field capability is invalid for its field type",
102 ));
103 }
104 }
105 if cardinality == IndexFieldCardinality::Multi
106 && capabilities.contains(&IndexFieldCapability::Order)
107 {
108 return Err(invalid(
109 "multi-valued typed JSON fields cannot declare ORDER",
110 ));
111 }
112 Ok(())
113}
114
115fn capability_allowed(field_type: &FieldType, capability: IndexFieldCapability) -> bool {
116 match field_type {
117 FieldType::Boolean(_) => matches!(
118 capability,
119 IndexFieldCapability::Exact | IndexFieldCapability::Facet
120 ),
121 FieldType::SignedInteger(_) | FieldType::UnsignedInteger(_) | FieldType::Float(_) => {
122 matches!(
123 capability,
124 IndexFieldCapability::Exact
125 | IndexFieldCapability::Range
126 | IndexFieldCapability::Order
127 | IndexFieldCapability::Facet
128 | IndexFieldCapability::Aggregate
129 )
130 }
131 FieldType::Keyword(_) => matches!(
132 capability,
133 IndexFieldCapability::Exact
134 | IndexFieldCapability::Prefix
135 | IndexFieldCapability::Range
136 | IndexFieldCapability::Order
137 | IndexFieldCapability::Facet
138 ),
139 FieldType::Text(_) => capability == IndexFieldCapability::FullText,
140 FieldType::Date(_) => matches!(
141 capability,
142 IndexFieldCapability::Exact
143 | IndexFieldCapability::Range
144 | IndexFieldCapability::Order
145 | IndexFieldCapability::Facet
146 ),
147 }
148}
149
150fn require_text(value: &str, message: &'static str) -> Result<(), TypedJsonValidationError> {
151 if value.is_empty() || value.contains('\0') {
152 Err(invalid(message))
153 } else {
154 Ok(())
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use crate::v1::index_field::FieldType;
161 use crate::v1::{
162 IndexField, IndexFieldCapability, IndexFieldCardinality, KeywordIndexField,
163 TypedJsonIndexSpec,
164 };
165
166 use super::validate_typed_json_specification;
167
168 #[test]
169 fn rejects_duplicate_fields_and_unorderable_physical_order() {
170 let field = IndexField {
171 name: "id".into(),
172 json_pointer: "/id".into(),
173 cardinality: IndexFieldCardinality::Single as i32,
174 capabilities: vec![IndexFieldCapability::Exact as i32],
175 field_type: Some(FieldType::Keyword(KeywordIndexField {})),
176 };
177 let mut specification = TypedJsonIndexSpec {
178 fields: vec![field.clone(), field],
179 physical_order: Vec::new(),
180 };
181 assert!(validate_typed_json_specification(&specification).is_err());
182 specification.fields.pop();
183 specification.physical_order.push(crate::v1::IndexOrder {
184 field: "id".into(),
185 direction: crate::v1::IndexOrderDirection::Ascending as i32,
186 });
187 assert!(validate_typed_json_specification(&specification).is_err());
188 }
189}