#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ExternalPathSegment {
Field(String),
Index(String),
}
impl ExternalPathSegment {
pub(crate) fn value(&self) -> &str {
match self {
Self::Field(value) | Self::Index(value) => value,
}
}
}
pub(crate) fn parse_external_path(path: &str) -> Result<Vec<ExternalPathSegment>, String> {
if path == "." {
return Ok(Vec::new());
}
let mut segments = Vec::new();
let mut current = String::new();
let mut chars = path.chars().peekable();
let mut after_index = false;
let mut expecting_segment = true;
while let Some(ch) = chars.next() {
if after_index {
match ch {
'.' => {
if chars.peek().is_none() {
return Err("configuration path cannot end with `.`".to_owned());
}
after_index = false;
expecting_segment = true;
}
'[' => {
let index = parse_array_index(&mut chars)?;
segments.push(ExternalPathSegment::Index(index));
after_index = true;
expecting_segment = false;
}
_ => {
return Err(
"expected `.` or `[` after an array index in configuration path".to_owned(),
);
}
}
continue;
}
match ch {
'.' => {
if current.is_empty() {
return Err("empty path segment in configuration path".to_owned());
}
segments.push(ExternalPathSegment::Field(std::mem::take(&mut current)));
expecting_segment = true;
}
'[' => {
if current.is_empty() {
return Err("array indices must follow a field name".to_owned());
}
segments.push(ExternalPathSegment::Field(std::mem::take(&mut current)));
let index = parse_array_index(&mut chars)?;
segments.push(ExternalPathSegment::Index(index));
after_index = true;
expecting_segment = false;
}
']' => return Err("unexpected `]` in configuration path".to_owned()),
_ => {
current.push(ch);
expecting_segment = false;
}
}
}
if expecting_segment && !segments.is_empty() && current.is_empty() && !after_index {
return Err("configuration path cannot end with `.`".to_owned());
}
if !current.is_empty() {
segments.push(ExternalPathSegment::Field(current));
}
Ok(segments)
}
pub(crate) fn render_external_path(segments: &[ExternalPathSegment]) -> String {
segments
.iter()
.map(ExternalPathSegment::value)
.collect::<Vec<_>>()
.join(".")
}
pub(crate) fn normalize_external_path(path: &str) -> Result<String, String> {
parse_external_path(path).map(|segments| render_external_path(&segments))
}
fn parse_array_index<I>(chars: &mut std::iter::Peekable<I>) -> Result<String, String>
where
I: Iterator<Item = char>,
{
let mut index = String::new();
let mut closed = false;
for next in chars.by_ref() {
if next == ']' {
closed = true;
break;
}
index.push(next);
}
if !closed {
return Err("unclosed `[` in configuration path".to_owned());
}
if index.is_empty() {
return Err("empty array index in configuration path".to_owned());
}
if !index.chars().all(|ch| ch.is_ascii_digit()) {
return Err("array indices in configuration paths must be numeric".to_owned());
}
index
.parse::<usize>()
.map(|value| value.to_string())
.map_err(|_| "array indices in configuration paths must fit in usize".to_owned())
}