use serde::Serialize;
use crate::{AozoraTree, Diagnostic, DiagnosticSource, Severity, Span};
pub const SCHEMA_VERSION: u32 = 1;
#[must_use]
pub fn serialize_diagnostics(diagnostics: &[Diagnostic]) -> String {
let entries: Vec<DiagnosticWire> = diagnostics.iter().map(DiagnosticWire::from).collect();
serialize_envelope(&entries)
}
#[must_use]
pub fn serialize_nodes(tree: &AozoraTree<'_>) -> String {
let entries: Vec<NodeWire> = tree
.source_nodes()
.iter()
.map(|sn| NodeWire {
kind: sn.node.kind().as_camel_case(),
span: sn.source_span.into(),
})
.collect();
serialize_envelope(&entries)
}
#[must_use]
pub fn serialize_pairs(tree: &AozoraTree<'_>) -> String {
let entries: Vec<PairWire> = tree
.pairs()
.iter()
.map(|link| PairWire {
kind: link.kind.as_camel_case(),
open: link.open.into(),
close: link.close.into(),
})
.collect();
serialize_envelope(&entries)
}
#[must_use]
pub fn serialize_container_pairs(tree: &AozoraTree<'_>) -> String {
let entries: Vec<ContainerPairWire> = tree
.container_pairs()
.iter()
.map(|pair| ContainerPairWire {
kind: container_kind_str(pair.kind),
open: OffsetWire {
offset: pair.open.get(),
},
close: OffsetWire {
offset: pair.close.get(),
},
})
.collect();
serialize_envelope(&entries)
}
const fn container_kind_str(kind: aozora_syntax::ContainerKind) -> &'static str {
use aozora_syntax::ContainerKind;
match kind {
ContainerKind::Indent { .. } => "indent",
ContainerKind::Warichu => "warichu",
ContainerKind::Keigakomi => "keigakomi",
ContainerKind::AlignEnd { .. } => "alignEnd",
_ => "unknown",
}
}
#[derive(Serialize)]
struct Envelope<'a, T> {
schema_version: u32,
data: &'a [T],
}
#[cfg(feature = "schema")]
#[must_use]
pub fn schema_diagnostics() -> serde_json::Value {
envelope_schema(
"AozoraDiagnosticsEnvelope",
"Envelope returned by aozora::wire::serialize_diagnostics.",
schemars::schema_for!(DiagnosticWire),
)
}
#[cfg(feature = "schema")]
#[must_use]
pub fn schema_nodes() -> serde_json::Value {
envelope_schema(
"AozoraNodesEnvelope",
"Envelope returned by aozora::wire::serialize_nodes.",
schemars::schema_for!(NodeWire),
)
}
#[cfg(feature = "schema")]
#[must_use]
pub fn schema_pairs() -> serde_json::Value {
envelope_schema(
"AozoraPairsEnvelope",
"Envelope returned by aozora::wire::serialize_pairs.",
schemars::schema_for!(PairWire),
)
}
#[cfg(feature = "schema")]
#[must_use]
pub fn schema_container_pairs() -> serde_json::Value {
envelope_schema(
"AozoraContainerPairsEnvelope",
"Envelope returned by aozora::wire::serialize_container_pairs.",
schemars::schema_for!(ContainerPairWire),
)
}
#[cfg(feature = "schema")]
fn envelope_schema(
title: &str,
description: &str,
item_schema: schemars::Schema,
) -> serde_json::Value {
serde_json::json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": title,
"description": description,
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "data"],
"properties": {
"schema_version": {
"description": "Wire schema version. See aozora::wire::SCHEMA_VERSION.",
"type": "integer",
"const": SCHEMA_VERSION,
},
"data": {
"description": "Per-entry payload array; one item per emitted diagnostic / node / pair.",
"type": "array",
"items": item_schema.to_value(),
},
},
})
}
fn serialize_envelope<T: Serialize>(data: &[T]) -> String {
let env = Envelope {
schema_version: SCHEMA_VERSION,
data,
};
serde_json::to_string(&env)
.unwrap_or_else(|_| format!(r#"{{"schema_version":{SCHEMA_VERSION},"data":[]}}"#))
}
#[derive(Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
struct SpanWire {
start: u32,
end: u32,
}
impl From<Span> for SpanWire {
fn from(s: Span) -> Self {
Self {
start: s.start,
end: s.end,
}
}
}
#[derive(Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
struct DiagnosticWire {
kind: &'static str,
severity: &'static str,
source: &'static str,
span: SpanWire,
#[serde(skip_serializing_if = "Option::is_none")]
codepoint: Option<char>,
}
impl From<&Diagnostic> for DiagnosticWire {
fn from(d: &Diagnostic) -> Self {
let codepoint = match d {
Diagnostic::SourceContainsPua { codepoint, .. } => Some(*codepoint),
_ => None,
};
let kind = d.code().rsplit("::").next().unwrap_or("unknown");
Self {
kind,
severity: severity_str(d.severity()),
source: source_str(d.source()),
span: d.span().into(),
codepoint,
}
}
}
const fn severity_str(s: Severity) -> &'static str {
match s {
Severity::Warning => "warning",
Severity::Note => "note",
Severity::Error | _ => "error",
}
}
const fn source_str(s: DiagnosticSource) -> &'static str {
match s {
DiagnosticSource::Source => "source",
DiagnosticSource::Internal | _ => "internal",
}
}
#[derive(Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
struct NodeWire {
kind: &'static str,
span: SpanWire,
}
#[derive(Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
struct PairWire {
kind: &'static str,
open: SpanWire,
close: SpanWire,
}
#[derive(Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
struct ContainerPairWire {
kind: &'static str,
open: OffsetWire,
close: OffsetWire,
}
#[derive(Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
struct OffsetWire {
offset: u32,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Document;
#[test]
fn schema_version_is_one() {
assert_eq!(SCHEMA_VERSION, 1);
}
#[test]
fn empty_diagnostics_round_trip_envelope() {
let json = serialize_diagnostics(&[]);
assert_eq!(json, r#"{"schema_version":1,"data":[]}"#);
}
#[test]
fn empty_nodes_round_trip_envelope() {
let doc = Document::new("plain");
let tree = doc.parse();
let json = serialize_nodes(&tree);
assert_eq!(json, r#"{"schema_version":1,"data":[]}"#);
}
#[test]
fn empty_pairs_round_trip_envelope() {
let doc = Document::new("plain");
let tree = doc.parse();
let json = serialize_pairs(&tree);
assert_eq!(json, r#"{"schema_version":1,"data":[]}"#);
}
#[test]
fn pua_collision_serialises_as_warning_kind() {
let doc = Document::new("abc\u{E001}def");
let tree = doc.parse();
let json = serialize_diagnostics(tree.diagnostics());
assert!(json.contains(r#""schema_version":1"#));
assert!(json.contains(r#""kind":"source_contains_pua""#));
assert!(json.contains(r#""codepoint":"""#) || json.contains(r#""codepoint":""#));
}
#[test]
fn ruby_serialises_with_kind_ruby_in_nodes() {
let doc = Document::new("|青梅《おうめ》");
let tree = doc.parse();
let json = serialize_nodes(&tree);
assert!(json.contains(r#""kind":"ruby""#));
assert!(json.contains(r#""schema_version":1"#));
}
#[test]
fn ruby_serialises_in_pairs() {
let doc = Document::new("|青梅《おうめ》");
let tree = doc.parse();
let json = serialize_pairs(&tree);
assert!(json.contains(r#""kind":"ruby""#));
assert!(json.contains(r#""open":"#));
assert!(json.contains(r#""close":"#));
}
#[test]
fn pair_kind_camel_case_covers_all_known_kinds() {
use crate::PairKind;
assert_eq!(PairKind::Bracket.as_camel_case(), "bracket");
assert_eq!(PairKind::Ruby.as_camel_case(), "ruby");
assert_eq!(PairKind::DoubleRuby.as_camel_case(), "doubleRuby");
assert_eq!(PairKind::Tortoise.as_camel_case(), "tortoise");
assert_eq!(PairKind::Quote.as_camel_case(), "quote");
}
}