use std::collections::BTreeMap;
use panproto_gat::Name;
use panproto_schema::{Constraint, Schema};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "slot", rename_all = "snake_case")]
pub enum TraceSlot {
Child {
kind: String,
},
Token {
text: String,
},
}
impl TraceSlot {
#[must_use]
pub fn encode(&self) -> String {
match self {
Self::Child { kind } => format!("C{kind}"),
Self::Token { text } => format!("T{text}"),
}
}
#[must_use]
pub fn decode(value: &str) -> Self {
match value.as_bytes().first() {
Some(b'C') => Self::Child {
kind: value[1..].to_owned(),
},
Some(b'T') => Self::Token {
text: value[1..].to_owned(),
},
_ => Self::Token {
text: value.to_owned(),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Interstitial {
pub slot: usize,
pub text: String,
pub start_byte: Option<usize>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct VertexComplement {
pub trace: Option<Vec<TraceSlot>>,
pub pre_alias: Option<String>,
pub chose_alt_fingerprint: Option<String>,
pub chose_alt_child_kinds: Option<String>,
pub interstitials: Vec<Interstitial>,
pub byte_span: Option<(usize, usize)>,
pub indent: Option<String>,
pub blank_lines_before: Option<usize>,
pub literal: Option<String>,
pub field_tokens: BTreeMap<String, String>,
}
impl VertexComplement {
#[must_use]
pub fn is_empty(&self) -> bool {
self == &Self::default()
}
#[must_use]
pub fn from_constraints(constraints: &[Constraint]) -> Self {
let mut out = Self::default();
let mut trace_slots: BTreeMap<usize, TraceSlot> = BTreeMap::new();
let mut inter: BTreeMap<usize, (String, Option<usize>)> = BTreeMap::new();
let mut start_byte: Option<usize> = None;
let mut end_byte: Option<usize> = None;
for c in constraints {
let sort = c.sort.as_ref();
if let Some(n) = sort.strip_prefix("ptrace-") {
if let Ok(idx) = n.parse::<usize>() {
trace_slots.insert(idx, TraceSlot::decode(&c.value));
}
} else if let Some(rest) = sort.strip_prefix("interstitial-") {
if let Some(n) = rest.strip_suffix("-start-byte") {
if let (Ok(idx), Ok(b)) = (n.parse::<usize>(), c.value.parse::<usize>()) {
inter.entry(idx).or_default().1 = Some(b);
}
} else if let Ok(idx) = rest.parse::<usize>() {
inter.entry(idx).or_default().0.clone_from(&c.value);
}
} else if let Some(field) = sort.strip_prefix("field:") {
out.field_tokens.insert(field.to_owned(), c.value.clone());
} else {
match sort {
"start-byte" => start_byte = c.value.parse().ok(),
"end-byte" => end_byte = c.value.parse().ok(),
"pre-alias-symbol" => out.pre_alias = Some(c.value.clone()),
"chose-alt-fingerprint" => out.chose_alt_fingerprint = Some(c.value.clone()),
"chose-alt-child-kinds" => out.chose_alt_child_kinds = Some(c.value.clone()),
"literal-value" => out.literal = Some(c.value.clone()),
"indent" => out.indent = Some(c.value.clone()),
"blank-lines-before" => out.blank_lines_before = c.value.parse().ok(),
_ => {}
}
}
}
if !trace_slots.is_empty() {
out.trace = Some(trace_slots.into_values().collect());
}
out.interstitials = inter
.into_iter()
.map(|(slot, (text, start_byte))| Interstitial {
slot,
text,
start_byte,
})
.collect();
out.byte_span = match (start_byte, end_byte) {
(Some(s), Some(e)) => Some((s, e)),
_ => None,
};
out
}
#[must_use]
pub fn to_constraints(&self) -> Vec<Constraint> {
let mut out = Vec::new();
let push = |out: &mut Vec<Constraint>, sort: String, value: String| {
out.push(Constraint {
sort: Name::from(sort),
value,
});
};
if let Some((s, e)) = self.byte_span {
push(&mut out, "start-byte".into(), s.to_string());
push(&mut out, "end-byte".into(), e.to_string());
}
if let Some(p) = &self.pre_alias {
push(&mut out, "pre-alias-symbol".into(), p.clone());
}
if let Some(l) = &self.literal {
push(&mut out, "literal-value".into(), l.clone());
}
for (field, text) in &self.field_tokens {
push(&mut out, format!("field:{field}"), text.clone());
}
for itl in &self.interstitials {
push(
&mut out,
format!("interstitial-{}", itl.slot),
itl.text.clone(),
);
if let Some(b) = itl.start_byte {
push(
&mut out,
format!("interstitial-{}-start-byte", itl.slot),
b.to_string(),
);
}
}
if let Some(fp) = &self.chose_alt_fingerprint {
push(&mut out, "chose-alt-fingerprint".into(), fp.clone());
}
if let Some(k) = &self.chose_alt_child_kinds {
push(&mut out, "chose-alt-child-kinds".into(), k.clone());
}
if let Some(t) = &self.trace {
for (i, slot) in t.iter().enumerate() {
push(&mut out, format!("ptrace-{i}"), slot.encode());
}
}
if let Some(ind) = &self.indent {
push(&mut out, "indent".into(), ind.clone());
}
if let Some(n) = self.blank_lines_before {
push(&mut out, "blank-lines-before".into(), n.to_string());
}
out
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct LayoutComplement {
pub vertices: BTreeMap<Name, VertexComplement>,
}
impl LayoutComplement {
#[must_use]
pub fn from_schema(schema: &Schema) -> Self {
let mut vertices = BTreeMap::new();
for (id, constraints) in &schema.constraints {
let vc = VertexComplement::from_constraints(constraints);
if !vc.is_empty() {
vertices.insert(id.clone(), vc);
}
}
Self { vertices }
}
#[must_use]
pub fn vertex(&self, id: &Name) -> Option<&VertexComplement> {
self.vertices.get(id)
}
pub fn write_into(&self, schema: &mut Schema) {
for (id, vc) in &self.vertices {
schema.constraints.insert(id.clone(), vc.to_constraints());
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn trace_slot_encode_decode_round_trip() {
let child = TraceSlot::Child {
kind: "binary_expression".into(),
};
let token = TraceSlot::Token { text: "+".into() };
assert_eq!(child.encode(), "Cbinary_expression");
assert_eq!(token.encode(), "T+");
assert_eq!(TraceSlot::decode(&child.encode()), child);
assert_eq!(TraceSlot::decode(&token.encode()), token);
let tricky = TraceSlot::Token { text: "Cat".into() };
assert_eq!(TraceSlot::decode(&tricky.encode()), tricky);
}
fn c(sort: &str, value: &str) -> Constraint {
Constraint {
sort: Name::from(sort),
value: value.to_owned(),
}
}
#[test]
fn from_constraints_decodes_all_fibres() {
let cs = vec![
c("start-byte", "0"),
c("end-byte", "7"),
c("ptrace-0", "Cnumber"),
c("ptrace-1", "T+"),
c("ptrace-2", "Cnumber"),
c("interstitial-0", " "),
c("interstitial-0-start-byte", "1"),
c("pre-alias-symbol", "command_binary"),
c("literal-value", "42"),
c("chose-alt-child-kinds", "number number"),
c("field:operator", "+"),
c("indent", " "),
c("blank-lines-before", "2"),
];
let vc = VertexComplement::from_constraints(&cs);
assert_eq!(
vc.trace,
Some(vec![
TraceSlot::Child {
kind: "number".into()
},
TraceSlot::Token { text: "+".into() },
TraceSlot::Child {
kind: "number".into()
},
])
);
assert_eq!(vc.byte_span, Some((0, 7)));
assert_eq!(vc.pre_alias.as_deref(), Some("command_binary"));
assert_eq!(vc.literal.as_deref(), Some("42"));
assert_eq!(vc.interstitials.len(), 1);
assert_eq!(vc.interstitials[0].text, " ");
assert_eq!(vc.interstitials[0].start_byte, Some(1));
assert_eq!(
vc.field_tokens.get("operator").map(String::as_str),
Some("+")
);
assert_eq!(vc.indent.as_deref(), Some(" "));
assert_eq!(vc.blank_lines_before, Some(2));
assert!(!vc.is_empty());
}
#[test]
fn constraints_round_trip_as_a_set() {
let cs = vec![
c("start-byte", "10"),
c("end-byte", "20"),
c("ptrace-0", "Cidentifier"),
c("ptrace-1", "T="),
c("interstitial-0", "\n "),
c("interstitial-0-start-byte", "11"),
c("literal-value", "x"),
c("field:op", "="),
c("indent", " "),
c("blank-lines-before", "1"),
c("chose-alt-fingerprint", "="),
];
let vc = VertexComplement::from_constraints(&cs);
let back = vc.to_constraints();
assert_eq!(VertexComplement::from_constraints(&back), vc);
let orig: std::collections::HashSet<(String, String)> = cs
.iter()
.map(|c| (c.sort.as_ref().to_owned(), c.value.clone()))
.collect();
let round: std::collections::HashSet<(String, String)> = back
.iter()
.map(|c| (c.sort.as_ref().to_owned(), c.value.clone()))
.collect();
assert_eq!(orig, round);
}
#[test]
fn empty_constraints_decode_to_empty() {
assert!(VertexComplement::from_constraints(&[]).is_empty());
assert!(VertexComplement::default().is_empty());
assert!(VertexComplement::from_constraints(&[c("maxLength", "3")]).is_empty());
}
fn schema_with(constraints: &[(&str, &[Constraint])]) -> Schema {
use panproto_schema::{Protocol, SchemaBuilder};
let mut b = SchemaBuilder::new(&Protocol::default());
for (vid, cs) in constraints {
b = b.vertex(vid, "object", None).unwrap();
for cn in *cs {
b = b.constraint(vid, cn.sort.as_ref(), &cn.value);
}
}
b.build().unwrap()
}
#[test]
fn schema_level_from_and_write() {
let schema = schema_with(&[
("v1", &[c("ptrace-0", "Cfoo"), c("literal-value", "bar")]),
("v2", &[c("maxLength", "9")]),
]);
let lc = LayoutComplement::from_schema(&schema);
assert!(lc.vertex(&Name::from("v1")).is_some());
assert!(lc.vertex(&Name::from("v2")).is_none());
let mut fresh = schema_with(&[("v1", &[])]);
lc.write_into(&mut fresh);
let lc2 = LayoutComplement::from_schema(&fresh);
assert_eq!(lc, lc2);
}
}