use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::document::node::NodeValue;
use crate::document::{EureDocument, NodeId};
use crate::identifier::Identifier;
use crate::parse::union::VARIANT;
use crate::parse::variant_path::VariantPath;
use crate::path::PathSegment;
use crate::source::{
BindSource, BindingSource, EureSource, SectionBody, SectionSource, SourceDocument, SourceId,
SourceKey, SourcePath, SourcePathSegment,
};
use crate::value::{ObjectKey, PartialObjectKey};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum LayoutStyle {
#[default]
Auto,
Passthrough,
Section,
Nested,
Binding,
SectionBinding,
SectionRootBinding,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct DocLayout {
pub style_rules: Vec<LayoutStyleRule>,
pub order_rules: Vec<LayoutOrderRule>,
pub fallback_style: LayoutStyle,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LayoutStyleRule {
pub path: Vec<PathSegment>,
pub style: LayoutStyle,
pub variant_constraints: Vec<VariantConstraint>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LayoutOrderRule {
pub parent_path: Vec<PathSegment>,
pub child_order: Vec<PathSegment>,
pub append_unlisted: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VariantConstraint {
pub union_path: Vec<PathSegment>,
pub variant: String,
pub requires_explicit_tag: bool,
}
impl DocLayout {
pub fn new() -> Self {
Self::default()
}
pub fn add_style_rule(&mut self, path: Vec<PathSegment>, style: LayoutStyle) {
self.style_rules.push(LayoutStyleRule {
path,
style,
variant_constraints: Vec::new(),
});
}
pub fn add_style_rule_with_constraints(
&mut self,
path: Vec<PathSegment>,
style: LayoutStyle,
variant_constraints: Vec<VariantConstraint>,
) {
self.style_rules.push(LayoutStyleRule {
path,
style,
variant_constraints,
});
}
pub fn add_order_rule(
&mut self,
parent_path: Vec<PathSegment>,
child_order: Vec<PathSegment>,
append_unlisted: bool,
) {
self.order_rules.push(LayoutOrderRule {
parent_path,
child_order,
append_unlisted,
});
}
fn order_rule_for_parent(&self, path: &[PathSegment]) -> Option<&LayoutOrderRule> {
self.order_rules
.iter()
.rev()
.find(|rule| rule.parent_path == path)
}
fn style_for_path(&self, doc: &EureDocument, path: &[PathSegment]) -> LayoutStyle {
let mut exact_style: Option<LayoutStyle> = None;
let mut exact_conflict = false;
let mut potential_style: Option<LayoutStyle> = None;
let mut potential_conflict = false;
for rule in self.style_rules.iter().filter(|rule| rule.path == path) {
match rule_match_status(doc, rule) {
RuleMatchStatus::NoMatch => {}
RuleMatchStatus::Exact(style) => {
if let Some(existing) = exact_style {
if existing != style {
exact_conflict = true;
}
} else {
exact_style = Some(style);
}
}
RuleMatchStatus::Potential(style) => {
if let Some(existing) = potential_style {
if existing != style {
potential_conflict = true;
}
} else {
potential_style = Some(style);
}
}
}
}
if exact_conflict {
return LayoutStyle::Auto;
}
if let Some(style) = exact_style {
return style;
}
if potential_conflict {
return LayoutStyle::Auto;
}
if let Some(style) = potential_style {
return style;
}
self.fallback_style
}
}
pub fn project_with_layout(doc: &EureDocument, layout: &DocLayout) -> SourceDocument {
let projected = LayoutBuilder::new(doc, layout).build();
projected.into_source_document(doc.clone())
}
#[derive(Debug, Clone)]
struct ProjectedDoc {
root: ProjectedBlock,
}
#[derive(Debug, Clone)]
struct ProjectedBlock {
value: Option<NodeId>,
bindings: Vec<ProjectedBinding>,
sections: Vec<ProjectedSection>,
}
#[derive(Debug, Clone)]
struct ProjectedBinding {
path: Vec<PathSegment>,
body: ProjectedBindingBody,
}
#[derive(Debug, Clone)]
enum ProjectedBindingBody {
Value(NodeId),
Block(ProjectedBlock),
}
#[derive(Debug, Clone)]
struct ProjectedSection {
path: Vec<PathSegment>,
body: ProjectedSectionBody,
}
#[derive(Debug, Clone)]
enum ProjectedSectionBody {
Items {
value: Option<NodeId>,
bindings: Vec<ProjectedBinding>,
},
Block(ProjectedBlock),
}
impl ProjectedDoc {
fn into_source_document(self, doc: EureDocument) -> SourceDocument {
let mut sources = Vec::new();
let root_id = build_source_block(&self.root, &mut sources);
debug_assert_eq!(root_id.0, 0, "root source must be index 0");
SourceDocument::new(doc, sources)
}
}
struct LayoutBuilder<'a> {
doc: &'a EureDocument,
layout: &'a DocLayout,
}
struct EntryContext<'a> {
bindings: &'a mut Vec<ProjectedBinding>,
sections: &'a mut Vec<ProjectedSection>,
node_path: &'a [PathSegment],
path_prefix: &'a [PathSegment],
allow_sections: bool,
}
#[derive(Debug, Clone)]
struct ChildEntry {
segment: PathSegment,
node_id: NodeId,
}
impl<'a> LayoutBuilder<'a> {
fn new(doc: &'a EureDocument, layout: &'a DocLayout) -> Self {
Self { doc, layout }
}
fn build(&self) -> ProjectedDoc {
let root_id = self.doc.get_root_id();
let root_node = self.doc.node(root_id);
let root_is_map = is_map_like(&root_node.content);
let value = if root_is_map { None } else { Some(root_id) };
let (bindings, sections) = self.build_entries(root_id, &[], &[], root_is_map, true);
ProjectedDoc {
root: ProjectedBlock {
value,
bindings,
sections,
},
}
}
fn build_block(&self, node_id: NodeId, node_path: &[PathSegment]) -> ProjectedBlock {
let node_is_map = is_map_like(&self.doc.node(node_id).content);
let (bindings, sections) = self.build_entries(node_id, node_path, &[], node_is_map, true);
ProjectedBlock {
value: None,
bindings,
sections,
}
}
fn build_entries(
&self,
node_id: NodeId,
node_path: &[PathSegment],
path_prefix: &[PathSegment],
emit_map_fields: bool,
allow_sections: bool,
) -> (Vec<ProjectedBinding>, Vec<ProjectedSection>) {
let mut bindings = Vec::new();
let mut sections = Vec::new();
let mut ctx = EntryContext {
bindings: &mut bindings,
sections: &mut sections,
node_path,
path_prefix,
allow_sections,
};
let node = self.doc.node(node_id);
let mut children = Vec::new();
for (ident, &child_id) in node.extensions.iter() {
children.push(ChildEntry {
segment: PathSegment::Extension(ident.clone()),
node_id: child_id,
});
}
if emit_map_fields {
children.extend(map_like_children(node));
}
let children = self.order_children(node_path, children);
for child in children {
self.append_child_entries(&mut ctx, child.segment, child.node_id);
}
(bindings, sections)
}
fn order_children(
&self,
parent_path: &[PathSegment],
children: Vec<ChildEntry>,
) -> Vec<ChildEntry> {
let Some(rule) = self.layout.order_rule_for_parent(parent_path) else {
return children;
};
if !rule.append_unlisted {
let mut out = children;
let listed: Vec<(usize, ChildEntry)> = out
.iter()
.enumerate()
.filter(|(_, entry)| {
rule.child_order
.iter()
.any(|segment| segment == &entry.segment)
})
.map(|(idx, entry)| (idx, entry.clone()))
.collect();
let mut sorted = listed
.iter()
.map(|(_, entry)| entry.clone())
.collect::<Vec<_>>();
sorted.sort_by_key(|entry| {
rule.child_order
.iter()
.position(|segment| segment == &entry.segment)
.unwrap_or(usize::MAX)
});
for ((idx, _), replacement) in listed.into_iter().zip(sorted.into_iter()) {
out[idx] = replacement;
}
return out;
}
let mut remaining = children;
let mut ordered = Vec::new();
for segment in &rule.child_order {
if let Some(pos) = remaining.iter().position(|entry| &entry.segment == segment) {
ordered.push(remaining.remove(pos));
}
}
ordered.extend(remaining);
ordered
}
fn append_child_entries(
&self,
ctx: &mut EntryContext<'_>,
child_seg: PathSegment,
child_id: NodeId,
) {
let child_node_path = concat_path(ctx.node_path, &child_seg);
let child_print_path = concat_path(ctx.path_prefix, &child_seg);
let style = self.layout.style_for_path(self.doc, &child_node_path);
if style == LayoutStyle::Passthrough {
let child_is_map = is_map_like(&self.doc.node(child_id).content);
let (b, s) = self.build_entries(
child_id,
&child_node_path,
&child_print_path,
child_is_map,
ctx.allow_sections,
);
ctx.bindings.extend(b);
ctx.sections.extend(s);
return;
}
let child_is_map = is_map_like(&self.doc.node(child_id).content);
let mut style = self.normalize_style(style, child_is_map, ctx.allow_sections);
if style == LayoutStyle::Binding
&& child_is_map
&& self.inline_binding_hides_descendant_entries(child_id, &child_node_path)
{
style = LayoutStyle::SectionBinding;
}
if style == LayoutStyle::Section && self.has_section_entries(child_id, &child_node_path) {
style = LayoutStyle::Nested;
}
match style {
LayoutStyle::Binding | LayoutStyle::Auto | LayoutStyle::Passthrough => {
ctx.bindings.push(ProjectedBinding {
path: child_print_path.clone(),
body: ProjectedBindingBody::Value(child_id),
});
let (b, s) = self.build_entries(
child_id,
&child_node_path,
&child_print_path,
false,
ctx.allow_sections,
);
ctx.bindings.extend(b);
ctx.sections.extend(s);
}
LayoutStyle::SectionBinding => {
if child_is_map {
let block = self.build_block(child_id, &child_node_path);
ctx.bindings.push(ProjectedBinding {
path: child_print_path,
body: ProjectedBindingBody::Block(block),
});
} else {
ctx.bindings.push(ProjectedBinding {
path: child_print_path.clone(),
body: ProjectedBindingBody::Value(child_id),
});
let (b, s) = self.build_entries(
child_id,
&child_node_path,
&child_print_path,
false,
ctx.allow_sections,
);
ctx.bindings.extend(b);
ctx.sections.extend(s);
}
}
LayoutStyle::Section => {
let (child_bindings, child_sections) =
self.build_entries(child_id, &child_node_path, &[], child_is_map, false);
debug_assert!(child_sections.is_empty());
ctx.sections.push(ProjectedSection {
path: child_print_path,
body: ProjectedSectionBody::Items {
value: None,
bindings: child_bindings,
},
});
}
LayoutStyle::SectionRootBinding => {
if child_is_map {
let (child_bindings, child_sections) =
self.build_entries(child_id, &child_node_path, &[], true, false);
debug_assert!(child_sections.is_empty());
ctx.sections.push(ProjectedSection {
path: child_print_path,
body: ProjectedSectionBody::Items {
value: None,
bindings: child_bindings,
},
});
} else {
let (child_bindings, child_sections) =
self.build_entries(child_id, &child_node_path, &[], false, false);
debug_assert!(child_sections.is_empty());
ctx.sections.push(ProjectedSection {
path: child_print_path,
body: ProjectedSectionBody::Items {
value: Some(child_id),
bindings: child_bindings,
},
});
}
}
LayoutStyle::Nested => {
if child_is_map {
let block = self.build_block(child_id, &child_node_path);
ctx.sections.push(ProjectedSection {
path: child_print_path,
body: ProjectedSectionBody::Block(block),
});
} else {
ctx.bindings.push(ProjectedBinding {
path: child_print_path.clone(),
body: ProjectedBindingBody::Value(child_id),
});
let (b, s) = self.build_entries(
child_id,
&child_node_path,
&child_print_path,
false,
ctx.allow_sections,
);
ctx.bindings.extend(b);
ctx.sections.extend(s);
}
}
}
}
fn normalize_style(
&self,
style: LayoutStyle,
node_is_map: bool,
allow_sections: bool,
) -> LayoutStyle {
let mut style = match style {
LayoutStyle::Auto | LayoutStyle::Passthrough => LayoutStyle::Binding,
other => other,
};
if !node_is_map {
style = match style {
LayoutStyle::SectionRootBinding => LayoutStyle::SectionRootBinding,
_ => LayoutStyle::Binding,
};
} else if style == LayoutStyle::SectionRootBinding {
style = LayoutStyle::Section;
}
if !allow_sections
&& matches!(
style,
LayoutStyle::Section | LayoutStyle::Nested | LayoutStyle::SectionRootBinding
)
{
style = if node_is_map {
LayoutStyle::SectionBinding
} else {
LayoutStyle::Binding
};
}
style
}
fn inline_binding_hides_descendant_entries(
&self,
node_id: NodeId,
node_path: &[PathSegment],
) -> bool {
for ChildEntry {
segment,
node_id: child_id,
} in map_like_children(self.doc.node(node_id))
{
let child_node_path = concat_path(node_path, &segment);
if self.subtree_has_deferred_entries(child_id, &child_node_path) {
return true;
}
}
false
}
fn subtree_has_deferred_entries(&self, node_id: NodeId, node_path: &[PathSegment]) -> bool {
let node = self.doc.node(node_id);
if !node.extensions.is_empty() {
return true;
}
if self.has_section_entries(node_id, node_path) {
return true;
}
if !is_map_like(&node.content) {
return false;
}
for ChildEntry {
segment,
node_id: child_id,
} in map_like_children(node)
{
let child_node_path = concat_path(node_path, &segment);
if self.subtree_has_deferred_entries(child_id, &child_node_path) {
return true;
}
}
false
}
fn has_section_entries(&self, node_id: NodeId, node_path: &[PathSegment]) -> bool {
let node = self.doc.node(node_id);
let node_is_map = is_map_like(&node.content);
for (ident, &child_id) in node.extensions.iter() {
let seg = PathSegment::Extension(ident.clone());
let child_node_path = concat_path(node_path, &seg);
if self.child_is_section(child_id, &child_node_path) {
return true;
}
}
if node_is_map {
for ChildEntry {
segment,
node_id: child_id,
} in map_like_children(node)
{
let child_node_path = concat_path(node_path, &segment);
if self.child_is_section(child_id, &child_node_path) {
return true;
}
}
}
false
}
fn child_is_section(&self, child_id: NodeId, child_node_path: &[PathSegment]) -> bool {
let style = self.layout.style_for_path(self.doc, child_node_path);
if style == LayoutStyle::Passthrough {
return self.has_section_entries(child_id, child_node_path);
}
let child_is_map = is_map_like(&self.doc.node(child_id).content);
let style = self.normalize_style(style, child_is_map, true);
matches!(
style,
LayoutStyle::Section | LayoutStyle::Nested | LayoutStyle::SectionRootBinding
)
}
}
fn is_map_like(content: &NodeValue) -> bool {
matches!(content, NodeValue::Map(_) | NodeValue::PartialMap(_))
}
fn map_like_children(node: &crate::document::node::Node) -> Vec<ChildEntry> {
match &node.content {
NodeValue::Map(map) => map
.iter()
.map(|(key, &child_id)| ChildEntry {
segment: PathSegment::Value(key.clone()),
node_id: child_id,
})
.collect(),
NodeValue::PartialMap(map) => map
.iter()
.map(|(key, &child_id)| ChildEntry {
segment: PathSegment::from_partial_object_key(key.clone()),
node_id: child_id,
})
.collect(),
_ => Vec::new(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RuleMatchStatus {
NoMatch,
Exact(LayoutStyle),
Potential(LayoutStyle),
}
fn rule_match_status(doc: &EureDocument, rule: &LayoutStyleRule) -> RuleMatchStatus {
if rule.variant_constraints.is_empty() {
return RuleMatchStatus::Exact(rule.style);
}
let mut inherited_variant: Option<VariantPath> = None;
let mut uncertain = false;
for constraint in &rule.variant_constraints {
let Some(union_node_id) = node_id_at_path(doc, &constraint.union_path) else {
return RuleMatchStatus::NoMatch;
};
let explicit_variant = if let Some(vp) = inherited_variant.as_ref()
&& !vp.is_empty()
{
Some(vp.clone())
} else {
match parse_variant_extension(doc, union_node_id) {
Ok(path) => path,
Err(()) => return RuleMatchStatus::NoMatch,
}
};
match explicit_variant {
Some(vp) if !vp.is_empty() => {
let Some(first) = vp.first() else {
return RuleMatchStatus::NoMatch;
};
if first.as_ref() != constraint.variant {
return RuleMatchStatus::NoMatch;
}
inherited_variant = vp.rest();
}
_ => {
inherited_variant = None;
if constraint.requires_explicit_tag {
return RuleMatchStatus::NoMatch;
}
uncertain = true;
}
}
}
if uncertain {
RuleMatchStatus::Potential(rule.style)
} else {
RuleMatchStatus::Exact(rule.style)
}
}
fn parse_variant_extension(doc: &EureDocument, node_id: NodeId) -> Result<Option<VariantPath>, ()> {
let node = doc.node(node_id);
let Some(&variant_node_id) = node.extensions.get(&VARIANT) else {
return Ok(None);
};
let Ok(variant_text) = doc.parse::<&str>(variant_node_id) else {
return Err(());
};
VariantPath::parse(variant_text).map(Some).map_err(|_| ())
}
fn node_id_at_path(doc: &EureDocument, path: &[PathSegment]) -> Option<NodeId> {
let mut current = doc.get_root_id();
for segment in path {
current = child_node_id(doc, current, segment)?;
}
Some(current)
}
fn build_source_block(block: &ProjectedBlock, sources: &mut Vec<EureSource>) -> SourceId {
let id = SourceId(sources.len());
sources.push(EureSource::default());
let mut eure = EureSource {
value: block.value,
..Default::default()
};
for binding in &block.bindings {
let path = to_source_path(&binding.path);
let bind = match &binding.body {
ProjectedBindingBody::Value(node_id) => BindSource::Value(*node_id),
ProjectedBindingBody::Block(inner) => {
let inner_id = build_source_block(inner, sources);
BindSource::Block(inner_id)
}
};
eure.bindings.push(BindingSource {
trivia_before: Vec::new(),
path,
bind,
trailing_comment: None,
});
}
for section in &block.sections {
let path = to_source_path(§ion.path);
let body = match §ion.body {
ProjectedSectionBody::Items { value, bindings } => {
let mut items = Vec::new();
for binding in bindings {
let path = to_source_path(&binding.path);
let bind = match &binding.body {
ProjectedBindingBody::Value(node_id) => BindSource::Value(*node_id),
ProjectedBindingBody::Block(inner) => {
let inner_id = build_source_block(inner, sources);
BindSource::Block(inner_id)
}
};
items.push(BindingSource {
trivia_before: Vec::new(),
path,
bind,
trailing_comment: None,
});
}
SectionBody::Items {
value: *value,
bindings: items,
}
}
ProjectedSectionBody::Block(inner) => {
let inner_id = build_source_block(inner, sources);
SectionBody::Block(inner_id)
}
};
eure.sections.push(SectionSource {
trivia_before: Vec::new(),
path,
body,
trailing_comment: None,
});
}
sources[id.0] = eure;
id
}
fn to_source_path(path: &[PathSegment]) -> SourcePath {
let mut out: Vec<SourcePathSegment> = Vec::new();
for seg in path {
match seg {
PathSegment::Ident(id) => out.push(SourcePathSegment::ident(id.clone())),
PathSegment::Extension(id) => out.push(SourcePathSegment::extension(id.clone())),
PathSegment::PartialValue(key) => out.push(SourcePathSegment {
key: partial_object_key_to_source_key(key),
array: None,
}),
PathSegment::HoleKey(label) => out.push(SourcePathSegment {
key: SourceKey::hole(label.clone()),
array: None,
}),
PathSegment::Value(key) => out.push(SourcePathSegment {
key: object_key_to_source_key(key),
array: None,
}),
PathSegment::TupleIndex(index) => out.push(SourcePathSegment {
key: SourceKey::TupleIndex(*index),
array: None,
}),
PathSegment::ArrayIndex(index) => {
if let Some(last) = out.last_mut() {
last.array = Some(*index);
}
}
}
}
out
}
fn object_key_to_source_key(key: &ObjectKey) -> SourceKey {
match key {
ObjectKey::String(s) => {
if let Ok(id) = s.parse::<Identifier>() {
SourceKey::Ident(id)
} else {
SourceKey::quoted(s.clone())
}
}
ObjectKey::Number(n) => {
if let Ok(n64) = i64::try_from(n) {
SourceKey::Integer(n64)
} else {
SourceKey::quoted(n.to_string())
}
}
ObjectKey::Tuple(keys) => {
SourceKey::Tuple(keys.iter().map(object_key_to_source_key).collect())
}
}
}
fn partial_object_key_to_source_key(key: &PartialObjectKey) -> SourceKey {
match key {
PartialObjectKey::String(s) => {
if let Ok(id) = s.parse::<Identifier>() {
SourceKey::Ident(id)
} else {
SourceKey::quoted(s.clone())
}
}
PartialObjectKey::Number(n) => {
if let Ok(n64) = i64::try_from(n) {
SourceKey::Integer(n64)
} else {
SourceKey::quoted(n.to_string())
}
}
PartialObjectKey::Hole(label) => SourceKey::hole(label.clone()),
PartialObjectKey::Tuple(keys) => {
SourceKey::Tuple(keys.iter().map(partial_object_key_to_source_key).collect())
}
}
}
fn concat_path(prefix: &[PathSegment], seg: &PathSegment) -> Vec<PathSegment> {
let mut out = Vec::with_capacity(prefix.len() + 1);
out.extend_from_slice(prefix);
out.push(seg.clone());
out
}
fn child_node_id(doc: &EureDocument, parent_id: NodeId, segment: &PathSegment) -> Option<NodeId> {
let parent = doc.node(parent_id);
match segment {
PathSegment::Extension(ext) => parent.extensions.get(ext).copied(),
PathSegment::Ident(ident) => match &parent.content {
NodeValue::Map(map) => map.get(&ObjectKey::String(ident.to_string())).copied(),
NodeValue::PartialMap(map) => map
.find(&PartialObjectKey::String(ident.to_string()))
.copied(),
_ => None,
},
PathSegment::Value(key) => match &parent.content {
NodeValue::Map(map) => map.get(key).copied(),
NodeValue::PartialMap(map) => map.find(&PartialObjectKey::from(key.clone())).copied(),
_ => None,
},
PathSegment::PartialValue(key) => match &parent.content {
NodeValue::Map(map) => ObjectKey::try_from(key.clone())
.ok()
.and_then(|object_key| map.get(&object_key))
.copied(),
NodeValue::PartialMap(map) => map.find(key).copied(),
_ => None,
},
PathSegment::ArrayIndex(index) => match &parent.content {
NodeValue::Array(array) => index.and_then(|i| array.get(i)),
_ => None,
},
PathSegment::TupleIndex(index) => match &parent.content {
NodeValue::Tuple(tuple) => tuple.get(*index as usize),
_ => None,
},
PathSegment::HoleKey(label) => match &parent.content {
NodeValue::PartialMap(map) => map.find(&PartialObjectKey::Hole(label.clone())).copied(),
_ => None,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::constructor::DocumentConstructor;
use crate::value::ObjectKey;
use alloc::vec;
fn make_doc() -> EureDocument {
let mut c = DocumentConstructor::new();
c.bind_empty_map().unwrap();
let scope = c.begin_scope();
c.navigate(PathSegment::Value(ObjectKey::String("a".to_string())))
.unwrap();
c.bind_empty_map().unwrap();
let inner = c.begin_scope();
c.navigate(PathSegment::Value(ObjectKey::String("x".to_string())))
.unwrap();
c.bind_primitive(crate::value::PrimitiveValue::Integer(1.into()))
.unwrap();
c.end_scope(inner).unwrap();
c.end_scope(scope).unwrap();
let scope = c.begin_scope();
c.navigate(PathSegment::Value(ObjectKey::String("b".to_string())))
.unwrap();
c.bind_empty_map().unwrap();
c.end_scope(scope).unwrap();
c.finish()
}
#[test]
fn applies_section_binding_rule() {
let doc = make_doc();
let mut layout = DocLayout::new();
layout.add_style_rule(
vec![PathSegment::Value(ObjectKey::String("a".to_string()))],
LayoutStyle::SectionBinding,
);
let source = project_with_layout(&doc, &layout);
let root = source.root_source();
assert_eq!(root.bindings.len(), 2);
assert!(matches!(root.bindings[0].bind, BindSource::Block(_)));
}
#[test]
fn applies_order_rule() {
let doc = make_doc();
let mut layout = DocLayout::new();
layout.add_order_rule(
Vec::new(),
vec![PathSegment::Value(ObjectKey::String("b".to_string()))],
true,
);
let source = project_with_layout(&doc, &layout);
let root = source.root_source();
let first = &root.bindings[0].path[0];
assert!(matches!(
first,
SourcePathSegment {
key: SourceKey::Ident(id),
..
} if id.as_ref() == "b"
));
}
#[test]
fn passthrough_flattens_parent_binding() {
let doc = make_doc();
let mut layout = DocLayout::new();
layout.add_style_rule(
vec![PathSegment::Value(ObjectKey::String("a".to_string()))],
LayoutStyle::Passthrough,
);
let source = project_with_layout(&doc, &layout);
let root = source.root_source();
assert!(!root.bindings.iter().any(|b| {
matches!(
b.path.as_slice(),
[SourcePathSegment {
key: SourceKey::Ident(id),
..
}] if id.as_ref() == "a"
)
}));
assert!(root.bindings.iter().any(|b| {
matches!(
b.path.as_slice(),
[
SourcePathSegment {
key: SourceKey::Ident(first),
..
},
SourcePathSegment {
key: SourceKey::Ident(second),
..
}
] if first.as_ref() == "a" && second.as_ref() == "x"
)
}));
}
#[test]
fn auto_promotes_inline_map_when_nested_extensions_would_be_lost() {
let mut c = DocumentConstructor::new();
c.bind_empty_map().unwrap();
let outer_scope = c.begin_scope();
c.navigate(PathSegment::Value(ObjectKey::String("outer".to_string())))
.unwrap();
c.bind_empty_map().unwrap();
let inner_scope = c.begin_scope();
c.navigate(PathSegment::Value(ObjectKey::String("inner".to_string())))
.unwrap();
c.bind_primitive(crate::value::PrimitiveValue::Integer(1.into()))
.unwrap();
c.set_extension("flag", true).unwrap();
c.end_scope(inner_scope).unwrap();
c.end_scope(outer_scope).unwrap();
let doc = c.finish();
let source = project_with_layout(&doc, &DocLayout::new());
let root = source.root_source();
assert_eq!(root.bindings.len(), 1);
let BindSource::Block(outer_block_id) = &root.bindings[0].bind else {
panic!("expected outer map to be promoted to a block binding");
};
let outer_block = source.source(*outer_block_id);
assert_eq!(outer_block.bindings.len(), 2);
}
}