///|
pub struct ValidationPolicy {
min_colors : Int
min_component_states : Int
min_logo_width : Int
require_metadata_license : Bool
require_component_tokens : Bool
} derive(Debug, Eq)
///|
pub fn ValidationPolicy::default() -> ValidationPolicy {
{
min_colors: 3,
min_component_states: 1,
min_logo_width: 16,
require_metadata_license: false,
require_component_tokens: false,
}
}
///|
pub fn ValidationPolicy::strict() -> ValidationPolicy {
{
min_colors: 3,
min_component_states: 2,
min_logo_width: 32,
require_metadata_license: true,
require_component_tokens: true,
}
}
///|
pub fn ValidationPolicy::with_min_colors(
self : ValidationPolicy,
count : Int,
) -> ValidationPolicy {
{ ..self, min_colors: if count < 0 { 0 } else { count } }
}
///|
pub fn ValidationPolicy::with_min_logo_width(
self : ValidationPolicy,
width : Int,
) -> ValidationPolicy {
{ ..self, min_logo_width: if width < 0 { 0 } else { width } }
}
///|
pub fn validate_identifier(text : String) -> Bool {
let value = normalize_identifier(text)
value != "" && value.length() <= 80
}
///|
pub fn validate_hex_token(text : String) -> Bool {
is_hex_color(text)
}
///|
pub fn validate_angle_degrees(angle : Int) -> Bool {
angle >= 0 && angle < 360
}
///|
pub fn validate_positive_integer(value : Int) -> Bool {
value > 0
}
///|
pub fn validate_non_negative_integer(value : Int) -> Bool {
value >= 0
}
///|
pub fn validate_name(text : String, max_length : Int) -> Bool {
let value = text.trim().to_owned()
value != "" &&
value.length() <= max_length &&
!value.contains("\n") &&
!value.contains("\r")
}
///|
pub fn AuditReport::error_count(self : AuditReport) -> Int {
let mut total = 0
for finding in self.findings {
if finding.severity == Severity::Error {
total += 1
}
}
total
}
///|
pub fn AuditReport::warning_count(self : AuditReport) -> Int {
let mut total = 0
for finding in self.findings {
if finding.severity == Severity::Warning {
total += 1
}
}
total
}
///|
pub fn AuditReport::info_count(self : AuditReport) -> Int {
let mut total = 0
for finding in self.findings {
if finding.severity == Severity::Info {
total += 1
}
}
total
}
///|
pub fn AuditReport::contains_path(self : AuditReport, path : String) -> Bool {
self.findings.any(fn(finding) { finding.path == path })
}
///|
pub fn AuditReport::paths(self : AuditReport) -> Array[String] {
let result : Array[String] = []
for finding in self.findings {
result.push(finding.path)
}
result
}
///|
pub fn AuditReport::to_markdown(self : AuditReport) -> String {
let lines : Array[String] = [
"# Validation",
"",
"Summary: " + self.summary(),
"",
"| Severity | Path | Message |",
"| --- | --- | --- |",
]
for finding in self.findings {
lines.push(
"| " +
finding.severity.label() +
" | `" +
finding.path +
"` | " +
finding.message +
" |",
)
}
lines.join("\n")
}
///|
pub fn BrandBook::validate_with_policy(
self : BrandBook,
policy : ValidationPolicy,
) -> AuditReport {
let findings = self.audit().findings
if self.palette.colors.length() < policy.min_colors {
findings.push(Finding::{
severity: Warning,
path: "palette.colors",
message: "policy expects at least " +
policy.min_colors.to_string() +
" color tokens",
})
}
if self.assets.primary_logo.min_width_px < policy.min_logo_width {
findings.push(Finding::{
severity: Error,
path: "assets.primary_logo.min_width_px",
message: "policy minimum logo width is " +
policy.min_logo_width.to_string() +
"px",
})
}
for i, component in self.components {
if component.states.length() < policy.min_component_states {
findings.push(Finding::{
severity: Warning,
path: "components[" + i.to_string() + "].states",
message: "policy expects at least " +
policy.min_component_states.to_string() +
" states",
})
}
if policy.require_component_tokens && component.tokens.length() == 0 {
findings.push(Finding::{
severity: Error,
path: "components[" + i.to_string() + "].tokens",
message: "policy requires token references for every component",
})
}
}
if policy.require_metadata_license &&
self.metadata.license.trim().to_owned() == "" {
findings.push(Finding::{
severity: Error,
path: "metadata.license",
message: "strict policy requires an explicit license",
})
}
AuditReport::from_findings(findings)
}
///|
pub fn BrandBook::is_publishable(self : BrandBook) -> Bool {
!self.audit().has_errors() &&
self.name.trim().to_owned() != "" &&
self.palette.colors.length() >= 3
}
///|
pub fn BrandBook::quality_score(self : BrandBook) -> Int {
let report = self.audit()
let mut score = 100 - report.error_count() * 20 - report.warning_count() * 5
if self.components.length() >= 3 {
score += 3
}
if self.typography.scale.length() >= 3 {
score += 3
}
if self.forbidden.length() >= 2 {
score += 2
}
if score < 0 {
0
} else if score > 100 {
100
} else {
score
}
}
///|
pub fn Palette::validate_colors(self : Palette) -> AuditReport {
let findings : Array[Finding] = []
for i, color in self.colors {
if !validate_identifier(color.name) {
findings.push(Finding::{
severity: Error,
path: "colors[" + i.to_string() + "].name",
message: "invalid token identifier",
})
}
if !validate_hex_token(color.value) {
findings.push(Finding::{
severity: Error,
path: "colors[" + i.to_string() + "].value",
message: "invalid six-digit hex token",
})
}
}
AuditReport::from_findings(findings)
}
///|
pub fn Typography::validate_scale(self : Typography) -> AuditReport {
let findings : Array[Finding] = []
let mut previous = 0
for i, step in self.scale {
if !validate_positive_integer(step.size_px) {
findings.push(Finding::{
severity: Error,
path: "scale[" + i.to_string() + "].size_px",
message: "size must be positive",
})
}
if step.size_px < previous {
findings.push(Finding::{
severity: Warning,
path: "scale[" + i.to_string() + "].size_px",
message: "type scale is not ascending",
})
}
previous = step.size_px
}
AuditReport::from_findings(findings)
}
///|
pub fn Meta::is_complete(self : Meta) -> Bool {
self.source.trim().to_owned() != "" && self.license.trim().to_owned() != ""
}