use super::error::{Located, Problem};
use super::record::{self, ConflictRec, Mode, Val};
use super::schema::{Item, Kind, Scalar, Shape, field};
use crate::yaml::{self, Out, Style, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SideShape {
Position,
Record,
Field(Shape),
}
#[must_use]
pub fn side_shape(host: Kind, path: &str) -> Option<SideShape> {
match path {
field::POSITION => return Some(SideShape::Position),
field::RECORD => return Some(SideShape::Record),
_ => {}
}
let mut kind = host;
let mut segments = path.split('.').peekable();
while let Some(segment) = segments.next() {
let (_, found) = kind.field(segment)?;
if segments.peek().is_none() {
return holds_no_keyed_record(found.shape).then_some(SideShape::Field(found.shape));
}
match found.shape {
Shape::One(Item::Record(inner)) if !inner.hosts_conflicts() => kind = inner,
_ => return None,
}
}
None
}
fn holds_no_keyed_record(shape: Shape) -> bool {
match shape {
Shape::One(Item::Record(kind)) | Shape::List(Item::Record(kind)) => !kind.is_keyed(),
_ => true,
}
}
pub(crate) fn check_conflicts(
node: &yaml::Node,
host: Kind,
mode: Mode,
) -> Result<Vec<ConflictRec>, Located> {
let items = match &node.value {
Value::Sequence(items) => items.as_slice(),
Value::Empty => &[],
_ => return Err(located(node, Problem::Expected("a list"))),
};
items
.iter()
.map(|item| check_conflict(item, host, mode))
.collect()
}
fn check_conflict(node: &yaml::Node, host: Kind, mode: Mode) -> Result<ConflictRec, Located> {
let Value::Mapping(entries) = &node.value else {
return Err(located(node, Problem::Expected("a mapping of fields")));
};
let path = entries
.iter()
.find(|entry| entry.key == field::FIELD)
.ok_or_else(|| located(node, Problem::MissingField(field::FIELD)))?;
let named = record::check_scalar(&path.value, Scalar::Identifier)?;
let text = named.text().unwrap_or_default();
let shape = side_shape(host, text).ok_or_else(|| {
located(
&path.value,
Problem::Invalid {
value: text.to_owned(),
expected: "a field of the record that holds the conflict",
},
)
})?;
let mut conflict = ConflictRec::new(text, [None, None, None]);
for entry in entries.iter().filter(|entry| entry.key != field::FIELD) {
let Some(side) = conflict.side_mut(&entry.key) else {
return Err(Located {
position: entry.key_position,
problem: Problem::UnknownField(entry.key.clone()),
});
};
*side = Some(check_side(&entry.value, shape, mode)?);
}
Ok(conflict)
}
fn check_side(node: &yaml::Node, shape: SideShape, mode: Mode) -> Result<Val, Located> {
match shape {
SideShape::Position => record::check_scalar(node, Scalar::Identifier),
SideShape::Record if is_null(node) => Ok(Val::Null {
position: node.position,
}),
SideShape::Record => record::check_scalar(node, Scalar::Key),
SideShape::Field(shape) => record::check_shape(node, shape, mode),
}
}
fn is_null(node: &yaml::Node) -> bool {
matches!(&node.value, Value::Scalar(scalar) if scalar.style == Style::Plain && scalar.text == "null")
}
fn located(node: &yaml::Node, problem: Problem) -> Located {
Located {
position: node.position,
problem,
}
}
pub(crate) fn conflicts_out(conflicts: &[ConflictRec], host: Kind) -> Out {
Out::Sequence(
conflicts
.iter()
.map(|conflict| conflict_out(conflict, host))
.collect(),
)
}
fn conflict_out(conflict: &ConflictRec, host: Kind) -> Out {
let shape =
side_shape(host, &conflict.field).expect("a conflict the reader or the merge checked");
let sides = conflict
.sides()
.into_iter()
.filter_map(|(name, value)| value.map(|value| (name, side_out(value, shape))));
let path = (field::FIELD, Out::Token(conflict.field.clone()));
Out::Mapping(std::iter::once(path).chain(sides).collect())
}
fn side_out(value: &Val, shape: SideShape) -> Out {
match (shape, value) {
(_, Val::Null { .. }) => Out::Null,
(SideShape::Field(shape), _) => record::value_out(value, shape),
(SideShape::Position | SideShape::Record, _) => {
Out::Token(value.text().unwrap_or_default().to_owned())
}
}
}