Skip to content

Commit c2a8d7b

Browse files
committed
wip(rust/signed_doc): add comment document validation
1 parent 25107e6 commit c2a8d7b

File tree

6 files changed

+317
-58
lines changed

6 files changed

+317
-58
lines changed

rust/signed_doc/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ ed25519-dalek = { version = "2.1.1", features = ["pem", "rand_core"] }
2323
hex = "0.4.3"
2424
strum = { version = "0.26.3", features = ["derive"] }
2525
clap = { version = "4.5.23", features = ["derive", "env"] }
26+
jsonschema = "0.28.3"
27+
jsonpath-rust = "0.7.5"
2628

2729

2830
[dev-dependencies]
Lines changed: 240 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,276 @@
11
//! 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>
33
44
use catalyst_types::problem_report::ProblemReport;
5+
use jsonpath_rust::JsonPath;
56

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+
};
1116

1217
/// 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);
1920

2021
impl CommentDocument {
2122
/// Try to build `CommentDocument` from `CatalystSignedDoc` doing all necessary
2223
/// stateless verifications,
23-
#[allow(dead_code)]
2424
pub(crate) fn from_signed_doc(
2525
doc: &CatalystSignedDocument, error_report: &ProblemReport,
2626
) -> 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";
2928
let mut failed = false;
3029

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+
}
3935
}
4036

41-
// TODO add other validation
42-
4337
if failed {
4438
anyhow::bail!("Failed to build `CommentDocument` from `CatalystSignedDoc`");
4539
}
4640

47-
let content = doc.doc_content().decoded_bytes().to_vec();
48-
Ok(Self { content })
41+
Ok(Self(doc.clone()))
4942
}
5043

5144
/// 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+
}
56115
}
57116
}
58117

59118
impl TryFrom<CatalystSignedDocument> for CommentDocument {
60119
type Error = CatalystSignedDocError;
61120

62121
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");
64123
let res = Self::from_signed_doc(&doc, &error_report)
65124
.map_err(|e| CatalystSignedDocError::new(error_report, e))?;
66125
Ok(res)
67126
}
68127
}
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+
}

rust/signed_doc/src/doc_types/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ pub enum DocumentType {
4646
}
4747

4848
/// Proposal template `UuidV4` type.
49-
const PROPOSAL_TEMPLATE_UUID_TYPE: Uuid =
49+
pub const PROPOSAL_TEMPLATE_UUID_TYPE: Uuid =
5050
Uuid::from_u128(0x0CE8_AB38_9258_4FBC_A62E_7FAA_6E58_318F);
5151
/// Comment document `UuidV4` type.
5252
const COMMENT_DOCUMENT_UUID_TYPE: Uuid = Uuid::from_u128(0xB679_DED3_0E7C_41BA_89F8_DA62_A178_98EA);

0 commit comments

Comments
 (0)