mod duplicate_keys;
mod json;
mod yaml;
use std::path::Path;
pub use json::parse_json;
pub use yaml::parse_yaml;
use crate::diagnostics::{
codes, emit, Diagnostic, DiagnosticCategory, DiagnosticReport, DiagnosticStage,
};
use crate::model::DataContract;
use crate::validation::{validate_with_options, ValidationOptions};
pub const MAX_PARSE_BYTES: u64 = 16 * 1024 * 1024;
#[derive(Debug, Clone)]
pub struct ParseResult {
pub contract: Option<DataContract>,
pub report: DiagnosticReport,
}
impl ParseResult {
pub fn into_contract(self) -> Result<DataContract, DiagnosticReport> {
match (self.contract, self.report.is_valid()) {
(Some(contract), true) => {
let validation_report = crate::validate(&contract);
if validation_report.is_valid() {
Ok(contract)
} else {
let mut report = self.report;
report.merge(validation_report);
Err(report)
}
}
(_, false) => Err(self.report),
(None, true) => Err(self.report),
}
}
#[must_use]
pub fn validate(self) -> DiagnosticReport {
self.validate_with_options(ValidationOptions::default_options())
}
#[must_use]
pub fn validate_with_options(self, options: ValidationOptions) -> DiagnosticReport {
let mut report = self.report;
if let Some(contract) = self.contract {
report.merge(validate_with_options(&contract, options));
}
report
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DocumentFormat {
Yaml,
Json,
}
impl DocumentFormat {
#[must_use]
pub fn from_path(path: &Path) -> Option<Self> {
match path.extension()?.to_str()? {
"yaml" | "yml" => Some(Self::Yaml),
"json" => Some(Self::Json),
_ => None,
}
}
}
#[must_use]
pub fn parse(content: &[u8], format: DocumentFormat) -> ParseResult {
if content.len() as u64 > MAX_PARSE_BYTES {
return failure_document_too_large();
}
match format {
DocumentFormat::Yaml => parse_yaml(content),
DocumentFormat::Json => parse_json(content),
}
}
pub fn parse_strict(
content: &[u8],
format: DocumentFormat,
) -> Result<DataContract, DiagnosticReport> {
parse(content, format).into_contract()
}
pub(crate) fn success(contract: DataContract) -> ParseResult {
ParseResult {
contract: Some(contract),
report: DiagnosticReport::new(),
}
}
pub(crate) fn failure_from_serde(code: &str, error: impl std::fmt::Display) -> ParseResult {
let message = error.to_string();
let mut diagnostic = Diagnostic::error(
code,
DiagnosticCategory::Syntax,
DiagnosticStage::Parse,
format!("failed to parse document: {message}"),
);
if let Some(object_ref) = extract_unknown_field_ref(&message) {
diagnostic = diagnostic
.with_object_ref(object_ref)
.with_remediation("remove the unknown field or use customProperties for extensions");
diagnostic.id = codes::UNKNOWN_FIELD.to_string();
} else if let Some(object_ref) = extract_serde_path(&message) {
diagnostic = diagnostic.with_object_ref(object_ref);
}
let mut report = DiagnosticReport::new();
emit(&mut report, diagnostic);
ParseResult {
contract: None,
report,
}
}
pub(crate) fn failure_duplicate_key(key: String) -> ParseResult {
let mut report = DiagnosticReport::new();
emit(
&mut report,
Diagnostic::error(
codes::DUPLICATE_KEY,
DiagnosticCategory::Syntax,
DiagnosticStage::Parse,
format!("duplicate key '{key}' in document"),
)
.with_object_ref(key)
.with_remediation("remove duplicate keys so each field appears once"),
);
ParseResult {
contract: None,
report,
}
}
fn failure_document_too_large() -> ParseResult {
let mut report = DiagnosticReport::new();
emit(
&mut report,
Diagnostic::error(
codes::DOCUMENT_TOO_LARGE,
DiagnosticCategory::Syntax,
DiagnosticStage::Parse,
format!("document exceeds maximum size of {MAX_PARSE_BYTES} bytes"),
)
.with_remediation("split the contract or reduce document size"),
);
ParseResult {
contract: None,
report,
}
}
fn extract_unknown_field_ref(message: &str) -> Option<String> {
let marker = "unknown field `";
let start = message.find(marker)? + marker.len();
let rest = &message[start..];
let end = rest.find('`')?;
Some(rest[..end].to_string())
}
fn extract_serde_path(message: &str) -> Option<String> {
if let Some(path) = message.strip_prefix("at ") {
return Some(path.trim().to_string());
}
let marker = " at line ";
if !message.contains(marker) {
return None;
}
message
.split_whitespace()
.find(|token| token.contains('.'))
.map(|token| token.trim_end_matches(',').to_string())
}
pub fn parse_file(path: impl AsRef<Path>) -> miette::Result<ParseResult> {
let path = path.as_ref();
let metadata = std::fs::metadata(path)
.map_err(|e| miette::miette!("failed to read {}: {e}", path.display()))?;
if metadata.len() > MAX_PARSE_BYTES {
return Ok(failure_document_too_large());
}
let content = std::fs::read(path)
.map_err(|e| miette::miette!("failed to read {}: {e}", path.display()))?;
let format = DocumentFormat::from_path(path).ok_or_else(|| {
miette::miette!(
"unsupported file extension for {}: expected .yaml, .yml, or .json",
path.display()
)
})?;
Ok(parse(&content, format))
}