• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

input-output-hk / catalyst-libs / 16646523141

31 Jul 2025 10:28AM UTC coverage: 67.341% (+3.8%) from 63.544%
16646523141

push

github

web-flow
feat(rust/signed-doc): Implement new Catalyst Signed Doc (#338)

* chore: add new line to open pr

Signed-off-by: bkioshn <bkioshn@gmail.com>

* chore: revert

Signed-off-by: bkioshn <bkioshn@gmail.com>

* feat(rust/signed-doc): add new type `DocType` (#339)

* feat(signed-doc): add new type DocType

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): add conversion policy

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): doc type

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): doc type error

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): seperate test

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): format

Signed-off-by: bkioshn <bkioshn@gmail.com>

---------

Signed-off-by: bkioshn <bkioshn@gmail.com>

* feat(rust/signed-doc): Add initial decoding tests for the Catalyst Signed Documents (#349)

* wip

* wip

* fix fmt

* fix spelling

* fix clippy

* fix(rust/signed-doc): Apply new `DocType` (#347)

* feat(signed-doc): add new type DocType

Signed-off-by: bkioshn <bkioshn@gmail.com>

* wip: apply doctype

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): add more function to DocType

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): map old doctype to new doctype

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): add eq to uuidv4

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): fix validator

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): minor fixes

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(catalyst-types): add hash to uuidv4

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): decoding test

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): doctype

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(signed-doc): minor fixes

Signed-off-by: bkioshn <bkioshn@gmail.com>

* chore(sign-doc): fix comment

Signed-off-by: bkioshn <bkioshn@gmail.com>

* fix(catalyst-types): add froms... (continued)

2453 of 2675 new or added lines in 38 files covered. (91.7%)

19 existing lines in 7 files now uncovered.

11312 of 16798 relevant lines covered (67.34%)

2525.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.82
/rust/signed_doc/src/validator/rules/template.rs
1
//! `template` rule type impl.
2

3
use std::fmt::Write;
4

5
use super::doc_ref::referenced_doc_check;
6
use crate::{
7
    metadata::ContentType, providers::CatalystSignedDocumentProvider,
8
    validator::utils::validate_doc_refs, CatalystSignedDocument, DocType,
9
};
10

11
/// Enum represents different content schemas, against which documents content would be
12
/// validated.
13
#[allow(dead_code)]
14
pub(crate) enum ContentSchema {
15
    /// Draft 7 JSON schema
16
    Json(jsonschema::Validator),
17
}
18

19
/// Document's content validation rule
20
pub(crate) enum ContentRule {
21
    /// Based on the 'template' field and loaded corresponding template document
22
    #[allow(dead_code)]
23
    Templated {
24
        /// expected `type` field of the template
25
        exp_template_type: DocType,
26
    },
27
    /// Statically defined document's content schema.
28
    /// `template` field should not been specified
29
    #[allow(dead_code)]
30
    Static(ContentSchema),
31
    /// 'template' field is not specified
32
    #[allow(dead_code)]
33
    NotSpecified,
34
}
35

36
impl ContentRule {
37
    /// Field validation rule
38
    #[allow(dead_code)]
39
    pub(crate) async fn check<Provider>(
30✔
40
        &self, doc: &CatalystSignedDocument, provider: &Provider,
30✔
41
    ) -> anyhow::Result<bool>
30✔
42
    where Provider: CatalystSignedDocumentProvider {
30✔
43
        let context = "Content/Template rule check";
30✔
44
        if let Self::Templated { exp_template_type } = self {
30✔
45
            let Some(template_ref) = doc.doc_meta().template() else {
19✔
46
                doc.report()
3✔
47
                    .missing_field("template", &format!("{context}, doc"));
3✔
48
                return Ok(false);
3✔
49
            };
50
            let template_validator = |template_doc: CatalystSignedDocument| {
16✔
51
                if !referenced_doc_check(&template_doc, exp_template_type, "template", doc.report())
15✔
52
                {
53
                    return false;
2✔
54
                }
13✔
55
                let Ok(template_content_type) = template_doc.doc_content_type() else {
13✔
56
                    doc.report().missing_field(
1✔
57
                        "content-type",
1✔
58
                        &format!("{context}, referenced document must have a content-type field"),
1✔
59
                    );
60
                    return false;
1✔
61
                };
62
                match template_content_type {
12✔
63
                    ContentType::Json => templated_json_schema_check(doc, &template_doc),
12✔
64
                    ContentType::Cddl | ContentType::Cbor | ContentType::JsonSchema => {
65
                        // TODO: not implemented yet
66
                        true
×
67
                    },
68
                }
69
            };
15✔
70
            return validate_doc_refs(template_ref, provider, doc.report(), template_validator)
16✔
71
                .await;
16✔
72
        }
11✔
73
        if let Self::Static(content_schema) = self {
11✔
74
            if let Some(template) = doc.doc_meta().template() {
9✔
75
                doc.report().unknown_field(
1✔
76
                    "template",
1✔
77
                    &template.to_string(),
1✔
78
                    &format!("{context} Static, Document does not expect to have a template field",)
1✔
79
                );
80
                return Ok(false);
1✔
81
            }
8✔
82

83
            return Ok(content_schema_check(doc, content_schema));
8✔
84
        }
2✔
85
        if let Self::NotSpecified = self {
2✔
86
            if let Some(template) = doc.doc_meta().template() {
2✔
87
                doc.report().unknown_field(
1✔
88
                    "template",
1✔
89
                    &template.to_string(),
1✔
90
                    &format!("{context} Not Specified, Document does not expect to have a template field",)
1✔
91
                );
92
                return Ok(false);
1✔
93
            }
1✔
94
        }
×
95

96
        Ok(true)
1✔
97
    }
30✔
98
}
99

100
/// Validate a provided `doc` against the `template` content's Json schema, assuming that
101
/// the `doc` content is JSON.
102
fn templated_json_schema_check(
12✔
103
    doc: &CatalystSignedDocument, template_doc: &CatalystSignedDocument,
12✔
104
) -> bool {
12✔
105
    let Ok(template_content) = template_doc.decoded_content() else {
12✔
NEW
106
        doc.report().functional_validation(
×
NEW
107
            "Invalid document content, cannot get decoded bytes",
×
NEW
108
            "Cannot get a referenced template document content during the templated validation",
×
109
        );
UNCOV
110
        return false;
×
111
    };
112
    let Ok(template_json_schema) = serde_json::from_slice(&template_content) else {
12✔
113
        doc.report().functional_validation(
2✔
114
            "Template document content must be json encoded",
2✔
115
            "Invalid referenced template document content",
2✔
116
        );
117
        return false;
2✔
118
    };
119
    let Ok(schema_validator) = jsonschema::options()
10✔
120
        .with_draft(jsonschema::Draft::Draft7)
10✔
121
        .build(&template_json_schema)
10✔
122
    else {
123
        doc.report().functional_validation(
×
124
            "Template document content must be Draft 7 JSON schema",
×
125
            "Invalid referenced template document content",
×
126
        );
127
        return false;
×
128
    };
129

130
    content_schema_check(doc, &ContentSchema::Json(schema_validator))
10✔
131
}
12✔
132
#[allow(dead_code)]
133
/// Validating the document's content against the provided schema
134
fn content_schema_check(doc: &CatalystSignedDocument, schema: &ContentSchema) -> bool {
18✔
135
    let Ok(doc_content) = doc.decoded_content() else {
18✔
NEW
136
        doc.report().functional_validation(
×
NEW
137
            "Invalid Document content, cannot get decoded bytes",
×
NEW
138
            "Cannot get a document content during the templated validation",
×
139
        );
NEW
140
        return false;
×
141
    };
142
    if doc_content.is_empty() {
18✔
143
        doc.report()
2✔
144
            .missing_field("payload", "Document must have a content");
2✔
145
        return false;
2✔
146
    }
16✔
147
    let Ok(doc_json) = serde_json::from_slice(&doc_content) else {
16✔
148
        doc.report().functional_validation(
2✔
149
            "Document content must be json encoded",
2✔
150
            "Invalid referenced template document content",
2✔
151
        );
152
        return false;
2✔
153
    };
154

155
    match schema {
14✔
156
        ContentSchema::Json(schema_validator) => {
14✔
157
            let schema_validation_errors =
14✔
158
                schema_validator
14✔
159
                    .iter_errors(&doc_json)
14✔
160
                    .fold(String::new(), |mut str, e| {
14✔
161
                        let _ = write!(str, "{{ {e} }}, ");
1✔
162
                        str
1✔
163
                    });
1✔
164

165
            if !schema_validation_errors.is_empty() {
14✔
166
                doc.report().functional_validation(
1✔
167
            &format!(
1✔
168
                "Proposal document content does not compliant with the json schema. [{schema_validation_errors}]"
1✔
169
            ),
1✔
170
            "Invalid Proposal document content",
1✔
171
        );
172
                return false;
1✔
173
            }
13✔
174
        },
175
    }
176
    true
13✔
177
}
18✔
178

179
#[cfg(test)]
180
mod tests {
181
    use catalyst_types::uuid::{UuidV4, UuidV7};
182

183
    use super::*;
184
    use crate::{
185
        builder::tests::Builder, metadata::SupportedField,
186
        providers::tests::TestCatalystSignedDocumentProvider, DocLocator, DocumentRef,
187
    };
188

189
    #[allow(clippy::too_many_lines)]
190
    #[tokio::test]
191
    async fn content_rule_templated_test() {
1✔
192
        let mut provider = TestCatalystSignedDocumentProvider::default();
1✔
193

194
        let exp_template_type = UuidV4::new();
1✔
195
        let content_type = ContentType::Json;
1✔
196
        let json_schema = serde_json::to_vec(&serde_json::json!({})).unwrap();
1✔
197
        let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap();
1✔
198

199
        let valid_template_doc_id = UuidV7::new();
1✔
200
        let another_type_template_doc_id = UuidV7::new();
1✔
201
        let missing_type_template_doc_id = UuidV7::new();
1✔
202
        let missing_content_type_template_doc_id = UuidV7::new();
1✔
203
        let missing_content_template_doc_id = UuidV7::new();
1✔
204
        let invalid_content_template_doc_id = UuidV7::new();
1✔
205

206
        // Prepare provider documents
207
        {
1✔
208
            let doc = Builder::new()
1✔
209
                .with_metadata_field(SupportedField::Id(valid_template_doc_id))
1✔
210
                .with_metadata_field(SupportedField::Ver(valid_template_doc_id))
1✔
211
                .with_metadata_field(SupportedField::Type(exp_template_type.into()))
1✔
212
                .with_metadata_field(SupportedField::ContentType(content_type))
1✔
213
                .with_content(json_schema.clone())
1✔
214
                .build();
1✔
215
            provider.add_document(None, &doc).unwrap();
1✔
216

1✔
217
            // reply doc with other `type` field
1✔
218
            let ref_doc = Builder::new()
1✔
219
                .with_metadata_field(SupportedField::Id(another_type_template_doc_id))
1✔
220
                .with_metadata_field(SupportedField::Ver(another_type_template_doc_id))
1✔
221
                .with_metadata_field(SupportedField::Type(UuidV4::new().into()))
1✔
222
                .with_metadata_field(SupportedField::ContentType(content_type))
1✔
223
                .with_content(json_schema.clone())
1✔
224
                .build();
1✔
225
            provider.add_document(None, &ref_doc).unwrap();
1✔
226

1✔
227
            // missing `type` field in the referenced document
1✔
228
            let ref_doc = Builder::new()
1✔
229
                .with_metadata_field(SupportedField::Id(missing_type_template_doc_id))
1✔
230
                .with_metadata_field(SupportedField::Ver(missing_type_template_doc_id))
1✔
231
                .with_metadata_field(SupportedField::ContentType(content_type))
1✔
232
                .with_content(json_schema.clone())
1✔
233
                .build();
1✔
234
            provider.add_document(None, &ref_doc).unwrap();
1✔
235

1✔
236
            // missing `content-type` field in the referenced document
1✔
237
            let ref_doc = Builder::new()
1✔
238
                .with_metadata_field(SupportedField::Id(missing_content_type_template_doc_id))
1✔
239
                .with_metadata_field(SupportedField::Ver(missing_content_type_template_doc_id))
1✔
240
                .with_metadata_field(SupportedField::Type(exp_template_type.into()))
1✔
241
                .with_content(json_schema.clone())
1✔
242
                .build();
1✔
243
            provider.add_document(None, &ref_doc).unwrap();
1✔
244

1✔
245
            // missing content
1✔
246
            let ref_doc = Builder::new()
1✔
247
                .with_metadata_field(SupportedField::Id(missing_content_template_doc_id))
1✔
248
                .with_metadata_field(SupportedField::Ver(missing_content_template_doc_id))
1✔
249
                .with_metadata_field(SupportedField::Type(exp_template_type.into()))
1✔
250
                .with_metadata_field(SupportedField::ContentType(content_type))
1✔
251
                .build();
1✔
252
            provider.add_document(None, &ref_doc).unwrap();
1✔
253

1✔
254
            // invalid content, must be json encoded
1✔
255
            let ref_doc = Builder::new()
1✔
256
                .with_metadata_field(SupportedField::Id(invalid_content_template_doc_id))
1✔
257
                .with_metadata_field(SupportedField::Ver(invalid_content_template_doc_id))
1✔
258
                .with_metadata_field(SupportedField::Type(exp_template_type.into()))
1✔
259
                .with_metadata_field(SupportedField::ContentType(content_type))
1✔
260
                .with_content(vec![])
1✔
261
                .build();
1✔
262
            provider.add_document(None, &ref_doc).unwrap();
1✔
263
        }
1✔
264

265
        // Create a document where `templates` field is required and referencing a valid document
266
        // in provider. Using doc ref of new implementation.
267
        let rule = ContentRule::Templated {
1✔
268
            exp_template_type: exp_template_type.into(),
1✔
269
        };
1✔
270
        let doc = Builder::new()
1✔
271
            .with_metadata_field(SupportedField::Template(
1✔
272
                vec![DocumentRef::new(
1✔
273
                    valid_template_doc_id,
1✔
274
                    valid_template_doc_id,
1✔
275
                    DocLocator::default(),
1✔
276
                )]
1✔
277
                .into(),
1✔
278
            ))
1✔
279
            .with_content(json_content.clone())
1✔
280
            .build();
1✔
281
        assert!(rule.check(&doc, &provider).await.unwrap());
1✔
282

283
        // missing `template` field, but its required
284
        let doc = Builder::new().with_content(json_content.clone()).build();
1✔
285
        assert!(!rule.check(&doc, &provider).await.unwrap());
1✔
286

287
        // missing content
288
        let rule = ContentRule::Templated {
1✔
289
            exp_template_type: exp_template_type.into(),
1✔
290
        };
1✔
291
        let doc = Builder::new()
1✔
292
            .with_metadata_field(SupportedField::Template(
1✔
293
                vec![DocumentRef::new(
1✔
294
                    valid_template_doc_id,
1✔
295
                    valid_template_doc_id,
1✔
296
                    DocLocator::default(),
1✔
297
                )]
1✔
298
                .into(),
1✔
299
            ))
1✔
300
            .build();
1✔
301
        assert!(!rule.check(&doc, &provider).await.unwrap());
1✔
302

303
        // content not a json encoded
304
        let rule = ContentRule::Templated {
1✔
305
            exp_template_type: exp_template_type.into(),
1✔
306
        };
1✔
307
        let doc = Builder::new()
1✔
308
            .with_metadata_field(SupportedField::Template(
1✔
309
                vec![DocumentRef::new(
1✔
310
                    valid_template_doc_id,
1✔
311
                    valid_template_doc_id,
1✔
312
                    DocLocator::default(),
1✔
313
                )]
1✔
314
                .into(),
1✔
315
            ))
1✔
316
            .with_content(vec![1, 2, 3])
1✔
317
            .build();
1✔
318
        assert!(!rule.check(&doc, &provider).await.unwrap());
1✔
319

320
        // reference to the document with another `type` field
321
        let doc = Builder::new()
1✔
322
            .with_metadata_field(SupportedField::Template(
1✔
323
                vec![DocumentRef::new(
1✔
324
                    another_type_template_doc_id,
1✔
325
                    another_type_template_doc_id,
1✔
326
                    DocLocator::default(),
1✔
327
                )]
1✔
328
                .into(),
1✔
329
            ))
1✔
330
            .with_content(json_content.clone())
1✔
331
            .build();
1✔
332
        assert!(!rule.check(&doc, &provider).await.unwrap());
1✔
333

334
        // missing `type` field in the referenced document
335
        let doc = Builder::new()
1✔
336
            .with_metadata_field(SupportedField::Template(
1✔
337
                vec![DocumentRef::new(
1✔
338
                    missing_type_template_doc_id,
1✔
339
                    missing_type_template_doc_id,
1✔
340
                    DocLocator::default(),
1✔
341
                )]
1✔
342
                .into(),
1✔
343
            ))
1✔
344
            .with_content(json_content.clone())
1✔
345
            .build();
1✔
346
        assert!(!rule.check(&doc, &provider).await.unwrap());
1✔
347

348
        // missing `content-type` field in the referenced doc
349
        let rule = ContentRule::Templated {
1✔
350
            exp_template_type: exp_template_type.into(),
1✔
351
        };
1✔
352
        let doc = Builder::new()
1✔
353
            .with_metadata_field(SupportedField::Template(
1✔
354
                vec![DocumentRef::new(
1✔
355
                    missing_content_type_template_doc_id,
1✔
356
                    missing_content_type_template_doc_id,
1✔
357
                    DocLocator::default(),
1✔
358
                )]
1✔
359
                .into(),
1✔
360
            ))
1✔
361
            .with_content(json_content.clone())
1✔
362
            .build();
1✔
363
        assert!(!rule.check(&doc, &provider).await.unwrap());
1✔
364

365
        // missing content in the referenced document
366
        let doc = Builder::new()
1✔
367
            .with_metadata_field(SupportedField::Template(
1✔
368
                vec![DocumentRef::new(
1✔
369
                    missing_content_template_doc_id,
1✔
370
                    missing_content_template_doc_id,
1✔
371
                    DocLocator::default(),
1✔
372
                )]
1✔
373
                .into(),
1✔
374
            ))
1✔
375
            .with_content(json_content.clone())
1✔
376
            .build();
1✔
377
        assert!(!rule.check(&doc, &provider).await.unwrap());
1✔
378

379
        // content not a json encoded in the referenced document
380
        let doc = Builder::new()
1✔
381
            .with_metadata_field(SupportedField::Template(
1✔
382
                vec![DocumentRef::new(
1✔
383
                    invalid_content_template_doc_id,
1✔
384
                    invalid_content_template_doc_id,
1✔
385
                    DocLocator::default(),
1✔
386
                )]
1✔
387
                .into(),
1✔
388
            ))
1✔
389
            .with_content(json_content.clone())
1✔
390
            .build();
1✔
391
        assert!(!rule.check(&doc, &provider).await.unwrap());
1✔
392

393
        // cannot find a referenced document
394
        let doc = Builder::new()
1✔
395
            .with_metadata_field(SupportedField::Template(
1✔
396
                vec![DocumentRef::new(
1✔
397
                    UuidV7::new(),
1✔
398
                    UuidV7::new(),
1✔
399
                    DocLocator::default(),
1✔
400
                )]
1✔
401
                .into(),
1✔
402
            ))
1✔
403
            .with_content(json_content.clone())
1✔
404
            .build();
1✔
405
        assert!(!rule.check(&doc, &provider).await.unwrap());
1✔
406
    }
1✔
407

408
    #[allow(clippy::too_many_lines)]
409
    #[tokio::test]
410
    async fn content_rule_static_test() {
1✔
411
        let provider = TestCatalystSignedDocumentProvider::default();
1✔
412

413
        let json_schema = ContentSchema::Json(
1✔
414
            jsonschema::options()
1✔
415
                .with_draft(jsonschema::Draft::Draft7)
1✔
416
                .build(&serde_json::json!({}))
1✔
417
                .unwrap(),
1✔
418
        );
1✔
419
        let json_content = serde_json::to_vec(&serde_json::json!({})).unwrap();
1✔
420

421
        // all correct
422
        let rule = ContentRule::Static(json_schema);
1✔
423
        let doc = Builder::new().with_content(json_content.clone()).build();
1✔
424
        assert!(rule.check(&doc, &provider).await.unwrap());
1✔
425

426
        // missing content
427
        let doc = Builder::new().build();
1✔
428
        assert!(!rule.check(&doc, &provider).await.unwrap());
1✔
429

430
        // content not a json encoded
431
        let doc = Builder::new().with_content(vec![1, 2, 3]).build();
1✔
432
        assert!(!rule.check(&doc, &provider).await.unwrap());
1✔
433

434
        // defined `template` field which should be absent
435
        let ref_id = UuidV7::new();
1✔
436
        let ref_ver = UuidV7::new();
1✔
437
        let doc = Builder::new()
1✔
438
            .with_metadata_field(SupportedField::Template(
1✔
439
                vec![DocumentRef::new(ref_id, ref_ver, DocLocator::default())].into(),
1✔
440
            ))
1✔
441
            .with_content(json_content)
1✔
442
            .build();
1✔
443
        assert!(!rule.check(&doc, &provider).await.unwrap());
1✔
444
    }
1✔
445

446
    #[tokio::test]
447
    async fn template_rule_not_specified_test() {
1✔
448
        let rule = ContentRule::NotSpecified;
1✔
449
        let provider = TestCatalystSignedDocumentProvider::default();
1✔
450

451
        let doc = Builder::new().build();
1✔
452
        assert!(rule.check(&doc, &provider).await.unwrap());
1✔
453

454
        // defined `template` field which should be absent
455
        let ref_id = UuidV7::new();
1✔
456
        let ref_ver = UuidV7::new();
1✔
457
        let doc = Builder::new()
1✔
458
            .with_metadata_field(SupportedField::Template(
1✔
459
                vec![DocumentRef::new(ref_id, ref_ver, DocLocator::default())].into(),
1✔
460
            ))
1✔
461
            .build();
1✔
462
        assert!(!rule.check(&doc, &provider).await.unwrap());
1✔
463
    }
1✔
464
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc