Skip to content

feat(rust/signed-doc): signed doc metadata serde refactoring #372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions rust/signed_doc/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ impl Builder {
/// # Errors
/// - Fails if it is invalid metadata fields JSON object.
pub fn with_json_metadata(mut self, json: serde_json::Value) -> anyhow::Result<Self> {
let metadata = serde_json::from_value(json)?;
self.metadata = Metadata::from_metadata_fields(metadata, &ProblemReport::new(""));
self.metadata = Metadata::from_json(json, &ProblemReport::new(""));
Ok(self)
}

Expand Down
202 changes: 97 additions & 105 deletions rust/signed_doc/src/metadata/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,65 +63,6 @@ const CATEGORY_ID_KEY: &str = "category_id";
#[derive(Clone, Debug, PartialEq, Default)]
pub struct Metadata(BTreeMap<SupportedLabel, SupportedField>);

/// An actual representation of all metadata fields.
// TODO: this is maintained as an implementation of `serde` and `coset` for `Metadata`
// and should be removed in case `serde` and `coset` are deprecated completely.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, Default)]
pub(crate) struct InnerMetadata {
/// Document Type, list of `UUIDv4`.
#[serde(rename = "type")]
doc_type: Option<DocType>,
/// Document ID `UUIDv7`.
id: Option<UuidV7>,
/// Document Version `UUIDv7`.
ver: Option<UuidV7>,
/// Document Payload Content Type.
#[serde(rename = "content-type")]
content_type: Option<ContentType>,
/// Document Payload Content Encoding.
#[serde(rename = "content-encoding")]
content_encoding: Option<ContentEncoding>,
/// Reference to the latest document.
#[serde(rename = "ref", skip_serializing_if = "Option::is_none")]
doc_ref: Option<DocumentRef>,
/// Reference to the document template.
#[serde(skip_serializing_if = "Option::is_none")]
template: Option<DocumentRef>,
/// Reference to the document reply.
#[serde(skip_serializing_if = "Option::is_none")]
reply: Option<DocumentRef>,
/// Reference to the document section.
#[serde(skip_serializing_if = "Option::is_none")]
section: Option<Section>,
/// Reference to the document collaborators. Collaborator type is TBD.
#[serde(default = "Vec::new", skip_serializing_if = "Vec::is_empty")]
collabs: Vec<String>,
/// Reference to the parameters document.
#[serde(skip_serializing_if = "Option::is_none")]
parameters: Option<DocumentRef>,
}

impl InnerMetadata {
/// Converts into an iterator over present fields fields.
fn into_iter(self) -> impl Iterator<Item = SupportedField> {
[
self.doc_type.map(SupportedField::Type),
self.id.map(SupportedField::Id),
self.ver.map(SupportedField::Ver),
self.content_type.map(SupportedField::ContentType),
self.content_encoding.map(SupportedField::ContentEncoding),
self.doc_ref.map(SupportedField::Ref),
self.template.map(SupportedField::Template),
self.reply.map(SupportedField::Reply),
self.section.map(SupportedField::Section),
(!self.collabs.is_empty()).then_some(SupportedField::Collabs(self.collabs)),
self.parameters.map(SupportedField::Parameters),
]
.into_iter()
.flatten()
}
}

impl Metadata {
/// Return Document Type `DocType` - a list of `UUIDv4`.
///
Expand Down Expand Up @@ -233,42 +174,52 @@ impl Metadata {
}

/// Build `Metadata` object from the metadata fields, doing all necessary validation.
pub(crate) fn from_metadata_fields(metadata: InnerMetadata, report: &ProblemReport) -> Self {
if metadata.doc_type.is_none() {
report.missing_field("type", "Missing type field in COSE protected header");
pub(crate) fn from_fields(fields: Vec<SupportedField>, report: &ProblemReport) -> Self {
const REPORT_CONTEXT: &str = "Metadata building";

let mut metadata = Metadata(BTreeMap::new());
for v in fields {
let k = v.discriminant();
if metadata.0.insert(k, v).is_some() {
report.duplicate_field(
&k.to_string(),
"Duplicate metadata fields are not allowed",
REPORT_CONTEXT,
);
}
}
if metadata.id.is_none() {
report.missing_field("id", "Missing id field in COSE protected header");

if metadata.doc_type().is_err() {
report.missing_field("type", REPORT_CONTEXT);
}
if metadata.ver.is_none() {
report.missing_field("ver", "Missing ver field in COSE protected header");
if metadata.doc_id().is_err() {
report.missing_field("id", REPORT_CONTEXT);
}

if metadata.content_type.is_none() {
report.missing_field(
"content type",
"Missing content_type field in COSE protected header",
);
if metadata.doc_ver().is_err() {
report.missing_field("ver", REPORT_CONTEXT);
}
if metadata.content_type().is_err() {
report.missing_field("content-type", REPORT_CONTEXT);
}

Self(
metadata
.into_iter()
.map(|field| (field.discriminant(), field))
.collect(),
)
metadata
}

/// Converting COSE Protected Header to Metadata.
pub(crate) fn from_protected_header(
protected: &coset::ProtectedHeader, context: &mut DecodeContext,
) -> Self {
let metadata = InnerMetadata::from_protected_header(protected, context);
Self::from_metadata_fields(metadata, context.report)
/// Build `Metadata` object from the metadata fields, doing all necessary validation.
pub(crate) fn from_json(fields: serde_json::Value, report: &ProblemReport) -> Self {
let fields = serde::Deserializer::deserialize_map(fields, MetadataDeserializeVisitor)
.inspect_err(|err| {
report.other(
&format!("Unable to deserialize json: {err}"),
"Metadata building from json",
);
})
.unwrap_or_default();
Self::from_fields(fields, report)
}
}

impl InnerMetadata {
impl Metadata {
/// Converting COSE Protected Header to Metadata fields, collecting decoding report
/// issues.
#[allow(
Expand All @@ -282,11 +233,11 @@ impl InnerMetadata {
/// header.
const COSE_DECODING_CONTEXT: &str = "COSE Protected Header to Metadata";

let mut metadata = Self::default();
let mut metadata_fields = vec![];

if let Some(value) = protected.header.content_type.as_ref() {
match ContentType::try_from(value) {
Ok(ct) => metadata.content_type = Some(ct),
Ok(ct) => metadata_fields.push(SupportedField::ContentType(ct)),
Err(e) => {
context.report.conversion_error(
"COSE protected header content type",
Expand All @@ -303,7 +254,7 @@ impl InnerMetadata {
|key| matches!(key, coset::Label::Text(label) if label.eq_ignore_ascii_case(CONTENT_ENCODING_KEY)),
) {
match ContentEncoding::try_from(value) {
Ok(ce) => metadata.content_encoding = Some(ce),
Ok(ce) => metadata_fields.push(SupportedField::ContentEncoding(ce)),
Err(e) => {
context.report.conversion_error(
"COSE protected header content encoding",
Expand All @@ -315,7 +266,7 @@ impl InnerMetadata {
}
}

metadata.doc_type = cose_protected_header_find(
if let Some(value) = cose_protected_header_find(
protected,
|key| matches!(key, coset::Label::Text(label) if label.eq_ignore_ascii_case(TYPE_KEY)),
)
Expand All @@ -325,48 +276,64 @@ impl InnerMetadata {
context,
)
.ok()
});
}) {
metadata_fields.push(SupportedField::Type(value));
}

metadata.id = decode_document_field_from_protected_header::<CborUuidV7>(
if let Some(value) = decode_document_field_from_protected_header::<CborUuidV7>(
protected,
ID_KEY,
COSE_DECODING_CONTEXT,
context.report,
)
.map(|v| v.0);
.map(|v| v.0)
{
metadata_fields.push(SupportedField::Id(value));
}

metadata.ver = decode_document_field_from_protected_header::<CborUuidV7>(
if let Some(value) = decode_document_field_from_protected_header::<CborUuidV7>(
protected,
VER_KEY,
COSE_DECODING_CONTEXT,
context.report,
)
.map(|v| v.0);
.map(|v| v.0)
{
metadata_fields.push(SupportedField::Ver(value));
}

metadata.doc_ref = decode_document_field_from_protected_header(
if let Some(value) = decode_document_field_from_protected_header(
protected,
REF_KEY,
COSE_DECODING_CONTEXT,
context.report,
);
metadata.template = decode_document_field_from_protected_header(
) {
metadata_fields.push(SupportedField::Ref(value));
}
if let Some(value) = decode_document_field_from_protected_header(
protected,
TEMPLATE_KEY,
COSE_DECODING_CONTEXT,
context.report,
);
metadata.reply = decode_document_field_from_protected_header(
) {
metadata_fields.push(SupportedField::Template(value));
}
if let Some(value) = decode_document_field_from_protected_header(
protected,
REPLY_KEY,
COSE_DECODING_CONTEXT,
context.report,
);
metadata.section = decode_document_field_from_protected_header(
) {
metadata_fields.push(SupportedField::Reply(value));
}
if let Some(value) = decode_document_field_from_protected_header(
protected,
SECTION_KEY,
COSE_DECODING_CONTEXT,
context.report,
);
) {
metadata_fields.push(SupportedField::Section(value));
}

// process `parameters` field and all its aliases
let (parameters, has_multiple_fields) = [
Expand All @@ -392,7 +359,9 @@ impl InnerMetadata {
"Validation of parameters field aliases"
);
}
metadata.parameters = parameters;
if let Some(value) = parameters {
metadata_fields.push(SupportedField::Parameters(value));
}

if let Some(cbor_doc_collabs) = cose_protected_header_find(protected, |key| {
key == &coset::Label::Text(COLLABS_KEY.to_string())
Expand All @@ -416,7 +385,9 @@ impl InnerMetadata {
},
}
}
metadata.collabs = c;
if !c.is_empty() {
metadata_fields.push(SupportedField::Collabs(c));
}
} else {
context.report.conversion_error(
"CBOR COSE protected header collaborators",
Expand All @@ -427,7 +398,7 @@ impl InnerMetadata {
};
}

metadata
Self::from_fields(metadata_fields, context.report)
}
}

Expand Down Expand Up @@ -560,3 +531,24 @@ impl minicbor::Decode<'_, crate::decode_context::DecodeContext<'_>> for Metadata
first_err.map_or(Ok(Self(metadata_map)), Err)
}
}

/// Implements [`serde::de::Visitor`], so that [`Metadata`] can be
/// deserialized by [`serde::Deserializer::deserialize_map`].
struct MetadataDeserializeVisitor;

impl<'de> serde::de::Visitor<'de> for MetadataDeserializeVisitor {
type Value = Vec<SupportedField>;

fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("Catalyst Signed Document metadata key-value pairs")
}

fn visit_map<A: serde::de::MapAccess<'de>>(self, mut d: A) -> Result<Self::Value, A::Error> {
let mut res = Vec::with_capacity(d.size_hint().unwrap_or(0));
while let Some(k) = d.next_key::<SupportedLabel>()? {
let v = d.next_value_seed(k)?;
res.push(v);
}
Ok(res)
}
}
Loading