use fraiseql_error::FraiseQLError;
use super::EmbeddedSpec;
use crate::routes::rest::params::{SelectEntry, helpers::validation_error};
const MAX_PARSE_DEPTH: usize = 32;
pub fn parse_select_entries(input: &str) -> Result<Vec<SelectEntry>, FraiseQLError> {
parse_select_entries_inner(input, 0)
}
fn parse_select_entries_inner(
input: &str,
depth: usize,
) -> Result<Vec<SelectEntry>, FraiseQLError> {
if depth > MAX_PARSE_DEPTH {
return Err(validation_error(format!(
"Select nesting depth exceeds maximum of {MAX_PARSE_DEPTH}. \
Reduce parenthetical nesting in `select` parameter."
)));
}
let mut entries = Vec::new();
let chars: Vec<char> = input.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
while i < len && (chars[i] == ',' || chars[i] == ' ') {
i += 1;
}
if i >= len {
break;
}
let name_start = i;
while i < len && chars[i] != '(' && chars[i] != ',' && chars[i] != '.' && chars[i] != ' ' {
i += 1;
}
let name = &input[name_start..i];
let name = name.trim();
if name.is_empty() {
return Err(validation_error("Empty field name in `select` parameter".to_string()));
}
while i < len && chars[i] == ' ' {
i += 1;
}
if i < len && chars[i] == '.' {
i += 1; let suffix_start = i;
while i < len && chars[i] != ',' && chars[i] != ' ' {
i += 1;
}
let suffix = &input[suffix_start..i];
if suffix == "count" {
entries.push(SelectEntry::Count(name.to_string()));
} else {
return Err(validation_error(format!(
"Unsupported dot-suffix '{suffix}' in `select`. Only `.count` is supported."
)));
}
} else if i < len && chars[i] == '(' {
let (rename, relationship) = if let Some(colon_pos) = name.find(':') {
(Some(name[..colon_pos].to_string()), name[colon_pos + 1..].to_string())
} else {
(None, name.to_string())
};
i += 1; let inner_start = i;
let mut depth = 1;
while i < len && depth > 0 {
if chars[i] == '(' {
depth += 1;
} else if chars[i] == ')' {
depth -= 1;
}
if depth > 0 {
i += 1;
}
}
if depth != 0 {
return Err(validation_error(format!(
"Unbalanced parentheses in `select` for '{relationship}'"
)));
}
let inner = &input[inner_start..i];
i += 1;
let sub_entries = parse_select_entries_inner(inner, depth + 1)?;
entries.push(SelectEntry::Embedded(EmbeddedSpec {
relationship,
rename,
fields: sub_entries,
}));
} else {
entries.push(SelectEntry::Field(name.to_string()));
}
}
Ok(entries)
}
pub fn validate_embedding_depth(
spec: &EmbeddedSpec,
current_depth: usize,
max_depth: usize,
) -> Result<(), FraiseQLError> {
if current_depth > max_depth {
return Err(validation_error(format!(
"Embedding depth {current_depth} exceeds maximum of {max_depth}. \
Reduce nesting in `select` parameter."
)));
}
for field in &spec.fields {
if let SelectEntry::Embedded(nested) = field {
validate_embedding_depth(nested, current_depth + 1, max_depth)?;
}
}
Ok(())
}