use crate::{SerializeError, Value};
use pattern_core::{Pattern, Subject};
use std::collections::HashMap;
pub fn to_gram_pattern(pattern: &Pattern<Subject>) -> Result<String, SerializeError> {
let format = select_format(pattern);
match format {
GramFormat::Node => serialize_node_pattern(pattern),
GramFormat::Relationship => serialize_relationship_pattern(pattern),
GramFormat::SubjectPattern => serialize_subject_pattern(pattern),
GramFormat::Annotation => serialize_annotation_pattern(pattern),
GramFormat::BareRecord => serialize_record(&pattern.value.properties),
}
}
pub fn to_gram(patterns: &[Pattern<Subject>]) -> Result<String, SerializeError> {
patterns
.iter()
.map(to_gram_pattern)
.collect::<Result<Vec<_>, _>>()
.map(|lines| lines.join("\n"))
}
pub fn to_gram_with_header(
header: crate::Record,
patterns: &[Pattern<Subject>],
) -> Result<String, SerializeError> {
let header_str = serialize_record(&header)?;
let patterns_str = to_gram(patterns)?;
if patterns_str.is_empty() {
Ok(header_str)
} else if header_str.is_empty() {
Ok(patterns_str)
} else {
Ok(format!("{}\n{}", header_str, patterns_str))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GramFormat {
Node,
Relationship,
SubjectPattern,
Annotation,
BareRecord,
}
fn select_format(pattern: &Pattern<Subject>) -> GramFormat {
let elem_count = pattern.elements.len();
if elem_count == 0 {
if pattern.value.identity.0.is_empty()
&& pattern.value.labels.is_empty()
&& !pattern.value.properties.is_empty()
{
GramFormat::BareRecord
} else {
GramFormat::Node
}
} else if elem_count == 1 {
if is_annotation_pattern(pattern) {
GramFormat::Annotation
} else {
GramFormat::SubjectPattern
}
} else if elem_count == 2 {
if is_relationship_pattern(pattern) {
GramFormat::Relationship
} else {
GramFormat::SubjectPattern
}
} else {
GramFormat::SubjectPattern
}
}
fn is_relationship_pattern(pattern: &Pattern<Subject>) -> bool {
if pattern.elements.len() != 2
|| !pattern.elements[0].elements.is_empty()
|| !pattern.elements[1].elements.is_empty()
{
return false;
}
true
}
fn is_annotation_pattern(pattern: &Pattern<Subject>) -> bool {
pattern.elements.len() == 1
&& (!pattern.value.identity.0.is_empty()
|| !pattern.value.labels.is_empty()
|| !pattern.value.properties.is_empty())
}
fn serialize_node_pattern(pattern: &Pattern<Subject>) -> Result<String, SerializeError> {
let subject_str = serialize_subject(&pattern.value)?;
Ok(format!("({})", subject_str))
}
fn serialize_relationship_pattern(pattern: &Pattern<Subject>) -> Result<String, SerializeError> {
if pattern.elements.len() != 2 {
return Err(SerializeError::invalid_structure(
"Relationship pattern requires exactly 2 elements",
));
}
let left = serialize_node_pattern(&pattern.elements[0])?;
let right = serialize_node_pattern(&pattern.elements[1])?;
let edge = if pattern.value.identity.0.is_empty()
&& pattern.value.labels.is_empty()
&& pattern.value.properties.is_empty()
{
String::new()
} else {
let edge_str = serialize_subject(&pattern.value)?;
format!("[{}]", edge_str)
};
Ok(format!("{}-{}->{}", left, edge, right))
}
fn serialize_subject_pattern(pattern: &Pattern<Subject>) -> Result<String, SerializeError> {
let subject_str = serialize_subject(&pattern.value)?;
let elements_str = pattern
.elements
.iter()
.map(to_gram_pattern)
.collect::<Result<Vec<_>, _>>()?
.join(", ");
Ok(format!("[{} | {}]", subject_str, elements_str))
}
fn serialize_annotation_pattern(pattern: &Pattern<Subject>) -> Result<String, SerializeError> {
if pattern.elements.len() != 1 {
return Err(SerializeError::invalid_structure(
"Annotation pattern requires exactly 1 element",
));
}
let mut annotations = Vec::new();
if !pattern.value.identity.0.is_empty() || !pattern.value.labels.is_empty() {
let mut identified = String::from("@@");
if !pattern.value.identity.0.is_empty() {
identified.push_str("e_identifier(&pattern.value.identity.0));
}
if !pattern.value.labels.is_empty() {
let mut labels: Vec<_> = pattern.value.labels.iter().collect();
labels.sort();
for label in labels {
identified.push(':');
identified.push_str("e_identifier(label));
}
}
annotations.push(identified);
}
let mut property_annotations: Vec<String> = pattern
.value
.properties
.iter()
.map(|(key, value)| {
let gram_value = value_from_pattern_value(value)?;
let value_str = gram_value.to_gram_notation();
Ok(format!("@{}({})", quote_identifier(key), value_str))
})
.collect::<Result<Vec<_>, SerializeError>>()?;
property_annotations.sort();
annotations.extend(property_annotations);
let element_str = to_gram_pattern(&pattern.elements[0])?;
Ok(format!("{} {}", annotations.join(" "), element_str))
}
fn serialize_subject(subject: &Subject) -> Result<String, SerializeError> {
let mut parts = Vec::new();
let mut id_with_labels = String::new();
if !subject.identity.0.is_empty() {
id_with_labels.push_str("e_identifier(&subject.identity.0));
}
if !subject.labels.is_empty() {
let mut labels: Vec<_> = subject.labels.iter().collect();
labels.sort(); for label in labels {
id_with_labels.push(':');
id_with_labels.push_str("e_identifier(label));
}
}
if !id_with_labels.is_empty() {
parts.push(id_with_labels);
}
if !subject.properties.is_empty() {
let record_str = serialize_record(&subject.properties)?;
parts.push(record_str);
}
Ok(parts.join(" "))
}
fn serialize_record(
properties: &HashMap<String, pattern_core::Value>,
) -> Result<String, SerializeError> {
if properties.is_empty() {
return Ok(String::new());
}
let mut props: Vec<_> = properties.iter().collect();
props.sort_by_key(|(k, _)| *k);
let prop_strs: Vec<String> = props
.iter()
.map(|(key, value)| {
let gram_value = value_from_pattern_value(value)?;
let value_str = gram_value.to_gram_notation();
Ok(format!("{}: {}", quote_identifier(key), value_str))
})
.collect::<Result<Vec<_>, SerializeError>>()?;
Ok(format!("{{{}}}", prop_strs.join(", ")))
}
fn value_from_pattern_value(value: &pattern_core::Value) -> Result<Value, SerializeError> {
match value {
pattern_core::Value::VString(s) => Ok(Value::String(s.clone())),
pattern_core::Value::VSymbol(s) => Ok(Value::String(s.clone())),
pattern_core::Value::VInteger(i) => Ok(Value::Integer(*i)),
pattern_core::Value::VDecimal(d) => Ok(Value::Decimal(*d)),
pattern_core::Value::VBoolean(b) => Ok(Value::Boolean(*b)),
pattern_core::Value::VArray(arr) => {
let values = arr
.iter()
.map(value_from_pattern_value)
.collect::<Result<Vec<_>, _>>()?;
Ok(Value::Array(values))
}
pattern_core::Value::VRange(range) => {
let lower = range.lower.ok_or_else(|| {
SerializeError::invalid_structure("Unbounded lower range not supported")
})? as i64;
let upper = range.upper.ok_or_else(|| {
SerializeError::invalid_structure("Unbounded upper range not supported")
})? as i64;
Ok(Value::Range { lower, upper })
}
pattern_core::Value::VTaggedString { tag, content } => Ok(Value::TaggedString {
tag: tag.clone(),
content: content.clone(),
}),
pattern_core::Value::VMap(_map) => {
Err(SerializeError::invalid_structure(
"Map values not supported in gram notation properties",
))
}
pattern_core::Value::VMeasurement { .. } => {
Err(SerializeError::invalid_structure(
"Measurement values not supported in gram notation",
))
}
}
}
fn quote_identifier(s: &str) -> String {
if needs_quoting(s) {
format!("`{}`", escape_backtick_string(s))
} else {
s.to_string()
}
}
fn needs_quoting(s: &str) -> bool {
if s.is_empty() {
return true;
}
let first = s.chars().next().unwrap();
if first.is_ascii_alphabetic() || first == '_' {
return s[first.len_utf8()..]
.chars()
.any(|c| !c.is_ascii_alphanumeric() && !matches!(c, '_' | '.' | '-' | '@'));
}
if first.is_ascii_digit() || first == '-' {
let digits_part = if first == '-' { &s[1..] } else { s };
if digits_part.is_empty() {
return true;
}
if !digits_part.chars().all(|c| c.is_ascii_digit()) {
return true;
}
if digits_part.len() > 1 && digits_part.starts_with('0') {
return true;
}
return false;
}
true
}
fn escape_backtick_string(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('`', "\\`")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}