Skip to content

Commit dceedfe

Browse files
authored
feat(rust/signed-doc): Update link_check to perform recursive parameters check (#677)
* initial * feat: initial recursion * chore: minor * feat: add problem report * chore: minor insert * refactor: correct implementation * chore: minor * fix: skip when params are missing instead of error * chore: comments * test: initial * chore: merge resolve * chore: remove println
1 parent c9e3085 commit dceedfe

File tree

2 files changed

+194
-42
lines changed

2 files changed

+194
-42
lines changed

rust/signed_doc/src/validator/rules/parameters/mod.rs

Lines changed: 75 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
#[cfg(test)]
44
mod tests;
55

6+
use std::collections::HashSet;
7+
68
use catalyst_signed_doc_spec::{
79
DocSpecs, is_required::IsRequired, metadata::parameters::Parameters,
810
};
911
use catalyst_types::problem_report::ProblemReport;
1012
use futures::FutureExt;
1113

1214
use crate::{
13-
CatalystSignedDocument, DocType, DocumentRefs,
15+
CatalystSignedDocument, DocType, DocumentRef, DocumentRefs,
1416
providers::{CatalystSignedDocumentAndCatalystIdProvider, CatalystSignedDocumentProvider},
1517
validator::{CatalystSignedDocumentValidationRule, rules::doc_ref::doc_refs_check},
1618
};
@@ -184,24 +186,16 @@ impl ParametersRule {
184186
}
185187
}
186188

187-
/// Performs a parameter link validation between a given reference field and the expected
188-
/// parameters.
189-
///
190-
/// Validates that all referenced documents
191-
/// have matching `parameters` with the current document's expected `exp_parameters`.
189+
/// Validates that all documents referenced by `ref_field` recursively contain
190+
/// `parameters` matching the expected `exp_parameters`.
192191
///
193-
/// # Returns
194-
/// - `Ok(true)` if:
195-
/// - `ref_field` is `None`, or
196-
/// - all referenced documents are successfully retrieved **and** each has a
197-
/// `parameters` field that matches `exp_parameters`.
192+
/// The check expands each referenced document's parameter chain and succeeds
193+
/// if any discovered parameter set equals `exp_parameters`.
198194
///
199-
/// - `Ok(false)` if:
200-
/// - any referenced document cannot be retrieved,
201-
/// - a referenced document is missing its `parameters` field, or
202-
/// - the parameters mismatch the expected ones.
203-
///
204-
/// - `Err(anyhow::Error)` if an unexpected error occurs while accessing the provider.
195+
/// Returns:
196+
/// - `Ok(true)` if `ref_field` is `None` or yield a matching parameter set.
197+
/// - `Ok(false)` if no recursive parameter set matches the expected one.
198+
/// - `Err` if an unexpected provider error occurs.
205199
pub(crate) async fn link_check(
206200
ref_field: Option<&DocumentRefs>,
207201
exp_parameters: &DocumentRefs,
@@ -213,39 +207,78 @@ pub(crate) async fn link_check(
213207
return Ok(true);
214208
};
215209

210+
let mut allowed_params = HashSet::new();
216211
let mut all_valid = true;
212+
for doc_ref in ref_field.iter() {
213+
let (valid, result) =
214+
collect_parameters_recursively(doc_ref, field_name, provider, report).await?;
215+
all_valid &= valid;
216+
allowed_params.extend(result);
217+
}
217218

218-
for dr in ref_field.iter() {
219-
if let Some(ref ref_doc) = provider.try_get_doc(dr).await? {
220-
let Some(ref_doc_parameters) = ref_doc.doc_meta().parameters() else {
221-
report.missing_field(
222-
"parameters",
223-
&format!(
224-
"Referenced document via {field_name} must have `parameters` field. Referenced Document: {ref_doc}"
225-
),
226-
);
227-
all_valid = false;
228-
continue;
229-
};
219+
if !all_valid {
220+
return Ok(false);
221+
}
230222

231-
if exp_parameters != ref_doc_parameters {
232-
report.invalid_value(
233-
"parameters",
234-
&format!("Reference doc param: {ref_doc_parameters}",),
235-
&format!("Doc param: {exp_parameters}"),
236-
&format!(
237-
"Referenced document via {field_name} `parameters` field must match. Referenced Document: {ref_doc}"
238-
),
239-
);
240-
all_valid = false;
223+
all_valid &= allowed_params
224+
.iter()
225+
.any(|ref_doc_parameters| exp_parameters == ref_doc_parameters);
226+
227+
if !all_valid {
228+
report.invalid_value(
229+
"parameters",
230+
&format!("Reference doc params: {allowed_params:?}",),
231+
&format!("Doc params: {exp_parameters}"),
232+
&format!("Referenced document via {field_name} `parameters` field must match one of the allowed params"),
233+
);
234+
}
235+
236+
Ok(all_valid)
237+
}
238+
239+
/// Recursively traverses the parameter chain starting from a given `root` document
240+
/// reference, collecting all discovered `parameters` sets.
241+
///
242+
/// Returns:
243+
/// - `(true, set)` if all referenced documents are retrievable.
244+
/// - `(false, set)` if any underlying document cannot be fetched.
245+
///
246+
/// All encountered parameter lists are returned; traversal is cycle-safe
247+
/// and explores deeper parameter references recursively.
248+
async fn collect_parameters_recursively(
249+
root: &DocumentRef,
250+
field_name: &str,
251+
provider: &dyn CatalystSignedDocumentProvider,
252+
report: &ProblemReport,
253+
) -> anyhow::Result<(bool, HashSet<DocumentRefs>)> {
254+
let mut all_valid = true;
255+
let mut result = HashSet::new();
256+
let mut visited = HashSet::new();
257+
let mut stack = vec![root.clone()];
258+
259+
while let Some(current) = stack.pop() {
260+
if !visited.insert(current.clone()) {
261+
continue;
262+
}
263+
264+
if let Some(doc) = provider.try_get_doc(&current).await? {
265+
if let Some(params) = doc.doc_meta().parameters() {
266+
result.insert(params.clone());
267+
268+
for param in params.iter() {
269+
if !visited.contains(param) {
270+
stack.push(param.clone());
271+
}
272+
}
241273
}
242274
} else {
243275
report.functional_validation(
244-
&format!("Cannot retrieve a document {dr}"),
245-
&format!("Referenced document link validation for the `{field_name}` field"),
276+
&format!("Cannot retrieve a document {current}"),
277+
&format!("Referenced document link validation for `{field_name}`"),
246278
);
247279
all_valid = false;
248280
}
249281
}
250-
Ok(all_valid)
282+
283+
Ok((all_valid, result))
251284
}

rust/signed_doc/src/validator/rules/parameters/tests.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,125 @@ use crate::{
418418
;
419419
"reference to the not known document"
420420
)]
421+
#[test_case(
422+
|exp_param_types, provider| {
423+
let parameter_doc = Builder::new()
424+
.with_metadata_field(SupportedField::Id(UuidV7::new()))
425+
.with_metadata_field(SupportedField::Ver(UuidV7::new()))
426+
.with_metadata_field(SupportedField::Type(exp_param_types[0].clone()))
427+
.build();
428+
provider.add_document(&parameter_doc).unwrap();
429+
430+
let params_field: DocumentRefs = vec![parameter_doc.doc_ref().unwrap()].into();
431+
432+
let t1_doc = Builder::new()
433+
.with_metadata_field(SupportedField::Id(UuidV7::new()))
434+
.with_metadata_field(SupportedField::Ver(UuidV7::new()))
435+
.with_metadata_field(SupportedField::Parameters(params_field.clone()))
436+
.build();
437+
provider.add_document(&t1_doc).unwrap();
438+
439+
let t2_doc = Builder::new()
440+
.with_metadata_field(SupportedField::Id(UuidV7::new()))
441+
.with_metadata_field(SupportedField::Ver(UuidV7::new()))
442+
.with_metadata_field(SupportedField::Parameters(
443+
vec![t1_doc.doc_ref().unwrap()].into()
444+
))
445+
.build();
446+
provider.add_document(&t2_doc).unwrap();
447+
448+
449+
Builder::new()
450+
.with_metadata_field(SupportedField::Ref(
451+
vec![t2_doc.doc_ref().unwrap()].into()
452+
))
453+
.with_metadata_field(SupportedField::Parameters(params_field))
454+
.build()
455+
}
456+
=> true
457+
;
458+
// doc (p1) -> t2 -> t1 (p1)
459+
"valid reference to valid one-level recursion parameters field"
460+
)]
461+
#[test_case(
462+
|exp_param_types, provider| {
463+
let parameter_doc = Builder::new()
464+
.with_metadata_field(SupportedField::Id(UuidV7::new()))
465+
.with_metadata_field(SupportedField::Ver(UuidV7::new()))
466+
.with_metadata_field(SupportedField::Type(exp_param_types[0].clone()))
467+
.build();
468+
provider.add_document(&parameter_doc).unwrap();
469+
470+
let params_field: DocumentRefs = vec![parameter_doc.doc_ref().unwrap()].into();
471+
472+
let t1_doc = Builder::new()
473+
.with_metadata_field(SupportedField::Id(UuidV7::new()))
474+
.with_metadata_field(SupportedField::Ver(UuidV7::new()))
475+
.with_metadata_field(SupportedField::Parameters(params_field.clone()))
476+
.build();
477+
provider.add_document(&t1_doc).unwrap();
478+
479+
let t2_doc = Builder::new()
480+
.with_metadata_field(SupportedField::Id(UuidV7::new()))
481+
.with_metadata_field(SupportedField::Ver(UuidV7::new()))
482+
.build();
483+
provider.add_document(&t2_doc).unwrap();
484+
485+
486+
Builder::new()
487+
.with_metadata_field(SupportedField::Ref(
488+
vec![t2_doc.doc_ref().unwrap()].into()
489+
))
490+
.with_metadata_field(SupportedField::Parameters(params_field))
491+
.build()
492+
}
493+
=> false
494+
;
495+
// doc (p1) -> t2 x-> t1 (p1)
496+
"reference to non-linked one-level recursion parameters field"
497+
)]
498+
#[test_case(
499+
|exp_param_types, provider| {
500+
let parameter_doc = Builder::new()
501+
.with_metadata_field(SupportedField::Id(UuidV7::new()))
502+
.with_metadata_field(SupportedField::Ver(UuidV7::new()))
503+
.with_metadata_field(SupportedField::Type(exp_param_types[0].clone()))
504+
.build();
505+
provider.add_document(&parameter_doc).unwrap();
506+
507+
let t1_doc = Builder::new()
508+
.with_metadata_field(SupportedField::Id(UuidV7::new()))
509+
.with_metadata_field(SupportedField::Ver(UuidV7::new()))
510+
.with_metadata_field(SupportedField::Parameters(
511+
vec![parameter_doc.doc_ref().unwrap()].into()
512+
))
513+
.build();
514+
provider.add_document(&t1_doc).unwrap();
515+
516+
let t2_doc = Builder::new()
517+
.with_metadata_field(SupportedField::Id(UuidV7::new()))
518+
.with_metadata_field(SupportedField::Ver(UuidV7::new()))
519+
.with_metadata_field(SupportedField::Parameters(
520+
vec![parameter_doc.doc_ref().unwrap(), t1_doc.doc_ref().unwrap()].into()
521+
))
522+
.build();
523+
provider.add_document(&t2_doc).unwrap();
524+
525+
526+
Builder::new()
527+
.with_metadata_field(SupportedField::Ref(
528+
vec![t2_doc.doc_ref().unwrap()].into()
529+
))
530+
.with_metadata_field(SupportedField::Parameters(
531+
vec![parameter_doc.doc_ref().unwrap()].into()
532+
))
533+
.build()
534+
}
535+
=> true
536+
;
537+
// doc (p1) -> t2 (p1) -> t1 (p1)
538+
"valid reference to valid one-level recursion parameters field, with the same parameters"
539+
)]
421540
#[tokio::test]
422541
async fn parameter_specified_test(
423542
doc_gen: impl FnOnce(&[DocType; 2], &mut TestCatalystProvider) -> CatalystSignedDocument

0 commit comments

Comments
 (0)