Skip to main content

Comment DSL

We have a comment DSL to help annotate the output code beyond what is possible just with CDDL.

@name

For example in an array-encoded group you can give explicit names just by the keys e.g.:

foo = [
bar: uint,
baz: text
]

but with map-encoded structs the keys are stored and for things like integer keys this isn't very helpful e.g.:

tx = {
? 0: [* input],
? 1: [* outputs],
}

we would end up with two fields: key_0 and key_1. We can instead end up with fields named inputs and outputs by doing:

tx = {
? 0: [* input], ; @name inputs
? 1: [* outputs], ; @name outputs
}

Note: the parsing can be finicky. For struct fields you must put the comment AFTER the comma, and the comma must exist even for the last field in a struct.

It is also possible to use @name with type choices:

foo = 0 ; @name mainnet
/ 1 ; @name testnet

and also for group choices:

script = [
; @name native
tag: 0, script: native_script //
; @name plutus_v1
tag: 1, script: plutus_v1_script //
; @name plutus_v2
tag: 2, script: plutus_v2_script
]

@newtype

With code like foo = uint this creates an alias e.g. pub type Foo = u64; in rust. When we use foo = uint ; @newtype it instead creates a pub struct Foo(u64);.

@newtype can also optionally specify a getter function e.g. foo = uint ; @newtype custom_getter will generate:

impl Foo {
pub fn custom_getter(&self) -> u64 {
self.0
}
}

@no_alias

foo = uint
bar = [
field: foo
]

This would normally result in:

pub type Foo = u64;
pub struct Bar {
field: Foo,
}

but if we use @no_alias it skips generating an alias and uses it directly e.g.:

foo = uint ; @no_alias
bar = [
field: foo
]

to

pub struct Bar {
field: u64,
}

@used_as_key

foo = [
x: uint,
y: uint,
] ; @used_as_Key

cddl-codegen automatically derives Ord/PartialOrd or Hash for any types used within as a key in another type. Putting this comment on a type forces that type to derive those traits even if it weren't used in a key in the cddl spec. This is useful for when you are writing utility code that would put them in a map and want the generated code to have it already, which is particularly useful for re-generating as it lets your mod.rs files remain untouched.

@custom_json

foo = uint ; @newtype @custom_json

Avoids generating and/or deriving json-related traits under the assumption that the user will supply their own implementation to be used in the generated library.

@custom_serialize / @custom_deserialize

custom_bytes = bytes ; @custom_serialize custom_serialize_bytes @custom_deserialize custom_deserialize_bytes

struct_with_custom_serialization = [
custom_bytes,
field: bytes, ; @custom_serialize custom_serialize_bytes @custom_deserialize custom_deserialize_bytes
overridden: custom_bytes, ; @custom_serialize write_hex_string @custom_deserialize read_hex_string
tagged1: #6.9(custom_bytes),
tagged2: #6.9(uint), ; @custom_serialize write_tagged_uint_str @custom_deserialize read_tagged_uint_str
]

This allows the overriding of serialization and/or deserialization for when a specific format must be maintained. This works even with primitives where CDDL_CODEGEN_EXTERN_TYPE would require making a wrapper type to use.

The string after @custom_serialize/@custom_deserialize will be directly called as a function in place of regular serialization/deserialization code. As such it must either be specified using fully qualified paths e.g. @custom_serialize crate::utils::custom_serialize_function, or post-generation it will need to be imported into the serialization code by hand e.g. adding import crate::utils::custom_serialize_function;.

With --preserve-encodings=true the encoding variables must be passed in in the order they are used in cddl-codegen with regular serialization. They are passed in as Option<cbor_event::Sz> for integers/tags, LenEncoding for lengths and StringEncoding for text/bytes. These are the same types as are stored in the *Encoding structs generated. The same must be returned for deserialization. When there are no encoding variables the deserialized value should be directly returned, and if not a tuple with the value and its encoding variables should be returned.

There are two ways to use this comment DSL:

  • Type level: e.g. custom_bytes. This will replace the (de)serialization everywhere you use this type.
  • Field level: e.g. struct_with_custom_serialization.field. This will entirely replace the (de)serialization logic for the entire field, including other encoding operations like tags, .cbor, etc.

Example function signatures for --preserve-encodings=false for custom_serialize_bytes / custom_deserialize_bytes above:

pub fn custom_serialize_bytes<'se, W: std::io::Write>(
serializer: &'se mut cbor_event::se::Serializer<W>,
bytes: &[u8],
) -> cbor_event::Result<&'se mut cbor_event::se::Serializer<W>>

pub fn custom_deserialize_bytes<R: std::io::BufRead + std::io::Seek>(
raw: &mut cbor_event::de::Deserializer<R>,
) -> Result<Vec<u8>, DeserializeError>

Example function signatures for --preserve-encodings=true for write_tagged_uint_str / read_tagged_uint_str above:

pub fn write_tagged_uint_str<'se, W: std::io::Write>(
serializer: &'se mut cbor_event::se::Serializer<W>,
uint: &u64,
tag_encoding: Option<cbor_event::Sz>,
text_encoding: Option<cbor_event::Sz>,
) -> cbor_event::Result<&'se mut cbor_event::se::Serializer<W>>

pub fn read_tagged_uint_str<R: std::io::BufRead + std::io::Seek>(
raw: &mut cbor_event::de::Deserializer<R>,
) -> Result<(u64, Option<cbor_event::Sz>, Option<cbor_event::Sz>), DeserializeError>

Note that as this is at the field-level it must handle the tag as well as the uint.

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).

@doc

This can be placed at field-level, struct-level or variant-level to specify a comment to be placed as a rust doc-comment.

docs = [
foo: text, ; @doc this is a field-level comment
bar: uint, ; @doc bar is a u64
] ; @doc struct documentation here

docs_groupchoice = [
; @name first @doc comment-about-first
0, uint //
; @doc comments about second @name second
text
] ; @doc type-level comment

Will generate:

/// struct documentation here
#[derive(Clone, Debug)]
pub struct Docs {
/// this is a field-level comment
pub foo: String,
/// bar is a u64
pub bar: u64,
}

impl Docs {
/// * `foo` - this is a field-level comment
/// * `bar` - bar is a u64
pub fn new(foo: String, bar: u64) -> Self {
Self { foo, bar }
}
}

/// type-level comment
#[derive(Clone, Debug)]
pub enum DocsGroupchoice {
/// comment-about-first
First(u64),
/// comments about second
Second(String),
}

Due to the comment dsl parsing this doc comment cannot contain the character @.

CDDL_CODEGEN_EXTERN_TYPE

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

foo = _CDDL_CODEGEN_EXTERN_TYPE_
bar = [
x: uint,
y: foo,
]

This will treat Foo as a type that will exist and that has implemented the Serialize and Deserialize traits, so the (de)serialization logic in Bar here will call Foo::serialize() and Foo::deserialize(). This can also be useful when you have a spec that is either very awkward to use (so you hand-write or hand-modify after generation) in some type so you don't generate those types and instead manually merge those hand-written/hand-modified structs back in to the code afterwards. This saves you from having to manually remove all code that is generated regarding Foo first before merging in your own.

This can also be useful when you have a spec that is either very awkward to use (so you hand-write or hand-modify after generation) in some type so you don't generate those types and instead manually merge those hand-written/hand-modified structs back in to the code afterwards. This saves you from having to manually remove all code that is generated regarding Foo first before merging in your own.

This also works with generics e.g. you can refer to foo<T>. As with other generics this will create a pub type FooT = Foo<T>; definition in rust to work with wasm-bindgen's restrictions (no generics) as on the wasm side there will be references to a FooT in wasm. The wasm type definition is not emitted as that will be implementation-dependent. For an example see extern_generic in the core unit test.

CDDL_CODEGEN_RAW_BYTES_TYPE

Allows encoding as bytes but imposing hand-written constraints defined elsewhere.

foo = _CDDL_CODEGEN_RAW_BYTES_TYPE_
bar = [
foo,
]

This will treat foo as some external type called Foo. This type must implement the exported (in serialization.rs) trait RawBytesEncoding. This can be useful for example when working with cryptographic primitives e.g. a hash or pubkey, as it allows users to have those crypto structs be from a crypto library then they only need to implement the trait for them and they will be able to be directly used without needing any useless generated wrapper struct for the in between.