1use std::fmt;
9
10use serde::Serialize;
11
12macro_rules! issue_codes {
23 ($( $variant:ident => $wire:literal, $stage:literal );* $(;)?) => {
24 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29 pub enum A3IssueCode {
30 $(
31 #[doc = $wire]
32 $variant,
33 )*
34 }
35
36 impl A3IssueCode {
37 pub fn code(self) -> &'static str {
39 match self {
40 $( A3IssueCode::$variant => $wire, )*
41 }
42 }
43
44 pub fn stage(self) -> u8 {
46 match self {
47 $( A3IssueCode::$variant => $stage, )*
48 }
49 }
50
51 pub const ALL: &'static [A3IssueCode] = &[ $( A3IssueCode::$variant, )* ];
53 }
54 };
55}
56
57issue_codes! {
58 DocNotObject => "A3E_DOC_NOT_OBJECT", 1;
60 EnvelopeSchemaMissing => "A3E_ENVELOPE_SCHEMA_MISSING", 1;
61 EnvelopeSchemaMismatch => "A3E_ENVELOPE_SCHEMA_MISMATCH", 1;
62 EnvelopeVersionMissing => "A3E_ENVELOPE_VERSION_MISSING", 1;
63 EnvelopeVersionMismatch => "A3E_ENVELOPE_VERSION_MISMATCH", 1;
64
65 UnknownField => "A3E_UNKNOWN_FIELD", 2;
67 SeqMissing => "A3E_SEQ_MISSING", 2;
68 SeqNotString => "A3E_SEQ_NOT_STRING", 2;
69 SeqTooShort => "A3E_SEQ_TOO_SHORT", 2;
70 SeqCharset => "A3E_SEQ_CHARSET", 2;
71 AnnotationsNotObject => "A3E_ANNOTATIONS_NOT_OBJECT", 2;
72 MetadataNotObject => "A3E_METADATA_NOT_OBJECT", 2;
73 MetadataFieldNotString => "A3E_METADATA_FIELD_NOT_STRING", 2;
74 FamilyNotObject => "A3E_FAMILY_NOT_OBJECT", 2;
75 VariantListNotArray => "A3E_VARIANT_LIST_NOT_ARRAY", 2;
76 NameEmpty => "A3E_NAME_EMPTY", 2;
77 EntryNotObject => "A3E_ENTRY_NOT_OBJECT", 2;
78 EntryTypeNotString => "A3E_ENTRY_TYPE_NOT_STRING", 2;
79 IndexMissing => "A3E_INDEX_MISSING", 2;
80 IndexNotArray => "A3E_INDEX_NOT_ARRAY", 2;
81 IndexElementType => "A3E_INDEX_ELEMENT_TYPE", 2;
82 IndexMixed => "A3E_INDEX_MIXED", 2;
83 RangeArity => "A3E_RANGE_ARITY", 2;
84 RangeEndpointNotInteger => "A3E_RANGE_ENDPOINT_NOT_INTEGER", 2;
85 VariantNotObject => "A3E_VARIANT_NOT_OBJECT", 2;
86 VariantPositionMissing => "A3E_VARIANT_POSITION_MISSING", 2;
87 VariantPositionNotInteger => "A3E_VARIANT_POSITION_NOT_INTEGER", 2;
88
89 PosNotPositive => "A3E_POS_NOT_POSITIVE", 3;
91 PosDuplicate => "A3E_POS_DUPLICATE", 3;
92 RangeEndpointNotPositive => "A3E_RANGE_ENDPOINT_NOT_POSITIVE", 3;
93 RangeOrder => "A3E_RANGE_ORDER", 3;
94 RangeOverlap => "A3E_RANGE_OVERLAP", 3;
95 VariantPositionNotPositive => "A3E_VARIANT_POSITION_NOT_POSITIVE", 3;
96
97 PosOutOfBounds => "A3E_POS_OUT_OF_BOUNDS", 4;
99 RangeOutOfBounds => "A3E_RANGE_OUT_OF_BOUNDS", 4;
100 VariantOutOfBounds => "A3E_VARIANT_OUT_OF_BOUNDS", 4;
101 VariantResidueMismatch => "A3E_VARIANT_RESIDUE_MISMATCH", 4;
102}
103
104impl fmt::Display for A3IssueCode {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 f.write_str(self.code())
107 }
108}
109
110impl Serialize for A3IssueCode {
111 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
112 serializer.serialize_str(self.code())
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
125pub struct A3Issue {
126 pub code: A3IssueCode,
128 pub path: String,
130 pub message: String,
132}
133
134impl A3Issue {
135 pub fn new(code: A3IssueCode, path: impl Into<String>, message: impl Into<String>) -> Self {
137 A3Issue {
138 code,
139 path: path.into(),
140 message: message.into(),
141 }
142 }
143
144 pub fn stage(&self) -> u8 {
146 self.code.stage()
147 }
148}
149
150impl fmt::Display for A3Issue {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 let where_ = if self.path.is_empty() {
155 "<document>"
156 } else {
157 &self.path
158 };
159 write!(f, "[{}] {}: {}", self.code.code(), where_, self.message)
160 }
161}
162
163pub fn escape_token(token: &str) -> String {
172 token.replace('~', "~0").replace('/', "~1")
173}
174
175pub fn pointer(base: &str, token: &str) -> String {
179 format!("{base}/{}", escape_token(token))
180}
181
182pub fn pointer_index(base: &str, index: usize) -> String {
186 format!("{base}/{index}")
187}
188
189#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn codes_are_unique() {
199 let mut seen = std::collections::HashSet::new();
200 for c in A3IssueCode::ALL {
201 assert!(seen.insert(c.code()), "duplicate code: {}", c.code());
202 }
203 assert_eq!(
204 seen.len(),
205 37,
206 "registry size changed — update spec/error-codes.md"
207 );
208 }
209
210 #[test]
211 fn every_code_has_a_valid_stage() {
212 for c in A3IssueCode::ALL {
213 assert!(
214 (1..=4).contains(&c.stage()),
215 "{} has stage {}",
216 c.code(),
217 c.stage()
218 );
219 }
220 }
221
222 #[test]
223 fn escapes_tilde_before_slash() {
224 assert_eq!(escape_token("a~1b"), "a~01b");
226 assert_eq!(escape_token("a/b"), "a~1b");
227 assert_eq!(escape_token("a~b"), "a~0b");
228 assert_eq!(escape_token("a.b"), "a.b");
229 assert_eq!(escape_token("$schema"), "$schema");
230 }
231
232 #[test]
233 fn builds_pointers() {
234 assert_eq!(
235 pointer("/annotations/site", "a/b"),
236 "/annotations/site/a~1b"
237 );
238 assert_eq!(pointer("", "$schema"), "/$schema");
239 assert_eq!(pointer("/annotations/site", ""), "/annotations/site/");
241 assert_eq!(
242 pointer_index("/annotations/variant", 3),
243 "/annotations/variant/3"
244 );
245 }
246}