feat(source): persist typed semantic reviews in schema five
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
//! Typed historical source leaves; live alias resolution remains in the parent.
|
||||
use super::*;
|
||||
use crate::semantic::{HistoricalSourceKind, SemanticBaseline, historical_source_slots};
|
||||
|
||||
fn error(ast: &Ast, message: impl Into<String>) -> Error {
|
||||
Error {
|
||||
code: "invalid_source",
|
||||
offset: ast.offset(),
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn raw(ast: &Ast) -> Result<Value, Error> {
|
||||
Ok(match ast {
|
||||
Ast::Literal(value, _) => value.clone(),
|
||||
Ast::Word(value, _) => Value::String(value.clone()),
|
||||
Ast::HistoricalUuid(value, _) => Value::String(value.to_string()),
|
||||
Ast::Reference(_, _) => {
|
||||
return Err(error(
|
||||
ast,
|
||||
"Historical semantic references require uuid \"...\", not @alias.",
|
||||
));
|
||||
}
|
||||
Ast::Array(values, _) => Value::Array(values.iter().map(raw).collect::<Result<_, _>>()?),
|
||||
Ast::Object(fields, _) => Value::Object(
|
||||
fields
|
||||
.iter()
|
||||
.map(|(key, value)| Ok((key.clone(), raw(value)?)))
|
||||
.collect::<Result<_, Error>>()?,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_tokens(
|
||||
ast: &Ast,
|
||||
path: &mut Vec<String>,
|
||||
slots: &BTreeMap<Vec<String>, HistoricalSourceKind>,
|
||||
) -> Result<(), Error> {
|
||||
match ast {
|
||||
Ast::Object(fields, _) => {
|
||||
for (key, value) in fields {
|
||||
path.push(key.clone());
|
||||
validate_tokens(value, path, slots)?;
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
Ast::Array(values, _) => {
|
||||
for (index, value) in values.iter().enumerate() {
|
||||
path.push(index.to_string());
|
||||
validate_tokens(value, path, slots)?;
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
_ => match (slots.get(path), ast) {
|
||||
(Some(HistoricalSourceKind::Reference), Ast::HistoricalUuid(..))
|
||||
| (Some(HistoricalSourceKind::Enum), Ast::Word(..)) => {}
|
||||
(Some(HistoricalSourceKind::Reference), _) => {
|
||||
return Err(error(
|
||||
ast,
|
||||
"This historical reference requires uuid \"...\".",
|
||||
));
|
||||
}
|
||||
(Some(HistoricalSourceKind::Enum), _) => {
|
||||
return Err(error(
|
||||
ast,
|
||||
"This typed historical enum requires an unquoted enum word.",
|
||||
));
|
||||
}
|
||||
(None, Ast::HistoricalUuid(..)) => {
|
||||
return Err(error(
|
||||
ast,
|
||||
"A uuid literal is allowed only in a typed historical reference slot.",
|
||||
));
|
||||
}
|
||||
(None, Ast::Word(..)) => {
|
||||
return Err(error(
|
||||
ast,
|
||||
"This historical text value requires a quoted string.",
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn resolve_baseline(
|
||||
ast: &Ast,
|
||||
aliases: &BTreeMap<String, Id>,
|
||||
version: u32,
|
||||
) -> Result<Value, Error> {
|
||||
let Ast::Object(fields, _) = ast else {
|
||||
return ast.resolve("baseline", aliases);
|
||||
};
|
||||
let mut resolved = Map::new();
|
||||
for (key, value) in fields {
|
||||
if key != "semantic" {
|
||||
resolved.insert(key.clone(), value.resolve(key, aliases)?);
|
||||
continue;
|
||||
}
|
||||
if version < 5 {
|
||||
return Err(error(
|
||||
value,
|
||||
"Semantic trace baselines require schema/source5; the older baseline contains prose only.",
|
||||
));
|
||||
}
|
||||
if matches!(value, Ast::Literal(Value::Null, _)) {
|
||||
resolved.insert(key.clone(), Value::Null);
|
||||
continue;
|
||||
}
|
||||
let json = raw(value)?;
|
||||
let baseline: SemanticBaseline = serde_json::from_value(json.clone())
|
||||
.map_err(|cause| error(value, format!("Invalid semantic baseline: {cause}")))?;
|
||||
let slots = historical_source_slots(&baseline).into_iter().collect();
|
||||
validate_tokens(value, &mut vec![], &slots)?;
|
||||
resolved.insert(key.clone(), json);
|
||||
}
|
||||
Ok(Value::Object(resolved))
|
||||
}
|
||||
|
||||
pub(super) fn write(out: &mut String, value: &Value, indent: usize) {
|
||||
if value.is_null() {
|
||||
out.push_str("none");
|
||||
return;
|
||||
}
|
||||
let baseline: SemanticBaseline =
|
||||
serde_json::from_value(value.clone()).expect("typed semantic baseline");
|
||||
let slots = historical_source_slots(&baseline).into_iter().collect();
|
||||
write_at(out, value, indent, &mut vec![], &slots);
|
||||
}
|
||||
|
||||
fn write_at(
|
||||
out: &mut String,
|
||||
value: &Value,
|
||||
indent: usize,
|
||||
path: &mut Vec<String>,
|
||||
slots: &BTreeMap<Vec<String>, HistoricalSourceKind>,
|
||||
) {
|
||||
match value {
|
||||
Value::Null => out.push_str("none"),
|
||||
Value::Bool(value) => out.push_str(if *value { "true" } else { "false" }),
|
||||
Value::Number(value) => out.push_str(&value.to_string()),
|
||||
Value::String(value) => match slots.get(path) {
|
||||
Some(HistoricalSourceKind::Reference) => {
|
||||
out.push_str("uuid ");
|
||||
out.push_str("e(value));
|
||||
}
|
||||
Some(HistoricalSourceKind::Enum) => out.push_str(value),
|
||||
None => out.push_str("e(value)),
|
||||
},
|
||||
Value::Array(values) => {
|
||||
out.push('[');
|
||||
for (index, value) in values.iter().enumerate() {
|
||||
if index != 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
path.push(index.to_string());
|
||||
write_at(out, value, indent, path, slots);
|
||||
path.pop();
|
||||
}
|
||||
out.push(']');
|
||||
}
|
||||
Value::Object(fields) => {
|
||||
out.push_str("{\n");
|
||||
for (key, value) in fields {
|
||||
out.push_str(&" ".repeat(indent + 1));
|
||||
out.push_str(key);
|
||||
out.push(' ');
|
||||
path.push(key.clone());
|
||||
write_at(out, value, indent + 1, path, slots);
|
||||
path.pop();
|
||||
out.push_str(";\n");
|
||||
}
|
||||
out.push_str(&" ".repeat(indent));
|
||||
out.push('}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recognize only complete typed facet rows. Their closed selector, with real
|
||||
/// identity provenance applied, owns comments when review rows reorder.
|
||||
pub(super) fn row_key(source: &str, origins: &BTreeMap<Id, Id>) -> Option<String> {
|
||||
let mut parser = Parser {
|
||||
source,
|
||||
tokens: strict_tokens(source).ok()?,
|
||||
cursor: 0,
|
||||
value_depth: 0,
|
||||
limits: ParseLimits::UNLIMITED,
|
||||
};
|
||||
let ast = parser.value().ok()?;
|
||||
let facet = serde_json::from_value(raw(&ast).ok()?).ok()?;
|
||||
let mut baseline = SemanticBaseline {
|
||||
facets: vec![facet],
|
||||
};
|
||||
crate::semantic::visit_semantic_references(&mut baseline, |id| {
|
||||
if let Some(original) = origins.get(id) {
|
||||
*id = *original;
|
||||
}
|
||||
});
|
||||
serde_json::to_string(&baseline.facets[0].selector())
|
||||
.ok()
|
||||
.map(|key| format!("semantic:{key}"))
|
||||
}
|
||||
Reference in New Issue
Block a user