Skip to content

Commit 4995282

Browse files
authored
feat(rust): Add uniform problem report type (#140)
* feat(rust): Add uniform problem report type * fix(docs): spelling * fix(rust): catalyst-types ci build
1 parent e3659b1 commit 4995282

File tree

6 files changed

+348
-3
lines changed

6 files changed

+348
-3
lines changed

.config/dictionaries/project.dic

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ Traceback
254254
txmonitor
255255
txns
256256
typenum
257+
uncategorized
257258
unfinalized
258259
unixfs
259260
unlinkat

rust/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ members = [
1010
"cbork-cddl-parser",
1111
"cbork-utils",
1212
"catalyst-voting",
13-
"catalyst-voting",
13+
"catalyst-types",
1414
"immutable-ledger",
1515
"vote-tx-v1",
1616
"vote-tx-v2",

rust/Earthfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ COPY_SRC:
99
Cargo.toml clippy.toml deny.toml rustfmt.toml \
1010
.cargo .config \
1111
c509-certificate \
12+
catalyst-types \
1213
cardano-blockchain-types \
1314
cardano-chain-follower \
1415
catalyst-voting vote-tx-v1 vote-tx-v2 \
@@ -55,7 +56,7 @@ build:
5556
DO rust-ci+EXECUTE \
5657
--cmd="/scripts/std_build.py" \
5758
--args1="--libs=c509-certificate --libs=cardano-blockchain-types --libs=cardano-chain-follower --libs=hermes-ipfs" \
58-
--args2="--libs=cbork-cddl-parser --libs=cbork-abnf-parser --libs=cbork-utils" \
59+
--args2="--libs=cbork-cddl-parser --libs=cbork-abnf-parser --libs=cbork-utils --libs=catalyst-types" \
5960
--args3="--libs=catalyst-voting --libs=immutable-ledger --libs=vote-tx-v1 --libs=vote-tx-v2" \
6061
--args4="--bins=cbork/cbork --libs=rbac-registration --libs=signed_doc" \
6162
--args5="--cov_report=$HOME/build/coverage-report.info" \

rust/catalyst-types/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ workspace = true
1616
name = "catalyst_types"
1717

1818
[dependencies]
19-
anyhow = "1.0.89"
19+
orx-concurrent-vec = "3.1.0"
20+
serde = { version = "1.0.217", features = ["derive"] }

rust/catalyst-types/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
//! Catalyst Generic Types
2+
3+
pub mod problem_report;
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
//! Problem Report type
2+
//!
3+
//! Problem reports are "soft errors" that indicate an issue with the type that holds
4+
//! them. They are not "hard errors" that prevent processing, but are intended to capture
5+
//! a list of issues related that may be fixed by the user.
6+
7+
use std::sync::Arc;
8+
9+
use orx_concurrent_vec::ConcurrentVec;
10+
use serde::{ser::SerializeSeq, Serialize};
11+
12+
/// The kind of problem being reported
13+
#[derive(Serialize, Clone)]
14+
#[serde(tag = "type")]
15+
enum Kind {
16+
/// Expected and Required field is missing
17+
MissingField {
18+
/// Name of the missing field
19+
field: String,
20+
},
21+
/// Unknown and unexpected field was detected
22+
UnknownField {
23+
/// field name
24+
field: String,
25+
/// the value of the field
26+
value: String,
27+
},
28+
/// Expected Field contains invalid value (Field Name, Found Value, Constraints)
29+
InvalidValue {
30+
/// Name of the field with an invalid value
31+
field: String,
32+
/// The detected invalid value
33+
value: String,
34+
/// The constraint of what is expected for a valid value
35+
constraint: String,
36+
},
37+
/// Expected Field was encoded incorrectly
38+
InvalidEncoding {
39+
/// Name of the invalidly encoded field
40+
field: String,
41+
/// Detected encoding
42+
encoded: String,
43+
/// Expected encoding
44+
expected: String,
45+
},
46+
/// Problem with functional validation, typically cross field validation
47+
FunctionalValidation {
48+
/// Explanation of the failed or problematic validation
49+
explanation: String,
50+
},
51+
/// An uncategorized problem was encountered. Use only for rare problems, otherwise
52+
/// make a new problem kind.
53+
Other {
54+
/// A description of the problem
55+
description: String,
56+
},
57+
}
58+
59+
/// Problem Report Entry
60+
#[derive(Serialize, Clone)]
61+
struct Entry {
62+
/// The kind of problem we are recording.
63+
kind: Kind,
64+
/// Any extra context information we want to add.
65+
context: String,
66+
}
67+
68+
/// The Problem Report list
69+
#[derive(Clone)]
70+
struct Report(ConcurrentVec<Entry>);
71+
72+
impl Serialize for Report {
73+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
74+
where S: serde::Serializer {
75+
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
76+
for e in self.0.iter_cloned() {
77+
seq.serialize_element(&e)?;
78+
}
79+
seq.end()
80+
}
81+
}
82+
83+
/// Problem Report
84+
#[derive(Clone, Serialize)]
85+
pub struct ProblemReport {
86+
/// What context does the whole report have
87+
context: Arc<String>,
88+
/// The report itself
89+
// Note, we use this because it allows:
90+
// 1. Cheap copy of this struct.
91+
// 2. Ergonomic Inner mutability.
92+
// 3. Safety for the Problem Report to be used across threads
93+
report: Report,
94+
}
95+
96+
impl ProblemReport {
97+
/// Creates a new `ProblemReport` with the given context string.
98+
///
99+
/// # Arguments
100+
/// * `context`: A reference to a string slice that is used as the context for the
101+
/// problem report.
102+
///
103+
/// # Returns
104+
/// A new instance of `ProblemReport`.
105+
///
106+
/// # Examples
107+
/// ```rust
108+
/// let report = ProblemReport::new("RBAC Registration Decoding");
109+
/// ```
110+
#[must_use]
111+
pub fn new(context: &str) -> Self {
112+
Self {
113+
context: Arc::new(context.to_string()),
114+
report: Report(ConcurrentVec::new()),
115+
}
116+
}
117+
118+
/// Determines if the problem report contains any issues.
119+
///
120+
/// This method checks whether there are any problems recorded in the report by
121+
/// examining the length of the internal `report` field. If the report is empty,
122+
/// it returns `false`, indicating that there are no problems. Otherwise, it
123+
/// returns `true`.
124+
///
125+
/// # Returns
126+
/// A boolean value:
127+
/// - `true` if the problem report contains one or more issues.
128+
/// - `false` if the problem report is empty and has no issues.
129+
///
130+
/// # Examples
131+
/// ```rust
132+
/// let report = ProblemReport::new("Example context");
133+
/// assert_eq!(report.problematic(), false); // Initially, there are no problems.
134+
/// ```
135+
#[must_use]
136+
pub fn problematic(&self) -> bool {
137+
!self.report.0.is_empty()
138+
}
139+
140+
/// Add an entry to the report
141+
fn add_entry(&self, kind: Kind, context: &str) {
142+
self.report.0.push(Entry {
143+
kind,
144+
context: context.to_owned(),
145+
});
146+
}
147+
148+
/// Report that a field was missing in the problem report.
149+
///
150+
/// This method adds an entry to the problem report indicating that a specified field
151+
/// is absent, along with any additional context provided.
152+
///
153+
/// # Arguments
154+
///
155+
/// * `field_name`: A string slice representing the name of the missing field.
156+
/// * `context`: A string slice providing additional context or information about
157+
/// where and why this field is missing.
158+
///
159+
/// # Example
160+
///
161+
/// ```rust
162+
/// // Assuming you have a ProblemReport instance `report`
163+
/// report.missing_field("name", "In the JSON payload for user creation");
164+
/// ```
165+
pub fn missing_field(&self, field_name: &str, context: &str) {
166+
self.add_entry(
167+
Kind::MissingField {
168+
field: field_name.to_owned(),
169+
},
170+
context,
171+
);
172+
}
173+
174+
/// Reports that an unknown and unexpected field was encountered in the problem
175+
/// report.
176+
///
177+
/// This method adds an entry to the problem report indicating that a specified field
178+
/// was found but is not recognized or expected, along with its value and any
179+
/// additional context provided.
180+
///
181+
/// # Arguments
182+
///
183+
/// * `field_name`: A string slice representing the name of the unknown field.
184+
/// * `value`: A string slice representing the value of the unknown field.
185+
/// * `context`: A string slice providing additional context or information about
186+
/// where and why this field is unexpected.
187+
///
188+
/// # Example
189+
///
190+
/// ```rust
191+
/// // Assuming you have a ProblemReport instance `report`
192+
/// report.unknown_field(
193+
/// "unsupported_option",
194+
/// "true",
195+
/// "In the JSON configuration file",
196+
/// );
197+
/// ```
198+
pub fn unknown_field(&self, field_name: &str, value: &str, context: &str) {
199+
self.add_entry(
200+
Kind::UnknownField {
201+
field: field_name.to_owned(),
202+
value: value.to_owned(),
203+
},
204+
context,
205+
);
206+
}
207+
208+
/// Reports that a field has an invalid value in the problem report.
209+
///
210+
/// This method adds an entry to the problem report indicating that a specified field
211+
/// contains a value which does not meet the required constraints, along with any
212+
/// additional context provided.
213+
///
214+
/// # Arguments
215+
///
216+
/// * `field_name`: A string slice representing the name of the field with the invalid
217+
/// value.
218+
/// * `found`: A string slice representing the actual value found in the field that is
219+
/// deemed invalid.
220+
/// * `constraint`: A string slice representing the constraint or expected format for
221+
/// the field's value.
222+
/// * `context`: A string slice providing additional context or information about
223+
/// where and why this field has an invalid value.
224+
///
225+
/// # Example
226+
///
227+
/// ```rust
228+
/// // Assuming you have a ProblemReport instance `report`
229+
/// report.invalid_value(
230+
/// "age",
231+
/// "300",
232+
/// "must be between 18 and 99",
233+
/// "During user registration",
234+
/// );
235+
/// ```
236+
pub fn invalid_value(&self, field_name: &str, found: &str, constraint: &str, context: &str) {
237+
self.add_entry(
238+
Kind::InvalidValue {
239+
field: field_name.to_owned(),
240+
value: found.to_owned(),
241+
constraint: constraint.to_owned(),
242+
},
243+
context,
244+
);
245+
}
246+
247+
/// Reports that a field has an invalid encoding in the problem report.
248+
///
249+
/// This method adds an entry to the problem report indicating that a specified field
250+
/// contains data which is encoded using a format that does not match the expected or
251+
/// required encoding, along with any additional context provided.
252+
///
253+
/// # Arguments
254+
///
255+
/// * `field_name`: A string slice representing the name of the field with the invalid
256+
/// encoding.
257+
/// * `detected_encoding`: A string slice representing the detected encoding of the
258+
/// data in the field.
259+
/// * `expected_encoding`: A string slice representing the expected or required
260+
/// encoding for the field's data.
261+
/// * `context`: A string slice providing additional context or information about
262+
/// where and why this field has an invalid encoding.
263+
///
264+
/// # Example
265+
///
266+
/// ```rust
267+
/// // Assuming you have a ProblemReport instance `report`
268+
/// report.invalid_encoding("data", "UTF-8", "ASCII", "During data import");
269+
/// ```
270+
pub fn invalid_encoding(
271+
&self, field_name: &str, detected_encoding: &str, expected_encoding: &str, context: &str,
272+
) {
273+
self.add_entry(
274+
Kind::InvalidEncoding {
275+
field: field_name.to_owned(),
276+
encoded: detected_encoding.to_owned(),
277+
expected: expected_encoding.to_owned(),
278+
},
279+
context,
280+
);
281+
}
282+
283+
/// Reports an invalid validation or cross-field validation error in the problem
284+
/// report.
285+
///
286+
/// This method adds an entry to the problem report indicating that there is a
287+
/// functional validation issue, typically involving multiple fields or data points
288+
/// not meeting specific validation criteria, along with any additional context
289+
/// provided.
290+
///
291+
/// # Arguments
292+
///
293+
/// * `explanation`: A string slice providing a detailed explanation of why the
294+
/// validation failed.
295+
/// * `context`: A string slice providing additional context or information about
296+
/// where and why this functional validation error occurred.
297+
///
298+
/// # Example
299+
///
300+
/// ```rust
301+
/// // Assuming you have a ProblemReport instance `report`
302+
/// report.functional_validation(
303+
/// "End date cannot be before start date",
304+
/// "During contract creation",
305+
/// );
306+
/// ```
307+
pub fn functional_validation(&self, explanation: &str, context: &str) {
308+
self.add_entry(
309+
Kind::FunctionalValidation {
310+
explanation: explanation.to_owned(),
311+
},
312+
context,
313+
);
314+
}
315+
316+
/// Reports an uncategorized problem with the given description and context.
317+
///
318+
/// This method is intended for use in rare situations where a specific type of
319+
/// problem has not been identified or defined. Using this method frequently can
320+
/// lead to disorganized reporting and difficulty in analyzing problems. For
321+
/// better clarity and organization, consider creating more distinct categories of
322+
/// problems to report using methods that specifically handle those types (e.g.,
323+
/// `other_problem`, `technical_issue`, etc.).
324+
///
325+
/// # Parameters:
326+
/// - `description`: A brief description of the problem. This should clearly convey
327+
/// what went wrong or what caused the issue.
328+
/// - `context`: Additional information that might help in understanding the context
329+
/// or environment where the problem occurred. This could include details about the
330+
/// system, user actions, or any other relevant data.
331+
pub fn other(&self, description: &str, context: &str) {
332+
self.add_entry(
333+
Kind::Other {
334+
description: description.to_owned(),
335+
},
336+
context,
337+
);
338+
}
339+
}

0 commit comments

Comments
 (0)