Skip to content

Commit a834be4

Browse files
authored
Merge 72f2706 into 4635022
2 parents 4635022 + 72f2706 commit a834be4

File tree

8 files changed

+140
-11
lines changed

8 files changed

+140
-11
lines changed

rust/catalyst-signed-doc-macro/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ quote = "1.0"
1919
proc-macro2 = "1.0"
2020
serde_json = "1.0.142"
2121
anyhow = "1.0.99"
22+
Inflector = "0.11.4"
2223
serde = { version = "1.0.219", features = ["derive"] }
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//! `ContentTypeRule` generation
2+
3+
use std::collections::HashMap;
4+
5+
use proc_macro2::TokenStream;
6+
use quote::quote;
7+
8+
use crate::signed_doc_spec::{self, ContentTypeSpec, ContentTypeTemplate, IsRequired};
9+
10+
/// Generating `ContentTypeRule` instantiation
11+
pub(crate) fn into_rule(
12+
content_types: &HashMap<ContentTypeTemplate, ContentTypeSpec>,
13+
field: &signed_doc_spec::content_type::ContentType,
14+
) -> anyhow::Result<TokenStream> {
15+
let is_field_empty = field.value.is_none()
16+
|| field
17+
.value
18+
.as_ref()
19+
.is_some_and(std::string::String::is_empty);
20+
21+
if matches!(field.required, IsRequired::Excluded) {
22+
anyhow::ensure!(
23+
is_field_empty,
24+
"'value' field must not exist when 'required' is 'excluded'"
25+
);
26+
27+
return Ok(quote! {
28+
crate::validator::rules::ContentTypeRule::NotSpecified
29+
});
30+
}
31+
32+
if matches!(field.required, IsRequired::Yes) {
33+
anyhow::ensure!(!is_field_empty, "'value' field must exist");
34+
}
35+
36+
let Some(value) = &field.value else {
37+
anyhow::bail!("'value' field must exist");
38+
};
39+
40+
let template = ContentTypeTemplate(value.clone());
41+
let Some(_) = content_types.get(&template) else {
42+
return Err(anyhow::anyhow!("Unsupported Content Type: {}", value));
43+
};
44+
let ident = template.ident();
45+
46+
Ok(quote! {
47+
crate::validator::rules::ContentTypeRule::Specified {
48+
exp: ContentType::#ident,
49+
}
50+
})
51+
}

rust/catalyst-signed-doc-macro/src/rules/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! `catalyst_signed_documents_rules!` macro implementation
22
3+
pub(crate) mod content_type;
34
pub(crate) mod doc_ref;
45

56
use proc_macro2::TokenStream;
@@ -15,13 +16,15 @@ pub(crate) fn catalyst_signed_documents_rules_impl() -> anyhow::Result<TokenStre
1516
for (doc_name, doc_spec) in spec.docs {
1617
let const_type_name_ident = doc_name.ident();
1718

19+
let content_type_rule =
20+
content_type::into_rule(&spec.content_types, &doc_spec.headers.content_type)?;
1821
let ref_rule = doc_ref::ref_rule(&doc_spec.metadata.doc_ref)?;
1922
// TODO: implement a proper initialization for all specific validation rules
2023
let rules = quote! {
2124
crate::validator::rules::Rules {
2225
id: crate::validator::rules::IdRule,
2326
ver: crate::validator::rules::VerRule,
24-
content_type: crate::validator::rules::ContentTypeRule::NotSpecified,
27+
content_type: #content_type_rule,
2528
content_encoding: crate::validator::rules::ContentEncodingRule::Specified {
2629
exp: ContentEncoding::Brotli,
2730
optional: false,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//! `signed_doc.json` headers content type field JSON definition
2+
3+
/// `signed_doc.json` "content type" field JSON object
4+
#[derive(serde::Deserialize)]
5+
#[allow(clippy::missing_docs_in_private_items)]
6+
pub(crate) struct ContentType {
7+
pub(crate) required: super::IsRequired,
8+
pub(crate) value: Option<String>,
9+
}

rust/catalyst-signed-doc-macro/src/signed_doc_spec/mod.rs

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,73 @@
11
//! Catalyst Signed Document spec type
22
3+
// cspell: words pascalcase
4+
5+
pub(crate) mod content_type;
36
pub(crate) mod doc_ref;
47

58
use std::{collections::HashMap, ops::Deref};
69

10+
use inflector::cases::pascalcase::to_pascal_case;
711
use proc_macro2::Ident;
812
use quote::format_ident;
913

1014
/// Catalyst Signed Document spec representation struct
1115
#[derive(serde::Deserialize)]
1216
pub(crate) struct CatalystSignedDocSpec {
17+
/// A collection of document's supported content types
18+
#[serde(rename = "contentTypes")]
19+
#[allow(dead_code)]
20+
pub(crate) content_types: HashMap<ContentTypeTemplate, ContentTypeSpec>,
1321
/// A collection of document's specs
1422
pub(crate) docs: HashMap<DocumentName, DocSpec>,
1523
}
1624

25+
// A thin wrapper over the RFC2046 content type strings.
26+
#[derive(serde::Deserialize, PartialEq, Eq, Hash)]
27+
pub(crate) struct ContentTypeTemplate(pub(crate) String);
28+
29+
impl ContentTypeTemplate {
30+
/// returns a content type template as a `Ident` in the following form.
31+
///
32+
/// text/css; charset=utf-8; template=handlebars
33+
/// => `CssHandlebars`
34+
///
35+
/// text/css; charset=utf-8
36+
/// => `Css`
37+
pub(crate) fn ident(&self) -> Ident {
38+
let raw = self.0.as_str();
39+
40+
// split into parts like "text/css; charset=utf-8; template=handlebars"
41+
let mut parts = raw.split(';').map(str::trim);
42+
43+
// first part is "type/subtype"
44+
let first = parts.next().unwrap_or_default(); // e.g. "text/css"
45+
let subtype = first.split('/').nth(1).unwrap_or_default(); // "css"
46+
47+
// look for "template=..."
48+
let template = parts
49+
.find_map(|p| p.strip_prefix("template="))
50+
.map(to_pascal_case);
51+
52+
// build PascalCase
53+
let mut ident = String::new();
54+
ident.push_str(&to_pascal_case(subtype));
55+
if let Some(t) = template {
56+
ident.push_str(&t);
57+
}
58+
59+
format_ident!("{}", ident)
60+
}
61+
}
62+
63+
/// Catalyst Signed Document supported content type declaration struct
64+
#[derive(serde::Deserialize)]
65+
pub(crate) struct ContentTypeSpec {
66+
/// CoAP Content-Formats
67+
#[allow(dead_code)]
68+
coap_type: Option<u32>,
69+
}
70+
1771
// A thin wrapper over the string document name values
1872
#[derive(serde::Deserialize, PartialEq, Eq, Hash)]
1973
pub(crate) struct DocumentName(String);
@@ -44,7 +98,8 @@ pub(crate) struct DocSpec {
4498
/// Document type UUID v4 value
4599
#[serde(rename = "type")]
46100
pub(crate) doc_type: String,
47-
101+
/// `headers` field
102+
pub(crate) headers: Headers,
48103
/// Document type metadata definitions
49104
pub(crate) metadata: Metadata,
50105
}
@@ -57,6 +112,14 @@ pub(crate) struct Metadata {
57112
pub(crate) doc_ref: doc_ref::Ref,
58113
}
59114

115+
/// Document's metadata fields definition
116+
#[derive(serde::Deserialize)]
117+
#[allow(clippy::missing_docs_in_private_items)]
118+
pub(crate) struct Headers {
119+
#[serde(rename = "content type")]
120+
pub(crate) content_type: content_type::ContentType,
121+
}
122+
60123
/// "required" field definition
61124
#[derive(serde::Deserialize)]
62125
#[serde(rename_all = "lowercase")]
@@ -103,7 +166,8 @@ impl CatalystSignedDocSpec {
103166
// #[allow(dependency_on_unit_never_type_fallback)]
104167
pub(crate) fn load_signed_doc_spec() -> anyhow::Result<CatalystSignedDocSpec> {
105168
let signed_doc_str = include_str!("../../../../specs/signed_doc.json");
106-
let signed_doc_spec = serde_json::from_str(signed_doc_str)?;
169+
let signed_doc_spec = serde_json::from_str(signed_doc_str)
170+
.map_err(|e| anyhow::anyhow!("Invalid Catalyst Signed Documents JSON Spec: {e}"))?;
107171
Ok(signed_doc_spec)
108172
}
109173
}

rust/signed_doc/src/metadata/content_type.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ pub enum ContentType {
1616
/// `application/json`
1717
#[strum(to_string = "application/json")]
1818
Json,
19-
/// `application/json+schema`
20-
#[strum(to_string = "application/json+schema")]
21-
JsonSchema,
19+
/// `application/schema+json`
20+
#[strum(to_string = "application/schema+json")]
21+
SchemaJson,
2222
/// `text/css; charset=utf-8`
2323
#[strum(to_string = "text/css; charset=utf-8")]
2424
Css,
@@ -53,7 +53,7 @@ impl FromStr for ContentType {
5353
"application/cbor" => Ok(Self::Cbor),
5454
"application/cddl" => Ok(Self::Cddl),
5555
"application/json" => Ok(Self::Json),
56-
"application/json+schema" => Ok(Self::JsonSchema),
56+
"application/schema+json" => Ok(Self::SchemaJson),
5757
"text/css; charset=utf-8" => Ok(Self::Css),
5858
"text/css; charset=utf-8; template=handlebars" => Ok(Self::CssHandlebars),
5959
"text/html; charset=utf-8" => Ok(Self::Html),
@@ -162,8 +162,8 @@ mod tests {
162162
"application/json"
163163
)]
164164
#[test_case(
165-
("application/json+schema", ContentType::JsonSchema);
166-
"application/json+schema"
165+
("application/schema+json", ContentType::SchemaJson);
166+
"application/schema+json"
167167
)]
168168
#[test_case(
169169
("text/css; charset=utf-8", ContentType::Css);

rust/signed_doc/src/validator/rules/content_type.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub(crate) enum ContentTypeRule {
1111
exp: ContentType,
1212
},
1313
/// Content Type field must not be present in the document.
14+
#[allow(dead_code)]
1415
NotSpecified,
1516
}
1617

@@ -92,7 +93,7 @@ impl ContentTypeRule {
9293
}
9394
},
9495
ContentType::Cddl
95-
| ContentType::JsonSchema
96+
| ContentType::SchemaJson
9697
| ContentType::Css
9798
| ContentType::CssHandlebars
9899
| ContentType::Html

rust/signed_doc/src/validator/rules/template.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ impl TemplateRule {
4949
return false;
5050
};
5151
match template_content_type {
52-
ContentType::Json | ContentType::JsonSchema => {
52+
ContentType::Json | ContentType::SchemaJson => {
5353
templated_json_schema_check(doc, template_doc)
5454
},
5555
ContentType::Cddl

0 commit comments

Comments
 (0)