Skip to content

Commit 710b79e

Browse files
authored
Add @doc comment DSL (#238)
This allows us to specify doc-comments for auto-generated types to avoid users having to manually put in comments afterwards which would be overwritten every re-gen. There is support for type-level (after the definition), field-level and variant-level (for group/type-choices).
1 parent eadc043 commit 710b79e

File tree

7 files changed

+206
-26
lines changed

7 files changed

+206
-26
lines changed

docs/docs/comment_dsl.mdx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,56 @@ Note that as this is at the field-level it must handle the tag as well as the `u
161161

162162
For more examples see `tests/custom_serialization` (used in the `core` and `core_no_wasm` tests) and `tests/custom_serialization_preserve` (used in the `preserve-encodings` test).
163163

164+
## @doc
165+
166+
This can be placed at field-level, struct-level or variant-level to specify a comment to be placed as a rust doc-comment.
167+
168+
```cddl
169+
docs = [
170+
foo: text, ; @doc this is a field-level comment
171+
bar: uint, ; @doc bar is a u64
172+
] ; @doc struct documentation here
173+
174+
docs_groupchoice = [
175+
; @name first @doc comment-about-first
176+
0, uint //
177+
; @doc comments about second @name second
178+
text
179+
] ; @doc type-level comment
180+
```
181+
182+
Will generate:
183+
```rust
184+
/// struct documentation here
185+
#[derive(Clone, Debug)]
186+
pub struct Docs {
187+
/// this is a field-level comment
188+
pub foo: String,
189+
/// bar is a u64
190+
pub bar: u64,
191+
}
192+
193+
impl Docs {
194+
/// * `foo` - this is a field-level comment
195+
/// * `bar` - bar is a u64
196+
pub fn new(foo: String, bar: u64) -> Self {
197+
Self { foo, bar }
198+
}
199+
}
200+
201+
/// type-level comment
202+
#[derive(Clone, Debug)]
203+
pub enum DocsGroupchoice {
204+
/// comment-about-first
205+
First(u64),
206+
/// comments about second
207+
Second(String),
208+
}
209+
```
210+
211+
Due to the comment dsl parsing this doc comment cannot contain the character `@`.
212+
213+
164214
## _CDDL_CODEGEN_EXTERN_TYPE_
165215

166216
While not as a comment, this allows you to compose in hand-written structs into a cddl spec.

src/comment_ast.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub struct RuleMetadata {
1515
pub custom_json: bool,
1616
pub custom_serialize: Option<String>,
1717
pub custom_deserialize: Option<String>,
18+
pub comment: Option<String>,
1819
}
1920

2021
pub fn merge_metadata(r1: &RuleMetadata, r2: &RuleMetadata) -> RuleMetadata {
@@ -53,6 +54,13 @@ pub fn merge_metadata(r1: &RuleMetadata, r2: &RuleMetadata) -> RuleMetadata {
5354
(val @ Some(_), _) => val.cloned(),
5455
(_, val) => val.cloned(),
5556
},
57+
comment: match (r1.comment.as_ref(), r2.comment.as_ref()) {
58+
(Some(val1), Some(val2)) => {
59+
panic!("Key \"comment\" specified twice: {:?} {:?}", val1, val2)
60+
}
61+
(val @ Some(_), _) => val.cloned(),
62+
(_, val) => val.cloned(),
63+
},
5664
};
5765
merged.verify();
5866
merged
@@ -66,6 +74,7 @@ enum ParseResult {
6674
CustomJson,
6775
CustomSerialize(String),
6876
CustomDeserialize(String),
77+
Comment(String),
6978
}
7079

7180
impl RuleMetadata {
@@ -120,6 +129,14 @@ impl RuleMetadata {
120129
}
121130
}
122131
}
132+
ParseResult::Comment(comment) => match base.comment.as_ref() {
133+
Some(old) => {
134+
panic!("Key \"comment\" specified twice: {:?} {:?}", old, comment)
135+
}
136+
None => {
137+
base.comment = Some(comment.to_string());
138+
}
139+
},
123140
}
124141
}
125142
base.verify();
@@ -188,6 +205,13 @@ fn tag_custom_deserialize(input: &str) -> IResult<&str, ParseResult> {
188205
))
189206
}
190207

208+
fn tag_comment(input: &str) -> IResult<&str, ParseResult> {
209+
let (input, _) = tag("@doc")(input)?;
210+
let (input, comment) = take_while1(|c| c != '@')(input)?;
211+
212+
Ok((input, ParseResult::Comment(comment.trim().to_string())))
213+
}
214+
191215
fn whitespace_then_tag(input: &str) -> IResult<&str, ParseResult> {
192216
let (input, _) = take_while(char::is_whitespace)(input)?;
193217
let (input, result) = alt((
@@ -198,6 +222,7 @@ fn whitespace_then_tag(input: &str) -> IResult<&str, ParseResult> {
198222
tag_custom_json,
199223
tag_custom_serialize,
200224
tag_custom_deserialize,
225+
tag_comment,
201226
))(input)?;
202227

203228
Ok((input, result))
@@ -242,6 +267,7 @@ fn parse_comment_name() {
242267
custom_json: false,
243268
custom_serialize: None,
244269
custom_deserialize: None,
270+
comment: None,
245271
}
246272
))
247273
);
@@ -261,6 +287,7 @@ fn parse_comment_newtype() {
261287
custom_json: false,
262288
custom_serialize: None,
263289
custom_deserialize: None,
290+
comment: None,
264291
}
265292
))
266293
);
@@ -280,6 +307,7 @@ fn parse_comment_newtype_and_name() {
280307
custom_json: false,
281308
custom_serialize: None,
282309
custom_deserialize: None,
310+
comment: None,
283311
}
284312
))
285313
);
@@ -299,6 +327,7 @@ fn parse_comment_newtype_and_name_and_used_as_key() {
299327
custom_json: false,
300328
custom_serialize: None,
301329
custom_deserialize: None,
330+
comment: None,
302331
}
303332
))
304333
);
@@ -318,6 +347,7 @@ fn parse_comment_used_as_key() {
318347
custom_json: false,
319348
custom_serialize: None,
320349
custom_deserialize: None,
350+
comment: None,
321351
}
322352
))
323353
);
@@ -337,6 +367,7 @@ fn parse_comment_newtype_and_name_inverse() {
337367
custom_json: false,
338368
custom_serialize: None,
339369
custom_deserialize: None,
370+
comment: None,
340371
}
341372
))
342373
);
@@ -356,6 +387,7 @@ fn parse_comment_name_noalias() {
356387
custom_json: false,
357388
custom_serialize: None,
358389
custom_deserialize: None,
390+
comment: None,
359391
}
360392
))
361393
);
@@ -375,6 +407,7 @@ fn parse_comment_newtype_and_custom_json() {
375407
custom_json: true,
376408
custom_serialize: None,
377409
custom_deserialize: None,
410+
comment: None,
378411
}
379412
))
380413
);
@@ -400,6 +433,7 @@ fn parse_comment_custom_serialize_deserialize() {
400433
custom_json: false,
401434
custom_serialize: Some("foo".to_string()),
402435
custom_deserialize: Some("bar".to_string()),
436+
comment: None,
403437
}
404438
))
405439
);
@@ -409,7 +443,7 @@ fn parse_comment_custom_serialize_deserialize() {
409443
#[test]
410444
fn parse_comment_all_except_no_alias() {
411445
assert_eq!(
412-
rule_metadata("@newtype @name baz @custom_serialize foo @custom_deserialize bar @used_as_key @custom_json"),
446+
rule_metadata("@newtype @name baz @custom_serialize foo @custom_deserialize bar @used_as_key @custom_json @doc this is a doc comment"),
413447
Ok((
414448
"",
415449
RuleMetadata {
@@ -420,6 +454,7 @@ fn parse_comment_all_except_no_alias() {
420454
custom_json: true,
421455
custom_serialize: Some("foo".to_string()),
422456
custom_deserialize: Some("bar".to_string()),
457+
comment: Some("this is a doc comment".to_string()),
423458
}
424459
))
425460
);

src/generation.rs

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5162,6 +5162,7 @@ fn codegen_struct(
51625162
}
51635163
wasm_new.vis("pub");
51645164
let mut wasm_new_args = Vec::new();
5165+
let mut wasm_new_comments = Vec::new();
51655166
for field in &record.fields {
51665167
// Fixed values don't need constructors or getters or fields in the rust code
51675168
if !field.rust_type.is_fixed_value() {
@@ -5249,6 +5250,9 @@ fn codegen_struct(
52495250
.from_wasm_boundary_clone(types, &field.name, false)
52505251
.into_iter(),
52515252
));
5253+
if let Some(comment) = &field.rule_metadata.comment {
5254+
wasm_new_comments.push(format!("* `{}` - {}", field.name, comment));
5255+
}
52525256
// do we want setters here later for mandatory types covered by new?
52535257
// getter
52545258
let mut getter = codegen::Function::new(&field.name);
@@ -5278,6 +5282,12 @@ fn codegen_struct(
52785282
wasm_new_args.join(", ")
52795283
));
52805284
}
5285+
if !wasm_new_comments.is_empty() {
5286+
wasm_new.doc(wasm_new_comments.join("\n"));
5287+
}
5288+
if let Some(doc) = config.doc.as_ref() {
5289+
wrapper.s.doc(doc);
5290+
}
52815291
wrapper.s_impl.push_fn(wasm_new);
52825292
wrapper.push(gen_scope, types);
52835293
}
@@ -5287,6 +5297,9 @@ fn codegen_struct(
52875297
// Struct (fields) + constructor
52885298
let (mut native_struct, mut native_impl) = create_base_rust_struct(types, name, false, cli);
52895299
native_struct.vis("pub");
5300+
if let Some(doc) = config.doc.as_ref() {
5301+
native_struct.doc(doc);
5302+
}
52905303
let mut native_new = codegen::Function::new("new");
52915304
let (ctor_ret, ctor_before) = if new_can_fail {
52925305
("Result<Self, DeserializeError>", "Ok(Self")
@@ -5298,6 +5311,7 @@ fn codegen_struct(
52985311
if new_can_fail {
52995312
native_new_block.after(")");
53005313
}
5314+
let mut native_new_comments = Vec::new();
53015315
// for clippy we generate a Default impl if new has no args
53025316
let mut new_arg_count = 0;
53035317
for field in &record.fields {
@@ -5313,37 +5327,35 @@ fn codegen_struct(
53135327
}
53145328
// Fixed values only exist in (de)serialization code (outside of preserve-encodings=true)
53155329
if !field.rust_type.is_fixed_value() {
5316-
if let Some(default_value) = &field.rust_type.config.default {
5317-
// field
5318-
native_struct.field(
5319-
&format!("pub {}", field.name),
5320-
field.rust_type.for_rust_member(types, false, cli),
5321-
);
5330+
let mut codegen_field = if let Some(default_value) = &field.rust_type.config.default {
53225331
// new
53235332
native_new_block.line(format!(
53245333
"{}: {},",
53255334
field.name,
53265335
default_value.to_primitive_str_assign()
53275336
));
5337+
// field
5338+
codegen::Field::new(
5339+
&format!("pub {}", field.name),
5340+
field.rust_type.for_rust_member(types, false, cli),
5341+
)
53285342
} else if field.optional {
5343+
// new
5344+
native_new_block.line(format!("{}: None,", field.name));
53295345
// field
5330-
native_struct.field(
5346+
codegen::Field::new(
53315347
&format!("pub {}", field.name),
53325348
format!(
53335349
"Option<{}>",
53345350
field.rust_type.for_rust_member(types, false, cli)
53355351
),
5336-
);
5337-
// new
5338-
native_new_block.line(format!("{}: None,", field.name));
5352+
)
53395353
} else {
5340-
// field
5341-
native_struct.field(
5342-
&format!("pub {}", field.name),
5343-
field.rust_type.for_rust_member(types, false, cli),
5344-
);
53455354
// new
53465355
native_new.arg(&field.name, field.rust_type.for_rust_move(types, cli));
5356+
if let Some(comment) = &field.rule_metadata.comment {
5357+
native_new_comments.push(format!("* `{}` - {}", field.name, comment));
5358+
}
53475359
new_arg_count += 1;
53485360
native_new_block.line(format!("{},", field.name));
53495361
if let Some(bounds) = field.rust_type.config.bounds.as_ref() {
@@ -5363,9 +5375,21 @@ fn codegen_struct(
53635375
}
53645376
}
53655377
}
5378+
// field
5379+
codegen::Field::new(
5380+
&format!("pub {}", field.name),
5381+
field.rust_type.for_rust_member(types, false, cli),
5382+
)
5383+
};
5384+
if let Some(comment) = &field.rule_metadata.comment {
5385+
codegen_field.doc(comment);
53665386
}
5387+
native_struct.push_field(codegen_field);
53675388
}
53685389
}
5390+
if !native_new_comments.is_empty() {
5391+
native_new.doc(native_new_comments.join("\n"));
5392+
}
53695393
let len_encoding_var = if cli.preserve_encodings {
53705394
let encoding_name = RustIdent::new(CDDLIdent::new(format!("{name}Encoding")));
53715395
native_struct.field(
@@ -6850,6 +6874,9 @@ fn generate_enum(
68506874
// rust enum containing the data
68516875
let mut e = codegen::Enum::new(name.to_string());
68526876
e.vis("pub");
6877+
if let Some(doc) = config.doc.as_ref() {
6878+
e.doc(doc);
6879+
}
68536880
let mut e_impl = codegen::Impl::new(name.to_string());
68546881
// instead of using create_serialize_impl() and having the length encoded there, we want to make it easier
68556882
// to offer definite length encoding even if we're mixing plain group members and non-plain group members (or mixed length plain ones)
@@ -6960,6 +6987,10 @@ fn generate_enum(
69606987
}
69616988
}
69626989
}
6990+
if let Some(doc) = &variant.doc {
6991+
// we must repurpose annotations since there is no doc support on enum variants
6992+
v.annotation(format!("/// {doc}"));
6993+
}
69636994
e.push_variant(v);
69646995
// new (particularly useful if we have encoding variables)
69656996
let mut new_func = codegen::Function::new(&format!("new_{variant_var_name}"));

0 commit comments

Comments
 (0)