use core::fmt;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DiagnosticLimits {
pub max_items: usize,
pub max_preview_bytes: usize,
}
impl Default for DiagnosticLimits {
fn default() -> Self {
Self {
max_items: 32,
max_preview_bytes: 256,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ByteSpan {
pub start: usize,
pub end: usize,
}
impl ByteSpan {
#[must_use]
pub const fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TextPreview {
byte_len: usize,
text: String,
truncated: bool,
}
impl TextPreview {
fn new(text: &str, max_bytes: usize) -> Self {
let mut end = text.len().min(max_bytes);
while !text.is_char_boundary(end) {
end -= 1;
}
Self {
byte_len: text.len(),
text: text[..end].to_owned(),
truncated: end < text.len(),
}
}
#[must_use]
pub const fn byte_len(&self) -> usize {
self.byte_len
}
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
#[must_use]
pub const fn is_truncated(&self) -> bool {
self.truncated
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MismatchDetails {
source_range: ByteSpan,
expected: TextPreview,
actual: TextPreview,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ValidationReport {
diagnostics: Vec<EditError>,
total_diagnostics: usize,
truncated: bool,
}
impl ValidationReport {
pub(crate) fn new(diagnostics: Vec<EditError>, total_diagnostics: usize) -> Self {
Self {
truncated: diagnostics.len() < total_diagnostics,
diagnostics,
total_diagnostics,
}
}
#[must_use]
pub const fn is_valid(&self) -> bool {
self.total_diagnostics == 0
}
#[must_use]
pub fn diagnostics(&self) -> &[EditError] {
&self.diagnostics
}
#[must_use]
pub const fn total_diagnostics(&self) -> usize {
self.total_diagnostics
}
#[must_use]
pub const fn is_truncated(&self) -> bool {
self.truncated
}
}
impl MismatchDetails {
#[must_use]
pub const fn source_range(&self) -> ByteSpan {
self.source_range
}
#[must_use]
pub const fn expected(&self) -> &TextPreview {
&self.expected
}
#[must_use]
pub const fn actual(&self) -> &TextPreview {
&self.actual
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ErrorCode {
SchemaMismatch,
InvalidPlan,
InvalidFile,
InvalidEdit,
InvalidPath,
UnprovenEdit,
PlanTooLarge,
PositionOutOfRange,
BeforeMismatch,
OverlappingEdits,
OutputTooLarge,
ValidationRejected,
}
impl ErrorCode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::SchemaMismatch => "SCHEMA_MISMATCH",
Self::InvalidPlan => "INVALID_PLAN",
Self::InvalidFile => "INVALID_FILE",
Self::InvalidEdit => "INVALID_EDIT",
Self::InvalidPath => "INVALID_PATH",
Self::UnprovenEdit => "UNPROVEN_EDIT",
Self::PlanTooLarge => "PLAN_TOO_LARGE",
Self::PositionOutOfRange => "POSITION_OUT_OF_RANGE",
Self::BeforeMismatch => "BEFORE_MISMATCH",
Self::OverlappingEdits => "OVERLAPPING_EDITS",
Self::OutputTooLarge => "OUTPUT_TOO_LARGE",
Self::ValidationRejected => "VALIDATION_REJECTED",
}
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EditError {
code: ErrorCode,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
file_index: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
edit_index: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
related_edit_index: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
mismatch: Option<Box<MismatchDetails>>,
}
impl EditError {
pub(crate) fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
file_index: None,
edit_index: None,
related_edit_index: None,
mismatch: None,
}
}
pub(crate) fn before_mismatch(
source_range: ByteSpan,
expected: &str,
actual: &str,
limits: DiagnosticLimits,
) -> Self {
Self {
code: ErrorCode::BeforeMismatch,
message: "source text does not match the exact before guard".to_owned(),
file_index: None,
edit_index: None,
related_edit_index: None,
mismatch: Some(Box::new(MismatchDetails {
source_range,
expected: TextPreview::new(expected, limits.max_preview_bytes),
actual: TextPreview::new(actual, limits.max_preview_bytes),
})),
}
}
pub(crate) const fn at_file(mut self, file_index: usize) -> Self {
self.file_index = Some(file_index);
self
}
pub(crate) const fn at_edit(mut self, edit_index: usize) -> Self {
self.edit_index = Some(edit_index);
self
}
pub(crate) const fn with_related_edit(mut self, edit_index: usize) -> Self {
self.related_edit_index = Some(edit_index);
self
}
#[must_use]
pub const fn code(&self) -> ErrorCode {
self.code
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
#[must_use]
pub const fn file_index(&self) -> Option<usize> {
self.file_index
}
#[must_use]
pub const fn edit_index(&self) -> Option<usize> {
self.edit_index
}
#[must_use]
pub const fn related_edit_index(&self) -> Option<usize> {
self.related_edit_index
}
#[must_use]
pub fn mismatch(&self) -> Option<&MismatchDetails> {
self.mismatch.as_deref()
}
}
impl fmt::Display for EditError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}: {}", self.code, self.message)
}
}
impl std::error::Error for EditError {}