///|
/// The import pipeline keeps parsing, policy validation, and rendering as
/// explicit stages so callers can inspect partial failures.
pub enum PipelineStage {
ReadInput
ParseDocument
ValidateSchema
ValidateAccessibility
ValidateContent
BuildExports
} derive(Debug, Eq)
///|
pub fn PipelineStage::label(self : PipelineStage) -> String {
match self {
ReadInput => "read input"
ParseDocument => "parse document"
ValidateSchema => "validate schema"
ValidateAccessibility => "validate accessibility"
ValidateContent => "validate content"
BuildExports => "build exports"
}
}
///|
pub fn PipelineStage::ordinal(self : PipelineStage) -> Int {
match self {
ReadInput => 1
ParseDocument => 2
ValidateSchema => 3
ValidateAccessibility => 4
ValidateContent => 5
BuildExports => 6
}
}
///|
pub struct PipelineEvent {
stage : PipelineStage
passed : Bool
message : String
} derive(Debug, Eq)
///|
pub fn PipelineEvent::new(
stage : PipelineStage,
passed : Bool,
message : String,
) -> PipelineEvent {
{ stage, passed, message }
}
///|
pub fn PipelineEvent::status(self : PipelineEvent) -> String {
if self.passed {
"pass"
} else {
"fail"
}
}
///|
pub struct PipelineResult {
value : BrandBook?
events : Array[PipelineEvent]
diagnostics : DiagnosticBag
exports : RenderBundle?
} derive(Debug, Eq)
///|
pub fn PipelineResult::succeeded(self : PipelineResult) -> Bool {
self.value is Some(_) &&
!self.diagnostics.has_errors() &&
self.events.all(fn(event) { event.passed })
}
///|
pub fn PipelineResult::event_count(self : PipelineResult) -> Int {
self.events.length()
}
///|
pub fn PipelineResult::failed_events(
self : PipelineResult,
) -> Array[PipelineEvent] {
self.events.filter(fn(event) { !event.passed })
}
///|
pub fn PipelineResult::event(
self : PipelineResult,
stage : PipelineStage,
) -> PipelineEvent? {
for event in self.events {
if event.stage == stage {
return Some(event)
}
}
None
}
///|
pub fn PipelineResult::summary(self : PipelineResult) -> String {
let passed = self.events.filter(fn(event) { event.passed }).length()
passed.to_string() +
"/" +
self.events.length().to_string() +
" stages passed; " +
self.diagnostics.summary()
}
///|
pub fn PipelineResult::to_markdown(self : PipelineResult) -> String {
let status = if self.succeeded() { "success" } else { "failed" }
let lines : Array[String] = [
"# Import pipeline",
"",
"Status: **" + status + "**",
"",
"| Stage | Status | Message |",
"| --- | --- | --- |",
]
for event in self.events {
lines.push(
"| " +
event.stage.label() +
" | " +
event.status() +
" | " +
event.message +
" |",
)
}
lines.push("")
lines.push("Diagnostics: " + self.diagnostics.summary())
lines.join("\n")
}
///|
pub fn PipelineResult::to_json(self : PipelineResult) -> String {
"{\"success\":" +
self.succeeded().to_string() +
",\"summary\":\"" +
json_escape(self.summary()) +
"\",\"events\":[" +
self.events
.map(fn(event) {
"{\"stage\":\"" +
event.stage.label() +
"\",\"passed\":" +
event.passed.to_string() +
",\"message\":\"" +
json_escape(event.message) +
"\"}"
})
.join(",") +
"]}"
}
///|
pub fn import_brand_book(
text : String,
format : DocumentFormat,
) -> PipelineResult {
let events : Array[PipelineEvent] = []
let diagnostics = DiagnosticBag::empty()
if text.trim().to_owned() == "" {
events.push(PipelineEvent::new(ReadInput, false, "input is empty"))
return {
value: None,
events,
diagnostics: diagnostics.push(
Diagnostic::error("$", "document is empty", None),
),
exports: None,
}
}
events.push(
PipelineEvent::new(ReadInput, true, text.length().to_string() + " bytes"),
)
let parsed = parse_brand_document_as(text, format)
let parse_passed = !parsed.diagnostics.has_errors()
events.push(
PipelineEvent::new(
ParseDocument,
parse_passed,
parsed.diagnostics.summary(),
),
)
if !parse_passed {
return {
value: None,
events,
diagnostics: parsed.diagnostics,
exports: None,
}
}
let book = parsed.value.unwrap()
let schema = book.validate_with_policy(ValidationPolicy::strict())
events.push(
PipelineEvent::new(ValidateSchema, !schema.has_errors(), schema.summary()),
)
let accessibility = book.accessibility_score()
events.push(
PipelineEvent::new(
ValidateAccessibility,
accessibility >= 70,
accessibility.to_string() + "/100",
),
)
let content = book.audit_content(ContentPolicy::default())
events.push(
PipelineEvent::new(
ValidateContent,
!content.has_errors(),
content.summary(),
),
)
let exports = book.render_bundle()
events.push(
PipelineEvent::new(
BuildExports,
exports.non_empty().length() == exports.len(),
exports.total_bytes().to_string() + " bytes",
),
)
let mut merged = parsed.diagnostics
for finding in schema.findings {
merged = merged.push(
Diagnostic::warning(finding.path, finding.message, None),
)
}
{ value: Some(book), events, diagnostics: merged, exports: Some(exports) }
}
///|
pub fn import_brand_book_auto(text : String) -> PipelineResult {
import_brand_book(text, DocumentFormat::auto())
}
///|
pub fn validate_pipeline(book : BrandBook) -> PipelineResult {
let events : Array[PipelineEvent] = []
let schema = book.validate_with_policy(ValidationPolicy::strict())
events.push(
PipelineEvent::new(ValidateSchema, !schema.has_errors(), schema.summary()),
)
let accessibility = book.accessibility_score()
events.push(
PipelineEvent::new(
ValidateAccessibility,
accessibility >= 70,
accessibility.to_string() + "/100",
),
)
let content = book.audit_content(ContentPolicy::default())
events.push(
PipelineEvent::new(
ValidateContent,
!content.has_errors(),
content.summary(),
),
)
let exports = book.render_bundle()
events.push(
PipelineEvent::new(
BuildExports,
exports.total_bytes() > 0,
exports.total_bytes().to_string() + " bytes",
),
)
let diagnostics = AuditReport::from_findings(schema.findings)
{
value: Some(book),
events,
diagnostics: diagnostics_to_bag(diagnostics),
exports: Some(exports),
}
}
///|
pub fn BrandBook::pipeline(self : BrandBook) -> PipelineResult {
validate_pipeline(self)
}
///|
fn diagnostics_to_bag(report : AuditReport) -> DiagnosticBag {
let mut bag = DiagnosticBag::empty()
for finding in report.findings {
bag = bag.push(Diagnostic::warning(finding.path, finding.message, None))
}
bag
}