diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index ebd535eb85..2efec71ecd 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -254,6 +254,7 @@ Traceback txmonitor txns typenum +uncategorized unfinalized unixfs unlinkat diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 6152cf80b4..f3b7ecc233 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -10,7 +10,7 @@ members = [ "cbork-cddl-parser", "cbork-utils", "catalyst-voting", - "catalyst-voting", + "catalyst-types", "immutable-ledger", "vote-tx-v1", "vote-tx-v2", diff --git a/rust/Earthfile b/rust/Earthfile index 7ededfe9f1..71a68d8fb3 100644 --- a/rust/Earthfile +++ b/rust/Earthfile @@ -9,6 +9,7 @@ COPY_SRC: Cargo.toml clippy.toml deny.toml rustfmt.toml \ .cargo .config \ c509-certificate \ + catalyst-types \ cardano-blockchain-types \ cardano-chain-follower \ catalyst-voting vote-tx-v1 vote-tx-v2 \ @@ -55,7 +56,7 @@ build: DO rust-ci+EXECUTE \ --cmd="/scripts/std_build.py" \ --args1="--libs=c509-certificate --libs=cardano-blockchain-types --libs=cardano-chain-follower --libs=hermes-ipfs" \ - --args2="--libs=cbork-cddl-parser --libs=cbork-abnf-parser --libs=cbork-utils" \ + --args2="--libs=cbork-cddl-parser --libs=cbork-abnf-parser --libs=cbork-utils --libs=catalyst-types" \ --args3="--libs=catalyst-voting --libs=immutable-ledger --libs=vote-tx-v1 --libs=vote-tx-v2" \ --args4="--bins=cbork/cbork --libs=rbac-registration --libs=signed_doc" \ --args5="--cov_report=$HOME/build/coverage-report.info" \ diff --git a/rust/catalyst-types/Cargo.toml b/rust/catalyst-types/Cargo.toml index b59ed1bfe6..59a20f0456 100644 --- a/rust/catalyst-types/Cargo.toml +++ b/rust/catalyst-types/Cargo.toml @@ -16,4 +16,5 @@ workspace = true name = "catalyst_types" [dependencies] -anyhow = "1.0.89" +orx-concurrent-vec = "3.1.0" +serde = { version = "1.0.217", features = ["derive"] } diff --git a/rust/catalyst-types/src/lib.rs b/rust/catalyst-types/src/lib.rs index e69de29bb2..427bbbecf9 100644 --- a/rust/catalyst-types/src/lib.rs +++ b/rust/catalyst-types/src/lib.rs @@ -0,0 +1,3 @@ +//! Catalyst Generic Types + +pub mod problem_report; diff --git a/rust/catalyst-types/src/problem_report.rs b/rust/catalyst-types/src/problem_report.rs new file mode 100644 index 0000000000..57143f0731 --- /dev/null +++ b/rust/catalyst-types/src/problem_report.rs @@ -0,0 +1,339 @@ +//! Problem Report type +//! +//! Problem reports are "soft errors" that indicate an issue with the type that holds +//! them. They are not "hard errors" that prevent processing, but are intended to capture +//! a list of issues related that may be fixed by the user. + +use std::sync::Arc; + +use orx_concurrent_vec::ConcurrentVec; +use serde::{ser::SerializeSeq, Serialize}; + +/// The kind of problem being reported +#[derive(Serialize, Clone)] +#[serde(tag = "type")] +enum Kind { + /// Expected and Required field is missing + MissingField { + /// Name of the missing field + field: String, + }, + /// Unknown and unexpected field was detected + UnknownField { + /// field name + field: String, + /// the value of the field + value: String, + }, + /// Expected Field contains invalid value (Field Name, Found Value, Constraints) + InvalidValue { + /// Name of the field with an invalid value + field: String, + /// The detected invalid value + value: String, + /// The constraint of what is expected for a valid value + constraint: String, + }, + /// Expected Field was encoded incorrectly + InvalidEncoding { + /// Name of the invalidly encoded field + field: String, + /// Detected encoding + encoded: String, + /// Expected encoding + expected: String, + }, + /// Problem with functional validation, typically cross field validation + FunctionalValidation { + /// Explanation of the failed or problematic validation + explanation: String, + }, + /// An uncategorized problem was encountered. Use only for rare problems, otherwise + /// make a new problem kind. + Other { + /// A description of the problem + description: String, + }, +} + +/// Problem Report Entry +#[derive(Serialize, Clone)] +struct Entry { + /// The kind of problem we are recording. + kind: Kind, + /// Any extra context information we want to add. + context: String, +} + +/// The Problem Report list +#[derive(Clone)] +struct Report(ConcurrentVec); + +impl Serialize for Report { + fn serialize(&self, serializer: S) -> Result + where S: serde::Serializer { + let mut seq = serializer.serialize_seq(Some(self.0.len()))?; + for e in self.0.iter_cloned() { + seq.serialize_element(&e)?; + } + seq.end() + } +} + +/// Problem Report +#[derive(Clone, Serialize)] +pub struct ProblemReport { + /// What context does the whole report have + context: Arc, + /// The report itself + // Note, we use this because it allows: + // 1. Cheap copy of this struct. + // 2. Ergonomic Inner mutability. + // 3. Safety for the Problem Report to be used across threads + report: Report, +} + +impl ProblemReport { + /// Creates a new `ProblemReport` with the given context string. + /// + /// # Arguments + /// * `context`: A reference to a string slice that is used as the context for the + /// problem report. + /// + /// # Returns + /// A new instance of `ProblemReport`. + /// + /// # Examples + /// ```rust + /// let report = ProblemReport::new("RBAC Registration Decoding"); + /// ``` + #[must_use] + pub fn new(context: &str) -> Self { + Self { + context: Arc::new(context.to_string()), + report: Report(ConcurrentVec::new()), + } + } + + /// Determines if the problem report contains any issues. + /// + /// This method checks whether there are any problems recorded in the report by + /// examining the length of the internal `report` field. If the report is empty, + /// it returns `false`, indicating that there are no problems. Otherwise, it + /// returns `true`. + /// + /// # Returns + /// A boolean value: + /// - `true` if the problem report contains one or more issues. + /// - `false` if the problem report is empty and has no issues. + /// + /// # Examples + /// ```rust + /// let report = ProblemReport::new("Example context"); + /// assert_eq!(report.problematic(), false); // Initially, there are no problems. + /// ``` + #[must_use] + pub fn problematic(&self) -> bool { + !self.report.0.is_empty() + } + + /// Add an entry to the report + fn add_entry(&self, kind: Kind, context: &str) { + self.report.0.push(Entry { + kind, + context: context.to_owned(), + }); + } + + /// Report that a field was missing in the problem report. + /// + /// This method adds an entry to the problem report indicating that a specified field + /// is absent, along with any additional context provided. + /// + /// # Arguments + /// + /// * `field_name`: A string slice representing the name of the missing field. + /// * `context`: A string slice providing additional context or information about + /// where and why this field is missing. + /// + /// # Example + /// + /// ```rust + /// // Assuming you have a ProblemReport instance `report` + /// report.missing_field("name", "In the JSON payload for user creation"); + /// ``` + pub fn missing_field(&self, field_name: &str, context: &str) { + self.add_entry( + Kind::MissingField { + field: field_name.to_owned(), + }, + context, + ); + } + + /// Reports that an unknown and unexpected field was encountered in the problem + /// report. + /// + /// This method adds an entry to the problem report indicating that a specified field + /// was found but is not recognized or expected, along with its value and any + /// additional context provided. + /// + /// # Arguments + /// + /// * `field_name`: A string slice representing the name of the unknown field. + /// * `value`: A string slice representing the value of the unknown field. + /// * `context`: A string slice providing additional context or information about + /// where and why this field is unexpected. + /// + /// # Example + /// + /// ```rust + /// // Assuming you have a ProblemReport instance `report` + /// report.unknown_field( + /// "unsupported_option", + /// "true", + /// "In the JSON configuration file", + /// ); + /// ``` + pub fn unknown_field(&self, field_name: &str, value: &str, context: &str) { + self.add_entry( + Kind::UnknownField { + field: field_name.to_owned(), + value: value.to_owned(), + }, + context, + ); + } + + /// Reports that a field has an invalid value in the problem report. + /// + /// This method adds an entry to the problem report indicating that a specified field + /// contains a value which does not meet the required constraints, along with any + /// additional context provided. + /// + /// # Arguments + /// + /// * `field_name`: A string slice representing the name of the field with the invalid + /// value. + /// * `found`: A string slice representing the actual value found in the field that is + /// deemed invalid. + /// * `constraint`: A string slice representing the constraint or expected format for + /// the field's value. + /// * `context`: A string slice providing additional context or information about + /// where and why this field has an invalid value. + /// + /// # Example + /// + /// ```rust + /// // Assuming you have a ProblemReport instance `report` + /// report.invalid_value( + /// "age", + /// "300", + /// "must be between 18 and 99", + /// "During user registration", + /// ); + /// ``` + pub fn invalid_value(&self, field_name: &str, found: &str, constraint: &str, context: &str) { + self.add_entry( + Kind::InvalidValue { + field: field_name.to_owned(), + value: found.to_owned(), + constraint: constraint.to_owned(), + }, + context, + ); + } + + /// Reports that a field has an invalid encoding in the problem report. + /// + /// This method adds an entry to the problem report indicating that a specified field + /// contains data which is encoded using a format that does not match the expected or + /// required encoding, along with any additional context provided. + /// + /// # Arguments + /// + /// * `field_name`: A string slice representing the name of the field with the invalid + /// encoding. + /// * `detected_encoding`: A string slice representing the detected encoding of the + /// data in the field. + /// * `expected_encoding`: A string slice representing the expected or required + /// encoding for the field's data. + /// * `context`: A string slice providing additional context or information about + /// where and why this field has an invalid encoding. + /// + /// # Example + /// + /// ```rust + /// // Assuming you have a ProblemReport instance `report` + /// report.invalid_encoding("data", "UTF-8", "ASCII", "During data import"); + /// ``` + pub fn invalid_encoding( + &self, field_name: &str, detected_encoding: &str, expected_encoding: &str, context: &str, + ) { + self.add_entry( + Kind::InvalidEncoding { + field: field_name.to_owned(), + encoded: detected_encoding.to_owned(), + expected: expected_encoding.to_owned(), + }, + context, + ); + } + + /// Reports an invalid validation or cross-field validation error in the problem + /// report. + /// + /// This method adds an entry to the problem report indicating that there is a + /// functional validation issue, typically involving multiple fields or data points + /// not meeting specific validation criteria, along with any additional context + /// provided. + /// + /// # Arguments + /// + /// * `explanation`: A string slice providing a detailed explanation of why the + /// validation failed. + /// * `context`: A string slice providing additional context or information about + /// where and why this functional validation error occurred. + /// + /// # Example + /// + /// ```rust + /// // Assuming you have a ProblemReport instance `report` + /// report.functional_validation( + /// "End date cannot be before start date", + /// "During contract creation", + /// ); + /// ``` + pub fn functional_validation(&self, explanation: &str, context: &str) { + self.add_entry( + Kind::FunctionalValidation { + explanation: explanation.to_owned(), + }, + context, + ); + } + + /// Reports an uncategorized problem with the given description and context. + /// + /// This method is intended for use in rare situations where a specific type of + /// problem has not been identified or defined. Using this method frequently can + /// lead to disorganized reporting and difficulty in analyzing problems. For + /// better clarity and organization, consider creating more distinct categories of + /// problems to report using methods that specifically handle those types (e.g., + /// `other_problem`, `technical_issue`, etc.). + /// + /// # Parameters: + /// - `description`: A brief description of the problem. This should clearly convey + /// what went wrong or what caused the issue. + /// - `context`: Additional information that might help in understanding the context + /// or environment where the problem occurred. This could include details about the + /// system, user actions, or any other relevant data. + pub fn other(&self, description: &str, context: &str) { + self.add_entry( + Kind::Other { + description: description.to_owned(), + }, + context, + ); + } +}