From 955e788625b5a68ae0ee3714fa6f8612c208679c Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 9 Sep 2025 18:36:22 +0400 Subject: [PATCH 01/14] added new catalyst-signed-doc-spec crate --- rust/Cargo.toml | 1 + rust/catalyst-signed-doc-spec/Cargo.toml | 19 +++ .../src/content_type.rs | 9 ++ rust/catalyst-signed-doc-spec/src/doc_ref.rs | 12 ++ rust/catalyst-signed-doc-spec/src/lib.rs | 140 ++++++++++++++++++ 5 files changed, 181 insertions(+) create mode 100644 rust/catalyst-signed-doc-spec/Cargo.toml create mode 100644 rust/catalyst-signed-doc-spec/src/content_type.rs create mode 100644 rust/catalyst-signed-doc-spec/src/doc_ref.rs create mode 100644 rust/catalyst-signed-doc-spec/src/lib.rs diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 0b841272355..14798029f5f 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -16,6 +16,7 @@ members = [ "vote-tx-v2", "signed_doc", "catalyst-signed-doc-macro", + "catalyst-signed-doc-spec", "rbac-registration", ] diff --git a/rust/catalyst-signed-doc-spec/Cargo.toml b/rust/catalyst-signed-doc-spec/Cargo.toml new file mode 100644 index 00000000000..0fd35cb644c --- /dev/null +++ b/rust/catalyst-signed-doc-spec/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "catalyst-signed-doc-spec" +version = "0.0.1" +edition.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true + +[dependencies] +serde_json = "1.0.142" +anyhow = "1.0.99" +serde = { version = "1.0.219", features = ["derive"] } + +quote = "1.0" +proc-macro2 = "1.0" + +[lints] +workspace = true diff --git a/rust/catalyst-signed-doc-spec/src/content_type.rs b/rust/catalyst-signed-doc-spec/src/content_type.rs new file mode 100644 index 00000000000..2f74653c017 --- /dev/null +++ b/rust/catalyst-signed-doc-spec/src/content_type.rs @@ -0,0 +1,9 @@ +//! `signed_doc.json` headers content type field JSON definition + +/// `signed_doc.json` "content type" field JSON object +#[derive(serde::Deserialize)] +#[allow(clippy::missing_docs_in_private_items)] +pub struct ContentType { + pub required: super::IsRequired, + pub value: Option, +} diff --git a/rust/catalyst-signed-doc-spec/src/doc_ref.rs b/rust/catalyst-signed-doc-spec/src/doc_ref.rs new file mode 100644 index 00000000000..64a0ef9c2b5 --- /dev/null +++ b/rust/catalyst-signed-doc-spec/src/doc_ref.rs @@ -0,0 +1,12 @@ +//! `signed_doc.json` "ref" field JSON definition + +use crate::{DocTypes, IsRequired}; + +/// `signed_doc.json` "ref" field JSON object +#[derive(serde::Deserialize)] +pub struct Ref { + pub required: IsRequired, + #[serde(rename = "type")] + pub doc_type: DocTypes, + pub multiple: Option, +} diff --git a/rust/catalyst-signed-doc-spec/src/lib.rs b/rust/catalyst-signed-doc-spec/src/lib.rs new file mode 100644 index 00000000000..4b980a9db24 --- /dev/null +++ b/rust/catalyst-signed-doc-spec/src/lib.rs @@ -0,0 +1,140 @@ +//! Catalyst Signed Document spec type + +#![allow(missing_docs, clippy::missing_docs_in_private_items)] + +pub mod content_type; +pub mod doc_ref; + +use std::{collections::HashMap, ops::Deref}; + +/// Catalyst Signed Document spec representation struct +#[derive(serde::Deserialize)] +pub struct CatalystSignedDocSpec { + /// A collection of document's supported content types + #[serde(rename = "contentTypes")] + #[allow(dead_code)] + pub content_types: HashMap, + /// A collection of document's specs + pub docs: HashMap, +} + +/// Catalyst Signed Document supported content type declaration struct +#[derive(serde::Deserialize)] +pub struct ContentTypeSpec { + /// CoAP Content-Formats + #[allow(dead_code)] + coap_type: Option, +} + +// A thin wrapper over the string document name values +#[derive(serde::Deserialize, PartialEq, Eq, Hash)] +pub struct DocumentName(String); + +impl DocumentName { + /// returns document name + pub fn name(&self) -> &str { + &self.0 + } + + /// returns a document name as a `Ident` in the following form + /// `"PROPOSAL_FORM_TEMPLATE"` + pub fn ident(&self) -> proc_macro2::Ident { + quote::format_ident!( + "{}", + self.0 + .split_whitespace() + .map(str::to_uppercase) + .collect::>() + .join("_") + ) + } +} + +/// Specific document type definition +#[derive(serde::Deserialize)] +pub struct DocSpec { + /// Document type UUID v4 value + #[serde(rename = "type")] + pub doc_type: String, + /// `headers` field + pub headers: Headers, + /// Document type metadata definitions + pub metadata: Metadata, +} + +/// Document's metadata fields definition +#[derive(serde::Deserialize)] +#[allow(clippy::missing_docs_in_private_items)] +pub struct Metadata { + #[serde(rename = "ref")] + pub doc_ref: doc_ref::Ref, +} + +/// Document's metadata fields definition +#[derive(serde::Deserialize)] +#[allow(clippy::missing_docs_in_private_items)] +pub struct Headers { + #[serde(rename = "content type")] + pub content_type: content_type::ContentType, +} + +/// "required" field definition +#[derive(serde::Deserialize)] +#[serde(rename_all = "lowercase")] +#[allow(clippy::missing_docs_in_private_items)] +pub enum IsRequired { + Yes, + Excluded, + Optional, +} + +/// A helper type for deserialization "type" metadata field +pub struct DocTypes(Vec); + +impl Deref for DocTypes { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'de> serde::Deserialize<'de> for DocTypes { + #[allow(clippy::missing_docs_in_private_items)] + fn deserialize(deserializer: D) -> Result + where D: serde::Deserializer<'de> { + #[derive(serde::Deserialize)] + #[serde(untagged)] + enum SingleOrVec { + Single(DocumentName), + Multiple(Vec), + } + let value = Option::::deserialize(deserializer)?; + let result = match value { + Some(SingleOrVec::Single(item)) => vec![item], + Some(SingleOrVec::Multiple(items)) => items, + None => vec![], + }; + Ok(Self(result)) + } +} + +impl CatalystSignedDocSpec { + /// Loading a Catalyst Signed Documents spec from the `signed_doc.json` + pub fn load_signed_doc_spec() -> anyhow::Result { + let signed_doc_str = include_str!("../../../specs/signed_doc.json"); + let signed_doc_spec = serde_json::from_str(signed_doc_str) + .map_err(|e| anyhow::anyhow!("Invalid Catalyst Signed Documents JSON Spec: {e}"))?; + Ok(signed_doc_spec) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn load_signed_doc_spec_test() { + assert!(CatalystSignedDocSpec::load_signed_doc_spec().is_ok()); + } +} From 3cf40f191e43cecd7842f40b91715f8921b21c63 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 9 Sep 2025 18:52:22 +0400 Subject: [PATCH 02/14] wip --- rust/catalyst-signed-doc-spec/Cargo.toml | 8 +- rust/catalyst-signed-doc-spec/build.rs | 4 + .../catalyst-signed-doc-spec/src/doc_types.rs | 36 ++++++++ .../src/{ => headers}/content_type.rs | 4 +- .../src/headers/mod.rs | 11 +++ .../src/is_required.rs | 11 +++ rust/catalyst-signed-doc-spec/src/lib.rs | 88 +++---------------- .../src/{ => metadata}/doc_ref.rs | 2 +- .../src/metadata/mod.rs | 11 +++ 9 files changed, 96 insertions(+), 79 deletions(-) create mode 100644 rust/catalyst-signed-doc-spec/build.rs create mode 100644 rust/catalyst-signed-doc-spec/src/doc_types.rs rename rust/catalyst-signed-doc-spec/src/{ => headers}/content_type.rs (79%) create mode 100644 rust/catalyst-signed-doc-spec/src/headers/mod.rs create mode 100644 rust/catalyst-signed-doc-spec/src/is_required.rs rename rust/catalyst-signed-doc-spec/src/{ => metadata}/doc_ref.rs (81%) create mode 100644 rust/catalyst-signed-doc-spec/src/metadata/mod.rs diff --git a/rust/catalyst-signed-doc-spec/Cargo.toml b/rust/catalyst-signed-doc-spec/Cargo.toml index 0fd35cb644c..39ffc6a0d88 100644 --- a/rust/catalyst-signed-doc-spec/Cargo.toml +++ b/rust/catalyst-signed-doc-spec/Cargo.toml @@ -7,6 +7,9 @@ homepage.workspace = true repository.workspace = true license.workspace = true +[lints] +workspace = true + [dependencies] serde_json = "1.0.142" anyhow = "1.0.99" @@ -14,6 +17,7 @@ serde = { version = "1.0.219", features = ["derive"] } quote = "1.0" proc-macro2 = "1.0" +build-info = "0.0.41" -[lints] -workspace = true +[build-dependencies] +build-info-build = "0.0.41" diff --git a/rust/catalyst-signed-doc-spec/build.rs b/rust/catalyst-signed-doc-spec/build.rs new file mode 100644 index 00000000000..76a7acdda99 --- /dev/null +++ b/rust/catalyst-signed-doc-spec/build.rs @@ -0,0 +1,4 @@ +//! Build +fn main() { + build_info_build::build_script(); +} diff --git a/rust/catalyst-signed-doc-spec/src/doc_types.rs b/rust/catalyst-signed-doc-spec/src/doc_types.rs new file mode 100644 index 00000000000..074d9268968 --- /dev/null +++ b/rust/catalyst-signed-doc-spec/src/doc_types.rs @@ -0,0 +1,36 @@ +//! + +use std::ops::Deref; + +use crate::DocumentName; + +/// A helper type for deserialization "type" metadata field +pub struct DocTypes(Vec); + +impl Deref for DocTypes { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'de> serde::Deserialize<'de> for DocTypes { + #[allow(clippy::missing_docs_in_private_items)] + fn deserialize(deserializer: D) -> Result + where D: serde::Deserializer<'de> { + #[derive(serde::Deserialize)] + #[serde(untagged)] + enum SingleOrVec { + Single(DocumentName), + Multiple(Vec), + } + let value = Option::::deserialize(deserializer)?; + let result = match value { + Some(SingleOrVec::Single(item)) => vec![item], + Some(SingleOrVec::Multiple(items)) => items, + None => vec![], + }; + Ok(Self(result)) + } +} diff --git a/rust/catalyst-signed-doc-spec/src/content_type.rs b/rust/catalyst-signed-doc-spec/src/headers/content_type.rs similarity index 79% rename from rust/catalyst-signed-doc-spec/src/content_type.rs rename to rust/catalyst-signed-doc-spec/src/headers/content_type.rs index 2f74653c017..381336199e1 100644 --- a/rust/catalyst-signed-doc-spec/src/content_type.rs +++ b/rust/catalyst-signed-doc-spec/src/headers/content_type.rs @@ -1,9 +1,11 @@ //! `signed_doc.json` headers content type field JSON definition +use crate::is_required::IsRequired; + /// `signed_doc.json` "content type" field JSON object #[derive(serde::Deserialize)] #[allow(clippy::missing_docs_in_private_items)] pub struct ContentType { - pub required: super::IsRequired, + pub required: IsRequired, pub value: Option, } diff --git a/rust/catalyst-signed-doc-spec/src/headers/mod.rs b/rust/catalyst-signed-doc-spec/src/headers/mod.rs new file mode 100644 index 00000000000..24a455be9ce --- /dev/null +++ b/rust/catalyst-signed-doc-spec/src/headers/mod.rs @@ -0,0 +1,11 @@ +//! 'headers' field definition + +pub mod content_type; + +/// Document's metadata fields definition +#[derive(serde::Deserialize)] +#[allow(clippy::missing_docs_in_private_items)] +pub struct Headers { + #[serde(rename = "content type")] + pub content_type: content_type::ContentType, +} diff --git a/rust/catalyst-signed-doc-spec/src/is_required.rs b/rust/catalyst-signed-doc-spec/src/is_required.rs new file mode 100644 index 00000000000..701dfd2d035 --- /dev/null +++ b/rust/catalyst-signed-doc-spec/src/is_required.rs @@ -0,0 +1,11 @@ +//! 'required' field allowed values definition + +/// "required" field definition +#[derive(serde::Deserialize)] +#[serde(rename_all = "lowercase")] +#[allow(clippy::missing_docs_in_private_items)] +pub enum IsRequired { + Yes, + Excluded, + Optional, +} diff --git a/rust/catalyst-signed-doc-spec/src/lib.rs b/rust/catalyst-signed-doc-spec/src/lib.rs index 4b980a9db24..e6d89a90247 100644 --- a/rust/catalyst-signed-doc-spec/src/lib.rs +++ b/rust/catalyst-signed-doc-spec/src/lib.rs @@ -2,30 +2,26 @@ #![allow(missing_docs, clippy::missing_docs_in_private_items)] -pub mod content_type; -pub mod doc_ref; +pub mod doc_types; +pub mod headers; +pub mod is_required; +pub mod metadata; -use std::{collections::HashMap, ops::Deref}; +use std::collections::HashMap; + +use build_info; + +use crate::{headers::Headers, metadata::Metadata}; + +build_info::build_info!(pub(crate) fn build_info); /// Catalyst Signed Document spec representation struct #[derive(serde::Deserialize)] pub struct CatalystSignedDocSpec { - /// A collection of document's supported content types - #[serde(rename = "contentTypes")] - #[allow(dead_code)] - pub content_types: HashMap, /// A collection of document's specs pub docs: HashMap, } -/// Catalyst Signed Document supported content type declaration struct -#[derive(serde::Deserialize)] -pub struct ContentTypeSpec { - /// CoAP Content-Formats - #[allow(dead_code)] - coap_type: Option, -} - // A thin wrapper over the string document name values #[derive(serde::Deserialize, PartialEq, Eq, Hash)] pub struct DocumentName(String); @@ -53,75 +49,17 @@ impl DocumentName { /// Specific document type definition #[derive(serde::Deserialize)] pub struct DocSpec { - /// Document type UUID v4 value #[serde(rename = "type")] pub doc_type: String, - /// `headers` field pub headers: Headers, - /// Document type metadata definitions pub metadata: Metadata, } -/// Document's metadata fields definition -#[derive(serde::Deserialize)] -#[allow(clippy::missing_docs_in_private_items)] -pub struct Metadata { - #[serde(rename = "ref")] - pub doc_ref: doc_ref::Ref, -} - -/// Document's metadata fields definition -#[derive(serde::Deserialize)] -#[allow(clippy::missing_docs_in_private_items)] -pub struct Headers { - #[serde(rename = "content type")] - pub content_type: content_type::ContentType, -} - -/// "required" field definition -#[derive(serde::Deserialize)] -#[serde(rename_all = "lowercase")] -#[allow(clippy::missing_docs_in_private_items)] -pub enum IsRequired { - Yes, - Excluded, - Optional, -} - -/// A helper type for deserialization "type" metadata field -pub struct DocTypes(Vec); - -impl Deref for DocTypes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'de> serde::Deserialize<'de> for DocTypes { - #[allow(clippy::missing_docs_in_private_items)] - fn deserialize(deserializer: D) -> Result - where D: serde::Deserializer<'de> { - #[derive(serde::Deserialize)] - #[serde(untagged)] - enum SingleOrVec { - Single(DocumentName), - Multiple(Vec), - } - let value = Option::::deserialize(deserializer)?; - let result = match value { - Some(SingleOrVec::Single(item)) => vec![item], - Some(SingleOrVec::Multiple(items)) => items, - None => vec![], - }; - Ok(Self(result)) - } -} - impl CatalystSignedDocSpec { /// Loading a Catalyst Signed Documents spec from the `signed_doc.json` pub fn load_signed_doc_spec() -> anyhow::Result { + let crate_version = build_info().crate_info.version.to_string(); + let signed_doc_str = include_str!("../../../specs/signed_doc.json"); let signed_doc_spec = serde_json::from_str(signed_doc_str) .map_err(|e| anyhow::anyhow!("Invalid Catalyst Signed Documents JSON Spec: {e}"))?; diff --git a/rust/catalyst-signed-doc-spec/src/doc_ref.rs b/rust/catalyst-signed-doc-spec/src/metadata/doc_ref.rs similarity index 81% rename from rust/catalyst-signed-doc-spec/src/doc_ref.rs rename to rust/catalyst-signed-doc-spec/src/metadata/doc_ref.rs index 64a0ef9c2b5..2a151243b39 100644 --- a/rust/catalyst-signed-doc-spec/src/doc_ref.rs +++ b/rust/catalyst-signed-doc-spec/src/metadata/doc_ref.rs @@ -1,6 +1,6 @@ //! `signed_doc.json` "ref" field JSON definition -use crate::{DocTypes, IsRequired}; +use crate::{doc_types::DocTypes, is_required::IsRequired}; /// `signed_doc.json` "ref" field JSON object #[derive(serde::Deserialize)] diff --git a/rust/catalyst-signed-doc-spec/src/metadata/mod.rs b/rust/catalyst-signed-doc-spec/src/metadata/mod.rs new file mode 100644 index 00000000000..f68a6c54c1c --- /dev/null +++ b/rust/catalyst-signed-doc-spec/src/metadata/mod.rs @@ -0,0 +1,11 @@ +//! `metadata` field definition + +pub mod doc_ref; + +/// Document's metadata fields definition +#[derive(serde::Deserialize)] +#[allow(clippy::missing_docs_in_private_items)] +pub struct Metadata { + #[serde(rename = "ref")] + pub doc_ref: doc_ref::Ref, +} From ef2d09491f21103c832e599e68131e77f4a5847e Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 9 Sep 2025 19:00:35 +0400 Subject: [PATCH 03/14] add versions check --- .../catalyst-signed-doc-spec/src/copyright.rs | 11 +++++++++ rust/catalyst-signed-doc-spec/src/lib.rs | 24 +++++++++++++------ 2 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 rust/catalyst-signed-doc-spec/src/copyright.rs diff --git a/rust/catalyst-signed-doc-spec/src/copyright.rs b/rust/catalyst-signed-doc-spec/src/copyright.rs new file mode 100644 index 00000000000..3bd07fc45e2 --- /dev/null +++ b/rust/catalyst-signed-doc-spec/src/copyright.rs @@ -0,0 +1,11 @@ +//! 'copyright' field defition + +#[derive(serde::Deserialize)] +pub struct Copyright { + pub versions: Vec, +} + +#[derive(serde::Deserialize)] +pub struct Version { + pub version: String, +} diff --git a/rust/catalyst-signed-doc-spec/src/lib.rs b/rust/catalyst-signed-doc-spec/src/lib.rs index e6d89a90247..a1c1324b860 100644 --- a/rust/catalyst-signed-doc-spec/src/lib.rs +++ b/rust/catalyst-signed-doc-spec/src/lib.rs @@ -2,6 +2,7 @@ #![allow(missing_docs, clippy::missing_docs_in_private_items)] +pub mod copyright; pub mod doc_types; pub mod headers; pub mod is_required; @@ -9,17 +10,17 @@ pub mod metadata; use std::collections::HashMap; -use build_info; +use build_info as build_info_lib; -use crate::{headers::Headers, metadata::Metadata}; +use crate::{copyright::Copyright, headers::Headers, metadata::Metadata}; -build_info::build_info!(pub(crate) fn build_info); +build_info_lib::build_info!(pub(crate) fn build_info); /// Catalyst Signed Document spec representation struct #[derive(serde::Deserialize)] pub struct CatalystSignedDocSpec { - /// A collection of document's specs pub docs: HashMap, + pub copyright: Copyright, } // A thin wrapper over the string document name values @@ -58,11 +59,20 @@ pub struct DocSpec { impl CatalystSignedDocSpec { /// Loading a Catalyst Signed Documents spec from the `signed_doc.json` pub fn load_signed_doc_spec() -> anyhow::Result { - let crate_version = build_info().crate_info.version.to_string(); - let signed_doc_str = include_str!("../../../specs/signed_doc.json"); - let signed_doc_spec = serde_json::from_str(signed_doc_str) + let signed_doc_spec: CatalystSignedDocSpec = serde_json::from_str(signed_doc_str) .map_err(|e| anyhow::anyhow!("Invalid Catalyst Signed Documents JSON Spec: {e}"))?; + + let crate_version = build_info().crate_info.version.to_string(); + let latest_version = signed_doc_spec + .copyright + .versions + .last() + .ok_or(anyhow::anyhow!( + "'versions' list must have at least one entry" + ))?; + anyhow::ensure!(latest_version.version == crate_version, "crate version should align with the latest version of the Catalyst Signed Documents specification"); + Ok(signed_doc_spec) } } From 6f3b7dcc84afb3110ec2c63dce084c634618c971 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 9 Sep 2025 19:02:29 +0400 Subject: [PATCH 04/14] fix --- rust/catalyst-signed-doc-spec/Cargo.toml | 2 +- rust/catalyst-signed-doc-spec/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/catalyst-signed-doc-spec/Cargo.toml b/rust/catalyst-signed-doc-spec/Cargo.toml index 39ffc6a0d88..92494219541 100644 --- a/rust/catalyst-signed-doc-spec/Cargo.toml +++ b/rust/catalyst-signed-doc-spec/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "catalyst-signed-doc-spec" -version = "0.0.1" +version = "0.1.3" edition.workspace = true authors.workspace = true homepage.workspace = true diff --git a/rust/catalyst-signed-doc-spec/src/lib.rs b/rust/catalyst-signed-doc-spec/src/lib.rs index a1c1324b860..72fcf2984f9 100644 --- a/rust/catalyst-signed-doc-spec/src/lib.rs +++ b/rust/catalyst-signed-doc-spec/src/lib.rs @@ -83,6 +83,6 @@ mod tests { #[test] fn load_signed_doc_spec_test() { - assert!(CatalystSignedDocSpec::load_signed_doc_spec().is_ok()); + CatalystSignedDocSpec::load_signed_doc_spec().unwrap(); } } From bf839b45be785a6dcc4f58bb7a7944e9893f9175 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 9 Sep 2025 20:21:15 +0400 Subject: [PATCH 05/14] fix --- rust/Earthfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rust/Earthfile b/rust/Earthfile index 388574fb581..756cb432dc7 100644 --- a/rust/Earthfile +++ b/rust/Earthfile @@ -18,6 +18,7 @@ COPY_SRC: hermes-ipfs \ signed_doc \ catalyst-signed-doc-macro \ + catalyst-signed-doc-spec \ rbac-registration \ immutable-ledger . @@ -73,6 +74,7 @@ build: --libs=rbac-registration \ --libs=catalyst-signed-doc \ --libs=catalyst-signed-doc-macro \ + --libs=catalyst-signed-doc-spec \ --bins=cbork/cbork LET ARCH=$(uname -m) From c376e068cda5ada1bac32bf9363d98e1a5a21cea Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 9 Sep 2025 20:21:53 +0400 Subject: [PATCH 06/14] fix spelling --- rust/catalyst-signed-doc-spec/src/copyright.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/catalyst-signed-doc-spec/src/copyright.rs b/rust/catalyst-signed-doc-spec/src/copyright.rs index 3bd07fc45e2..e9461ab9b7f 100644 --- a/rust/catalyst-signed-doc-spec/src/copyright.rs +++ b/rust/catalyst-signed-doc-spec/src/copyright.rs @@ -1,4 +1,4 @@ -//! 'copyright' field defition +//! 'copyright' field definition #[derive(serde::Deserialize)] pub struct Copyright { From 2d4fa105d4781e2aaf91e6e387c3307d9a39cbb4 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 9 Sep 2025 20:39:23 +0400 Subject: [PATCH 07/14] fix clippy --- rust/catalyst-signed-doc-spec/src/doc_types.rs | 2 +- rust/catalyst-signed-doc-spec/src/lib.rs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/rust/catalyst-signed-doc-spec/src/doc_types.rs b/rust/catalyst-signed-doc-spec/src/doc_types.rs index 074d9268968..ba8e5ae78ec 100644 --- a/rust/catalyst-signed-doc-spec/src/doc_types.rs +++ b/rust/catalyst-signed-doc-spec/src/doc_types.rs @@ -1,4 +1,4 @@ -//! +//! 'type' array field definition use std::ops::Deref; diff --git a/rust/catalyst-signed-doc-spec/src/lib.rs b/rust/catalyst-signed-doc-spec/src/lib.rs index 72fcf2984f9..f85b7fdc5a5 100644 --- a/rust/catalyst-signed-doc-spec/src/lib.rs +++ b/rust/catalyst-signed-doc-spec/src/lib.rs @@ -29,6 +29,7 @@ pub struct DocumentName(String); impl DocumentName { /// returns document name + #[must_use] pub fn name(&self) -> &str { &self.0 } @@ -58,6 +59,11 @@ pub struct DocSpec { impl CatalystSignedDocSpec { /// Loading a Catalyst Signed Documents spec from the `signed_doc.json` + /// + /// # Errors + /// - `signed_doc.json` filed loading and JSON parsing errors + /// - `catalyst-signed-doc-spec` crate version doesn't align with the latest version + /// of the `signed_doc.json` pub fn load_signed_doc_spec() -> anyhow::Result { let signed_doc_str = include_str!("../../../specs/signed_doc.json"); let signed_doc_spec: CatalystSignedDocSpec = serde_json::from_str(signed_doc_str) From 8dcfe4d8dce9b966c20151a14311de83fcbb501a Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 9 Sep 2025 23:10:37 +0400 Subject: [PATCH 08/14] wip --- .../src/is_required.rs | 2 +- rust/catalyst-signed-doc-spec/src/lib.rs | 11 ++++- rust/signed_doc/Cargo.toml | 1 + rust/signed_doc/src/validator/mod.rs | 8 ++-- .../src/validator/rules/content_type.rs | 27 +++++++++++ .../signed_doc/src/validator/rules/doc_ref.rs | 39 +++++++++++++++ rust/signed_doc/src/validator/rules/mod.rs | 47 +++++++++++++++++++ 7 files changed, 129 insertions(+), 6 deletions(-) diff --git a/rust/catalyst-signed-doc-spec/src/is_required.rs b/rust/catalyst-signed-doc-spec/src/is_required.rs index 701dfd2d035..0e514dfcdcc 100644 --- a/rust/catalyst-signed-doc-spec/src/is_required.rs +++ b/rust/catalyst-signed-doc-spec/src/is_required.rs @@ -1,7 +1,7 @@ //! 'required' field allowed values definition /// "required" field definition -#[derive(serde::Deserialize)] +#[derive(serde::Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] #[allow(clippy::missing_docs_in_private_items)] pub enum IsRequired { diff --git a/rust/catalyst-signed-doc-spec/src/lib.rs b/rust/catalyst-signed-doc-spec/src/lib.rs index f85b7fdc5a5..6e0ca1b7ae5 100644 --- a/rust/catalyst-signed-doc-spec/src/lib.rs +++ b/rust/catalyst-signed-doc-spec/src/lib.rs @@ -8,7 +8,7 @@ pub mod headers; pub mod is_required; pub mod metadata; -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Display}; use build_info as build_info_lib; @@ -27,6 +27,15 @@ pub struct CatalystSignedDocSpec { #[derive(serde::Deserialize, PartialEq, Eq, Hash)] pub struct DocumentName(String); +impl Display for DocumentName { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + ) -> std::fmt::Result { + self.0.fmt(f) + } +} + impl DocumentName { /// returns document name #[must_use] diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 50a95c2ebff..8f278f87f8f 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -15,6 +15,7 @@ catalyst-types = { version = "0.0.6", git = "https://github.com/input-output-hk/ cbork-utils = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "cbork-utils-v0.0.2" } catalyst-signed-doc-macro = { version = "0.0.1", path = "../catalyst-signed-doc-macro" } +catalyst-signed-doc-spec = { version = "0.1.3", path = "../catalyst-signed-doc-spec" } anyhow = "1.0.95" serde = { version = "1.0.217", features = ["derive"] } diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs index 390419c3777..ac71e61b323 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -5,7 +5,6 @@ pub(crate) mod rules; use std::{collections::HashMap, sync::LazyLock}; -use catalyst_signed_doc_macro; use catalyst_types::catalyst_id::role_index::RoleId; use rules::{ ContentEncodingRule, ContentRule, ContentSchema, ContentTypeRule, IdRule, OriginalAuthorRule, @@ -23,8 +22,6 @@ use crate::{ CatalystSignedDocument, ContentEncoding, ContentType, }; -catalyst_signed_doc_macro::catalyst_signed_documents_rules!(); - /// A table representing a full set or validation rules per document id. static DOCUMENT_RULES: LazyLock> = LazyLock::new(document_rules_init); @@ -166,8 +163,11 @@ fn proposal_submission_action_rule() -> Rules { } /// `DOCUMENT_RULES` initialization function +#[allow(clippy::expect_used)] fn document_rules_init() -> HashMap { - let mut document_rules_map: HashMap = documents_rules().collect(); + let mut document_rules_map: HashMap = Rules::documents_rules() + .expect("cannot fail to initialize validation rules") + .collect(); // TODO: remove this redefinitions of the validation rules after // `catalyst_signed_documents_rules!` macro would be fully finished diff --git a/rust/signed_doc/src/validator/rules/content_type.rs b/rust/signed_doc/src/validator/rules/content_type.rs index e0f9729198e..350f81af5d7 100644 --- a/rust/signed_doc/src/validator/rules/content_type.rs +++ b/rust/signed_doc/src/validator/rules/content_type.rs @@ -16,6 +16,33 @@ pub(crate) enum ContentTypeRule { } impl ContentTypeRule { + /// Generating `ContentTypeRule` from specs + pub(crate) fn new( + spec: &catalyst_signed_doc_spec::headers::content_type::ContentType + ) -> anyhow::Result { + if let catalyst_signed_doc_spec::is_required::IsRequired::Excluded = spec.required { + anyhow::ensure!( + spec.value.is_none(), + "'value' field must not exist when 'required' is 'excluded'" + ); + return Ok(Self::NotSpecified); + } + + anyhow::ensure!( + catalyst_signed_doc_spec::is_required::IsRequired::Optional == spec.required, + "'content type' field cannot been optional" + ); + + let value = spec + .value + .as_ref() + .ok_or(anyhow::anyhow!("'content type' 'value' field must exist"))?; + + Ok(Self::Specified { + exp: value.parse()?, + }) + } + /// Field validation rule #[allow(clippy::unused_async)] pub(crate) async fn check( diff --git a/rust/signed_doc/src/validator/rules/doc_ref.rs b/rust/signed_doc/src/validator/rules/doc_ref.rs index a84859030c5..b9b43a9c038 100644 --- a/rust/signed_doc/src/validator/rules/doc_ref.rs +++ b/rust/signed_doc/src/validator/rules/doc_ref.rs @@ -1,5 +1,7 @@ //! `ref` rule type impl. +use std::collections::HashMap; + use catalyst_types::problem_report::ProblemReport; use crate::{ @@ -69,6 +71,43 @@ impl RefRule { Ok(true) } + + /// Generating `RefRule` from specs + pub(crate) fn new( + docs: &HashMap, + ref_spec: &catalyst_signed_doc_spec::metadata::doc_ref::Ref, + ) -> anyhow::Result { + let optional = match ref_spec.required { + catalyst_signed_doc_spec::is_required::IsRequired::Yes => false, + catalyst_signed_doc_spec::is_required::IsRequired::Optional => true, + catalyst_signed_doc_spec::is_required::IsRequired::Excluded => { + return Ok(Self::NotSpecified); + }, + }; + + anyhow::ensure!(!ref_spec.doc_type.is_empty(), "'type' field should exists and has at least one entry for the required 'ref' metadata definition"); + + let exp_ref_types = ref_spec.doc_type.iter().try_fold( + Vec::new(), + |mut res, doc_name| -> anyhow::Result<_> { + let docs_spec = docs.get(doc_name).ok_or(anyhow::anyhow!( + "cannot find a document definition {doc_name}" + ))?; + res.push(docs_spec.doc_type.as_str().parse()?); + Ok(res) + }, + )?; + + let multiple = ref_spec.multiple.ok_or(anyhow::anyhow!( + "'multiple' field should exists for the required 'ref' metadata definition" + ))?; + + Ok(Self::Specified { + exp_ref_types, + multiple, + optional, + }) + } } /// Validate all the document references by the defined validation rules, diff --git a/rust/signed_doc/src/validator/rules/mod.rs b/rust/signed_doc/src/validator/rules/mod.rs index 47cf5b2675e..b2406d294f6 100644 --- a/rust/signed_doc/src/validator/rules/mod.rs +++ b/rust/signed_doc/src/validator/rules/mod.rs @@ -103,4 +103,51 @@ impl Rules { Ok(res) } + + /// Returns an iterator with all defined Catalyst Signed Documents validation rules + /// per corresponding document type based on the `signed_doc.json` file + /// + /// # Errors: + /// - `signed_doc.json` filed loading and JSON parsing errors. + /// - `catalyst-signed-doc-spec` crate version doesn't align with the latest version + /// of the `signed_doc.json`. + pub(crate) fn documents_rules( + ) -> anyhow::Result> + { + let spec = catalyst_signed_doc_spec::CatalystSignedDocSpec::load_signed_doc_spec()?; + + let mut doc_rules = Vec::new(); + for (_, doc_spec) in &spec.docs { + let rules = Self { + id: IdRule, + ver: VerRule, + content_type: ContentTypeRule::new(&doc_spec.headers.content_type)?, + content_encoding: ContentEncodingRule::NotSpecified, + template: TemplateRule::NotSpecified, + parameters: ParametersRule::NotSpecified, + doc_ref: RefRule::new(&spec.docs, &doc_spec.metadata.doc_ref)?, + reply: ReplyRule::NotSpecified, + section: SectionRule::NotSpecified, + content: ContentRule::Nil, + kid: SignatureKidRule { exp: &[] }, + signature: SignatureRule { mutlisig: false }, + original_author: OriginalAuthorRule, + }; + let doc_type = doc_spec.doc_type.parse()?; + + doc_rules.push((doc_type, rules)); + } + + Ok(doc_rules.into_iter()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rules_documents_rules_test() { + let _unused = Rules::documents_rules().unwrap(); + } } From 90c07af1f015e305c0fe268b335cc3e7b1bb843f Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 9 Sep 2025 23:16:14 +0400 Subject: [PATCH 09/14] cleanup --- rust/catalyst-signed-doc-macro/Cargo.toml | 5 +- rust/catalyst-signed-doc-macro/src/lib.rs | 16 -- .../src/rules/content_type.rs | 51 ----- .../src/rules/doc_ref.rs | 38 ---- .../src/rules/mod.rs | 60 ------ .../src/signed_doc_spec/content_type.rs | 9 - .../src/signed_doc_spec/doc_ref.rs | 13 -- .../src/signed_doc_spec/mod.rs | 183 ------------------ .../src/types_consts.rs | 3 +- rust/signed_doc/Cargo.toml | 2 +- 10 files changed, 6 insertions(+), 374 deletions(-) delete mode 100644 rust/catalyst-signed-doc-macro/src/rules/content_type.rs delete mode 100644 rust/catalyst-signed-doc-macro/src/rules/doc_ref.rs delete mode 100644 rust/catalyst-signed-doc-macro/src/rules/mod.rs delete mode 100644 rust/catalyst-signed-doc-macro/src/signed_doc_spec/content_type.rs delete mode 100644 rust/catalyst-signed-doc-macro/src/signed_doc_spec/doc_ref.rs delete mode 100644 rust/catalyst-signed-doc-macro/src/signed_doc_spec/mod.rs diff --git a/rust/catalyst-signed-doc-macro/Cargo.toml b/rust/catalyst-signed-doc-macro/Cargo.toml index 4ed591e3091..da93f24f246 100644 --- a/rust/catalyst-signed-doc-macro/Cargo.toml +++ b/rust/catalyst-signed-doc-macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "catalyst-signed-doc-macro" -version = "0.0.1" +version = "0.1.3" edition.workspace = true authors.workspace = true homepage.workspace = true @@ -21,3 +21,6 @@ serde_json = "1.0.142" anyhow = "1.0.99" Inflector = "0.11.4" serde = { version = "1.0.219", features = ["derive"] } + +catalyst-signed-doc-spec = { version = "0.1.3", path = "../catalyst-signed-doc-spec" } + diff --git a/rust/catalyst-signed-doc-macro/src/lib.rs b/rust/catalyst-signed-doc-macro/src/lib.rs index bd308212775..d479edb6f74 100644 --- a/rust/catalyst-signed-doc-macro/src/lib.rs +++ b/rust/catalyst-signed-doc-macro/src/lib.rs @@ -2,8 +2,6 @@ //! spec. mod error; -mod rules; -mod signed_doc_spec; mod types_consts; use crate::error::process_error; @@ -25,17 +23,3 @@ pub fn catalyst_signed_documents_types_consts( .unwrap_or_else(process_error) .into() } - -/// Defines `documents_rules` function which will return a defined -/// `catalyst_signed_doc::Rules` instances for each corresponding document type, which are -/// defined inside the `signed_doc.json` spec. -/// -/// ```ignore -/// fn documents_rules() -> impl Iterator -/// ``` -#[proc_macro] -pub fn catalyst_signed_documents_rules(_: proc_macro::TokenStream) -> proc_macro::TokenStream { - rules::catalyst_signed_documents_rules_impl() - .unwrap_or_else(process_error) - .into() -} diff --git a/rust/catalyst-signed-doc-macro/src/rules/content_type.rs b/rust/catalyst-signed-doc-macro/src/rules/content_type.rs deleted file mode 100644 index 51002ea8e49..00000000000 --- a/rust/catalyst-signed-doc-macro/src/rules/content_type.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! `ContentTypeRule` generation - -use std::collections::HashMap; - -use proc_macro2::TokenStream; -use quote::quote; - -use crate::signed_doc_spec::{self, ContentTypeSpec, ContentTypeTemplate, IsRequired}; - -/// Generating `ContentTypeRule` instantiation -pub(crate) fn into_rule( - content_types: &HashMap, - field: &signed_doc_spec::content_type::ContentType, -) -> anyhow::Result { - let is_field_empty = field.value.is_none() - || field - .value - .as_ref() - .is_some_and(std::string::String::is_empty); - - if matches!(field.required, IsRequired::Excluded) { - anyhow::ensure!( - is_field_empty, - "'value' field must not exist when 'required' is 'excluded'" - ); - - return Ok(quote! { - crate::validator::rules::ContentTypeRule::NotSpecified - }); - } - - if matches!(field.required, IsRequired::Yes) { - anyhow::ensure!(!is_field_empty, "'value' field must exist"); - } - - let Some(value) = &field.value else { - anyhow::bail!("'value' field must exist"); - }; - - let template = ContentTypeTemplate(value.clone()); - let Some(_) = content_types.get(&template) else { - return Err(anyhow::anyhow!("Unsupported Content Type: {}", value)); - }; - let ident = template.ident(); - - Ok(quote! { - crate::validator::rules::ContentTypeRule::Specified { - exp: ContentType::#ident, - } - }) -} diff --git a/rust/catalyst-signed-doc-macro/src/rules/doc_ref.rs b/rust/catalyst-signed-doc-macro/src/rules/doc_ref.rs deleted file mode 100644 index 8d460e0af63..00000000000 --- a/rust/catalyst-signed-doc-macro/src/rules/doc_ref.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! `RefRule` generation - -use proc_macro2::TokenStream; -use quote::quote; - -use crate::signed_doc_spec::{self, IsRequired}; - -/// Generating `RefRule` instantiation -pub(crate) fn ref_rule(ref_spec: &signed_doc_spec::doc_ref::Ref) -> anyhow::Result { - let optional = match ref_spec.required { - IsRequired::Yes => false, - IsRequired::Optional => true, - IsRequired::Excluded => { - return Ok(quote! { - crate::validator::rules::RefRule::NotSpecified - }); - }, - }; - - anyhow::ensure!(!ref_spec.doc_type.is_empty(), "'type' field should exists and has at least one entry for the required 'ref' metadata definition"); - - let const_type_name_idents = ref_spec.doc_type.iter().map(|doc_name| { - let const_type_name_ident = doc_name.ident(); - quote! { - crate::doc_types::#const_type_name_ident - } - }); - let multiple = ref_spec.multiple.ok_or(anyhow::anyhow!( - "'multiple' field should exists for the required 'ref' metadata definition" - ))?; - Ok(quote! { - crate::validator::rules::RefRule::Specified { - exp_ref_types: vec![ #(#const_type_name_idents,)* ], - multiple: #multiple, - optional: #optional, - } - }) -} diff --git a/rust/catalyst-signed-doc-macro/src/rules/mod.rs b/rust/catalyst-signed-doc-macro/src/rules/mod.rs deleted file mode 100644 index d1e5cf83318..00000000000 --- a/rust/catalyst-signed-doc-macro/src/rules/mod.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! `catalyst_signed_documents_rules!` macro implementation - -pub(crate) mod content_type; -pub(crate) mod doc_ref; - -use proc_macro2::TokenStream; -use quote::quote; - -use crate::signed_doc_spec::CatalystSignedDocSpec; - -/// `catalyst_signed_documents_rules` macro implementation -pub(crate) fn catalyst_signed_documents_rules_impl() -> anyhow::Result { - let spec = CatalystSignedDocSpec::load_signed_doc_spec()?; - - let mut rules_definitions = Vec::new(); - for (doc_name, doc_spec) in spec.docs { - let const_type_name_ident = doc_name.ident(); - - let content_type_rule = - content_type::into_rule(&spec.content_types, &doc_spec.headers.content_type)?; - let ref_rule = doc_ref::ref_rule(&doc_spec.metadata.doc_ref)?; - // TODO: implement a proper initialization for all specific validation rules - let rules = quote! { - crate::validator::rules::Rules { - id: crate::validator::rules::IdRule, - ver: crate::validator::rules::VerRule, - content_type: #content_type_rule, - content_encoding: crate::validator::rules::ContentEncodingRule::Specified { - exp: ContentEncoding::Brotli, - optional: false, - }, - template: crate::validator::rules::TemplateRule::NotSpecified, - parameters: crate::validator::rules::ParametersRule::NotSpecified, - doc_ref: #ref_rule, - reply: crate::validator::rules::ReplyRule::NotSpecified, - section: crate::validator::rules::SectionRule::NotSpecified, - content: crate::validator::rules::ContentRule::Nil, - kid: crate::validator::rules::SignatureKidRule { - exp: &[], - }, - signature: crate::validator::rules::SignatureRule { - mutlisig: false - }, - original_author: crate::validator::rules::OriginalAuthorRule, - } - }; - - let rule_definition = quote! { - (crate::doc_types::#const_type_name_ident, #rules), - }; - rules_definitions.push(rule_definition); - } - - Ok(quote! { - /// Returns an iterator with all defined Catalyst Signed Documents validation rules per corresponding document type - fn documents_rules() -> impl Iterator { - [ #(#rules_definitions)* ].into_iter() - } - }) -} diff --git a/rust/catalyst-signed-doc-macro/src/signed_doc_spec/content_type.rs b/rust/catalyst-signed-doc-macro/src/signed_doc_spec/content_type.rs deleted file mode 100644 index ce8567471d1..00000000000 --- a/rust/catalyst-signed-doc-macro/src/signed_doc_spec/content_type.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! `signed_doc.json` headers content type field JSON definition - -/// `signed_doc.json` "content type" field JSON object -#[derive(serde::Deserialize)] -#[allow(clippy::missing_docs_in_private_items)] -pub(crate) struct ContentType { - pub(crate) required: super::IsRequired, - pub(crate) value: Option, -} diff --git a/rust/catalyst-signed-doc-macro/src/signed_doc_spec/doc_ref.rs b/rust/catalyst-signed-doc-macro/src/signed_doc_spec/doc_ref.rs deleted file mode 100644 index 1384ebf1c6b..00000000000 --- a/rust/catalyst-signed-doc-macro/src/signed_doc_spec/doc_ref.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! `signed_doc.json` "ref" field JSON definition - -use crate::signed_doc_spec::{DocTypes, IsRequired}; - -/// `signed_doc.json` "ref" field JSON object -#[derive(serde::Deserialize)] -#[allow(clippy::missing_docs_in_private_items)] -pub(crate) struct Ref { - pub(crate) required: IsRequired, - #[serde(rename = "type")] - pub(crate) doc_type: DocTypes, - pub(crate) multiple: Option, -} diff --git a/rust/catalyst-signed-doc-macro/src/signed_doc_spec/mod.rs b/rust/catalyst-signed-doc-macro/src/signed_doc_spec/mod.rs deleted file mode 100644 index b3719e1dd32..00000000000 --- a/rust/catalyst-signed-doc-macro/src/signed_doc_spec/mod.rs +++ /dev/null @@ -1,183 +0,0 @@ -//! Catalyst Signed Document spec type - -// cspell: words pascalcase - -pub(crate) mod content_type; -pub(crate) mod doc_ref; - -use std::{collections::HashMap, ops::Deref}; - -use inflector::cases::pascalcase::to_pascal_case; -use proc_macro2::Ident; -use quote::format_ident; - -/// Catalyst Signed Document spec representation struct -#[derive(serde::Deserialize)] -pub(crate) struct CatalystSignedDocSpec { - /// A collection of document's supported content types - #[serde(rename = "contentTypes")] - #[allow(dead_code)] - pub(crate) content_types: HashMap, - /// A collection of document's specs - pub(crate) docs: HashMap, -} - -// A thin wrapper over the RFC2046 content type strings. -#[derive(serde::Deserialize, PartialEq, Eq, Hash)] -pub(crate) struct ContentTypeTemplate(pub(crate) String); - -impl ContentTypeTemplate { - /// returns a content type template as a `Ident` in the following form. - /// - /// text/css; charset=utf-8; template=handlebars - /// => `CssHandlebars` - /// - /// text/css; charset=utf-8 - /// => `Css` - pub(crate) fn ident(&self) -> Ident { - let raw = self.0.as_str(); - - // split into parts like "text/css; charset=utf-8; template=handlebars" - let mut parts = raw.split(';').map(str::trim); - - // first part is "type/subtype" - let first = parts.next().unwrap_or_default(); // e.g. "text/css" - let subtype = first.split('/').nth(1).unwrap_or_default(); // "css" - - // look for "template=..." - let template = parts - .find_map(|p| p.strip_prefix("template=")) - .map(to_pascal_case); - - // build PascalCase - let mut ident = String::new(); - ident.push_str(&to_pascal_case(subtype)); - if let Some(t) = template { - ident.push_str(&t); - } - - format_ident!("{}", ident) - } -} - -/// Catalyst Signed Document supported content type declaration struct -#[derive(serde::Deserialize)] -pub(crate) struct ContentTypeSpec { - /// CoAP Content-Formats - #[allow(dead_code)] - coap_type: Option, -} - -// A thin wrapper over the string document name values -#[derive(serde::Deserialize, PartialEq, Eq, Hash)] -pub(crate) struct DocumentName(String); - -impl DocumentName { - /// returns document name - pub(crate) fn name(&self) -> &str { - &self.0 - } - - /// returns a document name as a `Ident` in the following form - /// `"PROPOSAL_FORM_TEMPLATE"` - pub(crate) fn ident(&self) -> Ident { - format_ident!( - "{}", - self.0 - .split_whitespace() - .map(str::to_uppercase) - .collect::>() - .join("_") - ) - } -} - -/// Specific document type definition -#[derive(serde::Deserialize)] -pub(crate) struct DocSpec { - /// Document type UUID v4 value - #[serde(rename = "type")] - pub(crate) doc_type: String, - /// `headers` field - pub(crate) headers: Headers, - /// Document type metadata definitions - pub(crate) metadata: Metadata, -} - -/// Document's metadata fields definition -#[derive(serde::Deserialize)] -#[allow(clippy::missing_docs_in_private_items)] -pub(crate) struct Metadata { - #[serde(rename = "ref")] - pub(crate) doc_ref: doc_ref::Ref, -} - -/// Document's metadata fields definition -#[derive(serde::Deserialize)] -#[allow(clippy::missing_docs_in_private_items)] -pub(crate) struct Headers { - #[serde(rename = "content type")] - pub(crate) content_type: content_type::ContentType, -} - -/// "required" field definition -#[derive(serde::Deserialize)] -#[serde(rename_all = "lowercase")] -#[allow(clippy::missing_docs_in_private_items)] -pub(crate) enum IsRequired { - Yes, - Excluded, - Optional, -} - -/// A helper type for deserialization "type" metadata field -pub(crate) struct DocTypes(Vec); - -impl Deref for DocTypes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'de> serde::Deserialize<'de> for DocTypes { - #[allow(clippy::missing_docs_in_private_items)] - fn deserialize(deserializer: D) -> Result - where D: serde::Deserializer<'de> { - #[derive(serde::Deserialize)] - #[serde(untagged)] - enum SingleOrVec { - Single(DocumentName), - Multiple(Vec), - } - let value = Option::::deserialize(deserializer)?; - let result = match value { - Some(SingleOrVec::Single(item)) => vec![item], - Some(SingleOrVec::Multiple(items)) => items, - None => vec![], - }; - Ok(Self(result)) - } -} - -impl CatalystSignedDocSpec { - /// Loading a Catalyst Signed Documents spec from the `signed_doc.json` - // #[allow(dependency_on_unit_never_type_fallback)] - pub(crate) fn load_signed_doc_spec() -> anyhow::Result { - let signed_doc_str = include_str!("../../../../specs/signed_doc.json"); - let signed_doc_spec = serde_json::from_str(signed_doc_str) - .map_err(|e| anyhow::anyhow!("Invalid Catalyst Signed Documents JSON Spec: {e}"))?; - Ok(signed_doc_spec) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn load_signed_doc_spec_test() { - assert!(CatalystSignedDocSpec::load_signed_doc_spec().is_ok()); - } -} diff --git a/rust/catalyst-signed-doc-macro/src/types_consts.rs b/rust/catalyst-signed-doc-macro/src/types_consts.rs index ad407737ecf..77219db71e8 100644 --- a/rust/catalyst-signed-doc-macro/src/types_consts.rs +++ b/rust/catalyst-signed-doc-macro/src/types_consts.rs @@ -1,10 +1,9 @@ //! `catalyst_signed_documents_types_consts!` macro implementation +use catalyst_signed_doc_spec::CatalystSignedDocSpec; use proc_macro2::TokenStream; use quote::quote; -use crate::signed_doc_spec::CatalystSignedDocSpec; - /// `catalyst_signed_documents_types_consts` macro implementation pub(crate) fn catalyst_signed_documents_types_consts_impl() -> anyhow::Result { let spec = CatalystSignedDocSpec::load_signed_doc_spec()?; diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 8f278f87f8f..1145b0a2712 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -14,7 +14,7 @@ workspace = true catalyst-types = { version = "0.0.6", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "catalyst-types/v0.0.6" } cbork-utils = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "cbork-utils-v0.0.2" } -catalyst-signed-doc-macro = { version = "0.0.1", path = "../catalyst-signed-doc-macro" } +catalyst-signed-doc-macro = { version = "0.1.3", path = "../catalyst-signed-doc-macro" } catalyst-signed-doc-spec = { version = "0.1.3", path = "../catalyst-signed-doc-spec" } anyhow = "1.0.95" From cc06cd6d49f3af3905077d33db52f0fa56a9c3e1 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 9 Sep 2025 23:27:12 +0400 Subject: [PATCH 10/14] wip --- .../src/validator/rules/content_type.rs | 2 +- .../signed_doc/src/validator/rules/doc_ref.rs | 74 +++++++++---------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/rust/signed_doc/src/validator/rules/content_type.rs b/rust/signed_doc/src/validator/rules/content_type.rs index 350f81af5d7..e82c86dcf57 100644 --- a/rust/signed_doc/src/validator/rules/content_type.rs +++ b/rust/signed_doc/src/validator/rules/content_type.rs @@ -29,7 +29,7 @@ impl ContentTypeRule { } anyhow::ensure!( - catalyst_signed_doc_spec::is_required::IsRequired::Optional == spec.required, + catalyst_signed_doc_spec::is_required::IsRequired::Optional != spec.required, "'content type' field cannot been optional" ); diff --git a/rust/signed_doc/src/validator/rules/doc_ref.rs b/rust/signed_doc/src/validator/rules/doc_ref.rs index b9b43a9c038..e6356e8faa2 100644 --- a/rust/signed_doc/src/validator/rules/doc_ref.rs +++ b/rust/signed_doc/src/validator/rules/doc_ref.rs @@ -25,6 +25,43 @@ pub(crate) enum RefRule { NotSpecified, } impl RefRule { + /// Generating `RefRule` from specs + pub(crate) fn new( + docs: &HashMap, + ref_spec: &catalyst_signed_doc_spec::metadata::doc_ref::Ref, + ) -> anyhow::Result { + let optional = match ref_spec.required { + catalyst_signed_doc_spec::is_required::IsRequired::Yes => false, + catalyst_signed_doc_spec::is_required::IsRequired::Optional => true, + catalyst_signed_doc_spec::is_required::IsRequired::Excluded => { + return Ok(Self::NotSpecified); + }, + }; + + anyhow::ensure!(!ref_spec.doc_type.is_empty(), "'type' field should exists and has at least one entry for the required 'ref' metadata definition"); + + let exp_ref_types = ref_spec.doc_type.iter().try_fold( + Vec::new(), + |mut res, doc_name| -> anyhow::Result<_> { + let docs_spec = docs.get(doc_name).ok_or(anyhow::anyhow!( + "cannot find a document definition {doc_name}" + ))?; + res.push(docs_spec.doc_type.as_str().parse()?); + Ok(res) + }, + )?; + + let multiple = ref_spec.multiple.ok_or(anyhow::anyhow!( + "'multiple' field should exists for the required 'ref' metadata definition" + ))?; + + Ok(Self::Specified { + exp_ref_types, + multiple, + optional, + }) + } + /// Field validation rule pub(crate) async fn check( &self, @@ -71,43 +108,6 @@ impl RefRule { Ok(true) } - - /// Generating `RefRule` from specs - pub(crate) fn new( - docs: &HashMap, - ref_spec: &catalyst_signed_doc_spec::metadata::doc_ref::Ref, - ) -> anyhow::Result { - let optional = match ref_spec.required { - catalyst_signed_doc_spec::is_required::IsRequired::Yes => false, - catalyst_signed_doc_spec::is_required::IsRequired::Optional => true, - catalyst_signed_doc_spec::is_required::IsRequired::Excluded => { - return Ok(Self::NotSpecified); - }, - }; - - anyhow::ensure!(!ref_spec.doc_type.is_empty(), "'type' field should exists and has at least one entry for the required 'ref' metadata definition"); - - let exp_ref_types = ref_spec.doc_type.iter().try_fold( - Vec::new(), - |mut res, doc_name| -> anyhow::Result<_> { - let docs_spec = docs.get(doc_name).ok_or(anyhow::anyhow!( - "cannot find a document definition {doc_name}" - ))?; - res.push(docs_spec.doc_type.as_str().parse()?); - Ok(res) - }, - )?; - - let multiple = ref_spec.multiple.ok_or(anyhow::anyhow!( - "'multiple' field should exists for the required 'ref' metadata definition" - ))?; - - Ok(Self::Specified { - exp_ref_types, - multiple, - optional, - }) - } } /// Validate all the document references by the defined validation rules, From 02fe9ee560e7746804bf260a1467528d4739e9b9 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 9 Sep 2025 23:36:25 +0400 Subject: [PATCH 11/14] wip --- rust/catalyst-signed-doc-macro/Cargo.toml | 5 +---- rust/signed_doc/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/rust/catalyst-signed-doc-macro/Cargo.toml b/rust/catalyst-signed-doc-macro/Cargo.toml index da93f24f246..a5b4cad050d 100644 --- a/rust/catalyst-signed-doc-macro/Cargo.toml +++ b/rust/catalyst-signed-doc-macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "catalyst-signed-doc-macro" -version = "0.1.3" +version = "0.0.1" edition.workspace = true authors.workspace = true homepage.workspace = true @@ -17,10 +17,7 @@ proc-macro = true syn = { version = "2.0", features = ["full"] } quote = "1.0" proc-macro2 = "1.0" -serde_json = "1.0.142" anyhow = "1.0.99" -Inflector = "0.11.4" -serde = { version = "1.0.219", features = ["derive"] } catalyst-signed-doc-spec = { version = "0.1.3", path = "../catalyst-signed-doc-spec" } diff --git a/rust/signed_doc/Cargo.toml b/rust/signed_doc/Cargo.toml index 1145b0a2712..8f278f87f8f 100644 --- a/rust/signed_doc/Cargo.toml +++ b/rust/signed_doc/Cargo.toml @@ -14,7 +14,7 @@ workspace = true catalyst-types = { version = "0.0.6", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "catalyst-types/v0.0.6" } cbork-utils = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "cbork-utils-v0.0.2" } -catalyst-signed-doc-macro = { version = "0.1.3", path = "../catalyst-signed-doc-macro" } +catalyst-signed-doc-macro = { version = "0.0.1", path = "../catalyst-signed-doc-macro" } catalyst-signed-doc-spec = { version = "0.1.3", path = "../catalyst-signed-doc-spec" } anyhow = "1.0.95" From 2607d4d250ef7e61cf16cfa0cff67fe1def05f85 Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Tue, 9 Sep 2025 23:37:19 +0400 Subject: [PATCH 12/14] wip --- rust/signed_doc/src/validator/rules/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/signed_doc/src/validator/rules/mod.rs b/rust/signed_doc/src/validator/rules/mod.rs index b2406d294f6..476c664cf58 100644 --- a/rust/signed_doc/src/validator/rules/mod.rs +++ b/rust/signed_doc/src/validator/rules/mod.rs @@ -117,7 +117,7 @@ impl Rules { let spec = catalyst_signed_doc_spec::CatalystSignedDocSpec::load_signed_doc_spec()?; let mut doc_rules = Vec::new(); - for (_, doc_spec) in &spec.docs { + for doc_spec in spec.docs.values() { let rules = Self { id: IdRule, ver: VerRule, From 3b5b47ab48179ee1c4b1131b24f6a25f38010ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn=20Rosales?= Date: Tue, 9 Sep 2025 21:55:54 -0600 Subject: [PATCH 13/14] feat(rust/signed-doc): generate content encoding rules from macro --- .../src/headers/content_encoding.rs | 10 ++++ .../src/headers/mod.rs | 3 + rust/signed_doc/src/validator/mod.rs | 6 +- .../src/validator/rules/content_encoding.rs | 58 +++++++++++++++---- .../src/validator/rules/content_type.rs | 1 - rust/signed_doc/src/validator/rules/mod.rs | 2 +- 6 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 rust/catalyst-signed-doc-spec/src/headers/content_encoding.rs diff --git a/rust/catalyst-signed-doc-spec/src/headers/content_encoding.rs b/rust/catalyst-signed-doc-spec/src/headers/content_encoding.rs new file mode 100644 index 00000000000..9ff831d6e49 --- /dev/null +++ b/rust/catalyst-signed-doc-spec/src/headers/content_encoding.rs @@ -0,0 +1,10 @@ +//! `signed_doc.json` headers content encoding field JSON definition +//! Content Encoding + +use crate::is_required::IsRequired; + +#[derive(serde::Deserialize)] +pub struct ContentEncoding { + pub required: IsRequired, + pub value: Option>, +} diff --git a/rust/catalyst-signed-doc-spec/src/headers/mod.rs b/rust/catalyst-signed-doc-spec/src/headers/mod.rs index 24a455be9ce..bd220880165 100644 --- a/rust/catalyst-signed-doc-spec/src/headers/mod.rs +++ b/rust/catalyst-signed-doc-spec/src/headers/mod.rs @@ -1,5 +1,6 @@ //! 'headers' field definition +pub mod content_encoding; pub mod content_type; /// Document's metadata fields definition @@ -8,4 +9,6 @@ pub mod content_type; pub struct Headers { #[serde(rename = "content type")] pub content_type: content_type::ContentType, + #[serde(rename = "content-encoding")] + pub content_encoding: content_encoding::ContentEncoding, } diff --git a/rust/signed_doc/src/validator/mod.rs b/rust/signed_doc/src/validator/mod.rs index ac71e61b323..4f763eaa3bd 100644 --- a/rust/signed_doc/src/validator/mod.rs +++ b/rust/signed_doc/src/validator/mod.rs @@ -42,7 +42,7 @@ fn proposal_rule() -> Rules { exp: ContentType::Json, }, content_encoding: ContentEncodingRule::Specified { - exp: ContentEncoding::Brotli, + exp: vec![ContentEncoding::Brotli], optional: false, }, template: TemplateRule::Specified { @@ -81,7 +81,7 @@ fn proposal_comment_rule() -> Rules { exp: ContentType::Json, }, content_encoding: ContentEncodingRule::Specified { - exp: ContentEncoding::Brotli, + exp: vec![ContentEncoding::Brotli], optional: false, }, template: TemplateRule::Specified { @@ -138,7 +138,7 @@ fn proposal_submission_action_rule() -> Rules { exp: ContentType::Json, }, content_encoding: ContentEncodingRule::Specified { - exp: ContentEncoding::Brotli, + exp: vec![ContentEncoding::Brotli], optional: false, }, template: TemplateRule::NotSpecified, diff --git a/rust/signed_doc/src/validator/rules/content_encoding.rs b/rust/signed_doc/src/validator/rules/content_encoding.rs index 657f24e3a94..7f5ebe34800 100644 --- a/rust/signed_doc/src/validator/rules/content_encoding.rs +++ b/rust/signed_doc/src/validator/rules/content_encoding.rs @@ -1,5 +1,9 @@ //! `content-encoding` rule type impl. +use std::string::ToString; + +use catalyst_signed_doc_spec::is_required::IsRequired; + use crate::{metadata::ContentEncoding, CatalystSignedDocument}; /// `content-encoding` field validation rule. @@ -8,16 +12,47 @@ pub(crate) enum ContentEncodingRule { /// Content Encoding field is optionally present in the document. Specified { /// expected `content-encoding` field. - exp: ContentEncoding, + exp: Vec, /// optional flag for the `content-encoding` field. optional: bool, }, /// Content Encoding field must not be present in the document. - #[allow(dead_code)] NotSpecified, } impl ContentEncodingRule { + /// Create a new rule from specs. + pub(crate) fn new( + spec: &catalyst_signed_doc_spec::headers::content_encoding::ContentEncoding + ) -> anyhow::Result { + if let IsRequired::Excluded = spec.required { + anyhow::ensure!( + spec.value.is_none(), + "'content type' field must not exist when 'required' is 'excluded'" + ); + return Ok(Self::NotSpecified); + } + + if let IsRequired::Yes = spec.required { + anyhow::ensure!( + spec.value.is_some(), + "'content-encoding' field must have value when 'required' is 'yes'" + ); + } + + let optional = IsRequired::Optional == spec.required; + + let exp = spec + .value + .as_ref() + .ok_or(anyhow::anyhow!("'content-encoding' field must have value "))? + .iter() + .flat_map(|encoding| encoding.parse()) + .collect(); + + Ok(Self::Specified { exp, optional }) + } + /// Field validation rule #[allow(clippy::unused_async)] pub(crate) async fn check( @@ -40,12 +75,15 @@ impl ContentEncodingRule { }, Self::Specified { exp, optional } => { if let Some(content_encoding) = doc.doc_content_encoding() { - if content_encoding != *exp { + if !exp.contains(&content_encoding) { doc.report().invalid_value( "content-encoding", content_encoding.to_string().as_str(), - exp.to_string().as_str(), - "Invalid Document content-encoding value", + &exp.iter() + .map(ToString::to_string) + .collect::>() + .join(", "), + "Invalid document content-encoding value", ); return Ok(false); } @@ -53,10 +91,8 @@ impl ContentEncodingRule { doc.report().invalid_value( "payload", &hex::encode(doc.encoded_content()), - &format!( - "Document content (payload) must decodable by the set content encoding type: {content_encoding}" - ), - "Invalid Document content value", + content_encoding.to_string().as_str(), + "Document content is not decodable with the expected content-encoding", ); return Ok(false); } @@ -83,7 +119,7 @@ mod tests { let content_encoding = ContentEncoding::Brotli; let rule = ContentEncodingRule::Specified { - exp: content_encoding, + exp: vec![content_encoding], optional: true, }; @@ -103,7 +139,7 @@ mod tests { assert!(rule.check(&doc).await.unwrap()); let rule = ContentEncodingRule::Specified { - exp: content_encoding, + exp: vec![content_encoding], optional: false, }; assert!(!rule.check(&doc).await.unwrap()); diff --git a/rust/signed_doc/src/validator/rules/content_type.rs b/rust/signed_doc/src/validator/rules/content_type.rs index e82c86dcf57..2af5db4f164 100644 --- a/rust/signed_doc/src/validator/rules/content_type.rs +++ b/rust/signed_doc/src/validator/rules/content_type.rs @@ -11,7 +11,6 @@ pub(crate) enum ContentTypeRule { exp: ContentType, }, /// Content Type field must not be present in the document. - #[allow(dead_code)] NotSpecified, } diff --git a/rust/signed_doc/src/validator/rules/mod.rs b/rust/signed_doc/src/validator/rules/mod.rs index 476c664cf58..2dac7ae1eff 100644 --- a/rust/signed_doc/src/validator/rules/mod.rs +++ b/rust/signed_doc/src/validator/rules/mod.rs @@ -122,7 +122,7 @@ impl Rules { id: IdRule, ver: VerRule, content_type: ContentTypeRule::new(&doc_spec.headers.content_type)?, - content_encoding: ContentEncodingRule::NotSpecified, + content_encoding: ContentEncodingRule::new(&doc_spec.headers.content_encoding)?, template: TemplateRule::NotSpecified, parameters: ParametersRule::NotSpecified, doc_ref: RefRule::new(&spec.docs, &doc_spec.metadata.doc_ref)?, From 749f4d63d94911900a0a2a0447cbf89e7e4f0d5a Mon Sep 17 00:00:00 2001 From: Mr-Leshiy Date: Wed, 10 Sep 2025 12:13:50 +0400 Subject: [PATCH 14/14] cleanup --- rust/signed_doc/src/validator/rules/content_encoding.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/rust/signed_doc/src/validator/rules/content_encoding.rs b/rust/signed_doc/src/validator/rules/content_encoding.rs index 7f5ebe34800..9e24ea54d13 100644 --- a/rust/signed_doc/src/validator/rules/content_encoding.rs +++ b/rust/signed_doc/src/validator/rules/content_encoding.rs @@ -33,13 +33,6 @@ impl ContentEncodingRule { return Ok(Self::NotSpecified); } - if let IsRequired::Yes = spec.required { - anyhow::ensure!( - spec.value.is_some(), - "'content-encoding' field must have value when 'required' is 'yes'" - ); - } - let optional = IsRequired::Optional == spec.required; let exp = spec