use super::rules as edit_rules;
use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticResult};
use winnow::Parser;
use winnow::ascii::digit1;
use winnow::combinator::{delimited, eof, opt, separated, terminated};
use winnow::error::{ContextError, ErrMode};
use winnow::token::{any, take_while};
type ParseErr = ErrMode<ContextError>;
#[derive(Debug, Clone, PartialEq, Eq)]
struct RawPathSegment {
name: String,
index: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PathSegment {
pub name: String,
pub index: Option<i32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldPath {
pub segments: Vec<PathSegment>,
}
impl FieldPath {
pub fn is_simple(&self) -> bool {
self.segments.len() == 1 && self.segments[0].index.is_none()
}
pub fn as_simple(&self) -> Option<&str> {
if self.is_simple() {
Some(&self.segments[0].name)
} else {
None
}
}
pub fn has_terminal_index(&self) -> bool {
self.segments.last().is_some_and(|s| s.index.is_some())
}
#[cfg(test)]
pub fn normalize_aliases(mut self) -> Self {
for seg in &mut self.segments {
seg.name = normalize_segment_name(&seg.name);
}
self
}
pub fn collapse_legacy_prefixes(mut self) -> Self {
if self.segments.len() >= 2 && self.segments[0].index.is_none() {
let prefix = self.segments[0].name.as_str();
let field = self.segments[1].name.as_str();
if edit_rules::can_collapse_legacy_prefix(prefix, field) {
self.segments.remove(0);
}
}
self
}
}
impl std::fmt::Display for FieldPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (i, seg) in self.segments.iter().enumerate() {
if i > 0 {
f.write_str(".")?;
}
f.write_str(&seg.name)?;
if let Some(idx) = seg.index {
write!(f, "[{idx}]")?;
}
}
Ok(())
}
}
#[cfg(test)]
fn normalize_segment_name(name: &str) -> String {
edit_rules::normalize_alias(name).to_string()
}
#[cfg(test)]
pub fn parse_field_path(input: &str) -> DiagnosticResult<FieldPath> {
parse_raw_field_path(input).map(FieldPath::normalize_aliases)
}
pub fn parse_raw_field_path(input: &str) -> DiagnosticResult<FieldPath> {
if input.is_empty() {
return Err(Diagnostic::new(
DiagnosticCode::E0814InvalidPath,
"Field path cannot be empty",
"path",
));
}
let raw_segments = terminated(path_segments_parser, eof)
.parse(input)
.map_err(|_| {
Diagnostic::new(
DiagnosticCode::E0814InvalidPath,
format!("Invalid field path: {input}"),
"path",
)
})?;
let mut segments = Vec::with_capacity(raw_segments.len());
for raw in raw_segments {
let index = match raw.index {
Some(text) => Some(text.parse::<i32>().map_err(|_| {
Diagnostic::new(
DiagnosticCode::E0814InvalidPath,
format!("Invalid field path: invalid index '{text}'"),
"path",
)
})?),
None => None,
};
segments.push(PathSegment {
name: raw.name,
index,
});
}
Ok(FieldPath { segments })
}
fn path_segments_parser(input: &mut &str) -> Result<Vec<RawPathSegment>, ParseErr> {
separated(1.., parse_segment_raw, '.').parse_next(input)
}
fn parse_segment_raw(input: &mut &str) -> Result<RawPathSegment, ParseErr> {
let name = parse_name_raw(input)?;
let index = opt(parse_index_text).parse_next(input)?;
Ok(RawPathSegment { name, index })
}
fn parse_name_raw(rest: &mut &str) -> Result<String, ParseErr> {
let first = any.verify(|c: &char| is_name_start(*c)).parse_next(rest)?;
let suffix: &str = take_while(0.., is_name_char).parse_next(rest)?;
Ok(format!("{first}{suffix}"))
}
fn parse_index_text(rest: &mut &str) -> Result<String, ParseErr> {
let (sign, digits): (Option<char>, &str) =
delimited('[', (opt('-'), digit1), ']').parse_next(rest)?;
let mut idx = String::new();
if sign.is_some() {
idx.push('-');
}
idx.push_str(digits);
Ok(idx)
}
fn is_name_start(c: char) -> bool {
c.is_ascii_lowercase() || c == '_'
}
fn is_name_char(c: char) -> bool {
c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'
}
pub fn resolve_index(idx: i32, len: usize) -> DiagnosticResult<usize> {
let len_i = len as i32;
let actual = if idx < 0 { len_i + idx } else { idx };
if actual < 0 || actual >= len_i {
return Err(Diagnostic::new(
DiagnosticCode::E0816PathIndexOutOfBounds,
format!("Index {idx} out of range (array has {len} items)"),
"path",
));
}
Ok(actual as usize)
}
#[cfg(test)]
mod tests;