use crate::backend::Backend;
use crate::error::{QueryError, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum JsonSeg {
Key(String),
Index(usize),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsonPath {
segments: Vec<JsonSeg>,
}
impl JsonPath {
pub fn parse(input: &str) -> Result<Self> {
let s = input.trim();
if s.is_empty() {
return Err(QueryError::InvalidIdentifier(input.to_string()));
}
let body = s.strip_prefix('$').unwrap_or(s);
let body = body.strip_prefix('.').unwrap_or(body);
let mut segments = Vec::new();
let mut buf = String::new();
let mut chars = body.chars().peekable();
let flush_key = |buf: &mut String, segments: &mut Vec<JsonSeg>| -> Result<()> {
if buf.is_empty() {
return Ok(());
}
validate_key(buf)?;
segments.push(JsonSeg::Key(std::mem::take(buf)));
Ok(())
};
while let Some(c) = chars.next() {
match c {
'.' => {
let prev_was_index =
buf.is_empty() && matches!(segments.last(), Some(JsonSeg::Index(_)));
if buf.is_empty() && !prev_was_index {
return Err(QueryError::InvalidIdentifier(input.to_string()));
}
flush_key(&mut buf, &mut segments)?;
}
'[' => {
flush_key(&mut buf, &mut segments)?;
let mut num = String::new();
for nc in chars.by_ref() {
if nc == ']' {
break;
}
if !nc.is_ascii_digit() {
return Err(QueryError::InvalidIdentifier(input.to_string()));
}
num.push(nc);
}
if num.is_empty() {
return Err(QueryError::InvalidIdentifier(input.to_string()));
}
let idx: usize = num
.parse()
.map_err(|_| QueryError::InvalidIdentifier(input.to_string()))?;
segments.push(JsonSeg::Index(idx));
}
c if c.is_ascii_alphanumeric() || c == '_' => buf.push(c),
_ => return Err(QueryError::InvalidIdentifier(input.to_string())),
}
}
flush_key(&mut buf, &mut segments)?;
if segments.is_empty() {
return Err(QueryError::InvalidIdentifier(input.to_string()));
}
Ok(JsonPath { segments })
}
pub fn segments(&self) -> &[JsonSeg] {
&self.segments
}
pub fn render_mysql(&self) -> String {
let mut out = String::from("$");
for s in &self.segments {
match s {
JsonSeg::Key(k) => {
out.push('.');
out.push_str(k);
}
JsonSeg::Index(i) => {
out.push('[');
out.push_str(&i.to_string());
out.push(']');
}
}
}
out
}
pub fn render_postgres(&self) -> String {
let parts: Vec<String> = self
.segments
.iter()
.map(|s| match s {
JsonSeg::Key(k) => k.clone(),
JsonSeg::Index(i) => i.to_string(),
})
.collect();
format!("{{{}}}", parts.join(","))
}
}
fn validate_key(k: &str) -> Result<()> {
if k.is_empty() || k.len() > 64 {
return Err(QueryError::InvalidIdentifier(k.to_string()));
}
let mut chars = k.chars();
let first = chars.next().unwrap();
if !(first.is_ascii_alphabetic() || first == '_') {
return Err(QueryError::InvalidIdentifier(k.to_string()));
}
for c in chars {
if !(c.is_ascii_alphanumeric() || c == '_') {
return Err(QueryError::InvalidIdentifier(k.to_string()));
}
}
Ok(())
}
pub(crate) fn render_extract(
backend: Backend,
quoted_col: &str,
path: &JsonPath,
as_text: bool,
) -> String {
match backend {
Backend::MySql => {
let p = path.render_mysql();
if as_text {
format!("JSON_UNQUOTE(JSON_EXTRACT({}, '{}'))", quoted_col, p)
} else {
format!("JSON_EXTRACT({}, '{}')", quoted_col, p)
}
}
Backend::Sqlite => {
let _ = as_text;
let p = path.render_mysql();
format!("json_extract({}, '{}')", quoted_col, p)
}
Backend::Postgres => {
let p = path.render_postgres();
let op = if as_text { "#>>" } else { "#>" };
format!("{} {} '{}'", quoted_col, op, p)
}
}
}
pub(crate) fn render_contains(
backend: Backend,
quoted_col: &str,
placeholder: &str,
) -> Result<String> {
match backend {
Backend::MySql => Ok(format!("JSON_CONTAINS({}, {})", quoted_col, placeholder)),
Backend::Postgres => Ok(format!("{} @> {}::jsonb", quoted_col, placeholder)),
Backend::Sqlite => Err(QueryError::InvalidOperator(
"json_contains no soportado en SQLite".to_string(),
)),
}
}