1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
//! Serialize Netsuke diagnostics into a stable JSON document.
//!
//! This module owns Netsuke's machine-readable diagnostic schema rather than
//! exposing upstream formatter output directly. The schema is intentionally
//! small, versioned, and derived from `miette` diagnostics when available.
use miette::{Diagnostic, Report};
use serde::Serialize;
use std::error::Error as StdError;
use std::io::{self, Write};
use std::process::ExitCode;
use crate::json_envelope::{GeneratorInfo, SCHEMA_VERSION};
#[path = "diagnostic_json_support.rs"]
mod diagnostic_json_support;
use self::diagnostic_json_support::{
DiagnosticSource, DiagnosticSpan, collect_diagnostic_causes, collect_error_causes,
diagnostic_help, diagnostic_url, extract_source_and_labels, fallback_payload, severity_name,
};
/// Render a [`miette::Report`] as Netsuke's JSON diagnostic document.
///
/// # Errors
///
/// Returns an error if the document cannot be serialized to JSON.
pub fn render_report_json(report: &Report) -> serde_json::Result<String> {
render_diagnostic_json(report.as_ref())
}
/// Render a [`miette::Diagnostic`] as Netsuke's JSON diagnostic document.
///
/// # Errors
///
/// Returns an error if the document cannot be serialized to JSON.
pub fn render_diagnostic_json(diagnostic: &dyn Diagnostic) -> serde_json::Result<String> {
serde_json::to_string_pretty(&DiagnosticDocument::from_diagnostic(diagnostic))
}
/// Render a plain error as Netsuke's JSON diagnostic document.
///
/// This path is used for startup failures that do not carry `miette`
/// diagnostics, such as clap or configuration-load errors.
///
/// # Errors
///
/// Returns an error if the document cannot be serialized to JSON.
pub fn render_error_json(error: &(dyn StdError + 'static)) -> serde_json::Result<String> {
serde_json::to_string_pretty(&DiagnosticDocument::from_error(error))
}
/// Emit a rendered JSON diagnostic document to `stderr`, falling back to a
/// minimal schema-compatible payload when serialization fails.
#[must_use]
pub fn emit_or_fallback(result: serde_json::Result<String>) -> ExitCode {
let payload = result.unwrap_or_else(|err| fallback_payload(&err));
drop(writeln!(io::stderr(), "{payload}"));
ExitCode::FAILURE
}
/// The versioned JSON document wrapping one or more diagnostic entries.
#[derive(Debug, Serialize, PartialEq, Eq)]
struct DiagnosticDocument {
/// Schema version of the diagnostic envelope.
schema_version: u32,
/// Generator identity stamped from the build environment.
generator: GeneratorInfo,
/// The rendered diagnostic entries.
diagnostics: Vec<DiagnosticEntry>,
}
impl DiagnosticDocument {
/// Build a document from a miette diagnostic.
fn from_diagnostic(diagnostic: &dyn Diagnostic) -> Self {
Self {
schema_version: SCHEMA_VERSION,
generator: GeneratorInfo::current(),
diagnostics: vec![DiagnosticEntry::from_diagnostic(diagnostic)],
}
}
/// Build a document from a plain standard error.
fn from_error(error: &(dyn StdError + 'static)) -> Self {
Self {
schema_version: SCHEMA_VERSION,
generator: GeneratorInfo::current(),
diagnostics: vec![DiagnosticEntry::from_error(error)],
}
}
}
/// One miette diagnostic rendered into the JSON schema.
#[derive(Debug, Serialize, PartialEq, Eq)]
struct DiagnosticEntry {
/// The rendered diagnostic message.
message: String,
/// The diagnostic's machine-readable code, when it has one.
code: Option<String>,
/// The schema's severity name: `error`, `warning`, or `advice`.
severity: &'static str,
/// The diagnostic's help text, when provided.
help: Option<String>,
/// The documentation URL, when provided.
url: Option<String>,
/// The cause chain rendered as strings.
causes: Vec<String>,
/// The named source file, when labels are available.
source: Option<DiagnosticSource>,
/// The primary labelled span, when the source carries labels.
primary_span: Option<DiagnosticSpan>,
/// Every labelled span of the diagnostic.
labels: Vec<DiagnosticSpan>,
/// Diagnostics related to this one.
related: Vec<Self>,
}
impl DiagnosticEntry {
/// Build an entry from a miette diagnostic, capturing its spans and causes.
fn from_diagnostic(diagnostic: &dyn Diagnostic) -> Self {
let (source, primary_span, labels) = extract_source_and_labels(diagnostic);
let related = diagnostic
.related()
.map(|items| items.map(Self::from_diagnostic).collect())
.unwrap_or_default();
Self {
message: diagnostic.to_string(),
code: diagnostic.code().map(|value| value.to_string()),
severity: severity_name(diagnostic.severity()),
help: diagnostic_help(diagnostic),
url: diagnostic_url(diagnostic),
causes: collect_diagnostic_causes(diagnostic),
source,
primary_span,
labels,
related,
}
}
/// Build an entry from a plain standard error, without span data.
fn from_error(error: &(dyn StdError + 'static)) -> Self {
Self {
message: error.to_string(),
code: None,
severity: "error",
help: None,
url: None,
causes: collect_error_causes(error),
source: None,
primary_span: None,
labels: Vec::new(),
related: Vec::new(),
}
}
}
#[path = "diagnostic_json_tests.rs"]
#[cfg(test)]
mod tests;