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

input-output-hk / catalyst-libs / 21361213206

26 Jan 2026 02:26PM UTC coverage: 70.285% (+0.03%) from 70.257%
21361213206

Pull #813

github

web-flow
Merge 4355e2af8 into 16e9c4a0a
Pull Request #813: chore(rust/signed-doc): Remove the boolean flag from the parameters rule validation

48 of 53 new or added lines in 4 files covered. (90.57%)

17115 of 24351 relevant lines covered (70.28%)

2300.83 hits per line

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

89.77
/rust/signed_doc/src/validator/rules/parameters/mod.rs
1
//! `parameters` rule type impl.
2

3
#[cfg(test)]
4
mod tests;
5

6
use std::collections::HashSet;
7

8
use catalyst_signed_doc_spec::{
9
    DocSpecs, is_required::IsRequired, metadata::parameters::Parameters,
10
};
11
use catalyst_types::problem_report::ProblemReport;
12
use itertools::Itertools;
13

14
use crate::{
15
    CatalystSignedDocument, DocType, DocumentRef, DocumentRefs,
16
    providers::{CatalystSignedDocumentProvider, Provider},
17
    validator::{CatalystSignedDocumentValidationRule, rules::doc_ref::doc_refs_check},
18
};
19

20
/// `parameters` field validation rule
21
#[derive(Debug)]
22
pub(crate) enum ParametersRule {
23
    /// Is `parameters` specified
24
    Specified {
25
        /// expected `type` field of the parameter doc
26
        allowed_type: Vec<DocType>,
27
        /// optional flag for the `parameters` field
28
        optional: bool,
29
    },
30
    /// `parameters` is not specified
31
    NotSpecified,
32
}
33

34
impl CatalystSignedDocumentValidationRule for ParametersRule {
35
    fn check(
185✔
36
        &self,
185✔
37
        doc: &CatalystSignedDocument,
185✔
38
        provider: &dyn Provider,
185✔
39
    ) -> anyhow::Result<bool> {
185✔
40
        self.check_inner(doc, provider)?;
185✔
41
        Ok(!doc.report().is_problematic())
185✔
42
    }
185✔
43
}
44

45
impl ParametersRule {
46
    /// Generating `ParametersRule` from specs
47
    pub(crate) fn new(
3,087✔
48
        docs: &DocSpecs,
3,087✔
49
        spec: &Parameters,
3,087✔
50
    ) -> anyhow::Result<Self> {
3,087✔
51
        let optional = match spec.required {
3,087✔
52
            IsRequired::Yes => false,
2,793✔
53
            IsRequired::Optional => true,
×
54
            IsRequired::Excluded => {
55
                anyhow::ensure!(
294✔
56
                    spec.doc_type.is_empty() && !spec.multiple,
294✔
57
                    "'type' and 'multiple' fields could not been specified when 'required' is 'excluded' for 'parameters'  metadata definition"
×
58
                );
59
                return Ok(Self::NotSpecified);
294✔
60
            },
61
        };
62

63
        anyhow::ensure!(
2,793✔
64
            !spec.doc_type.is_empty(),
2,793✔
65
            "'type' field should exists and has at least one entry for the required 'parameters' metadata definition"
×
66
        );
67
        anyhow::ensure!(
2,793✔
68
            !spec.multiple,
2,793✔
69
            "'multiple' field should be only set to false for the required 'parameters' metadata definition"
×
70
        );
71

72
        let allowed_type = spec.doc_type.iter().try_fold(
2,793✔
73
            Vec::new(),
2,793✔
74
            |mut res, doc_name| -> anyhow::Result<_> {
5,145✔
75
                let docs_spec = docs.get(doc_name).ok_or(anyhow::anyhow!(
5,145✔
76
                    "cannot find a document definition {doc_name}"
5,145✔
77
                ))?;
×
78
                res.push(docs_spec.doc_type.as_str().parse()?);
5,145✔
79
                Ok(res)
5,145✔
80
            },
5,145✔
81
        )?;
×
82

83
        Ok(Self::Specified {
2,793✔
84
            allowed_type,
2,793✔
85
            optional,
2,793✔
86
        })
2,793✔
87
    }
3,087✔
88

89
    /// Field validation rule
90
    fn check_inner(
185✔
91
        &self,
185✔
92
        doc: &CatalystSignedDocument,
185✔
93
        provider: &dyn Provider,
185✔
94
    ) -> anyhow::Result<()> {
185✔
95
        let context: &str = "Parameter rule check";
185✔
96
        if let Self::Specified {
97
            allowed_type: exp_parameters_type,
174✔
98
            optional,
174✔
99
        } = self
185✔
100
        {
101
            if let Some(parameters_ref) = doc.doc_meta().parameters() {
174✔
102
                doc_refs_check(
154✔
103
                    parameters_ref,
154✔
104
                    exp_parameters_type,
154✔
105
                    false,
106
                    "parameters",
154✔
107
                    provider,
154✔
108
                    doc.report(),
154✔
109
                    |_| true,
NEW
110
                )?;
×
111

112
                link_check(
154✔
113
                    doc.doc_meta().template(),
154✔
114
                    parameters_ref,
154✔
115
                    "template",
154✔
116
                    provider,
154✔
117
                    doc.report(),
154✔
NEW
118
                )?;
×
119
                link_check(
154✔
120
                    doc.doc_meta().doc_ref(),
154✔
121
                    parameters_ref,
154✔
122
                    "ref",
154✔
123
                    provider,
154✔
124
                    doc.report(),
154✔
NEW
125
                )?;
×
126
                link_check(
154✔
127
                    doc.doc_meta().reply(),
154✔
128
                    parameters_ref,
154✔
129
                    "reply",
154✔
130
                    provider,
154✔
131
                    doc.report(),
154✔
NEW
132
                )?;
×
133
                let chain_field = doc
154✔
134
                    .doc_meta()
154✔
135
                    .chain()
154✔
136
                    .and_then(|v| v.document_ref().cloned())
154✔
137
                    .map(|v| vec![v].into());
154✔
138
                link_check(
154✔
139
                    chain_field.as_ref(),
154✔
140
                    parameters_ref,
154✔
141
                    "chain",
154✔
142
                    provider,
154✔
143
                    doc.report(),
154✔
NEW
144
                )?;
×
145
            } else if !optional {
20✔
146
                doc.report().missing_field(
19✔
147
                    "parameters",
19✔
148
                    &format!("{context}, document must have parameters field"),
19✔
149
                );
19✔
150
            }
19✔
151
        }
11✔
152
        if let Self::NotSpecified = self
185✔
153
            && let Some(parameters) = doc.doc_meta().parameters()
11✔
154
        {
1✔
155
            doc.report().unknown_field(
1✔
156
                "parameters",
1✔
157
                &parameters.to_string(),
1✔
158
                &format!("{context}, document does not expect to have a parameters field"),
1✔
159
            );
1✔
160
        }
184✔
161

162
        Ok(())
185✔
163
    }
185✔
164
}
165

166
/// Validates that all documents referenced by `ref_field` recursively contain
167
/// `parameters` matching the expected `exp_parameters`.
168
///
169
/// The check expands each referenced document's parameter chain and succeeds
170
/// if any discovered parameter set equals `exp_parameters`.
171
///
172
/// Returns:
173
/// - `Ok(true)` if `ref_field` is `None` or yield a matching parameter set.
174
/// - `Ok(false)` if no recursive parameter set matches the expected one.
175
/// - `Err` if an unexpected provider error occurs.
176
pub(crate) fn link_check(
616✔
177
    ref_field: Option<&DocumentRefs>,
616✔
178
    exp_parameters: &DocumentRefs,
616✔
179
    field_name: &str,
616✔
180
    provider: &dyn CatalystSignedDocumentProvider,
616✔
181
    report: &ProblemReport,
616✔
182
) -> anyhow::Result<()> {
616✔
183
    let Some(ref_field) = ref_field else {
616✔
184
        return Ok(());
501✔
185
    };
186

187
    let mut allowed_params = HashSet::new();
115✔
188
    for doc_ref in exp_parameters.iter() {
115✔
189
        let result = collect_parameters_recursively(doc_ref, field_name, provider, report)?;
115✔
190
        allowed_params.extend(result);
115✔
191
    }
192

193
    for doc_ref in ref_field.iter() {
115✔
194
        if let Some(referenced_doc) = provider.try_get_doc(doc_ref)? {
115✔
195
            if let Some(ref_params) = referenced_doc.doc_meta().parameters() {
109✔
196
                if !ref_params.iter().all(|v| allowed_params.contains(v)) {
102✔
197
                    report.invalid_value(
8✔
198
                        "parameters",
8✔
199
                        &format!("[{}]", ref_params.iter().map(ToString::to_string).join(",")),
8✔
200
                        &format!("[{}]", allowed_params.iter().map(ToString::to_string).join(",")),
8✔
201
                        &format!("Referenced document {doc_ref} via {field_name} `parameters` field must match one of the allowed params"),
8✔
202
                    );
8✔
203
                }
94✔
204
            } else {
7✔
205
                report.missing_field(
7✔
206
                    "'parameters'",
7✔
207
                    &format!("Referenced document {doc_ref} must have `parameters` field"),
7✔
208
                );
7✔
209
            }
7✔
210
        } else {
6✔
211
            report.functional_validation(
6✔
212
                &format!("Cannot retrieve a document {doc_ref}"),
6✔
213
                &format!("Referenced document link validation for `{field_name}`"),
6✔
214
            );
6✔
215
        }
6✔
216
    }
217

218
    Ok(())
115✔
219
}
616✔
220

221
/// Recursively traverses the parameter chain starting from a given `root` document
222
/// reference, collecting all discovered `parameters` sets.
223
///
224
/// Returns:
225
/// - `(true, set)` if all referenced documents are retrievable.
226
/// - `(false, set)` if any underlying document cannot be fetched.
227
///
228
/// All encountered parameter lists are returned; traversal is cycle-safe
229
/// and explores deeper parameter references recursively.
230
fn collect_parameters_recursively(
115✔
231
    root: &DocumentRef,
115✔
232
    field_name: &str,
115✔
233
    provider: &dyn CatalystSignedDocumentProvider,
115✔
234
    report: &ProblemReport,
115✔
235
) -> anyhow::Result<HashSet<DocumentRef>> {
115✔
236
    let mut result: HashSet<_> = HashSet::new();
115✔
237
    let mut visited = HashSet::new();
115✔
238
    let mut stack = vec![root.clone()];
115✔
239

240
    while let Some(current) = stack.pop() {
292✔
241
        if !visited.insert(current.clone()) {
177✔
242
            continue;
×
243
        }
177✔
244
        result.insert(current.clone());
177✔
245

246
        if let Some(doc) = provider.try_get_doc(&current)? {
177✔
247
            if let Some(params) = doc.doc_meta().parameters() {
177✔
248
                for param in params.iter() {
62✔
249
                    if !visited.contains(param) {
62✔
250
                        stack.push(param.clone());
62✔
251
                    }
62✔
252
                }
253
            }
115✔
254
        } else {
×
255
            report.functional_validation(
×
256
                &format!("Cannot retrieve a document {current}"),
×
257
                &format!("Referenced document link validation for `{field_name}`"),
×
258
            );
×
259
        }
×
260

261
        result.insert(current);
177✔
262
    }
263

264
    Ok(result)
115✔
265
}
115✔
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