33#[ cfg( test) ]
44mod tests;
55
6+ use std:: collections:: HashSet ;
7+
68use catalyst_signed_doc_spec:: {
79 DocSpecs , is_required:: IsRequired , metadata:: parameters:: Parameters ,
810} ;
911use catalyst_types:: problem_report:: ProblemReport ;
1012use futures:: FutureExt ;
1113
1214use 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.
205199pub ( 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}
0 commit comments