Skip to content

Commit 8d6b5c9

Browse files
no30bitMr-Leshiy
andauthored
feat(rust/signed-doc): signed doc metadata serde refactoring (#372)
* impl deserialize * refactor from_metadata_fields & remove InnerMetadata * rename visitor * handle aliasing * kebab-case only deserialization for SupportedLabel * cleanup * fmt --------- Co-authored-by: Alex Pozhylenkov <[email protected]>
1 parent 4bbc8ae commit 8d6b5c9

File tree

3 files changed

+133
-111
lines changed

3 files changed

+133
-111
lines changed

rust/signed_doc/src/builder.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ impl Builder {
4040
/// # Errors
4141
/// - Fails if it is invalid metadata fields JSON object.
4242
pub fn with_json_metadata(mut self, json: serde_json::Value) -> anyhow::Result<Self> {
43-
let metadata = serde_json::from_value(json)?;
44-
self.metadata = Metadata::from_metadata_fields(metadata, &ProblemReport::new(""));
43+
self.metadata = Metadata::from_json(json, &ProblemReport::new(""));
4544
Ok(self)
4645
}
4746

rust/signed_doc/src/metadata/mod.rs

Lines changed: 97 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -63,65 +63,6 @@ const CATEGORY_ID_KEY: &str = "category_id";
6363
#[derive(Clone, Debug, PartialEq, Default)]
6464
pub struct Metadata(BTreeMap<SupportedLabel, SupportedField>);
6565

66-
/// An actual representation of all metadata fields.
67-
// TODO: this is maintained as an implementation of `serde` and `coset` for `Metadata`
68-
// and should be removed in case `serde` and `coset` are deprecated completely.
69-
#[derive(Clone, Debug, PartialEq, serde::Deserialize, Default)]
70-
pub(crate) struct InnerMetadata {
71-
/// Document Type, list of `UUIDv4`.
72-
#[serde(rename = "type")]
73-
doc_type: Option<DocType>,
74-
/// Document ID `UUIDv7`.
75-
id: Option<UuidV7>,
76-
/// Document Version `UUIDv7`.
77-
ver: Option<UuidV7>,
78-
/// Document Payload Content Type.
79-
#[serde(rename = "content-type")]
80-
content_type: Option<ContentType>,
81-
/// Document Payload Content Encoding.
82-
#[serde(rename = "content-encoding")]
83-
content_encoding: Option<ContentEncoding>,
84-
/// Reference to the latest document.
85-
#[serde(rename = "ref", skip_serializing_if = "Option::is_none")]
86-
doc_ref: Option<DocumentRef>,
87-
/// Reference to the document template.
88-
#[serde(skip_serializing_if = "Option::is_none")]
89-
template: Option<DocumentRef>,
90-
/// Reference to the document reply.
91-
#[serde(skip_serializing_if = "Option::is_none")]
92-
reply: Option<DocumentRef>,
93-
/// Reference to the document section.
94-
#[serde(skip_serializing_if = "Option::is_none")]
95-
section: Option<Section>,
96-
/// Reference to the document collaborators. Collaborator type is TBD.
97-
#[serde(default = "Vec::new", skip_serializing_if = "Vec::is_empty")]
98-
collabs: Vec<String>,
99-
/// Reference to the parameters document.
100-
#[serde(skip_serializing_if = "Option::is_none")]
101-
parameters: Option<DocumentRef>,
102-
}
103-
104-
impl InnerMetadata {
105-
/// Converts into an iterator over present fields fields.
106-
fn into_iter(self) -> impl Iterator<Item = SupportedField> {
107-
[
108-
self.doc_type.map(SupportedField::Type),
109-
self.id.map(SupportedField::Id),
110-
self.ver.map(SupportedField::Ver),
111-
self.content_type.map(SupportedField::ContentType),
112-
self.content_encoding.map(SupportedField::ContentEncoding),
113-
self.doc_ref.map(SupportedField::Ref),
114-
self.template.map(SupportedField::Template),
115-
self.reply.map(SupportedField::Reply),
116-
self.section.map(SupportedField::Section),
117-
(!self.collabs.is_empty()).then_some(SupportedField::Collabs(self.collabs)),
118-
self.parameters.map(SupportedField::Parameters),
119-
]
120-
.into_iter()
121-
.flatten()
122-
}
123-
}
124-
12566
impl Metadata {
12667
/// Return Document Type `DocType` - a list of `UUIDv4`.
12768
///
@@ -233,42 +174,52 @@ impl Metadata {
233174
}
234175

235176
/// Build `Metadata` object from the metadata fields, doing all necessary validation.
236-
pub(crate) fn from_metadata_fields(metadata: InnerMetadata, report: &ProblemReport) -> Self {
237-
if metadata.doc_type.is_none() {
238-
report.missing_field("type", "Missing type field in COSE protected header");
177+
pub(crate) fn from_fields(fields: Vec<SupportedField>, report: &ProblemReport) -> Self {
178+
const REPORT_CONTEXT: &str = "Metadata building";
179+
180+
let mut metadata = Metadata(BTreeMap::new());
181+
for v in fields {
182+
let k = v.discriminant();
183+
if metadata.0.insert(k, v).is_some() {
184+
report.duplicate_field(
185+
&k.to_string(),
186+
"Duplicate metadata fields are not allowed",
187+
REPORT_CONTEXT,
188+
);
189+
}
239190
}
240-
if metadata.id.is_none() {
241-
report.missing_field("id", "Missing id field in COSE protected header");
191+
192+
if metadata.doc_type().is_err() {
193+
report.missing_field("type", REPORT_CONTEXT);
242194
}
243-
if metadata.ver.is_none() {
244-
report.missing_field("ver", "Missing ver field in COSE protected header");
195+
if metadata.doc_id().is_err() {
196+
report.missing_field("id", REPORT_CONTEXT);
245197
}
246-
247-
if metadata.content_type.is_none() {
248-
report.missing_field(
249-
"content type",
250-
"Missing content_type field in COSE protected header",
251-
);
198+
if metadata.doc_ver().is_err() {
199+
report.missing_field("ver", REPORT_CONTEXT);
200+
}
201+
if metadata.content_type().is_err() {
202+
report.missing_field("content-type", REPORT_CONTEXT);
252203
}
253204

254-
Self(
255-
metadata
256-
.into_iter()
257-
.map(|field| (field.discriminant(), field))
258-
.collect(),
259-
)
205+
metadata
260206
}
261207

262-
/// Converting COSE Protected Header to Metadata.
263-
pub(crate) fn from_protected_header(
264-
protected: &coset::ProtectedHeader, context: &mut DecodeContext,
265-
) -> Self {
266-
let metadata = InnerMetadata::from_protected_header(protected, context);
267-
Self::from_metadata_fields(metadata, context.report)
208+
/// Build `Metadata` object from the metadata fields, doing all necessary validation.
209+
pub(crate) fn from_json(fields: serde_json::Value, report: &ProblemReport) -> Self {
210+
let fields = serde::Deserializer::deserialize_map(fields, MetadataDeserializeVisitor)
211+
.inspect_err(|err| {
212+
report.other(
213+
&format!("Unable to deserialize json: {err}"),
214+
"Metadata building from json",
215+
);
216+
})
217+
.unwrap_or_default();
218+
Self::from_fields(fields, report)
268219
}
269220
}
270221

271-
impl InnerMetadata {
222+
impl Metadata {
272223
/// Converting COSE Protected Header to Metadata fields, collecting decoding report
273224
/// issues.
274225
#[allow(
@@ -282,11 +233,11 @@ impl InnerMetadata {
282233
/// header.
283234
const COSE_DECODING_CONTEXT: &str = "COSE Protected Header to Metadata";
284235

285-
let mut metadata = Self::default();
236+
let mut metadata_fields = vec![];
286237

287238
if let Some(value) = protected.header.content_type.as_ref() {
288239
match ContentType::try_from(value) {
289-
Ok(ct) => metadata.content_type = Some(ct),
240+
Ok(ct) => metadata_fields.push(SupportedField::ContentType(ct)),
290241
Err(e) => {
291242
context.report.conversion_error(
292243
"COSE protected header content type",
@@ -303,7 +254,7 @@ impl InnerMetadata {
303254
|key| matches!(key, coset::Label::Text(label) if label.eq_ignore_ascii_case(CONTENT_ENCODING_KEY)),
304255
) {
305256
match ContentEncoding::try_from(value) {
306-
Ok(ce) => metadata.content_encoding = Some(ce),
257+
Ok(ce) => metadata_fields.push(SupportedField::ContentEncoding(ce)),
307258
Err(e) => {
308259
context.report.conversion_error(
309260
"COSE protected header content encoding",
@@ -315,7 +266,7 @@ impl InnerMetadata {
315266
}
316267
}
317268

318-
metadata.doc_type = cose_protected_header_find(
269+
if let Some(value) = cose_protected_header_find(
319270
protected,
320271
|key| matches!(key, coset::Label::Text(label) if label.eq_ignore_ascii_case(TYPE_KEY)),
321272
)
@@ -325,48 +276,64 @@ impl InnerMetadata {
325276
context,
326277
)
327278
.ok()
328-
});
279+
}) {
280+
metadata_fields.push(SupportedField::Type(value));
281+
}
329282

330-
metadata.id = decode_document_field_from_protected_header::<CborUuidV7>(
283+
if let Some(value) = decode_document_field_from_protected_header::<CborUuidV7>(
331284
protected,
332285
ID_KEY,
333286
COSE_DECODING_CONTEXT,
334287
context.report,
335288
)
336-
.map(|v| v.0);
289+
.map(|v| v.0)
290+
{
291+
metadata_fields.push(SupportedField::Id(value));
292+
}
337293

338-
metadata.ver = decode_document_field_from_protected_header::<CborUuidV7>(
294+
if let Some(value) = decode_document_field_from_protected_header::<CborUuidV7>(
339295
protected,
340296
VER_KEY,
341297
COSE_DECODING_CONTEXT,
342298
context.report,
343299
)
344-
.map(|v| v.0);
300+
.map(|v| v.0)
301+
{
302+
metadata_fields.push(SupportedField::Ver(value));
303+
}
345304

346-
metadata.doc_ref = decode_document_field_from_protected_header(
305+
if let Some(value) = decode_document_field_from_protected_header(
347306
protected,
348307
REF_KEY,
349308
COSE_DECODING_CONTEXT,
350309
context.report,
351-
);
352-
metadata.template = decode_document_field_from_protected_header(
310+
) {
311+
metadata_fields.push(SupportedField::Ref(value));
312+
}
313+
if let Some(value) = decode_document_field_from_protected_header(
353314
protected,
354315
TEMPLATE_KEY,
355316
COSE_DECODING_CONTEXT,
356317
context.report,
357-
);
358-
metadata.reply = decode_document_field_from_protected_header(
318+
) {
319+
metadata_fields.push(SupportedField::Template(value));
320+
}
321+
if let Some(value) = decode_document_field_from_protected_header(
359322
protected,
360323
REPLY_KEY,
361324
COSE_DECODING_CONTEXT,
362325
context.report,
363-
);
364-
metadata.section = decode_document_field_from_protected_header(
326+
) {
327+
metadata_fields.push(SupportedField::Reply(value));
328+
}
329+
if let Some(value) = decode_document_field_from_protected_header(
365330
protected,
366331
SECTION_KEY,
367332
COSE_DECODING_CONTEXT,
368333
context.report,
369-
);
334+
) {
335+
metadata_fields.push(SupportedField::Section(value));
336+
}
370337

371338
// process `parameters` field and all its aliases
372339
let (parameters, has_multiple_fields) = [
@@ -392,7 +359,9 @@ impl InnerMetadata {
392359
"Validation of parameters field aliases"
393360
);
394361
}
395-
metadata.parameters = parameters;
362+
if let Some(value) = parameters {
363+
metadata_fields.push(SupportedField::Parameters(value));
364+
}
396365

397366
if let Some(cbor_doc_collabs) = cose_protected_header_find(protected, |key| {
398367
key == &coset::Label::Text(COLLABS_KEY.to_string())
@@ -416,7 +385,9 @@ impl InnerMetadata {
416385
},
417386
}
418387
}
419-
metadata.collabs = c;
388+
if !c.is_empty() {
389+
metadata_fields.push(SupportedField::Collabs(c));
390+
}
420391
} else {
421392
context.report.conversion_error(
422393
"CBOR COSE protected header collaborators",
@@ -427,7 +398,7 @@ impl InnerMetadata {
427398
};
428399
}
429400

430-
metadata
401+
Self::from_fields(metadata_fields, context.report)
431402
}
432403
}
433404

@@ -560,3 +531,24 @@ impl minicbor::Decode<'_, crate::decode_context::DecodeContext<'_>> for Metadata
560531
first_err.map_or(Ok(Self(metadata_map)), Err)
561532
}
562533
}
534+
535+
/// Implements [`serde::de::Visitor`], so that [`Metadata`] can be
536+
/// deserialized by [`serde::Deserializer::deserialize_map`].
537+
struct MetadataDeserializeVisitor;
538+
539+
impl<'de> serde::de::Visitor<'de> for MetadataDeserializeVisitor {
540+
type Value = Vec<SupportedField>;
541+
542+
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
543+
f.write_str("Catalyst Signed Document metadata key-value pairs")
544+
}
545+
546+
fn visit_map<A: serde::de::MapAccess<'de>>(self, mut d: A) -> Result<Self::Value, A::Error> {
547+
let mut res = Vec::with_capacity(d.size_hint().unwrap_or(0));
548+
while let Some(k) = d.next_key::<SupportedLabel>()? {
549+
let v = d.next_value_seed(k)?;
550+
res.push(v);
551+
}
552+
Ok(res)
553+
}
554+
}

0 commit comments

Comments
 (0)