use serde_json::Value as Json;
use crate::query::error::QueryError;
#[derive(Debug, Clone, PartialEq)]
pub struct QuerySpec {
pub source: String,
pub filter: Option<Json>,
pub fields: Vec<String>,
pub sort: Vec<SortKey>,
pub limit: Option<u64>,
pub skip: Option<u64>,
pub include: Vec<IncludeSpec>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct IncludeSpec {
pub relation: String,
pub fields: Vec<String>,
pub limit: Option<u64>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SortKey {
pub field: String,
pub dir: SortDir,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortDir {
Asc,
Desc,
}
fn invalid(msg: impl Into<String>) -> QueryError {
QueryError::InvalidEnvelope(msg.into())
}
pub fn parse(query: &Json) -> Result<QuerySpec, QueryError> {
let obj = query
.as_object()
.ok_or_else(|| invalid("query must be a JSON object"))?;
let source = obj
.get("source")
.and_then(|v| v.as_str())
.ok_or_else(|| invalid("missing required string field 'source'"))?
.to_string();
if source.is_empty() {
return Err(invalid("'source' must not be empty"));
}
let filter = obj.get("filter").filter(|v| !v.is_null()).cloned();
let fields = match obj.get("fields") {
None | Some(Json::Null) => Vec::new(),
Some(Json::Array(arr)) => {
let mut out = Vec::with_capacity(arr.len());
for (i, f) in arr.iter().enumerate() {
let s = f
.as_str()
.ok_or_else(|| invalid(format!("fields[{i}] must be a string")))?;
out.push(s.to_string());
}
out
}
Some(_) => return Err(invalid("'fields' must be an array of strings")),
};
let sort = parse_sort(obj.get("sort"))?;
let limit = parse_u64(obj.get("limit"), "limit")?;
let skip = parse_u64(obj.get("skip"), "skip")?;
let include = parse_include(obj.get("include"))?;
Ok(QuerySpec {
source,
filter,
fields,
sort,
limit,
skip,
include,
})
}
fn parse_include(v: Option<&Json>) -> Result<Vec<IncludeSpec>, QueryError> {
let obj = match v {
None | Some(Json::Null) => return Ok(Vec::new()),
Some(Json::Object(m)) => m,
Some(_) => {
return Err(invalid(
"'include' must be an object of relation → selection",
));
}
};
let mut out = Vec::with_capacity(obj.len());
for (relation, sel) in obj {
let sel = sel
.as_object()
.ok_or_else(|| invalid(format!("include.{relation} must be an object")))?;
let fields = match sel.get("fields") {
None | Some(Json::Null) => Vec::new(),
Some(Json::Array(arr)) => {
let mut fs = Vec::with_capacity(arr.len());
for (i, f) in arr.iter().enumerate() {
let s = f.as_str().ok_or_else(|| {
invalid(format!("include.{relation}.fields[{i}] must be a string"))
})?;
fs.push(s.to_string());
}
fs
}
Some(_) => {
return Err(invalid(format!(
"include.{relation}.fields must be an array"
)));
}
};
let limit = parse_u64(sel.get("limit"), &format!("include.{relation}.limit"))?;
out.push(IncludeSpec {
relation: relation.clone(),
fields,
limit,
});
}
Ok(out)
}
fn parse_sort(v: Option<&Json>) -> Result<Vec<SortKey>, QueryError> {
let arr = match v {
None | Some(Json::Null) => return Ok(Vec::new()),
Some(Json::Array(a)) => a,
Some(_) => return Err(invalid("'sort' must be an array")),
};
let mut out = Vec::with_capacity(arr.len());
for (i, entry) in arr.iter().enumerate() {
let obj = entry.as_object().ok_or_else(|| {
invalid(format!(
"sort[{i}] must be an object like {{\"field\":\"asc\"}}"
))
})?;
if obj.len() != 1 {
return Err(invalid(format!("sort[{i}] must have exactly one key")));
}
let (field, dirv) = obj.iter().next().expect("map has exactly one entry");
let dir = match dirv.as_str() {
Some("asc") => SortDir::Asc,
Some("desc") => SortDir::Desc,
_ => {
return Err(invalid(format!(
"sort[{i}].{field} must be \"asc\" or \"desc\""
)));
}
};
out.push(SortKey {
field: field.clone(),
dir,
});
}
Ok(out)
}
fn parse_u64(v: Option<&Json>, name: &str) -> Result<Option<u64>, QueryError> {
match v {
None | Some(Json::Null) => Ok(None),
Some(n) => {
let u = n
.as_u64()
.ok_or_else(|| invalid(format!("'{name}' must be a non-negative integer")))?;
Ok(Some(u))
}
}
}