|
1 | 1 | //! Comment Document object implementation |
2 | | -//! https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_docs/comment/#comment-document |
| 2 | +//! <https://input-output-hk.github.io/catalyst-libs/architecture/08_concepts/catalyst_docs/comment/#comment-document> |
3 | 3 |
|
4 | 4 | use catalyst_types::problem_report::ProblemReport; |
| 5 | +use jsonpath_rust::JsonPath; |
5 | 6 |
|
6 | | -use crate::{error::CatalystSignedDocError, CatalystSignedDocument}; |
7 | | - |
8 | | -/// Comment document `UuidV4` type. |
9 | | -pub const COMMENT_DOCUMENT_UUID_TYPE: uuid::Uuid = |
10 | | - uuid::Uuid::from_u128(0xB679_DED3_0E7C_41BA_89F8_DA62_A178_98EA); |
| 7 | +use crate::{ |
| 8 | + doc_types::{ |
| 9 | + COMMENT_DOCUMENT_UUID_TYPE, COMMENT_TEMPLATE_UUID_TYPE, PROPOSAL_DOCUMENT_UUID_TYPE, |
| 10 | + }, |
| 11 | + error::CatalystSignedDocError, |
| 12 | + metadata::{ContentEncoding, ContentType}, |
| 13 | + validator::{ValidationRule, Validator}, |
| 14 | + CatalystSignedDocument, |
| 15 | +}; |
11 | 16 |
|
12 | 17 | /// Comment Document struct |
13 | | -pub struct CommentDocument { |
14 | | - /// Proposal document content data |
15 | | - /// TODO: change it to `serde_json::Value` type |
16 | | - #[allow(dead_code)] |
17 | | - content: Vec<u8>, |
18 | | -} |
| 18 | +#[allow(dead_code)] |
| 19 | +pub struct CommentDocument(CatalystSignedDocument); |
19 | 20 |
|
20 | 21 | impl CommentDocument { |
21 | 22 | /// Try to build `CommentDocument` from `CatalystSignedDoc` doing all necessary |
22 | 23 | /// stateless verifications, |
23 | | - #[allow(dead_code)] |
24 | 24 | pub(crate) fn from_signed_doc( |
25 | 25 | doc: &CatalystSignedDocument, error_report: &ProblemReport, |
26 | 26 | ) -> anyhow::Result<Self> { |
27 | | - /// Context for error messages. |
28 | | - const CONTEXT: &str = "Catalyst Signed Document to Proposal Document"; |
| 27 | + let context = "Catalyst Signed Document to Comment Document"; |
29 | 28 | let mut failed = false; |
30 | 29 |
|
31 | | - if doc.doc_type().uuid() != COMMENT_DOCUMENT_UUID_TYPE { |
32 | | - error_report.invalid_value( |
33 | | - "`type`", |
34 | | - &doc.doc_type().to_string(), |
35 | | - &format!("Proposal Document type UUID value is {COMMENT_DOCUMENT_UUID_TYPE}"), |
36 | | - CONTEXT, |
37 | | - ); |
38 | | - failed = true; |
| 30 | + for rule in comment_document_validation_rules() { |
| 31 | + if !(rule.validator)(doc, error_report) { |
| 32 | + error_report.other(&rule.description, context); |
| 33 | + failed = true; |
| 34 | + } |
39 | 35 | } |
40 | 36 |
|
41 | | - // TODO add other validation |
42 | | - |
43 | 37 | if failed { |
44 | 38 | anyhow::bail!("Failed to build `CommentDocument` from `CatalystSignedDoc`"); |
45 | 39 | } |
46 | 40 |
|
47 | | - let content = doc.doc_content().decoded_bytes().to_vec(); |
48 | | - Ok(Self { content }) |
| 41 | + Ok(Self(doc.clone())) |
49 | 42 | } |
50 | 43 |
|
51 | 44 | /// A comprehensive validation of the `CommentDocument` content. |
52 | | - #[allow(clippy::unused_self)] |
53 | | - pub(crate) fn validate_with_report<F>(&self, _doc_getter: F, _error_report: &ProblemReport) |
54 | | - where F: FnMut() -> Option<CatalystSignedDocument> { |
55 | | - // TODO: implement the rest of the validation |
| 45 | + pub(crate) fn validate_with_report( |
| 46 | + &self, validator: &impl Validator, error_report: &ProblemReport, |
| 47 | + ) { |
| 48 | + let context = "Comment Document Comprehensive Validation"; |
| 49 | + let doc_ref = self.0.doc_meta().doc_ref(); |
| 50 | + if let Some(doc_ref) = doc_ref { |
| 51 | + match validator.get_doc_ref(&doc_ref) { |
| 52 | + Some(proposal_doc) => { |
| 53 | + for rule in reference_validation_rules() { |
| 54 | + if !(rule.validator)(&proposal_doc, error_report) { |
| 55 | + error_report.other( |
| 56 | + &rule.description, |
| 57 | + "During Comment Document reference validation", |
| 58 | + ); |
| 59 | + } |
| 60 | + } |
| 61 | + if let Some(reply_ref) = self.0.doc_meta().reply() { |
| 62 | + match validator.get_doc_ref(&reply_ref) { |
| 63 | + Some(reply_doc) => { |
| 64 | + let context = "During Comment Document reply validation"; |
| 65 | + for rule in reply_validation_rules() { |
| 66 | + if !(rule.validator)(&reply_doc, error_report) { |
| 67 | + error_report.other(&rule.description, context); |
| 68 | + } |
| 69 | + } |
| 70 | + let error_msg = "Reply document must reference the same proposal"; |
| 71 | + match reply_doc.doc_meta().doc_ref() { |
| 72 | + Some(reply_ref) => { |
| 73 | + if reply_ref != doc_ref { |
| 74 | + error_report.other(error_msg, context); |
| 75 | + } |
| 76 | + }, |
| 77 | + None => { |
| 78 | + error_report.other(error_msg, context); |
| 79 | + }, |
| 80 | + } |
| 81 | + }, |
| 82 | + None => { |
| 83 | + error_report.other("Unable to fetch Reply document", context); |
| 84 | + }, |
| 85 | + } |
| 86 | + } |
| 87 | + }, |
| 88 | + None => { |
| 89 | + error_report.other("Unable to fetch reference proposal document", context); |
| 90 | + }, |
| 91 | + } |
| 92 | + } |
| 93 | + // Validate content with template JSON Schema |
| 94 | + match self |
| 95 | + .0 |
| 96 | + .doc_meta() |
| 97 | + .template() |
| 98 | + .and_then(|t| validator.get_doc_ref(&t)) |
| 99 | + { |
| 100 | + Some(template_doc) => { |
| 101 | + // |
| 102 | + for rule in comment_document_template_validation_rules() { |
| 103 | + if !(rule.validator)(&template_doc, error_report) { |
| 104 | + error_report.other( |
| 105 | + &rule.description, |
| 106 | + "During Comment Document template validation", |
| 107 | + ); |
| 108 | + } |
| 109 | + } |
| 110 | + }, |
| 111 | + None => { |
| 112 | + error_report.other("No template was found", context); |
| 113 | + }, |
| 114 | + } |
56 | 115 | } |
57 | 116 | } |
58 | 117 |
|
59 | 118 | impl TryFrom<CatalystSignedDocument> for CommentDocument { |
60 | 119 | type Error = CatalystSignedDocError; |
61 | 120 |
|
62 | 121 | fn try_from(doc: CatalystSignedDocument) -> Result<Self, Self::Error> { |
63 | | - let error_report = ProblemReport::new("Proposal Document"); |
| 122 | + let error_report = ProblemReport::new("Comment Document"); |
64 | 123 | let res = Self::from_signed_doc(&doc, &error_report) |
65 | 124 | .map_err(|e| CatalystSignedDocError::new(error_report, e))?; |
66 | 125 | Ok(res) |
67 | 126 | } |
68 | 127 | } |
| 128 | + |
| 129 | +/// Stateles validation rules for Comment Document |
| 130 | +fn comment_document_validation_rules() -> Vec<ValidationRule<CatalystSignedDocument>> { |
| 131 | + vec![ |
| 132 | + ValidationRule { |
| 133 | + field: "content-type".to_string(), |
| 134 | + description: format!( |
| 135 | + "Comment Document content-type must be {}", |
| 136 | + ContentType::Json |
| 137 | + ), |
| 138 | + validator: |doc: &CatalystSignedDocument, _| { |
| 139 | + doc.doc_content_type() == ContentType::Json |
| 140 | + }, |
| 141 | + }, |
| 142 | + ValidationRule { |
| 143 | + field: "content-encoding".to_string(), |
| 144 | + description: format!( |
| 145 | + "Comment Document content-encoding must be {}", |
| 146 | + ContentEncoding::Brotli, |
| 147 | + ), |
| 148 | + validator: |doc: &CatalystSignedDocument, _| { |
| 149 | + match doc.doc_content_encoding() { |
| 150 | + Some(encoding) => encoding != ContentEncoding::Brotli, |
| 151 | + None => false, |
| 152 | + } |
| 153 | + }, |
| 154 | + }, |
| 155 | + ValidationRule { |
| 156 | + field: "type".to_string(), |
| 157 | + description: format!( |
| 158 | + "Comment Document type UUID value must be {COMMENT_DOCUMENT_UUID_TYPE}" |
| 159 | + ), |
| 160 | + validator: |doc: &CatalystSignedDocument, _| { |
| 161 | + doc.doc_type().uuid() != COMMENT_DOCUMENT_UUID_TYPE |
| 162 | + }, |
| 163 | + }, |
| 164 | + ValidationRule { |
| 165 | + field: "ref".to_string(), |
| 166 | + description: "Comment Document ref must be valid".to_string(), |
| 167 | + validator: |doc: &CatalystSignedDocument, _| { |
| 168 | + match doc.doc_meta().doc_ref() { |
| 169 | + Some(doc_ref) => doc_ref.is_valid(), |
| 170 | + None => true, |
| 171 | + } |
| 172 | + }, |
| 173 | + }, |
| 174 | + ValidationRule { |
| 175 | + field: "template".to_string(), |
| 176 | + description: format!( |
| 177 | + "Comment Document template UUID value must be {COMMENT_TEMPLATE_UUID_TYPE}" |
| 178 | + ), |
| 179 | + validator: |doc: &CatalystSignedDocument, _| { |
| 180 | + match doc.doc_meta().template() { |
| 181 | + Some(template) => template.id.uuid() != COMMENT_TEMPLATE_UUID_TYPE, |
| 182 | + None => false, |
| 183 | + } |
| 184 | + }, |
| 185 | + }, |
| 186 | + ValidationRule { |
| 187 | + field: "reply".to_string(), |
| 188 | + description: "Comment Document reply document reference must be valid".to_string(), |
| 189 | + validator: |doc: &CatalystSignedDocument, _| { |
| 190 | + match doc.doc_meta().reply() { |
| 191 | + Some(reply) => reply.is_valid(), |
| 192 | + None => true, |
| 193 | + } |
| 194 | + }, |
| 195 | + }, |
| 196 | + ValidationRule { |
| 197 | + field: "section".to_string(), |
| 198 | + description: "Comment Document section must be valid JSON path".to_string(), |
| 199 | + validator: |doc: &CatalystSignedDocument, _| { |
| 200 | + match doc.doc_meta().section() { |
| 201 | + Some(section) => { |
| 202 | + JsonPath::<serde_json::Value>::try_from(section.as_str()).is_ok() |
| 203 | + }, |
| 204 | + None => true, |
| 205 | + } |
| 206 | + }, |
| 207 | + }, |
| 208 | + ] |
| 209 | +} |
| 210 | + |
| 211 | +/// Functional validation rules for Comment Document reference |
| 212 | +fn reference_validation_rules() -> Vec<ValidationRule<CatalystSignedDocument>> { |
| 213 | + vec![ |
| 214 | + ValidationRule { |
| 215 | + field: "ref".to_string(), |
| 216 | + description: format!( |
| 217 | + "Comment Document reference document type must be {PROPOSAL_DOCUMENT_UUID_TYPE}" |
| 218 | + ), |
| 219 | + validator: |signed_doc: &CatalystSignedDocument, _| { |
| 220 | + signed_doc.doc_type().uuid() == PROPOSAL_DOCUMENT_UUID_TYPE |
| 221 | + }, |
| 222 | + }, |
| 223 | + ValidationRule { |
| 224 | + field: "ref".to_string(), |
| 225 | + description: format!( |
| 226 | + "Comment Document reference document type must be {PROPOSAL_DOCUMENT_UUID_TYPE}" |
| 227 | + ), |
| 228 | + validator: |signed_doc: &CatalystSignedDocument, _| { |
| 229 | + signed_doc.doc_type().uuid() == PROPOSAL_DOCUMENT_UUID_TYPE |
| 230 | + }, |
| 231 | + }, |
| 232 | + ] |
| 233 | +} |
| 234 | + |
| 235 | +/// Functional validation rules for Comment Document template |
| 236 | +fn comment_document_template_validation_rules() -> Vec<ValidationRule<CatalystSignedDocument>> { |
| 237 | + vec![ValidationRule { |
| 238 | + field: "template".to_string(), |
| 239 | + description: "Comment Document conforms to template schema".to_string(), |
| 240 | + validator: |signed_doc: &CatalystSignedDocument, error_report| { |
| 241 | + let mut success = false; |
| 242 | + match serde_json::from_slice(signed_doc.doc_content().decoded_bytes()) { |
| 243 | + Ok(template_json) => { |
| 244 | + match jsonschema::draft7::new(&template_json) { |
| 245 | + Ok(schema) => { |
| 246 | + success = schema.is_valid(&template_json); |
| 247 | + }, |
| 248 | + Err(e) => { |
| 249 | + error_report.other(&format!("Invalid JSON schema: {e:?}"), ""); |
| 250 | + }, |
| 251 | + } |
| 252 | + }, |
| 253 | + Err(e) => { |
| 254 | + error_report.other( |
| 255 | + &format!("Document does not conform to template schema: {e:?}"), |
| 256 | + "", |
| 257 | + ); |
| 258 | + }, |
| 259 | + } |
| 260 | + success |
| 261 | + }, |
| 262 | + }] |
| 263 | +} |
| 264 | + |
| 265 | +/// Functional validation rules for Comment Document reply |
| 266 | +fn reply_validation_rules() -> Vec<ValidationRule<CatalystSignedDocument>> { |
| 267 | + vec![ValidationRule { |
| 268 | + field: "ref".to_string(), |
| 269 | + description: format!( |
| 270 | + "Comment Document reference document type must be {COMMENT_DOCUMENT_UUID_TYPE}" |
| 271 | + ), |
| 272 | + validator: |signed_doc: &CatalystSignedDocument, _| { |
| 273 | + signed_doc.doc_type().uuid() != COMMENT_DOCUMENT_UUID_TYPE |
| 274 | + }, |
| 275 | + }] |
| 276 | +} |
0 commit comments