1use std::fmt;
5
6use serde::Serialize;
7use serde::de::DeserializeOwned;
8
9use crate::bounded::{ParseError, ParseLimits, parse_bounded};
10use crate::canonical::{CanonicalError, canonical_bytes, record_canonical_bytes, tagged_digest};
11use crate::ids::ContentDigest;
12
13pub trait Document: Serialize + DeserializeOwned {
15 const SCHEMA: &'static str;
17 const LIMITS: ParseLimits;
19 fn schema(&self) -> &str;
21
22 fn parse(bytes: &[u8]) -> Result<Self, DocumentError> {
24 let doc: Self = parse_bounded(bytes, &Self::LIMITS).map_err(DocumentError::Parse)?;
25 if doc.schema() != Self::SCHEMA {
26 return Err(DocumentError::Schema {
27 expected: Self::SCHEMA,
28 found: doc.schema().chars().take(80).collect(),
29 });
30 }
31 Ok(doc)
32 }
33
34 fn canonical(&self) -> Result<Vec<u8>, CanonicalError> {
36 canonical_bytes(self)
37 }
38
39 fn record_canonical(&self) -> Result<Vec<u8>, CanonicalError>
42 where
43 Self: PartialEq,
44 {
45 record_canonical_bytes(self)
46 }
47
48 fn record_digest(&self) -> Result<ContentDigest, CanonicalError>
51 where
52 Self: PartialEq,
53 {
54 Ok(ContentDigest::from_digest(tagged_digest(
55 Self::SCHEMA,
56 &self.record_canonical()?,
57 )))
58 }
59}
60
61pub trait Identified: Document {
63 type Id;
64 #[doc(hidden)]
65 fn wrap(digest: String) -> Self::Id;
66
67 fn id(&self) -> Result<Self::Id, CanonicalError> {
69 Ok(Self::wrap(tagged_digest(Self::SCHEMA, &self.canonical()?)))
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum DocumentError {
76 Parse(ParseError),
77 Schema {
78 expected: &'static str,
79 found: String,
80 },
81}
82
83impl fmt::Display for DocumentError {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 match self {
86 DocumentError::Parse(e) => write!(f, "{e}"),
87 DocumentError::Schema { expected, found } => {
88 write!(f, "schema {found:?} where {expected:?} is required")
89 }
90 }
91 }
92}
93
94impl std::error::Error for DocumentError {}
95
96macro_rules! document {
99 ($ty:ty, $schema:expr, $limits:expr) => {
100 impl $crate::Document for $ty {
101 const SCHEMA: &'static str = $schema;
102 const LIMITS: $crate::bounded::ParseLimits = $limits;
103 fn schema(&self) -> &str {
104 &self.schema
105 }
106 }
107 };
108 ($ty:ty, $schema:expr, $limits:expr, $id:ty) => {
109 $crate::document::document!($ty, $schema, $limits);
110 impl $crate::Identified for $ty {
111 type Id = $id;
112 fn wrap(digest: String) -> $id {
113 <$id>::from_digest(digest)
114 }
115 }
116 };
117}
118
119pub(crate) use document;
120
121pub(crate) const fn lists_schemas(expected: &str, schemas: &[&str]) -> bool {
125 const SEP: &[u8] = b" or ";
126 let e = expected.as_bytes();
127 let mut at = 0;
128 let mut i = 0;
129 while i < schemas.len() {
130 if i > 0 {
131 let mut k = 0;
132 while k < SEP.len() {
133 if at >= e.len() || e[at] != SEP[k] {
134 return false;
135 }
136 at += 1;
137 k += 1;
138 }
139 }
140 let s = schemas[i].as_bytes();
141 let mut k = 0;
142 while k < s.len() {
143 if at >= e.len() || e[at] != s[k] {
144 return false;
145 }
146 at += 1;
147 k += 1;
148 }
149 i += 1;
150 }
151 at == e.len()
152}
153
154macro_rules! versioned_document {
167 (@impl $ty:ty, [$first:expr $(, $rest:expr)*], $expected:literal, $limits:expr, $check:expr) => {
168 const _: () = assert!(
169 $crate::document::lists_schemas($expected, &[$first $(, $rest)*]),
170 "`expected` must list the accepted schemas, joined with \" or \"",
171 );
172 impl $crate::Document for $ty {
173 const SCHEMA: &'static str = $first;
174 const LIMITS: $crate::bounded::ParseLimits = $limits;
175 fn schema(&self) -> &str {
176 &self.schema
177 }
178
179 fn parse(bytes: &[u8]) -> Result<Self, $crate::DocumentError> {
180 let doc: Self = $crate::bounded::parse_bounded(bytes, &Self::LIMITS)
181 .map_err($crate::DocumentError::Parse)?;
182 if doc.schema != $first $(&& doc.schema != $rest)* {
183 return Err($crate::DocumentError::Schema {
184 expected: $expected,
185 found: doc.schema.chars().take(80).collect(),
186 });
187 }
188 ($check)(&doc).map_err(|message: String| {
189 $crate::DocumentError::Parse($crate::bounded::ParseError::Schema { message })
190 })?;
191 Ok(doc)
192 }
193
194 fn record_digest(
195 &self,
196 ) -> Result<$crate::ids::ContentDigest, $crate::canonical::CanonicalError> {
197 Ok($crate::ids::ContentDigest::from_digest(
198 $crate::canonical::tagged_digest(&self.schema, &self.record_canonical()?),
199 ))
200 }
201 }
202 };
203 ($ty:ty, [$first:expr $(, $rest:expr)*], $expected:literal, $limits:expr, check = $check:path) => {
204 $crate::document::versioned_document!(
205 @impl $ty, [$first $(, $rest)*], $expected, $limits, $check
206 );
207 };
208 ($ty:ty, [$first:expr $(, $rest:expr)*], $expected:literal, $limits:expr) => {
209 $crate::document::versioned_document!(
210 @impl $ty, [$first $(, $rest)*], $expected, $limits,
211 |_: &$ty| -> Result<(), String> { Ok(()) }
212 );
213 };
214 ($ty:ty, [$first:expr $(, $rest:expr)*], $expected:literal, $limits:expr, $id:ty) => {
215 $crate::document::versioned_document!($ty, [$first $(, $rest)*], $expected, $limits);
216 impl $crate::Identified for $ty {
217 type Id = $id;
218 fn wrap(digest: String) -> $id {
219 <$id>::from_digest(digest)
220 }
221
222 fn id(&self) -> Result<$id, $crate::canonical::CanonicalError> {
223 Ok(Self::wrap($crate::canonical::tagged_digest(
224 &self.schema,
225 &$crate::Document::canonical(self)?,
226 )))
227 }
228 }
229 };
230}
231
232pub(crate) use versioned_document;
233
234#[cfg(test)]
235mod tests {
236 use super::lists_schemas;
237
238 #[test]
239 fn expected_must_list_every_schema_in_order() {
240 assert!(lists_schemas("a/1 or a/2", &["a/1", "a/2"]));
241 assert!(lists_schemas("a/1", &["a/1"]));
242 assert!(!lists_schemas("a/1 or a/3", &["a/1", "a/2"]), "renamed");
243 assert!(!lists_schemas("a/1", &["a/1", "a/2"]), "missing");
244 assert!(
245 !lists_schemas("a/1 or a/2 or a/3", &["a/1", "a/2"]),
246 "extra"
247 );
248 assert!(!lists_schemas("a/2 or a/1", &["a/1", "a/2"]), "order");
249 }
250}