use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
pub const MAX_SOURCE_PROJECTION_PATTERNS: usize = 128;
pub const MAX_SOURCE_PROJECTION_PATTERN_BYTES: usize = 512;
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SearchSourceProjection {
Parsed,
#[default]
ParsedAndExact,
ParsedExactAndLogicalOccupancy,
Omit,
Filtered {
includes: Vec<String>,
excludes: Vec<String>,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SearchSourceProjectionError {
TooManyPatterns,
InvalidPattern,
SourceTooDeep,
}
impl SearchSourceProjection {
pub fn filtered(mut includes: Vec<String>, mut excludes: Vec<String>) -> Result<Self, SearchSourceProjectionError> {
if includes.len().saturating_add(excludes.len()) > MAX_SOURCE_PROJECTION_PATTERNS {
return Err(SearchSourceProjectionError::TooManyPatterns);
}
if includes.iter().chain(&excludes).any(|pattern| {
pattern.is_empty()
|| pattern.len() > MAX_SOURCE_PROJECTION_PATTERN_BYTES
|| pattern.split('.').any(|segment| segment.is_empty())
|| pattern.chars().any(char::is_control)
}) {
return Err(SearchSourceProjectionError::InvalidPattern);
}
includes.sort();
includes.dedup();
excludes.sort();
excludes.dedup();
Ok(Self::Filtered { includes, excludes })
}
pub fn validate(&self) -> Result<(), SearchSourceProjectionError> {
match self {
Self::Parsed | Self::ParsedAndExact | Self::ParsedExactAndLogicalOccupancy | Self::Omit => Ok(()),
Self::Filtered { includes, excludes } => {
let canonical = Self::filtered(includes.clone(), excludes.clone())?;
if canonical == *self {
Ok(())
} else {
Err(SearchSourceProjectionError::InvalidPattern)
}
}
}
}
#[must_use]
pub const fn needs_exact_source(&self) -> bool {
matches!(self, Self::ParsedAndExact | Self::ParsedExactAndLogicalOccupancy)
}
#[must_use]
pub const fn needs_logical_occupancy(&self) -> bool {
matches!(self, Self::ParsedExactAndLogicalOccupancy)
}
pub fn project(&self, source: &Value) -> Result<Option<Value>, SearchSourceProjectionError> {
match self {
Self::Parsed | Self::ParsedAndExact | Self::ParsedExactAndLogicalOccupancy => Ok(Some(source.clone())),
Self::Omit => Ok(None),
Self::Filtered { includes, excludes } => {
let filtered = filter_value(source, &[], includes, excludes, false, 0)?;
Ok(Some(filtered.unwrap_or_else(|| Value::Object(Map::new()))))
}
}
}
}
fn filter_value(
value: &Value,
path: &[&str],
includes: &[String],
excludes: &[String],
inherited_selection: bool,
depth: usize,
) -> Result<Option<Value>, SearchSourceProjectionError> {
if depth > 128 {
return Err(SearchSourceProjectionError::SourceTooDeep);
}
if excludes.iter().any(|pattern| pattern_matches(pattern, path)) {
return Ok(None);
}
let selected_here =
inherited_selection || includes.is_empty() || includes.iter().any(|pattern| pattern_matches(pattern, path));
match value {
Value::Object(object) => {
let mut filtered = Map::new();
for (name, child) in object {
let mut child_path = path.to_vec();
child_path.push(name);
if let Some(child) = filter_value(child, &child_path, includes, excludes, selected_here, depth + 1)? {
filtered.insert(name.clone(), child);
}
}
if selected_here || !filtered.is_empty() {
Ok(Some(Value::Object(filtered)))
} else {
Ok(None)
}
}
Value::Array(values) => {
let mut filtered = Vec::with_capacity(values.len());
for child in values {
if let Some(child) = filter_value(child, path, includes, excludes, selected_here, depth + 1)? {
filtered.push(child);
}
}
if selected_here || !filtered.is_empty() {
Ok(Some(Value::Array(filtered)))
} else {
Ok(None)
}
}
_ if selected_here || includes.iter().any(|pattern| ancestor_selected(pattern, path)) => {
Ok(Some(value.clone()))
}
_ => Ok(None),
}
}
fn ancestor_selected(pattern: &str, path: &[&str]) -> bool {
(1..path.len()).any(|length| pattern_matches(pattern, &path[..length]))
}
fn pattern_matches(pattern: &str, path: &[&str]) -> bool {
let segments = pattern.split('.').collect::<Vec<_>>();
match_segments(&segments, path)
}
fn match_segments(pattern: &[&str], path: &[&str]) -> bool {
match (pattern.first(), path.first()) {
(None, None) => true,
(Some(&"**"), _) => {
match_segments(&pattern[1..], path) || (!path.is_empty() && match_segments(pattern, &path[1..]))
}
(Some(segment), Some(value)) if wildcard_segment_matches(segment, value) => {
match_segments(&pattern[1..], &path[1..])
}
_ => false,
}
}
fn wildcard_segment_matches(pattern: &str, value: &str) -> bool {
if pattern == "*" {
return true;
}
let parts = pattern.split('*').collect::<Vec<_>>();
if parts.len() == 1 {
return pattern == value;
}
let mut remainder = value;
for (position, part) in parts.iter().enumerate() {
if part.is_empty() {
continue;
}
if position == 0 && !pattern.starts_with('*') {
let Some(next) = remainder.strip_prefix(part) else {
return false;
};
remainder = next;
continue;
}
let Some(offset) = remainder.find(part) else {
return false;
};
remainder = &remainder[offset + part.len()..];
}
pattern.ends_with('*') || remainder.is_empty()
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn default_preserves_existing_native_exact_source_contract() {
assert!(SearchSourceProjection::default().needs_exact_source());
assert!(!SearchSourceProjection::Parsed.needs_exact_source());
assert!(!SearchSourceProjection::ParsedAndExact.needs_logical_occupancy());
assert!(SearchSourceProjection::ParsedExactAndLogicalOccupancy.needs_logical_occupancy());
}
#[test]
fn filtered_projection_applies_includes_then_excludes_across_arrays() -> Result<(), SearchSourceProjectionError> {
let projection = SearchSourceProjection::filtered(
vec!["service.*".to_owned(), "events.**".to_owned()],
vec!["service.secret".to_owned(), "events.token".to_owned()],
)?;
let projected = projection.project(&json!({
"service": {"name": "api", "secret": "hidden"},
"events": [{"message": "one", "token": "a"}, {"message": "two", "token": "b"}],
"ignored": true
}))?;
assert_eq!(
projected,
Some(json!({
"service": {"name": "api"},
"events": [{"message": "one"}, {"message": "two"}]
}))
);
Ok(())
}
#[test]
fn filtered_projection_is_canonical_and_empty_selection_is_an_object() -> Result<(), SearchSourceProjectionError> {
let projection = SearchSourceProjection::filtered(
vec!["missing".to_owned(), "missing".to_owned()],
vec!["z".to_owned(), "a".to_owned()],
)?;
assert_eq!(
projection,
SearchSourceProjection::Filtered {
includes: vec!["missing".to_owned()],
excludes: vec!["a".to_owned(), "z".to_owned()]
}
);
assert_eq!(projection.project(&json!({"present": 1}))?, Some(json!({})));
Ok(())
}
#[test]
fn filtered_projection_rejects_unbounded_or_ambiguous_patterns() {
assert_eq!(
SearchSourceProjection::filtered(vec![String::new()], Vec::new()),
Err(SearchSourceProjectionError::InvalidPattern)
);
assert_eq!(
SearchSourceProjection::filtered(vec!["a..b".to_owned()], Vec::new()),
Err(SearchSourceProjectionError::InvalidPattern)
);
assert_eq!(
SearchSourceProjection::filtered(
(0..=MAX_SOURCE_PROJECTION_PATTERNS)
.map(|index| format!("field{index}"))
.collect(),
Vec::new()
),
Err(SearchSourceProjectionError::TooManyPatterns)
);
}
#[test]
fn execution_projection_is_not_a_second_serialized_request_authority() -> Result<(), serde_json::Error> {
let request = crate::SearchRequest {
execution_source_projection: SearchSourceProjection::Parsed,
..crate::SearchRequest::default()
};
let bytes = serde_json::to_vec(&request)?;
assert!(
!bytes
.windows(b"execution_source_projection".len())
.any(|window| { window == b"execution_source_projection" })
);
let decoded: crate::SearchRequest = serde_json::from_slice(&bytes)?;
assert_eq!(
decoded.execution_source_projection,
SearchSourceProjection::ParsedAndExact
);
Ok(())
}
}