use std::fmt;
use serde::Serialize;
macro_rules! issue_codes {
($( $variant:ident => $wire:literal, $stage:literal );* $(;)?) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum A3IssueCode {
$(
#[doc = $wire]
$variant,
)*
}
impl A3IssueCode {
pub fn code(self) -> &'static str {
match self {
$( A3IssueCode::$variant => $wire, )*
}
}
pub fn stage(self) -> u8 {
match self {
$( A3IssueCode::$variant => $stage, )*
}
}
pub const ALL: &'static [A3IssueCode] = &[ $( A3IssueCode::$variant, )* ];
}
};
}
issue_codes! {
DocNotObject => "A3E_DOC_NOT_OBJECT", 1;
EnvelopeSchemaMissing => "A3E_ENVELOPE_SCHEMA_MISSING", 1;
EnvelopeSchemaMismatch => "A3E_ENVELOPE_SCHEMA_MISMATCH", 1;
EnvelopeVersionMissing => "A3E_ENVELOPE_VERSION_MISSING", 1;
EnvelopeVersionMismatch => "A3E_ENVELOPE_VERSION_MISMATCH", 1;
UnknownField => "A3E_UNKNOWN_FIELD", 2;
SeqMissing => "A3E_SEQ_MISSING", 2;
SeqNotString => "A3E_SEQ_NOT_STRING", 2;
SeqTooShort => "A3E_SEQ_TOO_SHORT", 2;
SeqCharset => "A3E_SEQ_CHARSET", 2;
AnnotationsNotObject => "A3E_ANNOTATIONS_NOT_OBJECT", 2;
MetadataNotObject => "A3E_METADATA_NOT_OBJECT", 2;
MetadataFieldNotString => "A3E_METADATA_FIELD_NOT_STRING", 2;
FamilyNotObject => "A3E_FAMILY_NOT_OBJECT", 2;
VariantListNotArray => "A3E_VARIANT_LIST_NOT_ARRAY", 2;
NameEmpty => "A3E_NAME_EMPTY", 2;
EntryNotObject => "A3E_ENTRY_NOT_OBJECT", 2;
EntryTypeNotString => "A3E_ENTRY_TYPE_NOT_STRING", 2;
IndexMissing => "A3E_INDEX_MISSING", 2;
IndexNotArray => "A3E_INDEX_NOT_ARRAY", 2;
IndexElementType => "A3E_INDEX_ELEMENT_TYPE", 2;
IndexMixed => "A3E_INDEX_MIXED", 2;
RangeArity => "A3E_RANGE_ARITY", 2;
RangeEndpointNotInteger => "A3E_RANGE_ENDPOINT_NOT_INTEGER", 2;
VariantNotObject => "A3E_VARIANT_NOT_OBJECT", 2;
VariantPositionMissing => "A3E_VARIANT_POSITION_MISSING", 2;
VariantPositionNotInteger => "A3E_VARIANT_POSITION_NOT_INTEGER", 2;
PosNotPositive => "A3E_POS_NOT_POSITIVE", 3;
PosDuplicate => "A3E_POS_DUPLICATE", 3;
RangeEndpointNotPositive => "A3E_RANGE_ENDPOINT_NOT_POSITIVE", 3;
RangeOrder => "A3E_RANGE_ORDER", 3;
RangeOverlap => "A3E_RANGE_OVERLAP", 3;
VariantPositionNotPositive => "A3E_VARIANT_POSITION_NOT_POSITIVE", 3;
PosOutOfBounds => "A3E_POS_OUT_OF_BOUNDS", 4;
RangeOutOfBounds => "A3E_RANGE_OUT_OF_BOUNDS", 4;
VariantOutOfBounds => "A3E_VARIANT_OUT_OF_BOUNDS", 4;
VariantResidueMismatch => "A3E_VARIANT_RESIDUE_MISMATCH", 4;
}
impl fmt::Display for A3IssueCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.code())
}
}
impl Serialize for A3IssueCode {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.code())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct A3Issue {
pub code: A3IssueCode,
pub path: String,
pub message: String,
}
impl A3Issue {
pub fn new(code: A3IssueCode, path: impl Into<String>, message: impl Into<String>) -> Self {
A3Issue {
code,
path: path.into(),
message: message.into(),
}
}
pub fn stage(&self) -> u8 {
self.code.stage()
}
}
impl fmt::Display for A3Issue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let where_ = if self.path.is_empty() {
"<document>"
} else {
&self.path
};
write!(f, "[{}] {}: {}", self.code.code(), where_, self.message)
}
}
pub fn escape_token(token: &str) -> String {
token.replace('~', "~0").replace('/', "~1")
}
pub fn pointer(base: &str, token: &str) -> String {
format!("{base}/{}", escape_token(token))
}
pub fn pointer_index(base: &str, index: usize) -> String {
format!("{base}/{index}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn codes_are_unique() {
let mut seen = std::collections::HashSet::new();
for c in A3IssueCode::ALL {
assert!(seen.insert(c.code()), "duplicate code: {}", c.code());
}
assert_eq!(
seen.len(),
37,
"registry size changed — update spec/error-codes.md"
);
}
#[test]
fn every_code_has_a_valid_stage() {
for c in A3IssueCode::ALL {
assert!(
(1..=4).contains(&c.stage()),
"{} has stage {}",
c.code(),
c.stage()
);
}
}
#[test]
fn escapes_tilde_before_slash() {
assert_eq!(escape_token("a~1b"), "a~01b");
assert_eq!(escape_token("a/b"), "a~1b");
assert_eq!(escape_token("a~b"), "a~0b");
assert_eq!(escape_token("a.b"), "a.b");
assert_eq!(escape_token("$schema"), "$schema");
}
#[test]
fn builds_pointers() {
assert_eq!(
pointer("/annotations/site", "a/b"),
"/annotations/site/a~1b"
);
assert_eq!(pointer("", "$schema"), "/$schema");
assert_eq!(pointer("/annotations/site", ""), "/annotations/site/");
assert_eq!(
pointer_index("/annotations/variant", 3),
"/annotations/variant/3"
);
}
}