diff --git a/.changeset/flat-transforms.md b/.changeset/flat-transforms.md new file mode 100644 index 0000000..5da2dcc --- /dev/null +++ b/.changeset/flat-transforms.md @@ -0,0 +1,5 @@ +--- +'@codama/spec': major +--- + +Replace the nested type wrappers with a flat `transforms` array. The seven wrapper type nodes (`fixedSizeTypeNode`, `sizePrefixTypeNode`, `preOffsetTypeNode`, `postOffsetTypeNode`, `sentinelTypeNode`, `hiddenPrefixTypeNode`, `hiddenSuffixTypeNode`) and the `nestedTypeNode` recursive alias are removed. Instead, a new `transform` category defines one transform node per former wrapper (same attributes minus the inner type), and every member of the `typeNode` union — links included — carries an optional `transforms` array, applied in order with the first transform innermost. A type's `kind` is now stable whether or not it is modified, and attributes that pinned a wrapped inner kind (`accountNode.data`, enum variant payloads, numeric prefixes) become plain node references. diff --git a/docs/AccountNode.md b/docs/AccountNode.md index 4ff9ca7..230e298 100644 --- a/docs/AccountNode.md +++ b/docs/AccountNode.md @@ -17,12 +17,12 @@ An on-chain account: its name, data structure, optional fixed size, optional PDA ### Children -| Attribute | Type | Description | -| ---------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| `data` | [`NestedTypeNode`](./typeNodes/NestedTypeNode.md)<[`StructTypeNode`](./typeNodes/StructTypeNode.md)> | The struct describing the account data. It must be a struct so its fields can be referenced by other nodes — e.g. `accountFieldValueNode`. | -| `pda` | [`PdaLinkNode`](./linkNodes/PdaLinkNode.md) _(optional)_ | A link to the PDA the account is derived from, if applicable. | -| `discriminators` | [`DiscriminatorNode`](./discriminatorNodes/DiscriminatorNode.md)[] _(optional)_ | Discriminators that distinguish this account from others in the program. When multiple are listed, they are combined with a logical AND. | -| `plugins` | [`PluginNode`](./PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ---------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| `data` | [`StructTypeNode`](./typeNodes/StructTypeNode.md) | The struct describing the account data. It must be a struct so its fields can be referenced by other nodes — e.g. `accountFieldValueNode`. | +| `pda` | [`PdaLinkNode`](./linkNodes/PdaLinkNode.md) _(optional)_ | A link to the PDA the account is derived from, if applicable. | +| `discriminators` | [`DiscriminatorNode`](./discriminatorNodes/DiscriminatorNode.md)[] _(optional)_ | Discriminators that distinguish this account from others in the program. When multiple are listed, they are combined with a logical AND. | +| `plugins` | [`PluginNode`](./PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples diff --git a/docs/EventNode.md b/docs/EventNode.md index 2467093..e9514f7 100644 --- a/docs/EventNode.md +++ b/docs/EventNode.md @@ -39,12 +39,16 @@ eventNode({ ```typescript eventNode({ name: 'transferEvent', - data: hiddenPrefixTypeNode(structTypeNode([structFieldTypeNode({ name: 'amount', type: numberTypeNode('u64') })]), [ - constantValueNode(fixedSizeTypeNode(bytesTypeNode(), 8), bytesValueNode('base16', '0102030405060708')), - ]), + data: structTypeNode([structFieldTypeNode({ name: 'amount', type: numberTypeNode('u64') })], { + transforms: [ + hiddenPrefixTransformNode([ + constantValueNode(bytesTypeNode({ transforms: [fixedSizeTransformNode(8)] }), bytesValueNode('base16', '0102030405060708')), + ]), + ], + }), discriminators: [ constantDiscriminatorNode( - constantValueNode(fixedSizeTypeNode(bytesTypeNode(), 8), bytesValueNode('base16', '0102030405060708')), + constantValueNode(bytesTypeNode({ transforms: [fixedSizeTransformNode(8)] }), bytesValueNode('base16', '0102030405060708')), ), ], }); diff --git a/docs/README.md b/docs/README.md index b996ac4..966f7fe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,7 +4,7 @@ The canonical Codama node specification. Spec version: 1.9.2 · Other majors: [v1](https://github.com/codama-idl/spec/blob/1.x/v1/docs/README.md) -Pages marked _(abstract)_ document unions: sets of nodes that can be used interchangeably. Pages marked _(recursive)_ document nested unions: wrapper nodes that may nest before reaching a base type. +Pages marked _(abstract)_ document unions: sets of nodes that can be used interchangeably. ## Base attributes @@ -24,6 +24,7 @@ Codegen targets append them after each node's declared attributes, so they alway - [Link](./linkNodes/README.md) - Link nodes — references to other named entities (programs, PDAs, accounts, …). - [PdaSeed](./pdaSeedNodes/README.md) - PDA-seed nodes — the constants and variables a program uses to derive PDAs. - [Shared](./sharedNodes/README.md) - Shared enumerations referenced from multiple node categories. +- [Transform](./transformNodes/README.md) - Transform nodes — modifiers applied to the serialisation of the type node that carries them. - [Type](./typeNodes/README.md) - Type nodes — the building blocks of every value shape. - [Value](./valueNodes/README.md) - Value nodes — concrete values whose shape is described by a type node. diff --git a/docs/countNodes/PrefixedCountNode.md b/docs/countNodes/PrefixedCountNode.md index 5c65d39..d595eea 100644 --- a/docs/countNodes/PrefixedCountNode.md +++ b/docs/countNodes/PrefixedCountNode.md @@ -13,10 +13,10 @@ This enables nodes such as `arrayTypeNode` to represent collections whose length ### Children -| Attribute | Type | Description | -| --------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | -| `prefix` | [`NestedTypeNode`](../typeNodes/NestedTypeNode.md)<[`NumberTypeNode`](../typeNodes/NumberTypeNode.md)> | The numeric type used as the count prefix. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| --------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `prefix` | [`NumberTypeNode`](../typeNodes/NumberTypeNode.md) | The numeric type used as the count prefix. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples diff --git a/docs/discriminatorNodes/FieldDiscriminatorNode.md b/docs/discriminatorNodes/FieldDiscriminatorNode.md index 7b392fe..6319039 100644 --- a/docs/discriminatorNodes/FieldDiscriminatorNode.md +++ b/docs/discriminatorNodes/FieldDiscriminatorNode.md @@ -51,7 +51,7 @@ instructionNode({ arguments: [ instructionArgumentNode({ name: 'discriminator', - type: fixedSizeTypeNode(bytesTypeNode(), 8), + type: bytesTypeNode({ transforms: [fixedSizeTransformNode(8)] }), defaultValue: bytesValueNode('base16', '0011223344556677'), defaultValueStrategy: 'omitted', }), diff --git a/docs/linkNodes/DefinedTypeLinkNode.md b/docs/linkNodes/DefinedTypeLinkNode.md index d2510bf..aced7cd 100644 --- a/docs/linkNodes/DefinedTypeLinkNode.md +++ b/docs/linkNodes/DefinedTypeLinkNode.md @@ -13,10 +13,11 @@ A reference to a defined type — possibly in a different program. ### Children -| Attribute | Type | Description | -| --------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | -| `program` | [`ProgramLinkNode`](./ProgramLinkNode.md) _(optional)_ | The program the referenced type is defined in. When omitted, the surrounding program is assumed. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `program` | [`ProgramLinkNode`](./ProgramLinkNode.md) _(optional)_ | The program the referenced type is defined in. When omitted, the surrounding program is assumed. | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples diff --git a/docs/sharedNodes/PostOffsetStrategy.md b/docs/sharedNodes/PostOffsetStrategy.md index d8bab6d..60d28cf 100644 --- a/docs/sharedNodes/PostOffsetStrategy.md +++ b/docs/sharedNodes/PostOffsetStrategy.md @@ -1,11 +1,11 @@ # PostOffsetStrategy -How a post-offset modifier interprets its offset value after serialising the wrapped type. -See `postOffsetTypeNode` for an illustrated walkthrough of each strategy. +How a post-offset transform interprets its offset value after serialising the transformed type. +See `postOffsetTransformNode` for an illustrated walkthrough of each strategy. ## Variants - `absolute` - Move the cursor to the absolute byte position given by the offset; a negative offset counts backwards from the end of the buffer. - `padded` - Move the cursor like `relative` while growing the buffer by the offset amount; a negative offset moves the cursor backwards and shrinks the buffer. -- `preOffset` - Move the cursor by the offset bytes relative to the pre-offset — where the wrapped type started — rather than where it ended; a negative offset moves it to the left of that position. +- `preOffset` - Move the cursor by the offset bytes relative to the pre-offset — where the transformed type started — rather than where it ended; a negative offset moves it to the left of that position. - `relative` - Advance the cursor by the offset bytes relative to its current position; a negative offset moves it backwards. diff --git a/docs/sharedNodes/PreOffsetStrategy.md b/docs/sharedNodes/PreOffsetStrategy.md index 5f4edb6..212c38d 100644 --- a/docs/sharedNodes/PreOffsetStrategy.md +++ b/docs/sharedNodes/PreOffsetStrategy.md @@ -1,7 +1,7 @@ # PreOffsetStrategy -How a pre-offset modifier interprets its offset value before serialising the wrapped type. -See `preOffsetTypeNode` for an illustrated walkthrough of each strategy. +How a pre-offset transform interprets its offset value before serialising the transformed type. +See `preOffsetTransformNode` for an illustrated walkthrough of each strategy. ## Variants diff --git a/docs/sharedNodes/README.md b/docs/sharedNodes/README.md index 029c72e..ce0c1af 100644 --- a/docs/sharedNodes/README.md +++ b/docs/sharedNodes/README.md @@ -11,6 +11,6 @@ Shared enumerations referenced from multiple node categories. - [`InstructionLifecycle`](./InstructionLifecycle.md) - The lifecycle stage of an instruction. - [`NumberFormat`](./NumberFormat.md) - The wire format of a numeric serialization. - [`OptionalAccountStrategy`](./OptionalAccountStrategy.md) - How an absent optional account is represented when serialising an instruction. -- [`PostOffsetStrategy`](./PostOffsetStrategy.md) - How a post-offset modifier interprets its offset value after serialising the wrapped type. -- [`PreOffsetStrategy`](./PreOffsetStrategy.md) - How a pre-offset modifier interprets its offset value before serialising the wrapped type. +- [`PostOffsetStrategy`](./PostOffsetStrategy.md) - How a post-offset transform interprets its offset value after serialising the transformed type. +- [`PreOffsetStrategy`](./PreOffsetStrategy.md) - How a pre-offset transform interprets its offset value before serialising the transformed type. - [`ProgramOrigin`](./ProgramOrigin.md) - The toolchain that originally generated a program description. diff --git a/docs/transformNodes/FixedSizeTransformNode.md b/docs/transformNodes/FixedSizeTransformNode.md new file mode 100644 index 0000000..047ce1c --- /dev/null +++ b/docs/transformNodes/FixedSizeTransformNode.md @@ -0,0 +1,37 @@ +# FixedSizeTransformNode + +Asserts a fixed total byte size for the transformed type. Padding or truncation is applied as needed. + +## Attributes + +### Data + +| Attribute | Type | Description | +| --------- | -------------------------- | ----------------------------------------------------- | +| `kind` | `"fixedSizeTransformNode"` | The node discriminator. | +| `size` | `u64` | The total byte size the transformed type must occupy. | + +### Children + +| Attribute | Type | Description | +| --------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | + +## Examples + +### Fixed UTF-8 strings + +```typescript +stringTypeNode('utf8', { transforms: [fixedSizeTransformNode(10)] }); + +// Hello => 0x48656C6C6F0000000000 +``` + +### Fixed byte arrays + +```typescript +bytesTypeNode({ transforms: [fixedSizeTransformNode(4)] }); + +// [1, 2] => 0x01020000 +// [1, 2, 3, 4, 5] => 0x01020304 +``` diff --git a/docs/transformNodes/HiddenPrefixTransformNode.md b/docs/transformNodes/HiddenPrefixTransformNode.md new file mode 100644 index 0000000..35eaabe --- /dev/null +++ b/docs/transformNodes/HiddenPrefixTransformNode.md @@ -0,0 +1,44 @@ +# HiddenPrefixTransformNode + +Prefixes the transformed type with a list of constant values that are written and read but not surfaced as fields to consumers. +When decoding, the prefixed constants are consumed and checked against their expected values before being discarded. + +## Attributes + +### Data + +| Attribute | Type | Description | +| --------- | ----------------------------- | ----------------------- | +| `kind` | `"hiddenPrefixTransformNode"` | The node discriminator. | + +### Children + +| Attribute | Type | Description | +| --------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `prefix` | [`ConstantValueNode`](../valueNodes/ConstantValueNode.md)[] | The constant values written before the transformed type, in order. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | + +## Examples + +### A number prefixed with 0xFFFF + +```typescript +numberTypeNode('u32', { + transforms: [hiddenPrefixTransformNode([constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff'))])], +}); + +// 42 => 0xFFFF2A000000 +``` + +### A fixed UTF-8 string prefixed with "Hello" + +```typescript +stringTypeNode('utf8', { + transforms: [ + fixedSizeTransformNode(10), + hiddenPrefixTransformNode([constantValueNode(stringTypeNode('utf8'), stringValueNode('Hello'))]), + ], +}); + +// World => 0x48656C6C6F576F726C640000000000 +``` diff --git a/docs/transformNodes/HiddenSuffixTransformNode.md b/docs/transformNodes/HiddenSuffixTransformNode.md new file mode 100644 index 0000000..b82f597 --- /dev/null +++ b/docs/transformNodes/HiddenSuffixTransformNode.md @@ -0,0 +1,44 @@ +# HiddenSuffixTransformNode + +Suffixes the transformed type with a list of constant values that are written and read but not surfaced as fields to consumers. +When decoding, the suffixed constants are consumed and checked against their expected values before being discarded. + +## Attributes + +### Data + +| Attribute | Type | Description | +| --------- | ----------------------------- | ----------------------- | +| `kind` | `"hiddenSuffixTransformNode"` | The node discriminator. | + +### Children + +| Attribute | Type | Description | +| --------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `suffix` | [`ConstantValueNode`](../valueNodes/ConstantValueNode.md)[] | The constant values written after the transformed type, in order. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | + +## Examples + +### A number suffixed with 0xFFFF + +```typescript +numberTypeNode('u32', { + transforms: [hiddenSuffixTransformNode([constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff'))])], +}); + +// 42 => 0x2A000000FFFF +``` + +### A fixed UTF-8 string suffixed with "Hello" + +```typescript +stringTypeNode('utf8', { + transforms: [ + fixedSizeTransformNode(10), + hiddenSuffixTransformNode([constantValueNode(stringTypeNode('utf8'), stringValueNode('Hello'))]), + ], +}); + +// World => 0x576F726C64000000000048656c6c6F +``` diff --git a/docs/typeNodes/PostOffsetTypeNode.md b/docs/transformNodes/PostOffsetTransformNode.md similarity index 58% rename from docs/typeNodes/PostOffsetTypeNode.md rename to docs/transformNodes/PostOffsetTransformNode.md index 6ba4c15..6e430d7 100644 --- a/docs/typeNodes/PostOffsetTypeNode.md +++ b/docs/transformNodes/PostOffsetTransformNode.md @@ -1,10 +1,10 @@ -# PostOffsetTypeNode +# PostOffsetTransformNode -After serialising the wrapped type, advance the cursor by `offset` bytes interpreted via the chosen strategy. +After serialising the transformed type, advance the cursor by `offset` bytes interpreted via the chosen strategy. -Since the offset is applied _after_ the wrapped type runs, this node is useful to move the cursor around once the wrapped type has been processed. See `preOffsetTypeNode` for the opposite behaviour. +Since the offset is applied _after_ the transformed type runs, this transform is useful to move the cursor around once the transformed type has been processed. See `preOffsetTransformNode` for the opposite behaviour. -The strategies below are illustrated against the following buffer: the `99` byte represents the encoded value of the wrapped type and the `FF` byte represents the next bytes to be encoded after it, in order to show the _post_ cursor position. +The strategies below are illustrated against the following buffer: the `99` byte represents the encoded value of the transformed type and the `FF` byte represents the next bytes to be encoded after it, in order to show the _post_ cursor position. ``` 0x00000099FF000000; @@ -48,7 +48,7 @@ offset = -2 └-- Post-offset ``` -**`preOffset`** — the cursor is moved to the right of the pre-offset — i.e. where the wrapped type started — by the provided offset. A negative offset moves it to the left of the pre-offset instead. +**`preOffset`** — the cursor is moved to the right of the pre-offset — i.e. where the transformed type started — by the provided offset. A negative offset moves it to the left of the pre-offset instead. ``` offset = 2 @@ -63,25 +63,24 @@ offset = -2 ``` > [!IMPORTANT] -> Some type nodes affect the buffer that is available to us: depending on where we are in the type tree, we may not have access to the entire buffer. -> For instance, inside a `fixedSizeTypeNode`, the buffer is truncated or padded to match the provided fixed size once the wrapped content has been serialised — we are essentially "boxed" into a sub-buffer, and that sub-buffer is the one affected by the `absolute` strategy. -> The type nodes that create sub-buffers are: `fixedSizeTypeNode`, `sentinelTypeNode`, and `sizePrefixTypeNode`. +> Some transforms affect the buffer that is available to us: depending on where we are in the type tree, we may not have access to the entire buffer. +> For instance, under a `fixedSizeTransformNode`, the buffer is truncated or padded to match the provided fixed size once the transformed content has been serialised — we are essentially "boxed" into a sub-buffer, and that sub-buffer is the one affected by the `absolute` strategy. +> The transforms that create sub-buffers are: `fixedSizeTransformNode`, `sentinelTransformNode`, and `sizePrefixTransformNode`. ## Attributes ### Data -| Attribute | Type | Description | -| --------- | ---------------------- | ------------------------------------------------------------ | -| `kind` | `"postOffsetTypeNode"` | The node discriminator. | -| `offset` | `i64` | The signed byte offset to apply after the wrapped type runs. | +| Attribute | Type | Description | +| --------- | --------------------------- | ---------------------------------------------------------------- | +| `kind` | `"postOffsetTransformNode"` | The node discriminator. | +| `offset` | `i64` | The signed byte offset to apply after the transformed type runs. | ### Children | Attribute | Type | Description | | ---------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | | `strategy` | [`PostOffsetStrategy`](../sharedNodes/PostOffsetStrategy.md) | How the `offset` value is interpreted. | -| `type` | [`TypeNode`](./TypeNode.md) | The wrapped type whose serialisation is followed by the offset. | | `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples @@ -89,19 +88,19 @@ offset = -2 ### A relative post-offset (the default strategy) ```typescript -postOffsetTypeNode(numberTypeNode('u32'), 2); +numberTypeNode('u32', { transforms: [postOffsetTransformNode(2)] }); ``` ### An absolute post-offset from the end of the buffer ```typescript -postOffsetTypeNode(numberTypeNode('u32'), -2, 'absolute'); +numberTypeNode('u32', { transforms: [postOffsetTransformNode(-2, 'absolute')] }); ``` ### A right-padded u32 number ```typescript -postOffsetTypeNode(numberTypeNode('u32'), 4, 'padded'); +numberTypeNode('u32', { transforms: [postOffsetTransformNode(4, 'padded')] }); // 42 => 0x2A00000000000000 ``` @@ -109,7 +108,7 @@ postOffsetTypeNode(numberTypeNode('u32'), 4, 'padded'); ### A u32 number overwritten by a u16 number ```typescript -tupleTypeNode([postOffsetTypeNode(numberTypeNode('u32'), -2), numberTypeNode('u16')]); +tupleTypeNode([numberTypeNode('u32', { transforms: [postOffsetTransformNode(-2)] }), numberTypeNode('u16')]); // [1, 2] => 0x01000200 // [0xFFFFFFFF, 42] => 0xFFFF2A00 diff --git a/docs/typeNodes/PreOffsetTypeNode.md b/docs/transformNodes/PreOffsetTransformNode.md similarity index 58% rename from docs/typeNodes/PreOffsetTypeNode.md rename to docs/transformNodes/PreOffsetTransformNode.md index 24f95f7..ac86813 100644 --- a/docs/typeNodes/PreOffsetTypeNode.md +++ b/docs/transformNodes/PreOffsetTransformNode.md @@ -1,10 +1,10 @@ -# PreOffsetTypeNode +# PreOffsetTransformNode -Before serialising the wrapped type, advance the cursor by `offset` bytes interpreted via the chosen strategy. +Before serialising the transformed type, advance the cursor by `offset` bytes interpreted via the chosen strategy. -Since the offset is applied _before_ the wrapped type runs, this node is useful to move the encoded value of the wrapped type itself. See `postOffsetTypeNode` for the opposite behaviour. +Since the offset is applied _before_ the transformed type runs, this transform is useful to move the encoded value of the transformed type itself. See `postOffsetTransformNode` for the opposite behaviour. -The strategies below are illustrated against the following buffer: the `99` byte represents some previously encoded value for reference and the `FF` byte represents the encoded value of the wrapped type, which moves as its pre-offset changes. +The strategies below are illustrated against the following buffer: the `99` byte represents some previously encoded value for reference and the `FF` byte represents the encoded value of the transformed type, which moves as its pre-offset changes. ``` 0x00000099FF000000; @@ -48,25 +48,24 @@ offset = -2 ``` > [!IMPORTANT] -> Some type nodes affect the buffer that is available to us: depending on where we are in the type tree, we may not have access to the entire buffer. -> For instance, inside a `fixedSizeTypeNode`, the buffer is truncated or padded to match the provided fixed size once the wrapped content has been serialised — we are essentially "boxed" into a sub-buffer, and that sub-buffer is the one affected by the `absolute` strategy. -> The type nodes that create sub-buffers are: `fixedSizeTypeNode`, `sentinelTypeNode`, and `sizePrefixTypeNode`. +> Some transforms affect the buffer that is available to us: depending on where we are in the type tree, we may not have access to the entire buffer. +> For instance, under a `fixedSizeTransformNode`, the buffer is truncated or padded to match the provided fixed size once the transformed content has been serialised — we are essentially "boxed" into a sub-buffer, and that sub-buffer is the one affected by the `absolute` strategy. +> The transforms that create sub-buffers are: `fixedSizeTransformNode`, `sentinelTransformNode`, and `sizePrefixTransformNode`. ## Attributes ### Data -| Attribute | Type | Description | -| --------- | --------------------- | ------------------------------------------------------------- | -| `kind` | `"preOffsetTypeNode"` | The node discriminator. | -| `offset` | `i64` | The signed byte offset to apply before the wrapped type runs. | +| Attribute | Type | Description | +| --------- | -------------------------- | ----------------------------------------------------------------- | +| `kind` | `"preOffsetTransformNode"` | The node discriminator. | +| `offset` | `i64` | The signed byte offset to apply before the transformed type runs. | ### Children | Attribute | Type | Description | | ---------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `strategy` | [`PreOffsetStrategy`](../sharedNodes/PreOffsetStrategy.md) | How the `offset` value is interpreted. | -| `type` | [`TypeNode`](./TypeNode.md) | The wrapped type whose serialisation is preceded by the offset. | | `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples @@ -74,19 +73,19 @@ offset = -2 ### A relative pre-offset (the default strategy) ```typescript -preOffsetTypeNode(numberTypeNode('u32'), 2); +numberTypeNode('u32', { transforms: [preOffsetTransformNode(2)] }); ``` ### An absolute pre-offset ```typescript -preOffsetTypeNode(numberTypeNode('u32'), -2, 'absolute'); +numberTypeNode('u32', { transforms: [preOffsetTransformNode(-2, 'absolute')] }); ``` ### A left-padded u32 number ```typescript -preOffsetTypeNode(numberTypeNode('u32'), 4, 'padded'); +numberTypeNode('u32', { transforms: [preOffsetTransformNode(4, 'padded')] }); // 42 => 0x000000002A000000 ``` @@ -94,7 +93,7 @@ preOffsetTypeNode(numberTypeNode('u32'), 4, 'padded'); ### A u32 number overwritten by a u16 number ```typescript -tupleTypeNode([numberTypeNode('u32'), preOffsetTypeNode(numberTypeNode('u16'), -2)]); +tupleTypeNode([numberTypeNode('u32'), numberTypeNode('u16', { transforms: [preOffsetTransformNode(-2)] })]); // [1, 2] => 0x01000200 // [0xFFFFFFFF, 42] => 0xFFFF2A00 diff --git a/docs/transformNodes/README.md b/docs/transformNodes/README.md new file mode 100644 index 0000000..88009ae --- /dev/null +++ b/docs/transformNodes/README.md @@ -0,0 +1,18 @@ +# Transform + +Transform nodes — modifiers applied to the serialisation of the type node that carries them. +Every type node has an optional `transforms` array; transforms apply in array order, the first being the innermost. + +## Nodes + +- [`FixedSizeTransformNode`](./FixedSizeTransformNode.md) - Asserts a fixed total byte size for the transformed type. Padding or truncation is applied as needed. +- [`HiddenPrefixTransformNode`](./HiddenPrefixTransformNode.md) - Prefixes the transformed type with a list of constant values that are written and read but not surfaced as fields to consumers. +- [`HiddenSuffixTransformNode`](./HiddenSuffixTransformNode.md) - Suffixes the transformed type with a list of constant values that are written and read but not surfaced as fields to consumers. +- [`PostOffsetTransformNode`](./PostOffsetTransformNode.md) - After serialising the transformed type, advance the cursor by `offset` bytes interpreted via the chosen strategy. +- [`PreOffsetTransformNode`](./PreOffsetTransformNode.md) - Before serialising the transformed type, advance the cursor by `offset` bytes interpreted via the chosen strategy. +- [`SentinelTransformNode`](./SentinelTransformNode.md) - Delimits the transformed type with a constant sentinel value written immediately after it. +- [`SizePrefixTransformNode`](./SizePrefixTransformNode.md) - Precedes the transformed type with a numeric prefix indicating its byte length. + +## Unions + +- [`TransformNode`](./TransformNode.md) - A modifier applied to the serialisation of the type node that carries it. diff --git a/docs/transformNodes/SentinelTransformNode.md b/docs/transformNodes/SentinelTransformNode.md new file mode 100644 index 0000000..aa780ae --- /dev/null +++ b/docs/transformNodes/SentinelTransformNode.md @@ -0,0 +1,35 @@ +# SentinelTransformNode + +Delimits the transformed type with a constant sentinel value written immediately after it. + +When decoding, the transformed type is decoded until the sentinel value is encountered, at which point decoding stops and the sentinel is discarded. + +> [!IMPORTANT] +> For this transform to work, the sentinel value must never occur within the encoded bytes of the transformed type. + +## Attributes + +### Data + +| Attribute | Type | Description | +| --------- | ------------------------- | ----------------------- | +| `kind` | `"sentinelTransformNode"` | The node discriminator. | + +### Children + +| Attribute | Type | Description | +| ---------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `sentinel` | [`ConstantValueNode`](../valueNodes/ConstantValueNode.md) | The constant value written immediately after the transformed type to mark its end. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | + +## Examples + +### A UTF-8 string terminated by 0xFF + +```typescript +stringTypeNode('utf8', { + transforms: [sentinelTransformNode(constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ff')))], +}); + +// Hello => 0x48656C6C6FFF +``` diff --git a/docs/transformNodes/SizePrefixTransformNode.md b/docs/transformNodes/SizePrefixTransformNode.md new file mode 100644 index 0000000..873d22b --- /dev/null +++ b/docs/transformNodes/SizePrefixTransformNode.md @@ -0,0 +1,30 @@ +# SizePrefixTransformNode + +Precedes the transformed type with a numeric prefix indicating its byte length. +When decoding, the size is read first and determines how many bytes the transformed type may consume. + +## Attributes + +### Data + +| Attribute | Type | Description | +| --------- | --------------------------- | ----------------------- | +| `kind` | `"sizePrefixTransformNode"` | The node discriminator. | + +### Children + +| Attribute | Type | Description | +| --------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `prefix` | [`NumberTypeNode`](../typeNodes/NumberTypeNode.md) | The numeric type used as the size prefix. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | + +## Examples + +### A UTF-8 string prefixed with a u16 size + +```typescript +stringTypeNode('utf8', { transforms: [sizePrefixTransformNode(numberTypeNode('u16'))] }); + +// "" => 0x0000 +// "Hello" => 0x050048656C6C6F +``` diff --git a/docs/transformNodes/TransformNode.md b/docs/transformNodes/TransformNode.md new file mode 100644 index 0000000..713ebc4 --- /dev/null +++ b/docs/transformNodes/TransformNode.md @@ -0,0 +1,14 @@ +# TransformNode (abstract) + +A modifier applied to the serialisation of the type node that carries it. +Every type node has an optional `transforms` array. Transforms apply in array order, the first being the innermost: a `stringTypeNode` with `transforms: [sentinel, fixedSize]` first delimits the string with the sentinel, then fixes the total byte size — exactly the v1 nesting `fixedSizeTypeNode(sentinelTypeNode(stringTypeNode))` read inside-out. + +One of the following: + +- [`FixedSizeTransformNode`](./FixedSizeTransformNode.md) +- [`HiddenPrefixTransformNode`](./HiddenPrefixTransformNode.md) +- [`HiddenSuffixTransformNode`](./HiddenSuffixTransformNode.md) +- [`PostOffsetTransformNode`](./PostOffsetTransformNode.md) +- [`PreOffsetTransformNode`](./PreOffsetTransformNode.md) +- [`SentinelTransformNode`](./SentinelTransformNode.md) +- [`SizePrefixTransformNode`](./SizePrefixTransformNode.md) diff --git a/docs/typeNodes/AmountTypeNode.md b/docs/typeNodes/AmountTypeNode.md index 2bcdecb..723d27b 100644 --- a/docs/typeNodes/AmountTypeNode.md +++ b/docs/typeNodes/AmountTypeNode.md @@ -7,18 +7,19 @@ Particularly useful for representing financial values as integers, since floatin ### Data -| Attribute | Type | Description | -| ---------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| `kind` | `"amountTypeNode"` | The node discriminator. | -| `decimals` | `u32` | The number of decimal places the wrapped integer carries. For example, an integer value of 12345 with 2 decimal places represents 123.45. | -| `unit` | `string` _(optional)_ | The unit of the amount — e.g. "USD" or "%". | +| Attribute | Type | Description | +| ---------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| `kind` | `"amountTypeNode"` | The node discriminator. | +| `decimals` | `u32` | The number of decimal places the inner integer carries. For example, an integer value of 12345 with 2 decimal places represents 123.45. | +| `unit` | `string` _(optional)_ | The unit of the amount — e.g. "USD" or "%". | ### Children -| Attribute | Type | Description | -| --------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `number` | [`NestedTypeNode`](./NestedTypeNode.md)<[`NumberTypeNode`](./NumberTypeNode.md)> | The number type the amount wraps. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `number` | [`NumberTypeNode`](./NumberTypeNode.md) | The number type the amount wraps. | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples diff --git a/docs/typeNodes/ArrayTypeNode.md b/docs/typeNodes/ArrayTypeNode.md index 84d070d..e76e31c 100644 --- a/docs/typeNodes/ArrayTypeNode.md +++ b/docs/typeNodes/ArrayTypeNode.md @@ -12,11 +12,12 @@ A homogeneous list of items. The item type is defined by `item`; the length is d ### Children -| Attribute | Type | Description | -| --------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `item` | [`TypeNode`](./TypeNode.md) | The type of each item in the array. | -| `count` | [`CountNode`](../countNodes/CountNode.md) | The strategy used to determine the number of items. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `item` | [`TypeNode`](./TypeNode.md) | The type of each item in the array. | +| `count` | [`CountNode`](../countNodes/CountNode.md) | The strategy used to determine the number of items. | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples diff --git a/docs/typeNodes/BooleanTypeNode.md b/docs/typeNodes/BooleanTypeNode.md index 3647709..0969636 100644 --- a/docs/typeNodes/BooleanTypeNode.md +++ b/docs/typeNodes/BooleanTypeNode.md @@ -1,6 +1,6 @@ # BooleanTypeNode -A boolean serialised as a numeric value. The wrapped number type determines the byte width. +A boolean serialised as a numeric value. The inner number type determines the byte width. A decoded number of `1` yields `true`; any other value yields `false`. ## Attributes @@ -13,10 +13,11 @@ A decoded number of `1` yields `true`; any other value yields `false`. ### Children -| Attribute | Type | Description | -| --------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `size` | [`NestedTypeNode`](./NestedTypeNode.md)<[`NumberTypeNode`](./NumberTypeNode.md)> | The numeric type used to serialise the boolean. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `size` | [`NumberTypeNode`](./NumberTypeNode.md) | The numeric type used to serialise the boolean. | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples diff --git a/docs/typeNodes/BytesTypeNode.md b/docs/typeNodes/BytesTypeNode.md index cda1720..b94beca 100644 --- a/docs/typeNodes/BytesTypeNode.md +++ b/docs/typeNodes/BytesTypeNode.md @@ -1,6 +1,6 @@ # BytesTypeNode -A raw sequence of bytes. Typically used inside a fixed-size, size-prefixed, or sentinel-terminated wrapper. +A raw sequence of bytes. Typically carries a fixed-size, size-prefix, or sentinel transform to bound its extent. ## Attributes @@ -12,9 +12,10 @@ A raw sequence of bytes. Typically used inside a fixed-size, size-prefixed, or s ### Children -| Attribute | Type | Description | -| --------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples diff --git a/docs/typeNodes/DateTimeTypeNode.md b/docs/typeNodes/DateTimeTypeNode.md index c78aa03..058bc3d 100644 --- a/docs/typeNodes/DateTimeTypeNode.md +++ b/docs/typeNodes/DateTimeTypeNode.md @@ -1,6 +1,6 @@ # DateTimeTypeNode -A timestamp encoded as a number, typically seconds since the Unix epoch. The wrapped number type determines the byte width. +A timestamp encoded as a number, typically seconds since the Unix epoch. The inner number type determines the byte width. ## Attributes @@ -12,10 +12,11 @@ A timestamp encoded as a number, typically seconds since the Unix epoch. The wra ### Children -| Attribute | Type | Description | -| --------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `number` | [`NestedTypeNode`](./NestedTypeNode.md)<[`NumberTypeNode`](./NumberTypeNode.md)> | The numeric type used to serialise the timestamp. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `number` | [`NumberTypeNode`](./NumberTypeNode.md) | The numeric type used to serialise the timestamp. | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples diff --git a/docs/typeNodes/EnumStructVariantTypeNode.md b/docs/typeNodes/EnumStructVariantTypeNode.md index 8e86aea..3566315 100644 --- a/docs/typeNodes/EnumStructVariantTypeNode.md +++ b/docs/typeNodes/EnumStructVariantTypeNode.md @@ -16,7 +16,7 @@ A variant of an enum that carries a struct payload (named fields). | Attribute | Type | Description | | --------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `struct` | [`NestedTypeNode`](./NestedTypeNode.md)<[`StructTypeNode`](./StructTypeNode.md)> | The struct of named fields carried by the variant. | +| `struct` | [`StructTypeNode`](./StructTypeNode.md) | The struct of named fields carried by the variant. | | `display` | [`EnumVariantDisplayNode`](../displayNodes/EnumVariantDisplayNode.md) _(optional)_ | Display metadata describing how the variant is presented. | | `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | diff --git a/docs/typeNodes/EnumTupleVariantTypeNode.md b/docs/typeNodes/EnumTupleVariantTypeNode.md index e55ce0f..efbc093 100644 --- a/docs/typeNodes/EnumTupleVariantTypeNode.md +++ b/docs/typeNodes/EnumTupleVariantTypeNode.md @@ -16,7 +16,7 @@ A variant of an enum that carries a tuple payload (positional fields). | Attribute | Type | Description | | --------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `tuple` | [`NestedTypeNode`](./NestedTypeNode.md)<[`TupleTypeNode`](./TupleTypeNode.md)> | The tuple of positional fields carried by the variant. | +| `tuple` | [`TupleTypeNode`](./TupleTypeNode.md) | The tuple of positional fields carried by the variant. | | `display` | [`EnumVariantDisplayNode`](../displayNodes/EnumVariantDisplayNode.md) _(optional)_ | Display metadata describing how the variant is presented. | | `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | diff --git a/docs/typeNodes/EnumTypeNode.md b/docs/typeNodes/EnumTypeNode.md index 944b771..c6b00ac 100644 --- a/docs/typeNodes/EnumTypeNode.md +++ b/docs/typeNodes/EnumTypeNode.md @@ -12,11 +12,12 @@ A tagged union: a numeric discriminator followed by one of several variant paylo ### Children -| Attribute | Type | Description | -| ---------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `variants` | [`EnumVariantTypeNode`](./EnumVariantTypeNode.md)[] | The variants of the enum, in declaration order. | -| `size` | [`NestedTypeNode`](./NestedTypeNode.md)<[`NumberTypeNode`](./NumberTypeNode.md)> | The numeric type used to serialise the discriminator. The discriminator prepends the serialised variant payload to identify which variant was selected. By default it is the index of the variant (starting at 0), unless the variant provides its own custom discriminator value. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `variants` | [`EnumVariantTypeNode`](./EnumVariantTypeNode.md)[] | The variants of the enum, in declaration order. | +| `size` | [`NumberTypeNode`](./NumberTypeNode.md) | The numeric type used to serialise the discriminator. The discriminator prepends the serialised variant payload to identify which variant was selected. By default it is the index of the variant (starting at 0), unless the variant provides its own custom discriminator value. | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples diff --git a/docs/typeNodes/FixedSizeTypeNode.md b/docs/typeNodes/FixedSizeTypeNode.md deleted file mode 100644 index 91dc2cd..0000000 --- a/docs/typeNodes/FixedSizeTypeNode.md +++ /dev/null @@ -1,44 +0,0 @@ -# FixedSizeTypeNode - -Wraps another type and asserts a fixed total byte size. Padding or truncation is applied as needed. - -## Attributes - -### Data - -| Attribute | Type | Description | -| --------- | --------------------- | ------------------------------------------------- | -| `kind` | `"fixedSizeTypeNode"` | The node discriminator. | -| `size` | `u64` | The total byte size the wrapped type must occupy. | - -### Children - -| Attribute | Type | Description | -| --------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `type` | [`TypeNode`](./TypeNode.md) | The wrapped type whose serialisation is constrained. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | - -## Examples - -### Create a fixed size type node from a type node and a byte length - -```typescript -const node = fixedSizeTypeNode(stringTypeNode('utf8'), 32); -``` - -### Fixed UTF-8 strings - -```typescript -fixedSizeTypeNode(stringTypeNode('utf8'), 10); - -// Hello => 0x48656C6C6F0000000000 -``` - -### Fixed byte arrays - -```typescript -fixedSizeTypeNode(bytesTypeNode(), 4); - -// [1, 2] => 0x01020000 -// [1, 2, 3, 4, 5] => 0x01020304 -``` diff --git a/docs/typeNodes/HiddenPrefixTypeNode.md b/docs/typeNodes/HiddenPrefixTypeNode.md deleted file mode 100644 index 29cd45b..0000000 --- a/docs/typeNodes/HiddenPrefixTypeNode.md +++ /dev/null @@ -1,48 +0,0 @@ -# HiddenPrefixTypeNode - -Prefixes another type with a list of constant values that are written and read but not surfaced as fields to consumers. -When decoding, the prefixed constants are consumed and checked against their expected values before being discarded. - -## Attributes - -### Data - -| Attribute | Type | Description | -| --------- | ------------------------ | ----------------------- | -| `kind` | `"hiddenPrefixTypeNode"` | The node discriminator. | - -### Children - -| Attribute | Type | Description | -| --------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `type` | [`TypeNode`](./TypeNode.md) | The wrapped type whose serialisation is preceded by the hidden prefix. | -| `prefix` | [`ConstantValueNode`](../valueNodes/ConstantValueNode.md)[] | The constant values written before the wrapped type, in order. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | - -## Examples - -### Create a hidden prefix type node from a type node and constant value nodes - -```typescript -const node = hiddenPrefixTypeNode(numberTypeNode('u32'), [ - constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff')), -]); -``` - -### A number prefixed with 0xFFFF - -```typescript -hiddenPrefixTypeNode(numberTypeNode('u32'), [constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff'))]); - -// 42 => 0xFFFF2A000000 -``` - -### A fixed UTF-8 string prefixed with "Hello" - -```typescript -hiddenPrefixTypeNode(fixedSizeTypeNode(stringTypeNode('utf8'), 10), [ - constantValueNode(stringTypeNode('utf8'), stringValueNode('Hello')), -]); - -// World => 0x48656C6C6F576F726C640000000000 -``` diff --git a/docs/typeNodes/HiddenSuffixTypeNode.md b/docs/typeNodes/HiddenSuffixTypeNode.md deleted file mode 100644 index f81b16f..0000000 --- a/docs/typeNodes/HiddenSuffixTypeNode.md +++ /dev/null @@ -1,48 +0,0 @@ -# HiddenSuffixTypeNode - -Suffixes another type with a list of constant values that are written and read but not surfaced as fields to consumers. -When decoding, the suffixed constants are consumed and checked against their expected values before being discarded. - -## Attributes - -### Data - -| Attribute | Type | Description | -| --------- | ------------------------ | ----------------------- | -| `kind` | `"hiddenSuffixTypeNode"` | The node discriminator. | - -### Children - -| Attribute | Type | Description | -| --------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `type` | [`TypeNode`](./TypeNode.md) | The wrapped type whose serialisation is followed by the hidden suffix. | -| `suffix` | [`ConstantValueNode`](../valueNodes/ConstantValueNode.md)[] | The constant values written after the wrapped type, in order. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | - -## Examples - -### Create a hidden suffix type node from a type node and constant value nodes - -```typescript -const node = hiddenSuffixTypeNode(numberTypeNode('u32'), [ - constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff')), -]); -``` - -### A number suffixed with 0xFFFF - -```typescript -hiddenSuffixTypeNode(numberTypeNode('u32'), [constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff'))]); - -// 42 => 0x2A000000FFFF -``` - -### A fixed UTF-8 string suffixed with "Hello" - -```typescript -hiddenSuffixTypeNode(fixedSizeTypeNode(stringTypeNode('utf8'), 10), [ - constantValueNode(stringTypeNode('utf8'), stringValueNode('Hello')), -]); - -// World => 0x576F726C64000000000048656c6c6F -``` diff --git a/docs/typeNodes/MapTypeNode.md b/docs/typeNodes/MapTypeNode.md index 00ef5eb..3801c54 100644 --- a/docs/typeNodes/MapTypeNode.md +++ b/docs/typeNodes/MapTypeNode.md @@ -14,12 +14,13 @@ Entries are serialised one after the other, each key immediately followed by its ### Children -| Attribute | Type | Description | -| --------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `key` | [`TypeNode`](./TypeNode.md) | The type of each entry key. | -| `value` | [`TypeNode`](./TypeNode.md) | The type of each entry value. | -| `count` | [`CountNode`](../countNodes/CountNode.md) | The strategy used to determine the number of entries. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `key` | [`TypeNode`](./TypeNode.md) | The type of each entry key. | +| `value` | [`TypeNode`](./TypeNode.md) | The type of each entry value. | +| `count` | [`CountNode`](../countNodes/CountNode.md) | The strategy used to determine the number of entries. | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples @@ -33,7 +34,7 @@ const node = mapTypeNode(publicKeyTypeNode(), numberTypeNode('u32'), prefixedCou ```typescript mapTypeNode( - fixedSizeTypeNode(stringTypeNode('utf8'), 1), // Key: Single UTF-8 character. + stringTypeNode('utf8', { transforms: [fixedSizeTransformNode(1)] }), // Key: Single UTF-8 character. numberTypeNode('u16'), // Value: 16-bit unsigned integer. prefixedCountNode(numberTypeNode('u8')), // Count: map length is prefixed with a u8. ); diff --git a/docs/typeNodes/NestedTypeNode.md b/docs/typeNodes/NestedTypeNode.md deleted file mode 100644 index a69ea42..0000000 --- a/docs/typeNodes/NestedTypeNode.md +++ /dev/null @@ -1,17 +0,0 @@ -# NestedTypeNode (recursive) - -A type, possibly wrapped in zero-or-more size, offset, sentinel, or hidden prefix/suffix modifiers. -The wrapping is recursive: each modifier wraps another `nestedTypeNode` until the inner `T` is reached. -For example, a `nestedTypeNode` can be fulfilled by a plain `stringTypeNode`, by a `fixedSizeTypeNode` wrapping a `stringTypeNode`, or by any deeper nesting such as `hiddenPrefixTypeNode>>`. - -Base: [`TypeNode`](./TypeNode.md) - -## Wrappers - -- [`FixedSizeTypeNode`](./FixedSizeTypeNode.md) -- [`SizePrefixTypeNode`](./SizePrefixTypeNode.md) -- [`PreOffsetTypeNode`](./PreOffsetTypeNode.md) -- [`PostOffsetTypeNode`](./PostOffsetTypeNode.md) -- [`SentinelTypeNode`](./SentinelTypeNode.md) -- [`HiddenPrefixTypeNode`](./HiddenPrefixTypeNode.md) -- [`HiddenSuffixTypeNode`](./HiddenSuffixTypeNode.md) diff --git a/docs/typeNodes/NumberTypeNode.md b/docs/typeNodes/NumberTypeNode.md index 173c5d2..90cda49 100644 --- a/docs/typeNodes/NumberTypeNode.md +++ b/docs/typeNodes/NumberTypeNode.md @@ -12,12 +12,13 @@ A numeric type with a fixed wire format and byte order. ### Children -| Attribute | Type | Description | -| --------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | -| `format` | [`NumberFormat`](../sharedNodes/NumberFormat.md) | The wire format used to serialise the number. | -| `endian` | [`Endianness`](../sharedNodes/Endianness.md) | The byte order used to serialise the number. | -| `display` | [`NumberDisplayNode`](../displayNodes/NumberDisplayNode.md) _(optional)_ | Display metadata describing how the number is presented. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | +| `format` | [`NumberFormat`](../sharedNodes/NumberFormat.md) | The wire format used to serialise the number. | +| `endian` | [`Endianness`](../sharedNodes/Endianness.md) | The byte order used to serialise the number. | +| `display` | [`NumberDisplayNode`](../displayNodes/NumberDisplayNode.md) _(optional)_ | Display metadata describing how the number is presented. | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples diff --git a/docs/typeNodes/OptionTypeNode.md b/docs/typeNodes/OptionTypeNode.md index 59e4f1e..d565246 100644 --- a/docs/typeNodes/OptionTypeNode.md +++ b/docs/typeNodes/OptionTypeNode.md @@ -13,11 +13,12 @@ A value that may be present or absent (Some/None), with an explicit numeric pref ### Children -| Attribute | Type | Description | -| --------- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `item` | [`TypeNode`](./TypeNode.md) | The type carried by the option when present. | -| `prefix` | [`NestedTypeNode`](./NestedTypeNode.md)<[`NumberTypeNode`](./NumberTypeNode.md)> | The numeric type used as the presence flag. A prefix value of `1` means the item is present and follows the prefix; a value of `0` means the item is absent and nothing further is serialised. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `item` | [`TypeNode`](./TypeNode.md) | The type carried by the option when present. | +| `prefix` | [`NumberTypeNode`](./NumberTypeNode.md) | The numeric type used as the presence flag. A prefix value of `1` means the item is present and follows the prefix; a value of `0` means the item is absent and nothing further is serialised. | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples diff --git a/docs/typeNodes/PublicKeyTypeNode.md b/docs/typeNodes/PublicKeyTypeNode.md index 45223ec..6c7a97f 100644 --- a/docs/typeNodes/PublicKeyTypeNode.md +++ b/docs/typeNodes/PublicKeyTypeNode.md @@ -12,9 +12,10 @@ A 32-byte Solana public key. ### Children -| Attribute | Type | Description | -| --------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples diff --git a/docs/typeNodes/README.md b/docs/typeNodes/README.md index f843b75..1e1fe92 100644 --- a/docs/typeNodes/README.md +++ b/docs/typeNodes/README.md @@ -6,27 +6,20 @@ Type nodes — the building blocks of every value shape. - [`AmountTypeNode`](./AmountTypeNode.md) - Wraps a number type to provide additional context such as decimal places and a unit. - [`ArrayTypeNode`](./ArrayTypeNode.md) - A homogeneous list of items. The item type is defined by `item`; the length is determined by the `count` strategy. -- [`BooleanTypeNode`](./BooleanTypeNode.md) - A boolean serialised as a numeric value. The wrapped number type determines the byte width. -- [`BytesTypeNode`](./BytesTypeNode.md) - A raw sequence of bytes. Typically used inside a fixed-size, size-prefixed, or sentinel-terminated wrapper. -- [`DateTimeTypeNode`](./DateTimeTypeNode.md) - A timestamp encoded as a number, typically seconds since the Unix epoch. The wrapped number type determines the byte width. +- [`BooleanTypeNode`](./BooleanTypeNode.md) - A boolean serialised as a numeric value. The inner number type determines the byte width. +- [`BytesTypeNode`](./BytesTypeNode.md) - A raw sequence of bytes. Typically carries a fixed-size, size-prefix, or sentinel transform to bound its extent. +- [`DateTimeTypeNode`](./DateTimeTypeNode.md) - A timestamp encoded as a number, typically seconds since the Unix epoch. The inner number type determines the byte width. - [`EnumEmptyVariantTypeNode`](./EnumEmptyVariantTypeNode.md) - A unit-style variant of an enum that carries no payload. - [`EnumStructVariantTypeNode`](./EnumStructVariantTypeNode.md) - A variant of an enum that carries a struct payload (named fields). - [`EnumTupleVariantTypeNode`](./EnumTupleVariantTypeNode.md) - A variant of an enum that carries a tuple payload (positional fields). - [`EnumTypeNode`](./EnumTypeNode.md) - A tagged union: a numeric discriminator followed by one of several variant payloads. -- [`FixedSizeTypeNode`](./FixedSizeTypeNode.md) - Wraps another type and asserts a fixed total byte size. Padding or truncation is applied as needed. -- [`HiddenPrefixTypeNode`](./HiddenPrefixTypeNode.md) - Prefixes another type with a list of constant values that are written and read but not surfaced as fields to consumers. -- [`HiddenSuffixTypeNode`](./HiddenSuffixTypeNode.md) - Suffixes another type with a list of constant values that are written and read but not surfaced as fields to consumers. - [`MapTypeNode`](./MapTypeNode.md) - A keyed map. - [`NumberTypeNode`](./NumberTypeNode.md) - A numeric type with a fixed wire format and byte order. - [`OptionTypeNode`](./OptionTypeNode.md) - A value that may be present or absent (Some/None), with an explicit numeric prefix indicating presence. -- [`PostOffsetTypeNode`](./PostOffsetTypeNode.md) - After serialising the wrapped type, advance the cursor by `offset` bytes interpreted via the chosen strategy. -- [`PreOffsetTypeNode`](./PreOffsetTypeNode.md) - Before serialising the wrapped type, advance the cursor by `offset` bytes interpreted via the chosen strategy. - [`PublicKeyTypeNode`](./PublicKeyTypeNode.md) - A 32-byte Solana public key. - [`RemainderOptionTypeNode`](./RemainderOptionTypeNode.md) - A value that may be present or absent. Presence is signalled by whether any bytes remain to be read, with no explicit prefix. -- [`SentinelTypeNode`](./SentinelTypeNode.md) - Wraps another type and delimits it with a constant sentinel value written immediately after the wrapped type. - [`SetTypeNode`](./SetTypeNode.md) - A unique-valued collection. The item type is defined by `item`; the size is determined by the `count` strategy. -- [`SizePrefixTypeNode`](./SizePrefixTypeNode.md) - Wraps another type with a numeric prefix indicating the byte length of the wrapped type. -- [`SolAmountTypeNode`](./SolAmountTypeNode.md) - A SOL amount expressed in lamports under the wrapped numeric type. +- [`SolAmountTypeNode`](./SolAmountTypeNode.md) - A SOL amount expressed in lamports under the inner numeric type. - [`StringTypeNode`](./StringTypeNode.md) - A string value. - [`StructFieldTypeNode`](./StructFieldTypeNode.md) - A named field within a struct type. - [`StructTypeNode`](./StructTypeNode.md) - A composite type made of an ordered list of named fields. Fields are encoded and decoded in declaration order. @@ -39,7 +32,3 @@ Type nodes — the building blocks of every value shape. - [`RegisteredTypeNode`](./RegisteredTypeNode.md) - Every node tagged as a type-shaped node, including variants and struct fields. - [`StandaloneTypeNode`](./StandaloneTypeNode.md) - Every type node that can be used as a top-level type. - [`TypeNode`](./TypeNode.md) - The composable form: any standalone type, or a reference to a defined type via `definedTypeLinkNode`. - -## Nested unions - -- [`NestedTypeNode`](./NestedTypeNode.md) - A type, possibly wrapped in zero-or-more size, offset, sentinel, or hidden prefix/suffix modifiers. diff --git a/docs/typeNodes/RemainderOptionTypeNode.md b/docs/typeNodes/RemainderOptionTypeNode.md index d76a5d6..596c256 100644 --- a/docs/typeNodes/RemainderOptionTypeNode.md +++ b/docs/typeNodes/RemainderOptionTypeNode.md @@ -12,10 +12,11 @@ A value that may be present or absent. Presence is signalled by whether any byte ### Children -| Attribute | Type | Description | -| --------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `item` | [`TypeNode`](./TypeNode.md) | The type carried by the option when present. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `item` | [`TypeNode`](./TypeNode.md) | The type carried by the option when present. | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples diff --git a/docs/typeNodes/SentinelTypeNode.md b/docs/typeNodes/SentinelTypeNode.md deleted file mode 100644 index df48b8f..0000000 --- a/docs/typeNodes/SentinelTypeNode.md +++ /dev/null @@ -1,34 +0,0 @@ -# SentinelTypeNode - -Wraps another type and delimits it with a constant sentinel value written immediately after the wrapped type. - -When decoding, the wrapped type is decoded until the sentinel value is encountered, at which point decoding stops and the sentinel is discarded. - -> [!IMPORTANT] -> For this node to work, the sentinel value must never occur within the encoded bytes of the wrapped type. - -## Attributes - -### Data - -| Attribute | Type | Description | -| --------- | -------------------- | ----------------------- | -| `kind` | `"sentinelTypeNode"` | The node discriminator. | - -### Children - -| Attribute | Type | Description | -| ---------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `type` | [`TypeNode`](./TypeNode.md) | The wrapped type whose extent is delimited by the sentinel. | -| `sentinel` | [`ConstantValueNode`](../valueNodes/ConstantValueNode.md) | The constant value written immediately after the wrapped type to mark its end. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | - -## Examples - -### A UTF-8 string terminated by 0xFF - -```typescript -sentinelTypeNode(stringTypeNode('utf8'), constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ff'))); - -// Hello => 0x48656C6C6FFF -``` diff --git a/docs/typeNodes/SetTypeNode.md b/docs/typeNodes/SetTypeNode.md index 50752a7..5c1f42b 100644 --- a/docs/typeNodes/SetTypeNode.md +++ b/docs/typeNodes/SetTypeNode.md @@ -12,11 +12,12 @@ A unique-valued collection. The item type is defined by `item`; the size is dete ### Children -| Attribute | Type | Description | -| --------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `item` | [`TypeNode`](./TypeNode.md) | The type of each item in the set. | -| `count` | [`CountNode`](../countNodes/CountNode.md) | The strategy used to determine the number of items. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `item` | [`TypeNode`](./TypeNode.md) | The type of each item in the set. | +| `count` | [`CountNode`](../countNodes/CountNode.md) | The strategy used to determine the number of items. | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples diff --git a/docs/typeNodes/SizePrefixTypeNode.md b/docs/typeNodes/SizePrefixTypeNode.md deleted file mode 100644 index d2eb1f9..0000000 --- a/docs/typeNodes/SizePrefixTypeNode.md +++ /dev/null @@ -1,31 +0,0 @@ -# SizePrefixTypeNode - -Wraps another type with a numeric prefix indicating the byte length of the wrapped type. -When decoding, the size is read first and determines how many bytes the wrapped type may consume. - -## Attributes - -### Data - -| Attribute | Type | Description | -| --------- | ---------------------- | ----------------------- | -| `kind` | `"sizePrefixTypeNode"` | The node discriminator. | - -### Children - -| Attribute | Type | Description | -| --------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `type` | [`TypeNode`](./TypeNode.md) | The wrapped type whose serialisation is preceded by its size. | -| `prefix` | [`NestedTypeNode`](./NestedTypeNode.md)<[`NumberTypeNode`](./NumberTypeNode.md)> | The numeric type used as the size prefix. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | - -## Examples - -### A UTF-8 string prefixed with a u16 size - -```typescript -sizePrefixTypeNode(stringTypeNode('utf8'), numberTypeNode('u16')); - -// "" => 0x0000 -// "Hello" => 0x050048656C6C6F -``` diff --git a/docs/typeNodes/SolAmountTypeNode.md b/docs/typeNodes/SolAmountTypeNode.md index c7c7b97..8dcd345 100644 --- a/docs/typeNodes/SolAmountTypeNode.md +++ b/docs/typeNodes/SolAmountTypeNode.md @@ -1,6 +1,6 @@ # SolAmountTypeNode -A SOL amount expressed in lamports under the wrapped numeric type. +A SOL amount expressed in lamports under the inner numeric type. Equivalent to an `amountTypeNode` with 9 decimals and `SOL` as the unit. ## Attributes @@ -13,10 +13,11 @@ Equivalent to an `amountTypeNode` with 9 decimals and `SOL` as the unit. ### Children -| Attribute | Type | Description | -| --------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `number` | [`NestedTypeNode`](./NestedTypeNode.md)<[`NumberTypeNode`](./NumberTypeNode.md)> | The numeric type used to serialise the lamport amount. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `number` | [`NumberTypeNode`](./NumberTypeNode.md) | The numeric type used to serialise the lamport amount. | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples diff --git a/docs/typeNodes/StandaloneTypeNode.md b/docs/typeNodes/StandaloneTypeNode.md index b560acb..59b4a10 100644 --- a/docs/typeNodes/StandaloneTypeNode.md +++ b/docs/typeNodes/StandaloneTypeNode.md @@ -10,19 +10,12 @@ One of the following: - [`BytesTypeNode`](./BytesTypeNode.md) - [`DateTimeTypeNode`](./DateTimeTypeNode.md) - [`EnumTypeNode`](./EnumTypeNode.md) -- [`FixedSizeTypeNode`](./FixedSizeTypeNode.md) -- [`HiddenPrefixTypeNode`](./HiddenPrefixTypeNode.md) -- [`HiddenSuffixTypeNode`](./HiddenSuffixTypeNode.md) - [`MapTypeNode`](./MapTypeNode.md) - [`NumberTypeNode`](./NumberTypeNode.md) - [`OptionTypeNode`](./OptionTypeNode.md) -- [`PostOffsetTypeNode`](./PostOffsetTypeNode.md) -- [`PreOffsetTypeNode`](./PreOffsetTypeNode.md) - [`PublicKeyTypeNode`](./PublicKeyTypeNode.md) - [`RemainderOptionTypeNode`](./RemainderOptionTypeNode.md) -- [`SentinelTypeNode`](./SentinelTypeNode.md) - [`SetTypeNode`](./SetTypeNode.md) -- [`SizePrefixTypeNode`](./SizePrefixTypeNode.md) - [`SolAmountTypeNode`](./SolAmountTypeNode.md) - [`StringTypeNode`](./StringTypeNode.md) - [`StructTypeNode`](./StructTypeNode.md) diff --git a/docs/typeNodes/StringTypeNode.md b/docs/typeNodes/StringTypeNode.md index beb1892..41e4bbe 100644 --- a/docs/typeNodes/StringTypeNode.md +++ b/docs/typeNodes/StringTypeNode.md @@ -2,7 +2,7 @@ A string value. The encoding describes how its bytes are written. -The byte length is determined by an enclosing wrapper such as `sizePrefixTypeNode` or `fixedSizeTypeNode`. +The byte length is determined by a transform such as `sizePrefixTransformNode` or `fixedSizeTransformNode`. ## Attributes @@ -14,11 +14,12 @@ The byte length is determined by an enclosing wrapper such as `sizePrefixTypeNod ### Children -| Attribute | Type | Description | -| ---------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | -| `encoding` | [`BytesEncoding`](../sharedNodes/BytesEncoding.md) | The byte encoding used to serialise the string. | -| `display` | [`StringDisplayNode`](../displayNodes/StringDisplayNode.md) _(optional)_ | Display metadata describing how the string is presented. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | +| `encoding` | [`BytesEncoding`](../sharedNodes/BytesEncoding.md) | The byte encoding used to serialise the string. | +| `display` | [`StringDisplayNode`](../displayNodes/StringDisplayNode.md) _(optional)_ | Display metadata describing how the string is presented. | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples diff --git a/docs/typeNodes/StructTypeNode.md b/docs/typeNodes/StructTypeNode.md index b043af0..c9cee0e 100644 --- a/docs/typeNodes/StructTypeNode.md +++ b/docs/typeNodes/StructTypeNode.md @@ -12,10 +12,11 @@ A composite type made of an ordered list of named fields. Fields are encoded and ### Children -| Attribute | Type | Description | -| --------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `fields` | [`StructFieldTypeNode`](./StructFieldTypeNode.md)[] | The fields of the struct, in declaration order. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `fields` | [`StructFieldTypeNode`](./StructFieldTypeNode.md)[] | The fields of the struct, in declaration order. | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples @@ -23,7 +24,7 @@ A composite type made of an ordered list of named fields. Fields are encoded and ```typescript structTypeNode([ - structFieldTypeNode({ name: 'name', type: fixedSizeTypeNode(stringTypeNode('utf8'), 10) }), + structFieldTypeNode({ name: 'name', type: stringTypeNode('utf8', { transforms: [fixedSizeTransformNode(10)] }) }), structFieldTypeNode({ name: 'age', type: numberTypeNode('u8') }), ]); diff --git a/docs/typeNodes/TupleTypeNode.md b/docs/typeNodes/TupleTypeNode.md index 2742d13..0fb6d11 100644 --- a/docs/typeNodes/TupleTypeNode.md +++ b/docs/typeNodes/TupleTypeNode.md @@ -12,17 +12,18 @@ A heterogeneous fixed-length sequence in which each positional slot has its own ### Children -| Attribute | Type | Description | -| --------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `items` | [`TypeNode`](./TypeNode.md)[] | The type of each positional slot, in order. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `items` | [`TypeNode`](./TypeNode.md)[] | The type of each positional slot, in order. | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples ### A tuple storing a person's name and age ```typescript -tupleTypeNode([fixedSizeTypeNode(stringTypeNode('utf8'), 10), numberTypeNode('u8')]); +tupleTypeNode([stringTypeNode('utf8', { transforms: [fixedSizeTransformNode(10)] }), numberTypeNode('u8')]); // (Alice, 42) => 0x416C69636500000000002A ``` diff --git a/docs/typeNodes/ZeroableOptionTypeNode.md b/docs/typeNodes/ZeroableOptionTypeNode.md index e084699..70c8159 100644 --- a/docs/typeNodes/ZeroableOptionTypeNode.md +++ b/docs/typeNodes/ZeroableOptionTypeNode.md @@ -12,11 +12,12 @@ An optional value whose absence is signalled by a designated zero value rather t ### Children -| Attribute | Type | Description | -| ----------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| `item` | [`TypeNode`](./TypeNode.md) | The type carried by the option when present. Must be of fixed size. | -| `zeroValue` | [`ConstantValueNode`](../valueNodes/ConstantValueNode.md) _(optional)_ | The constant value that signals absence. When omitted, the all-zero byte pattern of the item type is used. | -| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | +| Attribute | Type | Description | +| ------------ | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `item` | [`TypeNode`](./TypeNode.md) | The type carried by the option when present. Must be of fixed size. | +| `zeroValue` | [`ConstantValueNode`](../valueNodes/ConstantValueNode.md) _(optional)_ | The constant value that signals absence. When omitted, the all-zero byte pattern of the item type is used. | +| `transforms` | [`TransformNode`](../transformNodes/TransformNode.md)[] _(optional)_ | Transforms applied to the serialisation of this type, in order — the first is the innermost. | +| `plugins` | [`PluginNode`](../PluginNode.md)[] _(optional)_ | Namespaced plugins with custom structured data. The universal extension point for renderer-specific or not-yet-standardised metadata. | ## Examples diff --git a/generators/docs/render/renderPages.ts b/generators/docs/render/renderPages.ts index 376691f..dd53dab 100644 --- a/generators/docs/render/renderPages.ts +++ b/generators/docs/render/renderPages.ts @@ -242,11 +242,15 @@ export function renderRootIndexPage(spec: Spec, ctx: RenderCtx): DocPage { markup.paragraph(ROOT_DESCRIPTION), // version, with a switcher to the docs of previous majors (hosted on their maintenance branches) markup.paragraph(specVersionLine(spec.version, markup)), - // legend for the (abstract)/(recursive) heading suffixes + // legend for the (abstract)/(recursive) heading suffixes; the recursive sentence only + // renders when the spec actually declares nested unions markup.paragraph( `Pages marked ${markup.italic('(abstract)')} document unions: sets of nodes that can be used ` + - `interchangeably. Pages marked ${markup.italic('(recursive)')} document nested unions: wrapper ` + - `nodes that may nest before reaching a base type.`, + `interchangeably.` + + (spec.categories.some(category => category.nestedUnions.length > 0) + ? ` Pages marked ${markup.italic('(recursive)')} document nested unions: wrapper ` + + `nodes that may nest before reaching a base type.` + : ''), ), // base attributes shared by every node (omitted when the spec declares none) renderBaseSection(spec, ctx, linkTo), diff --git a/spec.json b/spec.json index a449972..ecc39ab 100644 --- a/spec.json +++ b/spec.json @@ -44,7 +44,7 @@ "width": "u32" }, "docs": [ - "The number of decimal places the wrapped integer carries.", + "The number of decimal places the inner integer carries.", "For example, an integer value of 12345 with 2 decimal places represents 123.45." ] }, @@ -61,13 +61,26 @@ { "name": "number", "type": { - "kind": "nestedUnion", - "alias": "nestedTypeNode", + "kind": "node", "name": "numberTypeNode" }, "docs": [ "The number type the amount wraps." ] + }, + { + "name": "transforms", + "type": { + "kind": "array", + "of": { + "kind": "union", + "name": "transformNode" + } + }, + "optional": true, + "docs": [ + "Transforms applied to the serialisation of this type, in order — the first is the innermost." + ] } ], "examples": [ @@ -113,6 +126,20 @@ "docs": [ "The strategy used to determine the number of items." ] + }, + { + "name": "transforms", + "type": { + "kind": "array", + "of": { + "kind": "union", + "name": "transformNode" + } + }, + "optional": true, + "docs": [ + "Transforms applied to the serialisation of this type, in order — the first is the innermost." + ] } ], "examples": [ @@ -148,20 +175,33 @@ { "kind": "booleanTypeNode", "docs": [ - "A boolean serialised as a numeric value. The wrapped number type determines the byte width.", + "A boolean serialised as a numeric value. The inner number type determines the byte width.", "A decoded number of `1` yields `true`; any other value yields `false`." ], "attributes": [ { "name": "size", "type": { - "kind": "nestedUnion", - "alias": "nestedTypeNode", + "kind": "node", "name": "numberTypeNode" }, "docs": [ "The numeric type used to serialise the boolean." ] + }, + { + "name": "transforms", + "type": { + "kind": "array", + "of": { + "kind": "union", + "name": "transformNode" + } + }, + "optional": true, + "docs": [ + "Transforms applied to the serialisation of this type, in order — the first is the innermost." + ] } ], "examples": [ @@ -198,9 +238,24 @@ { "kind": "bytesTypeNode", "docs": [ - "A raw sequence of bytes. Typically used inside a fixed-size, size-prefixed, or sentinel-terminated wrapper." + "A raw sequence of bytes. Typically carries a fixed-size, size-prefix, or sentinel transform to bound its extent." + ], + "attributes": [ + { + "name": "transforms", + "type": { + "kind": "array", + "of": { + "kind": "union", + "name": "transformNode" + } + }, + "optional": true, + "docs": [ + "Transforms applied to the serialisation of this type, in order — the first is the innermost." + ] + } ], - "attributes": [], "examples": [ { "title": "Create a bytes type node", @@ -218,19 +273,32 @@ { "kind": "dateTimeTypeNode", "docs": [ - "A timestamp encoded as a number, typically seconds since the Unix epoch. The wrapped number type determines the byte width." + "A timestamp encoded as a number, typically seconds since the Unix epoch. The inner number type determines the byte width." ], "attributes": [ { "name": "number", "type": { - "kind": "nestedUnion", - "alias": "nestedTypeNode", + "kind": "node", "name": "numberTypeNode" }, "docs": [ "The numeric type used to serialise the timestamp." ] + }, + { + "name": "transforms", + "type": { + "kind": "array", + "of": { + "kind": "union", + "name": "transformNode" + } + }, + "optional": true, + "docs": [ + "Transforms applied to the serialisation of this type, in order — the first is the innermost." + ] } ], "examples": [ @@ -343,8 +411,7 @@ { "name": "struct", "type": { - "kind": "nestedUnion", - "alias": "nestedTypeNode", + "kind": "node", "name": "structTypeNode" }, "docs": [ @@ -413,8 +480,7 @@ { "name": "tuple", "type": { - "kind": "nestedUnion", - "alias": "nestedTypeNode", + "kind": "node", "name": "tupleTypeNode" }, "docs": [ @@ -469,14 +535,27 @@ { "name": "size", "type": { - "kind": "nestedUnion", - "alias": "nestedTypeNode", + "kind": "node", "name": "numberTypeNode" }, "docs": [ "The numeric type used to serialise the discriminator.", "The discriminator prepends the serialised variant payload to identify which variant was selected. By default it is the index of the variant (starting at 0), unless the variant provides its own custom discriminator value." ] + }, + { + "name": "transforms", + "type": { + "kind": "array", + "of": { + "kind": "union", + "name": "transformNode" + } + }, + "optional": true, + "docs": [ + "Transforms applied to the serialisation of this type, in order — the first is the innermost." + ] } ], "examples": [ @@ -508,67 +587,83 @@ ] }, { - "kind": "fixedSizeTypeNode", + "kind": "mapTypeNode", "docs": [ - "Wraps another type and asserts a fixed total byte size. Padding or truncation is applied as needed." + "A keyed map.", + "The key and value types are described by their respective type nodes; the entry count is determined by a count strategy.", + "Entries are serialised one after the other, each key immediately followed by its value — e.g. key A, value A, key B, value B." ], "attributes": [ { - "name": "size", + "name": "key", "type": { - "kind": "integer", - "width": "u64" + "kind": "union", + "name": "typeNode" }, "docs": [ - "The total byte size the wrapped type must occupy." + "The type of each entry key." ] }, { - "name": "type", + "name": "value", "type": { "kind": "union", "name": "typeNode" }, "docs": [ - "The wrapped type whose serialisation is constrained." + "The type of each entry value." ] - } - ], - "examples": [ + }, { - "title": "Create a fixed size type node from a type node and a byte length", - "code": [ - { - "language": "typescript", - "content": [ - "const node = fixedSizeTypeNode(stringTypeNode('utf8'), 32);" - ] - } + "name": "count", + "type": { + "kind": "union", + "name": "countNode" + }, + "docs": [ + "The strategy used to determine the number of entries." ] }, { - "title": "Fixed UTF-8 strings", + "name": "transforms", + "type": { + "kind": "array", + "of": { + "kind": "union", + "name": "transformNode" + } + }, + "optional": true, + "docs": [ + "Transforms applied to the serialisation of this type, in order — the first is the innermost." + ] + } + ], + "examples": [ + { + "title": "Create a map type node from a key type, a value type, and a count node", "code": [ { "language": "typescript", "content": [ - "fixedSizeTypeNode(stringTypeNode('utf8'), 10);", - "", - "// Hello => 0x48656C6C6F0000000000" + "const node = mapTypeNode(publicKeyTypeNode(), numberTypeNode('u32'), prefixedCountNode(numberTypeNode('u32')));" ] } ] }, { - "title": "Fixed byte arrays", + "title": "A histogram that counts letters", "code": [ { "language": "typescript", "content": [ - "fixedSizeTypeNode(bytesTypeNode(), 4);", + "mapTypeNode(", + " stringTypeNode('utf8', { transforms: [fixedSizeTransformNode(1)] }), // Key: Single UTF-8 character.", + " numberTypeNode('u16'), // Value: 16-bit unsigned integer.", + " prefixedCountNode(numberTypeNode('u8')), // Count: map length is prefixed with a u8.", + ");", "", - "// [1, 2] => 0x01020000", - "// [1, 2, 3, 4, 5] => 0x01020304" + "// { A: 42, B: 1, C: 16 } => 0x03412A00420100431000" ] } ] @@ -576,74 +671,108 @@ ] }, { - "kind": "hiddenPrefixTypeNode", + "kind": "numberTypeNode", "docs": [ - "Prefixes another type with a list of constant values that are written and read but not surfaced as fields to consumers.", - "When decoding, the prefixed constants are consumed and checked against their expected values before being discarded." + "A numeric type with a fixed wire format and byte order." ], "attributes": [ { - "name": "type", + "name": "format", + "type": { + "kind": "enumeration", + "name": "numberFormat" + }, + "docs": [ + "The wire format used to serialise the number." + ] + }, + { + "name": "endian", + "type": { + "kind": "enumeration", + "name": "endianness" + }, + "docs": [ + "The byte order used to serialise the number." + ] + }, + { + "name": "display", "type": { "kind": "union", - "name": "typeNode" + "name": "numberDisplayNode" }, + "optional": true, "docs": [ - "The wrapped type whose serialisation is preceded by the hidden prefix." + "Display metadata describing how the number is presented." ] }, { - "name": "prefix", + "name": "transforms", "type": { "kind": "array", "of": { - "kind": "node", - "name": "constantValueNode" + "kind": "union", + "name": "transformNode" } }, + "optional": true, "docs": [ - "The constant values written before the wrapped type, in order." + "Transforms applied to the serialisation of this type, in order — the first is the innermost." ] } ], "examples": [ { - "title": "Create a hidden prefix type node from a type node and constant value nodes", + "title": "Encoding `u32` integers", + "docs": [ + "![Diagram](https://github.com/codama-idl/codama/assets/3642397/4bb1ae23-c69f-4c9f-a7ec-8f971d061667)" + ], "code": [ { "language": "typescript", "content": [ - "const node = hiddenPrefixTypeNode(numberTypeNode('u32'), [", - " constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff')),", - "]);" + "numberTypeNode('u32');", + "", + "// 5 => 0x05000000", + "// 42 => 0x2A000000", + "// 65535 => 0xFFFF0000" ] } ] }, { - "title": "A number prefixed with 0xFFFF", + "title": "Encoding `f32` big-endian decimal numbers", + "docs": [ + "![Diagram](https://github.com/codama-idl/codama/assets/3642397/d9cbfd3c-b8a2-4c13-a8a8-a11e7ed5d422)" + ], "code": [ { "language": "typescript", "content": [ - "hiddenPrefixTypeNode(numberTypeNode('u32'), [constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff'))]);", + "numberTypeNode('f32', 'be');", "", - "// 42 => 0xFFFF2A000000" + "// 1 => 0x3F800000", + "// -42 => 0xC2280000", + "// 3.1415 => 0x40490E56" ] } ] }, { - "title": "A fixed UTF-8 string prefixed with \"Hello\"", + "title": "Encoding `shortU16` integers", + "docs": [ + "![Diagram](https://github.com/codama-idl/codama/assets/3642397/73e12166-cdaa-4fca-ae2a-67937f8b130e)" + ], "code": [ { "language": "typescript", "content": [ - "hiddenPrefixTypeNode(fixedSizeTypeNode(stringTypeNode('utf8'), 10), [", - " constantValueNode(stringTypeNode('utf8'), stringValueNode('Hello')),", - "]);", + "numberTypeNode('shortU16');", "", - "// World => 0x48656C6C6F576F726C640000000000" + "// 42 => 0x2A", + "// 128 => 0x8001", + "// 16384 => 0x808001" ] } ] @@ -651,74 +780,83 @@ ] }, { - "kind": "hiddenSuffixTypeNode", + "kind": "optionTypeNode", "docs": [ - "Suffixes another type with a list of constant values that are written and read but not surfaced as fields to consumers.", - "When decoding, the suffixed constants are consumed and checked against their expected values before being discarded." + "A value that may be present or absent (Some/None), with an explicit numeric prefix indicating presence." ], "attributes": [ { - "name": "type", + "name": "fixed", + "type": { + "kind": "boolean" + }, + "optional": true, + "docs": [ + "When `true`, the absent variant still occupies the byte size of the present variant (zero-padded). Defaults to `false`.", + "Must only be set to `true` when the `item` type is of fixed size." + ] + }, + { + "name": "item", "type": { "kind": "union", "name": "typeNode" }, "docs": [ - "The wrapped type whose serialisation is followed by the hidden suffix." + "The type carried by the option when present." ] }, { - "name": "suffix", + "name": "prefix", + "type": { + "kind": "node", + "name": "numberTypeNode" + }, + "docs": [ + "The numeric type used as the presence flag.", + "A prefix value of `1` means the item is present and follows the prefix; a value of `0` means the item is absent and nothing further is serialised." + ] + }, + { + "name": "transforms", "type": { "kind": "array", "of": { - "kind": "node", - "name": "constantValueNode" + "kind": "union", + "name": "transformNode" } }, + "optional": true, "docs": [ - "The constant values written after the wrapped type, in order." + "Transforms applied to the serialisation of this type, in order — the first is the innermost." ] } ], "examples": [ { - "title": "Create a hidden suffix type node from a type node and constant value nodes", - "code": [ - { - "language": "typescript", - "content": [ - "const node = hiddenSuffixTypeNode(numberTypeNode('u32'), [", - " constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff')),", - "]);" - ] - } - ] - }, - { - "title": "A number suffixed with 0xFFFF", + "title": "An optional UTF-8 with a u16 prefix", "code": [ { "language": "typescript", "content": [ - "hiddenSuffixTypeNode(numberTypeNode('u32'), [constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff'))]);", + "optionTypeNode(stringTypeNode('utf8'), { prefix: numberTypeNode('u16') });", "", - "// 42 => 0x2A000000FFFF" + "// None => 0x0000", + "// Some(\"Hello\") => 0x010048656C6C6F" ] } ] }, { - "title": "A fixed UTF-8 string suffixed with \"Hello\"", + "title": "A fixed optional u32 number", "code": [ { "language": "typescript", "content": [ - "hiddenSuffixTypeNode(fixedSizeTypeNode(stringTypeNode('utf8'), 10), [", - " constantValueNode(stringTypeNode('utf8'), stringValueNode('Hello')),", - "]);", + "optionTypeNode(numberTypeNode('u32'), { fixed: true });", "", - "// World => 0x576F726C64000000000048656c6c6F" + "// None => 0x0000000000", + "// Some(42) => 0x012A000000" ] } ] @@ -726,69 +864,82 @@ ] }, { - "kind": "mapTypeNode", + "kind": "publicKeyTypeNode", "docs": [ - "A keyed map.", - "The key and value types are described by their respective type nodes; the entry count is determined by a count strategy.", - "Entries are serialised one after the other, each key immediately followed by its value — e.g. key A, value A, key B, value B." + "A 32-byte Solana public key." ], "attributes": [ { - "name": "key", + "name": "transforms", "type": { - "kind": "union", - "name": "typeNode" + "kind": "array", + "of": { + "kind": "union", + "name": "transformNode" + } }, + "optional": true, "docs": [ - "The type of each entry key." + "Transforms applied to the serialisation of this type, in order — the first is the innermost." ] - }, + } + ], + "examples": [ { - "name": "value", + "title": "Create a public key type node", + "code": [ + { + "language": "typescript", + "content": [ + "const node = publicKeyTypeNode();" + ] + } + ] + } + ] + }, + { + "kind": "remainderOptionTypeNode", + "docs": [ + "A value that may be present or absent. Presence is signalled by whether any bytes remain to be read, with no explicit prefix." + ], + "attributes": [ + { + "name": "item", "type": { "kind": "union", "name": "typeNode" }, "docs": [ - "The type of each entry value." + "The type carried by the option when present." ] }, { - "name": "count", + "name": "transforms", "type": { - "kind": "union", - "name": "countNode" + "kind": "array", + "of": { + "kind": "union", + "name": "transformNode" + } }, + "optional": true, "docs": [ - "The strategy used to determine the number of entries." + "Transforms applied to the serialisation of this type, in order — the first is the innermost." ] } ], "examples": [ { - "title": "Create a map type node from a key type, a value type, and a count node", - "code": [ - { - "language": "typescript", - "content": [ - "const node = mapTypeNode(publicKeyTypeNode(), numberTypeNode('u32'), prefixedCountNode(numberTypeNode('u32')));" - ] - } - ] - }, - { - "title": "A histogram that counts letters", + "title": "An optional UTF-8 string using remaining bytes", "code": [ { "language": "typescript", "content": [ - "mapTypeNode(", - " fixedSizeTypeNode(stringTypeNode('utf8'), 1), // Key: Single UTF-8 character.", - " numberTypeNode('u16'), // Value: 16-bit unsigned integer.", - " prefixedCountNode(numberTypeNode('u8')), // Count: map length is prefixed with a u8.", - ");", + "remainderOptionTypeNode(stringTypeNode('utf8'));", "", - "// { A: 42, B: 1, C: 16 } => 0x03412A00420100431000" + "// None => 0x", + "// Some(\"Hello\") => 0x48656C6C6F" ] } ] @@ -796,94 +947,105 @@ ] }, { - "kind": "numberTypeNode", + "kind": "setTypeNode", "docs": [ - "A numeric type with a fixed wire format and byte order." + "A unique-valued collection. The item type is defined by `item`; the size is determined by the `count` strategy." ], "attributes": [ { - "name": "format", + "name": "item", "type": { - "kind": "enumeration", - "name": "numberFormat" + "kind": "union", + "name": "typeNode" }, "docs": [ - "The wire format used to serialise the number." + "The type of each item in the set." ] }, { - "name": "endian", + "name": "count", "type": { - "kind": "enumeration", - "name": "endianness" + "kind": "union", + "name": "countNode" }, "docs": [ - "The byte order used to serialise the number." + "The strategy used to determine the number of items." ] }, { - "name": "display", + "name": "transforms", "type": { - "kind": "union", - "name": "numberDisplayNode" + "kind": "array", + "of": { + "kind": "union", + "name": "transformNode" + } }, "optional": true, "docs": [ - "Display metadata describing how the number is presented." + "Transforms applied to the serialisation of this type, in order — the first is the innermost." ] } ], "examples": [ { - "title": "Encoding `u32` integers", - "docs": [ - "![Diagram](https://github.com/codama-idl/codama/assets/3642397/4bb1ae23-c69f-4c9f-a7ec-8f971d061667)" - ], + "title": "u32 prefixed set of u8 numbers", "code": [ { "language": "typescript", "content": [ - "numberTypeNode('u32');", + "setTypeNode(numberTypeNode('u8'), prefixedCountNode(numberTypeNode('u32')));", "", - "// 5 => 0x05000000", - "// 42 => 0x2A000000", - "// 65535 => 0xFFFF0000" + "// Set (1, 2, 3) => 0x03000000010203" ] } ] - }, + } + ] + }, + { + "kind": "solAmountTypeNode", + "docs": [ + "A SOL amount expressed in lamports under the inner numeric type.", + "Equivalent to an `amountTypeNode` with 9 decimals and `SOL` as the unit." + ], + "attributes": [ { - "title": "Encoding `f32` big-endian decimal numbers", + "name": "number", + "type": { + "kind": "node", + "name": "numberTypeNode" + }, "docs": [ - "![Diagram](https://github.com/codama-idl/codama/assets/3642397/d9cbfd3c-b8a2-4c13-a8a8-a11e7ed5d422)" - ], - "code": [ - { - "language": "typescript", - "content": [ - "numberTypeNode('f32', 'be');", - "", - "// 1 => 0x3F800000", - "// -42 => 0xC2280000", - "// 3.1415 => 0x40490E56" - ] - } + "The numeric type used to serialise the lamport amount." ] }, { - "title": "Encoding `shortU16` integers", + "name": "transforms", + "type": { + "kind": "array", + "of": { + "kind": "union", + "name": "transformNode" + } + }, + "optional": true, "docs": [ - "![Diagram](https://github.com/codama-idl/codama/assets/3642397/73e12166-cdaa-4fca-ae2a-67937f8b130e)" - ], + "Transforms applied to the serialisation of this type, in order — the first is the innermost." + ] + } + ], + "examples": [ + { + "title": "u64 Solana amounts", "code": [ { "language": "typescript", "content": [ - "numberTypeNode('shortU16');", + "solAmountTypeNode(numberTypeNode('u64'));", "", - "// 42 => 0x2A", - "// 128 => 0x8001", - "// 16384 => 0x808001" + "// 1.5 SOL => 0x002F685900000000", + "// 300 SOL => 0x00B864D945000000" ] } ] @@ -891,70 +1053,57 @@ ] }, { - "kind": "optionTypeNode", + "kind": "stringTypeNode", "docs": [ - "A value that may be present or absent (Some/None), with an explicit numeric prefix indicating presence." + "A string value.", + "The encoding describes how its bytes are written.", + "The byte length is determined by a transform such as `sizePrefixTransformNode` or `fixedSizeTransformNode`." ], "attributes": [ { - "name": "fixed", + "name": "encoding", "type": { - "kind": "boolean" + "kind": "enumeration", + "name": "bytesEncoding" }, - "optional": true, "docs": [ - "When `true`, the absent variant still occupies the byte size of the present variant (zero-padded). Defaults to `false`.", - "Must only be set to `true` when the `item` type is of fixed size." + "The byte encoding used to serialise the string." ] }, { - "name": "item", + "name": "display", "type": { - "kind": "union", - "name": "typeNode" + "kind": "node", + "name": "stringDisplayNode" }, + "optional": true, "docs": [ - "The type carried by the option when present." + "Display metadata describing how the string is presented." ] }, { - "name": "prefix", + "name": "transforms", "type": { - "kind": "nestedUnion", - "alias": "nestedTypeNode", - "name": "numberTypeNode" + "kind": "array", + "of": { + "kind": "union", + "name": "transformNode" + } }, + "optional": true, "docs": [ - "The numeric type used as the presence flag.", - "A prefix value of `1` means the item is present and follows the prefix; a value of `0` means the item is absent and nothing further is serialised." + "Transforms applied to the serialisation of this type, in order — the first is the innermost." ] } ], "examples": [ { - "title": "An optional UTF-8 with a u16 prefix", - "code": [ - { - "language": "typescript", - "content": [ - "optionTypeNode(stringTypeNode('utf8'), { prefix: numberTypeNode('u16') });", - "", - "// None => 0x0000", - "// Some(\"Hello\") => 0x010048656C6C6F" - ] - } - ] - }, - { - "title": "A fixed optional u32 number", + "title": "Create a string type node from an encoding", "code": [ { "language": "typescript", "content": [ - "optionTypeNode(numberTypeNode('u32'), { fixed: true });", - "", - "// None => 0x0000000000", - "// Some(42) => 0x012A000000" + "const node = stringTypeNode('utf8');" ] } ] @@ -962,94 +1111,40 @@ ] }, { - "kind": "postOffsetTypeNode", + "kind": "structFieldTypeNode", "docs": [ - "After serialising the wrapped type, advance the cursor by `offset` bytes interpreted via the chosen strategy.", - "", - "Since the offset is applied _after_ the wrapped type runs, this node is useful to move the cursor around once the wrapped type has been processed. See `preOffsetTypeNode` for the opposite behaviour.", - "", - "The strategies below are illustrated against the following buffer: the `99` byte represents the encoded value of the wrapped type and the `FF` byte represents the next bytes to be encoded after it, in order to show the _post_ cursor position.", - "", - "```", - "0x00000099FF000000;", - " | └-- Initial post-offset", - " └-- Pre-offset", - "```", - "", - "**`relative`** — the cursor is moved to the right by the provided offset. A negative offset moves it to the left instead.", - "", - "```", - "offset = 2", - "0x000000990000FF00;", - " └-- Post-offset", - "", - "offset = -2", - "0x0000FF9900000000;", - " └-- Post-offset", - "```", - "", - "**`absolute`** — the cursor is moved to an absolute position in the buffer. A negative offset moves it backwards from the end of the buffer.", - "", - "```", - "offset = 0", - "0xFF00009900000000;", - " └-- Post-offset", - "", - "offset = -2", - "0x000000990000FF00;", - " └-- Post-offset", - "```", - "", - "**`padded`** — the cursor is moved to the right by the provided offset **and the buffer size is increased** by the offset amount, allowing padding bytes to be added. Reciprocally, a negative offset moves the cursor to the left and decreases the buffer size.", - "", - "```", - "offset = 2", - "0x000000990000FF000000; <- Size = 10 (initially 8)", - " └-- Post-offset", - "", - "offset = -2", - "0x0000FF990000; <- Size = 6 (initially 8)", - " └-- Post-offset", - "```", - "", - "**`preOffset`** — the cursor is moved to the right of the pre-offset — i.e. where the wrapped type started — by the provided offset. A negative offset moves it to the left of the pre-offset instead.", - "", - "```", - "offset = 2", - "0x0000009900FF0000;", - " | └-- Post-offset = Pre-offset + 2", - " └-- Pre-offset", - "", - "offset = -2", - "0x00FF009900000000;", - " | └-- Pre-offset", - " └-- Post-offset = Pre-offset - 2", - "```", - "", - "> [!IMPORTANT]", - "> Some type nodes affect the buffer that is available to us: depending on where we are in the type tree, we may not have access to the entire buffer.", - "> For instance, inside a `fixedSizeTypeNode`, the buffer is truncated or padded to match the provided fixed size once the wrapped content has been serialised — we are essentially \"boxed\" into a sub-buffer, and that sub-buffer is the one affected by the `absolute` strategy.", - "> The type nodes that create sub-buffers are: `fixedSizeTypeNode`, `sentinelTypeNode`, and `sizePrefixTypeNode`." + "A named field within a struct type." ], "attributes": [ { - "name": "offset", + "name": "name", "type": { - "kind": "integer", - "width": "i64" + "kind": "string", + "constraint": "identifier" }, "docs": [ - "The signed byte offset to apply after the wrapped type runs." + "The name of the field." ] }, { - "name": "strategy", + "name": "defaultValueStrategy", "type": { "kind": "enumeration", - "name": "postOffsetStrategy" + "name": "defaultValueStrategy" + }, + "optional": true, + "docs": [ + "How a configured default value is exposed in generated APIs. Required when `defaultValue` is set." + ] + }, + { + "name": "docs", + "type": { + "kind": "docs" }, + "optional": true, "docs": [ - "How the `offset` value is interpreted." + "Markdown documentation for the field." ] }, { @@ -1059,56 +1154,150 @@ "name": "typeNode" }, "docs": [ - "The wrapped type whose serialisation is followed by the offset." + "The type of the field." + ] + }, + { + "name": "defaultValue", + "type": { + "kind": "union", + "name": "valueNode" + }, + "optional": true, + "docs": [ + "A default value used when the field is omitted by callers." + ] + }, + { + "name": "display", + "type": { + "kind": "node", + "name": "structFieldDisplayNode" + }, + "optional": true, + "docs": [ + "Display metadata describing how the field is presented." ] } ], "examples": [ { - "title": "A relative post-offset (the default strategy)", + "title": "A struct field with a default value", "code": [ { "language": "typescript", "content": [ - "postOffsetTypeNode(numberTypeNode('u32'), 2);" + "structFieldTypeNode({", + " name: 'age',", + " type: numberTypeNode('u8'),", + " defaultValue: numberValueNode(42),", + "});", + "", + "// {} => 0x2A", + "// { age: 29 } => 0x1D" ] } ] - }, + } + ] + }, + { + "kind": "structTypeNode", + "docs": [ + "A composite type made of an ordered list of named fields. Fields are encoded and decoded in declaration order." + ], + "attributes": [ { - "title": "An absolute post-offset from the end of the buffer", - "code": [ - { - "language": "typescript", - "content": [ - "postOffsetTypeNode(numberTypeNode('u32'), -2, 'absolute');" - ] + "name": "fields", + "type": { + "kind": "array", + "of": { + "kind": "node", + "name": "structFieldTypeNode" } + }, + "docs": [ + "The fields of the struct, in declaration order." ] }, { - "title": "A right-padded u32 number", + "name": "transforms", + "type": { + "kind": "array", + "of": { + "kind": "union", + "name": "transformNode" + } + }, + "optional": true, + "docs": [ + "Transforms applied to the serialisation of this type, in order — the first is the innermost." + ] + } + ], + "examples": [ + { + "title": "A struct storing a person's name and age", "code": [ { "language": "typescript", "content": [ - "postOffsetTypeNode(numberTypeNode('u32'), 4, 'padded');", + "structTypeNode([", + " structFieldTypeNode({ name: 'name', type: stringTypeNode('utf8', { transforms: [fixedSizeTransformNode(10)] }) }),", + " structFieldTypeNode({ name: 'age', type: numberTypeNode('u8') }),", + "]);", "", - "// 42 => 0x2A00000000000000" + "// { name: Alice, age: 42 } => 0x416C69636500000000002A" ] } ] + } + ] + }, + { + "kind": "tupleTypeNode", + "docs": [ + "A heterogeneous fixed-length sequence in which each positional slot has its own type." + ], + "attributes": [ + { + "name": "items", + "type": { + "kind": "array", + "of": { + "kind": "union", + "name": "typeNode" + } + }, + "docs": [ + "The type of each positional slot, in order." + ] }, { - "title": "A u32 number overwritten by a u16 number", + "name": "transforms", + "type": { + "kind": "array", + "of": { + "kind": "union", + "name": "transformNode" + } + }, + "optional": true, + "docs": [ + "Transforms applied to the serialisation of this type, in order — the first is the innermost." + ] + } + ], + "examples": [ + { + "title": "A tuple storing a person's name and age", "code": [ { "language": "typescript", "content": [ - "tupleTypeNode([postOffsetTypeNode(numberTypeNode('u32'), -2), numberTypeNode('u16')]);", + "tupleTypeNode([stringTypeNode('utf8', { transforms: [fixedSizeTransformNode(10)] }), numberTypeNode('u8')]);", "", - "// [1, 2] => 0x01000200", - "// [0xFFFFFFFF, 42] => 0xFFFF2A00" + "// (Alice, 42) => 0x416C69636500000000002A" ] } ] @@ -1116,138 +1305,264 @@ ] }, { - "kind": "preOffsetTypeNode", + "kind": "zeroableOptionTypeNode", "docs": [ - "Before serialising the wrapped type, advance the cursor by `offset` bytes interpreted via the chosen strategy.", - "", - "Since the offset is applied _before_ the wrapped type runs, this node is useful to move the encoded value of the wrapped type itself. See `postOffsetTypeNode` for the opposite behaviour.", - "", - "The strategies below are illustrated against the following buffer: the `99` byte represents some previously encoded value for reference and the `FF` byte represents the encoded value of the wrapped type, which moves as its pre-offset changes.", - "", - "```", - "0x00000099FF000000;", - " └-- Initial pre-offset", - "```", - "", - "**`relative`** — the cursor is moved to the right by the provided offset. A negative offset moves it to the left instead.", - "", - "```", - "offset = 2", - "0x000000990000FF00;", - " └-- Pre-offset", - "", - "offset = -2", - "0x0000FF9900000000;", - " └-- Pre-offset", - "```", - "", - "**`absolute`** — the cursor is moved to an absolute position in the buffer. A negative offset moves it backwards from the end of the buffer.", - "", - "```", - "offset = 0", - "0xFF00009900000000;", - " └-- Pre-offset", - "", - "offset = -2", - "0x000000990000FF00;", - " └-- Pre-offset", - "```", - "", - "**`padded`** — the cursor is moved to the right by the provided offset **and the buffer size is increased** by the offset amount, allowing padding bytes to be added. Reciprocally, a negative offset moves the cursor to the left and decreases the buffer size.", - "", - "```", - "offset = 2", - "0x000000990000FF000000; <- Size = 10 (initially 8)", - " └-- Pre-offset", - "", - "offset = -2", - "0x0000FF990000; <- Size = 6 (initially 8)", - " └-- Pre-offset", - "```", - "", - "> [!IMPORTANT]", - "> Some type nodes affect the buffer that is available to us: depending on where we are in the type tree, we may not have access to the entire buffer.", - "> For instance, inside a `fixedSizeTypeNode`, the buffer is truncated or padded to match the provided fixed size once the wrapped content has been serialised — we are essentially \"boxed\" into a sub-buffer, and that sub-buffer is the one affected by the `absolute` strategy.", - "> The type nodes that create sub-buffers are: `fixedSizeTypeNode`, `sentinelTypeNode`, and `sizePrefixTypeNode`." + "An optional value whose absence is signalled by a designated zero value rather than a presence flag." ], "attributes": [ { - "name": "offset", + "name": "item", "type": { - "kind": "integer", - "width": "i64" + "kind": "union", + "name": "typeNode" }, "docs": [ - "The signed byte offset to apply before the wrapped type runs." + "The type carried by the option when present. Must be of fixed size." ] }, { - "name": "strategy", + "name": "zeroValue", "type": { - "kind": "enumeration", - "name": "preOffsetStrategy" + "kind": "node", + "name": "constantValueNode" }, + "optional": true, "docs": [ - "How the `offset` value is interpreted." + "The constant value that signals absence. When omitted, the all-zero byte pattern of the item type is used." ] }, { - "name": "type", + "name": "transforms", "type": { - "kind": "union", - "name": "typeNode" + "kind": "array", + "of": { + "kind": "union", + "name": "transformNode" + } }, + "optional": true, "docs": [ - "The wrapped type whose serialisation is preceded by the offset." + "Transforms applied to the serialisation of this type, in order — the first is the innermost." ] } ], "examples": [ { - "title": "A relative pre-offset (the default strategy)", + "title": "a u32 zeroable option", "code": [ { "language": "typescript", "content": [ - "preOffsetTypeNode(numberTypeNode('u32'), 2);" + "zeroableOptionTypeNode(numberTypeNode('u32'));", + "", + "// None => 0x00000000", + "// Some(42) => 0x2A000000" ] } ] }, { - "title": "An absolute pre-offset", + "title": "a u32 zeroable option with a custom zero value", "code": [ { "language": "typescript", "content": [ - "preOffsetTypeNode(numberTypeNode('u32'), -2, 'absolute');" + "zeroableOptionTypeNode(numberTypeNode('u32'), constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffffffff')));", + "", + "// None => 0xFFFFFFFF", + "// Some(42) => 0x2A000000" ] } ] + } + ] + } + ], + "unions": [ + { + "name": "standaloneTypeNode", + "members": [ + { + "kind": "node", + "name": "amountTypeNode" }, { - "title": "A left-padded u32 number", + "kind": "node", + "name": "arrayTypeNode" + }, + { + "kind": "node", + "name": "booleanTypeNode" + }, + { + "kind": "node", + "name": "bytesTypeNode" + }, + { + "kind": "node", + "name": "dateTimeTypeNode" + }, + { + "kind": "node", + "name": "enumTypeNode" + }, + { + "kind": "node", + "name": "mapTypeNode" + }, + { + "kind": "node", + "name": "numberTypeNode" + }, + { + "kind": "node", + "name": "optionTypeNode" + }, + { + "kind": "node", + "name": "publicKeyTypeNode" + }, + { + "kind": "node", + "name": "remainderOptionTypeNode" + }, + { + "kind": "node", + "name": "setTypeNode" + }, + { + "kind": "node", + "name": "solAmountTypeNode" + }, + { + "kind": "node", + "name": "stringTypeNode" + }, + { + "kind": "node", + "name": "structTypeNode" + }, + { + "kind": "node", + "name": "tupleTypeNode" + }, + { + "kind": "node", + "name": "zeroableOptionTypeNode" + } + ], + "docs": [ + "Every type node that can be used as a top-level type." + ] + }, + { + "name": "enumVariantTypeNode", + "members": [ + { + "kind": "node", + "name": "enumEmptyVariantTypeNode" + }, + { + "kind": "node", + "name": "enumStructVariantTypeNode" + }, + { + "kind": "node", + "name": "enumTupleVariantTypeNode" + } + ], + "docs": [ + "The variant flavours of an `enumTypeNode`." + ] + }, + { + "name": "typeNode", + "members": [ + { + "kind": "union", + "name": "standaloneTypeNode" + }, + { + "kind": "node", + "name": "definedTypeLinkNode" + } + ], + "docs": [ + "The composable form: any standalone type, or a reference to a defined type via `definedTypeLinkNode`." + ] + }, + { + "name": "registeredTypeNode", + "members": [ + { + "kind": "union", + "name": "standaloneTypeNode" + }, + { + "kind": "union", + "name": "enumVariantTypeNode" + }, + { + "kind": "node", + "name": "structFieldTypeNode" + } + ], + "docs": [ + "Every node tagged as a type-shaped node, including variants and struct fields." + ] + } + ], + "enumerations": [], + "nestedUnions": [] + }, + { + "name": "transform", + "docs": [ + "Transform nodes — modifiers applied to the serialisation of the type node that carries them.", + "Every type node has an optional `transforms` array; transforms apply in array order, the first being the innermost." + ], + "nodes": [ + { + "kind": "fixedSizeTransformNode", + "docs": [ + "Asserts a fixed total byte size for the transformed type. Padding or truncation is applied as needed." + ], + "attributes": [ + { + "name": "size", + "type": { + "kind": "integer", + "width": "u64" + }, + "docs": [ + "The total byte size the transformed type must occupy." + ] + } + ], + "examples": [ + { + "title": "Fixed UTF-8 strings", "code": [ { "language": "typescript", "content": [ - "preOffsetTypeNode(numberTypeNode('u32'), 4, 'padded');", + "stringTypeNode('utf8', { transforms: [fixedSizeTransformNode(10)] });", "", - "// 42 => 0x000000002A000000" + "// Hello => 0x48656C6C6F0000000000" ] } ] }, { - "title": "A u32 number overwritten by a u16 number", + "title": "Fixed byte arrays", "code": [ { "language": "typescript", "content": [ - "tupleTypeNode([numberTypeNode('u32'), preOffsetTypeNode(numberTypeNode('u16'), -2)]);", + "bytesTypeNode({ transforms: [fixedSizeTransformNode(4)] });", "", - "// [1, 2] => 0x01000200", - "// [0xFFFFFFFF, 42] => 0xFFFF2A00" + "// [1, 2] => 0x01020000", + "// [1, 2, 3, 4, 5] => 0x01020304" ] } ] @@ -1255,19 +1570,56 @@ ] }, { - "kind": "publicKeyTypeNode", + "kind": "hiddenPrefixTransformNode", "docs": [ - "A 32-byte Solana public key." + "Prefixes the transformed type with a list of constant values that are written and read but not surfaced as fields to consumers.", + "When decoding, the prefixed constants are consumed and checked against their expected values before being discarded." + ], + "attributes": [ + { + "name": "prefix", + "type": { + "kind": "array", + "of": { + "kind": "node", + "name": "constantValueNode" + } + }, + "docs": [ + "The constant values written before the transformed type, in order." + ] + } ], - "attributes": [], "examples": [ { - "title": "Create a public key type node", + "title": "A number prefixed with 0xFFFF", "code": [ { "language": "typescript", "content": [ - "const node = publicKeyTypeNode();" + "numberTypeNode('u32', {", + " transforms: [hiddenPrefixTransformNode([constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff'))])],", + "});", + "", + "// 42 => 0xFFFF2A000000" + ] + } + ] + }, + { + "title": "A fixed UTF-8 string prefixed with \"Hello\"", + "code": [ + { + "language": "typescript", + "content": [ + "stringTypeNode('utf8', {", + " transforms: [", + " fixedSizeTransformNode(10),", + " hiddenPrefixTransformNode([constantValueNode(stringTypeNode('utf8'), stringValueNode('Hello'))]),", + " ],", + "});", + "", + "// World => 0x48656C6C6F576F726C640000000000" ] } ] @@ -1275,33 +1627,56 @@ ] }, { - "kind": "remainderOptionTypeNode", + "kind": "hiddenSuffixTransformNode", "docs": [ - "A value that may be present or absent. Presence is signalled by whether any bytes remain to be read, with no explicit prefix." + "Suffixes the transformed type with a list of constant values that are written and read but not surfaced as fields to consumers.", + "When decoding, the suffixed constants are consumed and checked against their expected values before being discarded." ], "attributes": [ { - "name": "item", + "name": "suffix", "type": { - "kind": "union", - "name": "typeNode" + "kind": "array", + "of": { + "kind": "node", + "name": "constantValueNode" + } }, "docs": [ - "The type carried by the option when present." + "The constant values written after the transformed type, in order." ] } ], "examples": [ { - "title": "An optional UTF-8 string using remaining bytes", + "title": "A number suffixed with 0xFFFF", "code": [ { "language": "typescript", "content": [ - "remainderOptionTypeNode(stringTypeNode('utf8'));", + "numberTypeNode('u32', {", + " transforms: [hiddenSuffixTransformNode([constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff'))])],", + "});", "", - "// None => 0x", - "// Some(\"Hello\") => 0x48656C6C6F" + "// 42 => 0x2A000000FFFF" + ] + } + ] + }, + { + "title": "A fixed UTF-8 string suffixed with \"Hello\"", + "code": [ + { + "language": "typescript", + "content": [ + "stringTypeNode('utf8', {", + " transforms: [", + " fixedSizeTransformNode(10),", + " hiddenSuffixTransformNode([constantValueNode(stringTypeNode('utf8'), stringValueNode('Hello'))]),", + " ],", + "});", + "", + "// World => 0x576F726C64000000000048656c6c6F" ] } ] @@ -1309,172 +1684,143 @@ ] }, { - "kind": "sentinelTypeNode", + "kind": "postOffsetTransformNode", "docs": [ - "Wraps another type and delimits it with a constant sentinel value written immediately after the wrapped type.", + "After serialising the transformed type, advance the cursor by `offset` bytes interpreted via the chosen strategy.", + "", + "Since the offset is applied _after_ the transformed type runs, this transform is useful to move the cursor around once the transformed type has been processed. See `preOffsetTransformNode` for the opposite behaviour.", + "", + "The strategies below are illustrated against the following buffer: the `99` byte represents the encoded value of the transformed type and the `FF` byte represents the next bytes to be encoded after it, in order to show the _post_ cursor position.", + "", + "```", + "0x00000099FF000000;", + " | └-- Initial post-offset", + " └-- Pre-offset", + "```", + "", + "**`relative`** — the cursor is moved to the right by the provided offset. A negative offset moves it to the left instead.", + "", + "```", + "offset = 2", + "0x000000990000FF00;", + " └-- Post-offset", + "", + "offset = -2", + "0x0000FF9900000000;", + " └-- Post-offset", + "```", + "", + "**`absolute`** — the cursor is moved to an absolute position in the buffer. A negative offset moves it backwards from the end of the buffer.", + "", + "```", + "offset = 0", + "0xFF00009900000000;", + " └-- Post-offset", "", - "When decoding, the wrapped type is decoded until the sentinel value is encountered, at which point decoding stops and the sentinel is discarded.", + "offset = -2", + "0x000000990000FF00;", + " └-- Post-offset", + "```", + "", + "**`padded`** — the cursor is moved to the right by the provided offset **and the buffer size is increased** by the offset amount, allowing padding bytes to be added. Reciprocally, a negative offset moves the cursor to the left and decreases the buffer size.", + "", + "```", + "offset = 2", + "0x000000990000FF000000; <- Size = 10 (initially 8)", + " └-- Post-offset", + "", + "offset = -2", + "0x0000FF990000; <- Size = 6 (initially 8)", + " └-- Post-offset", + "```", + "", + "**`preOffset`** — the cursor is moved to the right of the pre-offset — i.e. where the transformed type started — by the provided offset. A negative offset moves it to the left of the pre-offset instead.", + "", + "```", + "offset = 2", + "0x0000009900FF0000;", + " | └-- Post-offset = Pre-offset + 2", + " └-- Pre-offset", + "", + "offset = -2", + "0x00FF009900000000;", + " | └-- Pre-offset", + " └-- Post-offset = Pre-offset - 2", + "```", "", "> [!IMPORTANT]", - "> For this node to work, the sentinel value must never occur within the encoded bytes of the wrapped type." + "> Some transforms affect the buffer that is available to us: depending on where we are in the type tree, we may not have access to the entire buffer.", + "> For instance, under a `fixedSizeTransformNode`, the buffer is truncated or padded to match the provided fixed size once the transformed content has been serialised — we are essentially \"boxed\" into a sub-buffer, and that sub-buffer is the one affected by the `absolute` strategy.", + "> The transforms that create sub-buffers are: `fixedSizeTransformNode`, `sentinelTransformNode`, and `sizePrefixTransformNode`." ], "attributes": [ { - "name": "type", + "name": "offset", "type": { - "kind": "union", - "name": "typeNode" + "kind": "integer", + "width": "i64" }, "docs": [ - "The wrapped type whose extent is delimited by the sentinel." + "The signed byte offset to apply after the transformed type runs." ] }, { - "name": "sentinel", + "name": "strategy", "type": { - "kind": "node", - "name": "constantValueNode" + "kind": "enumeration", + "name": "postOffsetStrategy" }, "docs": [ - "The constant value written immediately after the wrapped type to mark its end." + "How the `offset` value is interpreted." ] } ], "examples": [ { - "title": "A UTF-8 string terminated by 0xFF", + "title": "A relative post-offset (the default strategy)", "code": [ { "language": "typescript", "content": [ - "sentinelTypeNode(stringTypeNode('utf8'), constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ff')));", - "", - "// Hello => 0x48656C6C6FFF" + "numberTypeNode('u32', { transforms: [postOffsetTransformNode(2)] });" ] } ] - } - ] - }, - { - "kind": "setTypeNode", - "docs": [ - "A unique-valued collection. The item type is defined by `item`; the size is determined by the `count` strategy." - ], - "attributes": [ - { - "name": "item", - "type": { - "kind": "union", - "name": "typeNode" - }, - "docs": [ - "The type of each item in the set." - ] }, { - "name": "count", - "type": { - "kind": "union", - "name": "countNode" - }, - "docs": [ - "The strategy used to determine the number of items." - ] - } - ], - "examples": [ - { - "title": "u32 prefixed set of u8 numbers", + "title": "An absolute post-offset from the end of the buffer", "code": [ { "language": "typescript", "content": [ - "setTypeNode(numberTypeNode('u8'), prefixedCountNode(numberTypeNode('u32')));", - "", - "// Set (1, 2, 3) => 0x03000000010203" + "numberTypeNode('u32', { transforms: [postOffsetTransformNode(-2, 'absolute')] });" ] } ] - } - ] - }, - { - "kind": "sizePrefixTypeNode", - "docs": [ - "Wraps another type with a numeric prefix indicating the byte length of the wrapped type.", - "When decoding, the size is read first and determines how many bytes the wrapped type may consume." - ], - "attributes": [ - { - "name": "type", - "type": { - "kind": "union", - "name": "typeNode" - }, - "docs": [ - "The wrapped type whose serialisation is preceded by its size." - ] }, { - "name": "prefix", - "type": { - "kind": "nestedUnion", - "alias": "nestedTypeNode", - "name": "numberTypeNode" - }, - "docs": [ - "The numeric type used as the size prefix." - ] - } - ], - "examples": [ - { - "title": "A UTF-8 string prefixed with a u16 size", + "title": "A right-padded u32 number", "code": [ { "language": "typescript", "content": [ - "sizePrefixTypeNode(stringTypeNode('utf8'), numberTypeNode('u16'));", + "numberTypeNode('u32', { transforms: [postOffsetTransformNode(4, 'padded')] });", "", - "// \"\" => 0x0000", - "// \"Hello\" => 0x050048656C6C6F" + "// 42 => 0x2A00000000000000" ] } ] - } - ] - }, - { - "kind": "solAmountTypeNode", - "docs": [ - "A SOL amount expressed in lamports under the wrapped numeric type.", - "Equivalent to an `amountTypeNode` with 9 decimals and `SOL` as the unit." - ], - "attributes": [ - { - "name": "number", - "type": { - "kind": "nestedUnion", - "alias": "nestedTypeNode", - "name": "numberTypeNode" - }, - "docs": [ - "The numeric type used to serialise the lamport amount." - ] - } - ], - "examples": [ + }, { - "title": "u64 Solana amounts", + "title": "A u32 number overwritten by a u16 number", "code": [ { "language": "typescript", "content": [ - "solAmountTypeNode(numberTypeNode('u64'));", + "tupleTypeNode([numberTypeNode('u32', { transforms: [postOffsetTransformNode(-2)] }), numberTypeNode('u16')]);", "", - "// 1.5 SOL => 0x002F685900000000", - "// 300 SOL => 0x00B864D945000000" + "// [1, 2] => 0x01000200", + "// [0xFFFFFFFF, 42] => 0xFFFF2A00" ] } ] @@ -1482,173 +1828,128 @@ ] }, { - "kind": "stringTypeNode", + "kind": "preOffsetTransformNode", "docs": [ - "A string value.", - "The encoding describes how its bytes are written.", - "The byte length is determined by an enclosing wrapper such as `sizePrefixTypeNode` or `fixedSizeTypeNode`." + "Before serialising the transformed type, advance the cursor by `offset` bytes interpreted via the chosen strategy.", + "", + "Since the offset is applied _before_ the transformed type runs, this transform is useful to move the encoded value of the transformed type itself. See `postOffsetTransformNode` for the opposite behaviour.", + "", + "The strategies below are illustrated against the following buffer: the `99` byte represents some previously encoded value for reference and the `FF` byte represents the encoded value of the transformed type, which moves as its pre-offset changes.", + "", + "```", + "0x00000099FF000000;", + " └-- Initial pre-offset", + "```", + "", + "**`relative`** — the cursor is moved to the right by the provided offset. A negative offset moves it to the left instead.", + "", + "```", + "offset = 2", + "0x000000990000FF00;", + " └-- Pre-offset", + "", + "offset = -2", + "0x0000FF9900000000;", + " └-- Pre-offset", + "```", + "", + "**`absolute`** — the cursor is moved to an absolute position in the buffer. A negative offset moves it backwards from the end of the buffer.", + "", + "```", + "offset = 0", + "0xFF00009900000000;", + " └-- Pre-offset", + "", + "offset = -2", + "0x000000990000FF00;", + " └-- Pre-offset", + "```", + "", + "**`padded`** — the cursor is moved to the right by the provided offset **and the buffer size is increased** by the offset amount, allowing padding bytes to be added. Reciprocally, a negative offset moves the cursor to the left and decreases the buffer size.", + "", + "```", + "offset = 2", + "0x000000990000FF000000; <- Size = 10 (initially 8)", + " └-- Pre-offset", + "", + "offset = -2", + "0x0000FF990000; <- Size = 6 (initially 8)", + " └-- Pre-offset", + "```", + "", + "> [!IMPORTANT]", + "> Some transforms affect the buffer that is available to us: depending on where we are in the type tree, we may not have access to the entire buffer.", + "> For instance, under a `fixedSizeTransformNode`, the buffer is truncated or padded to match the provided fixed size once the transformed content has been serialised — we are essentially \"boxed\" into a sub-buffer, and that sub-buffer is the one affected by the `absolute` strategy.", + "> The transforms that create sub-buffers are: `fixedSizeTransformNode`, `sentinelTransformNode`, and `sizePrefixTransformNode`." ], "attributes": [ { - "name": "encoding", + "name": "offset", "type": { - "kind": "enumeration", - "name": "bytesEncoding" + "kind": "integer", + "width": "i64" }, "docs": [ - "The byte encoding used to serialise the string." + "The signed byte offset to apply before the transformed type runs." ] }, { - "name": "display", + "name": "strategy", "type": { - "kind": "node", - "name": "stringDisplayNode" + "kind": "enumeration", + "name": "preOffsetStrategy" }, - "optional": true, "docs": [ - "Display metadata describing how the string is presented." + "How the `offset` value is interpreted." ] } ], "examples": [ { - "title": "Create a string type node from an encoding", + "title": "A relative pre-offset (the default strategy)", "code": [ { "language": "typescript", "content": [ - "const node = stringTypeNode('utf8');" + "numberTypeNode('u32', { transforms: [preOffsetTransformNode(2)] });" ] } - ] - } - ] - }, - { - "kind": "structFieldTypeNode", - "docs": [ - "A named field within a struct type." - ], - "attributes": [ - { - "name": "name", - "type": { - "kind": "string", - "constraint": "identifier" - }, - "docs": [ - "The name of the field." - ] - }, - { - "name": "defaultValueStrategy", - "type": { - "kind": "enumeration", - "name": "defaultValueStrategy" - }, - "optional": true, - "docs": [ - "How a configured default value is exposed in generated APIs. Required when `defaultValue` is set." - ] - }, - { - "name": "docs", - "type": { - "kind": "docs" - }, - "optional": true, - "docs": [ - "Markdown documentation for the field." - ] - }, - { - "name": "type", - "type": { - "kind": "union", - "name": "typeNode" - }, - "docs": [ - "The type of the field." - ] - }, - { - "name": "defaultValue", - "type": { - "kind": "union", - "name": "valueNode" - }, - "optional": true, - "docs": [ - "A default value used when the field is omitted by callers." - ] - }, - { - "name": "display", - "type": { - "kind": "node", - "name": "structFieldDisplayNode" - }, - "optional": true, - "docs": [ - "Display metadata describing how the field is presented." - ] - } - ], - "examples": [ + ] + }, { - "title": "A struct field with a default value", + "title": "An absolute pre-offset", "code": [ { "language": "typescript", "content": [ - "structFieldTypeNode({", - " name: 'age',", - " type: numberTypeNode('u8'),", - " defaultValue: numberValueNode(42),", - "});", - "", - "// {} => 0x2A", - "// { age: 29 } => 0x1D" + "numberTypeNode('u32', { transforms: [preOffsetTransformNode(-2, 'absolute')] });" ] } ] - } - ] - }, - { - "kind": "structTypeNode", - "docs": [ - "A composite type made of an ordered list of named fields. Fields are encoded and decoded in declaration order." - ], - "attributes": [ + }, { - "name": "fields", - "type": { - "kind": "array", - "of": { - "kind": "node", - "name": "structFieldTypeNode" + "title": "A left-padded u32 number", + "code": [ + { + "language": "typescript", + "content": [ + "numberTypeNode('u32', { transforms: [preOffsetTransformNode(4, 'padded')] });", + "", + "// 42 => 0x000000002A000000" + ] } - }, - "docs": [ - "The fields of the struct, in declaration order." ] - } - ], - "examples": [ + }, { - "title": "A struct storing a person's name and age", + "title": "A u32 number overwritten by a u16 number", "code": [ { "language": "typescript", "content": [ - "structTypeNode([", - " structFieldTypeNode({ name: 'name', type: fixedSizeTypeNode(stringTypeNode('utf8'), 10) }),", - " structFieldTypeNode({ name: 'age', type: numberTypeNode('u8') }),", - "]);", + "tupleTypeNode([numberTypeNode('u32'), numberTypeNode('u16', { transforms: [preOffsetTransformNode(-2)] })]);", "", - "// { name: Alice, age: 42 } => 0x416C69636500000000002A" + "// [1, 2] => 0x01000200", + "// [0xFFFFFFFF, 42] => 0xFFFF2A00" ] } ] @@ -1656,35 +1957,39 @@ ] }, { - "kind": "tupleTypeNode", + "kind": "sentinelTransformNode", "docs": [ - "A heterogeneous fixed-length sequence in which each positional slot has its own type." + "Delimits the transformed type with a constant sentinel value written immediately after it.", + "", + "When decoding, the transformed type is decoded until the sentinel value is encountered, at which point decoding stops and the sentinel is discarded.", + "", + "> [!IMPORTANT]", + "> For this transform to work, the sentinel value must never occur within the encoded bytes of the transformed type." ], "attributes": [ { - "name": "items", + "name": "sentinel", "type": { - "kind": "array", - "of": { - "kind": "union", - "name": "typeNode" - } + "kind": "node", + "name": "constantValueNode" }, "docs": [ - "The type of each positional slot, in order." + "The constant value written immediately after the transformed type to mark its end." ] } ], "examples": [ { - "title": "A tuple storing a person's name and age", + "title": "A UTF-8 string terminated by 0xFF", "code": [ { "language": "typescript", "content": [ - "tupleTypeNode([fixedSizeTypeNode(stringTypeNode('utf8'), 10), numberTypeNode('u8')]);", + "stringTypeNode('utf8', {", + " transforms: [sentinelTransformNode(constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ff')))],", + "});", "", - "// (Alice, 42) => 0x416C69636500000000002A" + "// Hello => 0x48656C6C6FFF" ] } ] @@ -1692,58 +1997,34 @@ ] }, { - "kind": "zeroableOptionTypeNode", + "kind": "sizePrefixTransformNode", "docs": [ - "An optional value whose absence is signalled by a designated zero value rather than a presence flag." + "Precedes the transformed type with a numeric prefix indicating its byte length.", + "When decoding, the size is read first and determines how many bytes the transformed type may consume." ], "attributes": [ { - "name": "item", - "type": { - "kind": "union", - "name": "typeNode" - }, - "docs": [ - "The type carried by the option when present. Must be of fixed size." - ] - }, - { - "name": "zeroValue", + "name": "prefix", "type": { "kind": "node", - "name": "constantValueNode" + "name": "numberTypeNode" }, - "optional": true, "docs": [ - "The constant value that signals absence. When omitted, the all-zero byte pattern of the item type is used." + "The numeric type used as the size prefix." ] } ], "examples": [ { - "title": "a u32 zeroable option", - "code": [ - { - "language": "typescript", - "content": [ - "zeroableOptionTypeNode(numberTypeNode('u32'));", - "", - "// None => 0x00000000", - "// Some(42) => 0x2A000000" - ] - } - ] - }, - { - "title": "a u32 zeroable option with a custom zero value", + "title": "A UTF-8 string prefixed with a u16 size", "code": [ { "language": "typescript", "content": [ - "zeroableOptionTypeNode(numberTypeNode('u32'), constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffffffff')));", + "stringTypeNode('utf8', { transforms: [sizePrefixTransformNode(numberTypeNode('u16'))] });", "", - "// None => 0xFFFFFFFF", - "// Some(42) => 0x2A000000" + "// \"\" => 0x0000", + "// \"Hello\" => 0x050048656C6C6F" ] } ] @@ -1753,190 +2034,45 @@ ], "unions": [ { - "name": "standaloneTypeNode", + "name": "transformNode", "members": [ { "kind": "node", - "name": "amountTypeNode" - }, - { - "kind": "node", - "name": "arrayTypeNode" - }, - { - "kind": "node", - "name": "booleanTypeNode" - }, - { - "kind": "node", - "name": "bytesTypeNode" - }, - { - "kind": "node", - "name": "dateTimeTypeNode" - }, - { - "kind": "node", - "name": "enumTypeNode" - }, - { - "kind": "node", - "name": "fixedSizeTypeNode" - }, - { - "kind": "node", - "name": "hiddenPrefixTypeNode" - }, - { - "kind": "node", - "name": "hiddenSuffixTypeNode" - }, - { - "kind": "node", - "name": "mapTypeNode" - }, - { - "kind": "node", - "name": "numberTypeNode" - }, - { - "kind": "node", - "name": "optionTypeNode" - }, - { - "kind": "node", - "name": "postOffsetTypeNode" - }, - { - "kind": "node", - "name": "preOffsetTypeNode" + "name": "fixedSizeTransformNode" }, { "kind": "node", - "name": "publicKeyTypeNode" - }, - { - "kind": "node", - "name": "remainderOptionTypeNode" - }, - { - "kind": "node", - "name": "sentinelTypeNode" - }, - { - "kind": "node", - "name": "setTypeNode" - }, - { - "kind": "node", - "name": "sizePrefixTypeNode" - }, - { - "kind": "node", - "name": "solAmountTypeNode" - }, - { - "kind": "node", - "name": "stringTypeNode" - }, - { - "kind": "node", - "name": "structTypeNode" - }, - { - "kind": "node", - "name": "tupleTypeNode" + "name": "hiddenPrefixTransformNode" }, { "kind": "node", - "name": "zeroableOptionTypeNode" - } - ], - "docs": [ - "Every type node that can be used as a top-level type." - ] - }, - { - "name": "enumVariantTypeNode", - "members": [ - { - "kind": "node", - "name": "enumEmptyVariantTypeNode" + "name": "hiddenSuffixTransformNode" }, { "kind": "node", - "name": "enumStructVariantTypeNode" + "name": "postOffsetTransformNode" }, { "kind": "node", - "name": "enumTupleVariantTypeNode" - } - ], - "docs": [ - "The variant flavours of an `enumTypeNode`." - ] - }, - { - "name": "typeNode", - "members": [ - { - "kind": "union", - "name": "standaloneTypeNode" + "name": "preOffsetTransformNode" }, { "kind": "node", - "name": "definedTypeLinkNode" - } - ], - "docs": [ - "The composable form: any standalone type, or a reference to a defined type via `definedTypeLinkNode`." - ] - }, - { - "name": "registeredTypeNode", - "members": [ - { - "kind": "union", - "name": "standaloneTypeNode" - }, - { - "kind": "union", - "name": "enumVariantTypeNode" + "name": "sentinelTransformNode" }, { "kind": "node", - "name": "structFieldTypeNode" + "name": "sizePrefixTransformNode" } ], "docs": [ - "Every node tagged as a type-shaped node, including variants and struct fields." + "A modifier applied to the serialisation of the type node that carries it.", + "Every type node has an optional `transforms` array. Transforms apply in array order, the first being the innermost: a `stringTypeNode` with `transforms: [sentinel, fixedSize]` first delimits the string with the sentinel, then fixes the total byte size — exactly the v1 nesting `fixedSizeTypeNode(sentinelTypeNode(stringTypeNode))` read inside-out." ] } ], "enumerations": [], - "nestedUnions": [ - { - "name": "nestedTypeNode", - "docs": [ - "A type, possibly wrapped in zero-or-more size, offset, sentinel, or hidden prefix/suffix modifiers.", - "The wrapping is recursive: each modifier wraps another `nestedTypeNode` until the inner `T` is reached.", - "For example, a `nestedTypeNode` can be fulfilled by a plain `stringTypeNode`, by a `fixedSizeTypeNode` wrapping a `stringTypeNode`, or by any deeper nesting such as `hiddenPrefixTypeNode>>`." - ], - "base": { - "kind": "union", - "name": "typeNode" - }, - "wrappers": [ - "fixedSizeTypeNode", - "sizePrefixTypeNode", - "preOffsetTypeNode", - "postOffsetTypeNode", - "sentinelTypeNode", - "hiddenPrefixTypeNode", - "hiddenSuffixTypeNode" - ] - } - ] + "nestedUnions": [] }, { "name": "value", @@ -2849,6 +2985,20 @@ "docs": [ "The name of the referenced defined type." ] + }, + { + "name": "transforms", + "type": { + "kind": "array", + "of": { + "kind": "union", + "name": "transformNode" + } + }, + "optional": true, + "docs": [ + "Transforms applied to the serialisation of this type, in order — the first is the innermost." + ] } ], "examples": [ @@ -3381,8 +3531,7 @@ { "name": "prefix", "type": { - "kind": "nestedUnion", - "alias": "nestedTypeNode", + "kind": "node", "name": "numberTypeNode" }, "docs": [ @@ -3635,7 +3784,7 @@ " arguments: [", " instructionArgumentNode({", " name: 'discriminator',", - " type: fixedSizeTypeNode(bytesTypeNode(), 8),", + " type: bytesTypeNode({ transforms: [fixedSizeTransformNode(8)] }),", " defaultValue: bytesValueNode('base16', '0011223344556677'),", " defaultValueStrategy: 'omitted',", " }),", @@ -5486,7 +5635,7 @@ { "name": "preOffset", "docs": [ - "Move the cursor by the offset bytes relative to the pre-offset — where the wrapped type started — rather than where it ended; a negative offset moves it to the left of that position." + "Move the cursor by the offset bytes relative to the pre-offset — where the transformed type started — rather than where it ended; a negative offset moves it to the left of that position." ] }, { @@ -5497,8 +5646,8 @@ } ], "docs": [ - "How a post-offset modifier interprets its offset value after serialising the wrapped type.", - "See `postOffsetTypeNode` for an illustrated walkthrough of each strategy." + "How a post-offset transform interprets its offset value after serialising the transformed type.", + "See `postOffsetTransformNode` for an illustrated walkthrough of each strategy." ] }, { @@ -5524,8 +5673,8 @@ } ], "docs": [ - "How a pre-offset modifier interprets its offset value before serialising the wrapped type.", - "See `preOffsetTypeNode` for an illustrated walkthrough of each strategy." + "How a pre-offset transform interprets its offset value before serialising the transformed type.", + "See `preOffsetTransformNode` for an illustrated walkthrough of each strategy." ] }, { @@ -5599,8 +5748,7 @@ { "name": "data", "type": { - "kind": "nestedUnion", - "alias": "nestedTypeNode", + "kind": "node", "name": "structTypeNode" }, "docs": [ @@ -5975,12 +6123,16 @@ "content": [ "eventNode({", " name: 'transferEvent',", - " data: hiddenPrefixTypeNode(structTypeNode([structFieldTypeNode({ name: 'amount', type: numberTypeNode('u64') })]), [", - " constantValueNode(fixedSizeTypeNode(bytesTypeNode(), 8), bytesValueNode('base16', '0102030405060708')),", - " ]),", + " data: structTypeNode([structFieldTypeNode({ name: 'amount', type: numberTypeNode('u64') })], {", + " transforms: [", + " hiddenPrefixTransformNode([", + " constantValueNode(bytesTypeNode({ transforms: [fixedSizeTransformNode(8)] }), bytesValueNode('base16', '0102030405060708')),", + " ]),", + " ],", + " }),", " discriminators: [", " constantDiscriminatorNode(", - " constantValueNode(fixedSizeTypeNode(bytesTypeNode(), 8), bytesValueNode('base16', '0102030405060708')),", + " constantValueNode(bytesTypeNode({ transforms: [fixedSizeTransformNode(8)] }), bytesValueNode('base16', '0102030405060708')),", " ),", " ],", "});" diff --git a/src/spec/enumerations.ts b/src/spec/enumerations.ts index aab3aaa..b8cbddf 100644 --- a/src/spec/enumerations.ts +++ b/src/spec/enumerations.ts @@ -117,8 +117,8 @@ export const optionalAccountStrategy = defineEnumeration('optionalAccountStrateg export const preOffsetStrategy = defineEnumeration('preOffsetStrategy', { docs: [ - 'How a pre-offset modifier interprets its offset value before serialising the wrapped type.', - 'See `preOffsetTypeNode` for an illustrated walkthrough of each strategy.', + 'How a pre-offset transform interprets its offset value before serialising the transformed type.', + 'See `preOffsetTransformNode` for an illustrated walkthrough of each strategy.', ], variants: [ variant('absolute', { @@ -141,8 +141,8 @@ export const preOffsetStrategy = defineEnumeration('preOffsetStrategy', { export const postOffsetStrategy = defineEnumeration('postOffsetStrategy', { docs: [ - 'How a post-offset modifier interprets its offset value after serialising the wrapped type.', - 'See `postOffsetTypeNode` for an illustrated walkthrough of each strategy.', + 'How a post-offset transform interprets its offset value after serialising the transformed type.', + 'See `postOffsetTransformNode` for an illustrated walkthrough of each strategy.', ], variants: [ variant('absolute', { @@ -157,7 +157,7 @@ export const postOffsetStrategy = defineEnumeration('postOffsetStrategy', { }), variant('preOffset', { docs: [ - 'Move the cursor by the offset bytes relative to the pre-offset — where the wrapped type started — rather than where it ended; a negative offset moves it to the left of that position.', + 'Move the cursor by the offset bytes relative to the pre-offset — where the transformed type started — rather than where it ended; a negative offset moves it to the left of that position.', ], }), variant('relative', { diff --git a/src/spec/index.ts b/src/spec/index.ts index 006c0ec..84db0c0 100644 --- a/src/spec/index.ts +++ b/src/spec/index.ts @@ -12,7 +12,6 @@ import type { CategorySpec, EnumerationSpec, NodeSpec, Spec, UnionSpec } from '../api'; import { array, defineBase, defineCategory, node, optionalAttribute, validate } from '../api'; import { ALL_ENUMERATIONS } from './enumerations'; -import { nestedTypeNode } from './nestedUnions'; import { accountNode } from './nodes/AccountNode'; import { constantNode } from './nodes/ConstantNode'; import { ALL_CONTEXTUAL_VALUE_NODE_UNIONS, ALL_CONTEXTUAL_VALUE_NODES } from './nodes/contextualValueNodes'; @@ -36,6 +35,7 @@ import { pluginNode } from './nodes/PluginNode'; import { programNode } from './nodes/ProgramNode'; import { providedNode } from './nodes/ProvidedNode'; import { rootNode } from './nodes/RootNode'; +import { ALL_TRANSFORM_NODE_UNIONS, ALL_TRANSFORM_NODES } from './nodes/transformNodes'; import { ALL_TYPE_NODE_UNIONS, ALL_TYPE_NODES } from './nodes/typeNodes'; import { ALL_VALUE_NODE_UNIONS, ALL_VALUE_NODES } from './nodes/valueNodes'; import { SPEC_VERSION } from './version'; @@ -60,11 +60,19 @@ const BASE = defineBase({ const TYPE_CATEGORY = defineCategory('type', { docs: ['Type nodes — the building blocks of every value shape.'], - nestedUnions: [nestedTypeNode], nodes: [...ALL_TYPE_NODES], unions: [...ALL_TYPE_NODE_UNIONS], }); +const TRANSFORM_CATEGORY = defineCategory('transform', { + docs: [ + 'Transform nodes — modifiers applied to the serialisation of the type node that carries them.', + 'Every type node has an optional `transforms` array; transforms apply in array order, the first being the innermost.', + ], + nodes: [...ALL_TRANSFORM_NODES], + unions: [...ALL_TRANSFORM_NODE_UNIONS], +}); + const VALUE_CATEGORY = defineCategory('value', { docs: ['Value nodes — concrete values whose shape is described by a type node.'], nodes: [...ALL_VALUE_NODES], @@ -139,6 +147,7 @@ const TOP_LEVEL_CATEGORY = defineCategory('topLevel', { const ALL_CATEGORIES: readonly CategorySpec[] = [ TYPE_CATEGORY, + TRANSFORM_CATEGORY, VALUE_CATEGORY, LINK_CATEGORY, PDA_SEED_CATEGORY, diff --git a/src/spec/nestedUnions.ts b/src/spec/nestedUnions.ts deleted file mode 100644 index 317acd5..0000000 --- a/src/spec/nestedUnions.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Nested-union recursive aliases declared by the spec. - * - * Today there's only one — `nestedTypeNode` — used by attributes like - * `accountNode.data` (which holds a `structTypeNode` possibly wrapped in - * size, offset, sentinel, or hidden prefix/suffix modifiers). - */ - -import { defineNestedUnion, union } from '../api'; - -export const nestedTypeNode = defineNestedUnion('nestedTypeNode', { - docs: [ - 'A type, possibly wrapped in zero-or-more size, offset, sentinel, or hidden prefix/suffix modifiers.', - 'The wrapping is recursive: each modifier wraps another `nestedTypeNode` until the inner `T` is reached.', - 'For example, a `nestedTypeNode` can be fulfilled by a plain `stringTypeNode`, by a `fixedSizeTypeNode` wrapping a `stringTypeNode`, or by any deeper nesting such as `hiddenPrefixTypeNode>>`.', - ], - base: union('typeNode'), - wrappers: [ - 'fixedSizeTypeNode', - 'sizePrefixTypeNode', - 'preOffsetTypeNode', - 'postOffsetTypeNode', - 'sentinelTypeNode', - 'hiddenPrefixTypeNode', - 'hiddenSuffixTypeNode', - ], -}); diff --git a/src/spec/nodes/AccountNode.ts b/src/spec/nodes/AccountNode.ts index 31a417e..d4276d8 100644 --- a/src/spec/nodes/AccountNode.ts +++ b/src/spec/nodes/AccountNode.ts @@ -4,7 +4,6 @@ import { byteSize, defineNode, docs, - nestedUnion, node, optionalAttribute, stringIdentifier, @@ -28,7 +27,7 @@ export const accountNode = defineNode('accountNode', { optionalAttribute('docs', docs(), { docs: ['Markdown documentation for the account.'], }), - attribute('data', nestedUnion('nestedTypeNode', 'structTypeNode'), { + attribute('data', node('structTypeNode'), { docs: [ 'The struct describing the account data.', 'It must be a struct so its fields can be referenced by other nodes — e.g. `accountFieldValueNode`.', diff --git a/src/spec/nodes/EventNode.examples.ts b/src/spec/nodes/EventNode.examples.ts index 359c5e2..a8696d6 100644 --- a/src/spec/nodes/EventNode.examples.ts +++ b/src/spec/nodes/EventNode.examples.ts @@ -23,12 +23,16 @@ eventNode({ ` eventNode({ name: 'transferEvent', - data: hiddenPrefixTypeNode(structTypeNode([structFieldTypeNode({ name: 'amount', type: numberTypeNode('u64') })]), [ - constantValueNode(fixedSizeTypeNode(bytesTypeNode(), 8), bytesValueNode('base16', '0102030405060708')), - ]), + data: structTypeNode([structFieldTypeNode({ name: 'amount', type: numberTypeNode('u64') })], { + transforms: [ + hiddenPrefixTransformNode([ + constantValueNode(bytesTypeNode({ transforms: [fixedSizeTransformNode(8)] }), bytesValueNode('base16', '0102030405060708')), + ]), + ], + }), discriminators: [ constantDiscriminatorNode( - constantValueNode(fixedSizeTypeNode(bytesTypeNode(), 8), bytesValueNode('base16', '0102030405060708')), + constantValueNode(bytesTypeNode({ transforms: [fixedSizeTransformNode(8)] }), bytesValueNode('base16', '0102030405060708')), ), ], }); diff --git a/src/spec/nodes/countNodes/PrefixedCountNode.ts b/src/spec/nodes/countNodes/PrefixedCountNode.ts index 7a1400b..ef379eb 100644 --- a/src/spec/nodes/countNodes/PrefixedCountNode.ts +++ b/src/spec/nodes/countNodes/PrefixedCountNode.ts @@ -1,4 +1,4 @@ -import { attribute, defineNode, nestedUnion } from '../../../api'; +import { attribute, defineNode, node } from '../../../api'; import { examples } from './PrefixedCountNode.examples'; export const prefixedCountNode = defineNode('prefixedCountNode', { @@ -7,7 +7,7 @@ export const prefixedCountNode = defineNode('prefixedCountNode', { 'This enables nodes such as `arrayTypeNode` to represent collections whose length is stored as a prefix.', ], attributes: [ - attribute('prefix', nestedUnion('nestedTypeNode', 'numberTypeNode'), { + attribute('prefix', node('numberTypeNode'), { docs: ['The numeric type used as the count prefix.'], }), ], diff --git a/src/spec/nodes/discriminatorNodes/FieldDiscriminatorNode.examples.ts b/src/spec/nodes/discriminatorNodes/FieldDiscriminatorNode.examples.ts index 4f618b2..205eef4 100644 --- a/src/spec/nodes/discriminatorNodes/FieldDiscriminatorNode.examples.ts +++ b/src/spec/nodes/discriminatorNodes/FieldDiscriminatorNode.examples.ts @@ -40,7 +40,7 @@ instructionNode({ arguments: [ instructionArgumentNode({ name: 'discriminator', - type: fixedSizeTypeNode(bytesTypeNode(), 8), + type: bytesTypeNode({ transforms: [fixedSizeTransformNode(8)] }), defaultValue: bytesValueNode('base16', '0011223344556677'), defaultValueStrategy: 'omitted', }), diff --git a/src/spec/nodes/linkNodes/DefinedTypeLinkNode.ts b/src/spec/nodes/linkNodes/DefinedTypeLinkNode.ts index d9d8165..9222dd6 100644 --- a/src/spec/nodes/linkNodes/DefinedTypeLinkNode.ts +++ b/src/spec/nodes/linkNodes/DefinedTypeLinkNode.ts @@ -1,4 +1,5 @@ import { attribute, defineNode, node, optionalAttribute, stringIdentifier } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './DefinedTypeLinkNode.examples'; export const definedTypeLinkNode = defineNode('definedTypeLinkNode', { @@ -10,6 +11,7 @@ export const definedTypeLinkNode = defineNode('definedTypeLinkNode', { attribute('name', stringIdentifier(), { docs: ['The name of the referenced defined type.'], }), + transformsAttribute(), ], examples, }); diff --git a/src/spec/nodes/typeNodes/FixedSizeTypeNode.examples.ts b/src/spec/nodes/transformNodes/FixedSizeTransformNode.examples.ts similarity index 58% rename from src/spec/nodes/typeNodes/FixedSizeTypeNode.examples.ts rename to src/spec/nodes/transformNodes/FixedSizeTransformNode.examples.ts index 5f0f74a..5f022d7 100644 --- a/src/spec/nodes/typeNodes/FixedSizeTypeNode.examples.ts +++ b/src/spec/nodes/transformNodes/FixedSizeTransformNode.examples.ts @@ -1,21 +1,12 @@ import { code, example, type DocExamples } from '../../../api'; export const examples: DocExamples = [ - example( - 'Create a fixed size type node from a type node and a byte length', - code( - 'typescript', - ` -const node = fixedSizeTypeNode(stringTypeNode('utf8'), 32); -`, - ), - ), example( 'Fixed UTF-8 strings', code( 'typescript', ` -fixedSizeTypeNode(stringTypeNode('utf8'), 10); +stringTypeNode('utf8', { transforms: [fixedSizeTransformNode(10)] }); // Hello => 0x48656C6C6F0000000000 `, @@ -26,7 +17,7 @@ fixedSizeTypeNode(stringTypeNode('utf8'), 10); code( 'typescript', ` -fixedSizeTypeNode(bytesTypeNode(), 4); +bytesTypeNode({ transforms: [fixedSizeTransformNode(4)] }); // [1, 2] => 0x01020000 // [1, 2, 3, 4, 5] => 0x01020304 diff --git a/src/spec/nodes/transformNodes/FixedSizeTransformNode.ts b/src/spec/nodes/transformNodes/FixedSizeTransformNode.ts new file mode 100644 index 0000000..65fb366 --- /dev/null +++ b/src/spec/nodes/transformNodes/FixedSizeTransformNode.ts @@ -0,0 +1,12 @@ +import { attribute, byteSize, defineNode } from '../../../api'; +import { examples } from './FixedSizeTransformNode.examples'; + +export const fixedSizeTransformNode = defineNode('fixedSizeTransformNode', { + docs: ['Asserts a fixed total byte size for the transformed type. Padding or truncation is applied as needed.'], + attributes: [ + attribute('size', byteSize(), { + docs: ['The total byte size the transformed type must occupy.'], + }), + ], + examples, +}); diff --git a/src/spec/nodes/transformNodes/HiddenPrefixTransformNode.examples.ts b/src/spec/nodes/transformNodes/HiddenPrefixTransformNode.examples.ts new file mode 100644 index 0000000..5cc21d3 --- /dev/null +++ b/src/spec/nodes/transformNodes/HiddenPrefixTransformNode.examples.ts @@ -0,0 +1,33 @@ +import { code, example, type DocExamples } from '../../../api'; + +export const examples: DocExamples = [ + example( + 'A number prefixed with 0xFFFF', + code( + 'typescript', + ` +numberTypeNode('u32', { + transforms: [hiddenPrefixTransformNode([constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff'))])], +}); + +// 42 => 0xFFFF2A000000 +`, + ), + ), + example( + 'A fixed UTF-8 string prefixed with "Hello"', + code( + 'typescript', + ` +stringTypeNode('utf8', { + transforms: [ + fixedSizeTransformNode(10), + hiddenPrefixTransformNode([constantValueNode(stringTypeNode('utf8'), stringValueNode('Hello'))]), + ], +}); + +// World => 0x48656C6C6F576F726C640000000000 +`, + ), + ), +]; diff --git a/src/spec/nodes/transformNodes/HiddenPrefixTransformNode.ts b/src/spec/nodes/transformNodes/HiddenPrefixTransformNode.ts new file mode 100644 index 0000000..c0b3829 --- /dev/null +++ b/src/spec/nodes/transformNodes/HiddenPrefixTransformNode.ts @@ -0,0 +1,15 @@ +import { array, attribute, defineNode, node } from '../../../api'; +import { examples } from './HiddenPrefixTransformNode.examples'; + +export const hiddenPrefixTransformNode = defineNode('hiddenPrefixTransformNode', { + docs: [ + 'Prefixes the transformed type with a list of constant values that are written and read but not surfaced as fields to consumers.', + 'When decoding, the prefixed constants are consumed and checked against their expected values before being discarded.', + ], + attributes: [ + attribute('prefix', array(node('constantValueNode')), { + docs: ['The constant values written before the transformed type, in order.'], + }), + ], + examples, +}); diff --git a/src/spec/nodes/transformNodes/HiddenSuffixTransformNode.examples.ts b/src/spec/nodes/transformNodes/HiddenSuffixTransformNode.examples.ts new file mode 100644 index 0000000..f7d223b --- /dev/null +++ b/src/spec/nodes/transformNodes/HiddenSuffixTransformNode.examples.ts @@ -0,0 +1,33 @@ +import { code, example, type DocExamples } from '../../../api'; + +export const examples: DocExamples = [ + example( + 'A number suffixed with 0xFFFF', + code( + 'typescript', + ` +numberTypeNode('u32', { + transforms: [hiddenSuffixTransformNode([constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff'))])], +}); + +// 42 => 0x2A000000FFFF +`, + ), + ), + example( + 'A fixed UTF-8 string suffixed with "Hello"', + code( + 'typescript', + ` +stringTypeNode('utf8', { + transforms: [ + fixedSizeTransformNode(10), + hiddenSuffixTransformNode([constantValueNode(stringTypeNode('utf8'), stringValueNode('Hello'))]), + ], +}); + +// World => 0x576F726C64000000000048656c6c6F +`, + ), + ), +]; diff --git a/src/spec/nodes/transformNodes/HiddenSuffixTransformNode.ts b/src/spec/nodes/transformNodes/HiddenSuffixTransformNode.ts new file mode 100644 index 0000000..48a2509 --- /dev/null +++ b/src/spec/nodes/transformNodes/HiddenSuffixTransformNode.ts @@ -0,0 +1,15 @@ +import { array, attribute, defineNode, node } from '../../../api'; +import { examples } from './HiddenSuffixTransformNode.examples'; + +export const hiddenSuffixTransformNode = defineNode('hiddenSuffixTransformNode', { + docs: [ + 'Suffixes the transformed type with a list of constant values that are written and read but not surfaced as fields to consumers.', + 'When decoding, the suffixed constants are consumed and checked against their expected values before being discarded.', + ], + attributes: [ + attribute('suffix', array(node('constantValueNode')), { + docs: ['The constant values written after the transformed type, in order.'], + }), + ], + examples, +}); diff --git a/src/spec/nodes/typeNodes/PostOffsetTypeNode.examples.ts b/src/spec/nodes/transformNodes/PostOffsetTransformNode.examples.ts similarity index 69% rename from src/spec/nodes/typeNodes/PostOffsetTypeNode.examples.ts rename to src/spec/nodes/transformNodes/PostOffsetTransformNode.examples.ts index 1134abd..e8fed21 100644 --- a/src/spec/nodes/typeNodes/PostOffsetTypeNode.examples.ts +++ b/src/spec/nodes/transformNodes/PostOffsetTransformNode.examples.ts @@ -6,7 +6,7 @@ export const examples: DocExamples = [ code( 'typescript', ` -postOffsetTypeNode(numberTypeNode('u32'), 2); +numberTypeNode('u32', { transforms: [postOffsetTransformNode(2)] }); `, ), ), @@ -15,7 +15,7 @@ postOffsetTypeNode(numberTypeNode('u32'), 2); code( 'typescript', ` -postOffsetTypeNode(numberTypeNode('u32'), -2, 'absolute'); +numberTypeNode('u32', { transforms: [postOffsetTransformNode(-2, 'absolute')] }); `, ), ), @@ -24,7 +24,7 @@ postOffsetTypeNode(numberTypeNode('u32'), -2, 'absolute'); code( 'typescript', ` -postOffsetTypeNode(numberTypeNode('u32'), 4, 'padded'); +numberTypeNode('u32', { transforms: [postOffsetTransformNode(4, 'padded')] }); // 42 => 0x2A00000000000000 `, @@ -35,7 +35,7 @@ postOffsetTypeNode(numberTypeNode('u32'), 4, 'padded'); code( 'typescript', ` -tupleTypeNode([postOffsetTypeNode(numberTypeNode('u32'), -2), numberTypeNode('u16')]); +tupleTypeNode([numberTypeNode('u32', { transforms: [postOffsetTransformNode(-2)] }), numberTypeNode('u16')]); // [1, 2] => 0x01000200 // [0xFFFFFFFF, 42] => 0xFFFF2A00 diff --git a/src/spec/nodes/typeNodes/PostOffsetTypeNode.ts b/src/spec/nodes/transformNodes/PostOffsetTransformNode.ts similarity index 59% rename from src/spec/nodes/typeNodes/PostOffsetTypeNode.ts rename to src/spec/nodes/transformNodes/PostOffsetTransformNode.ts index 07bd2fb..4be3083 100644 --- a/src/spec/nodes/typeNodes/PostOffsetTypeNode.ts +++ b/src/spec/nodes/transformNodes/PostOffsetTransformNode.ts @@ -1,13 +1,13 @@ -import { attribute, defineNode, enumeration, i64, union } from '../../../api'; -import { examples } from './PostOffsetTypeNode.examples'; +import { attribute, byteOffset, defineNode, enumeration } from '../../../api'; +import { examples } from './PostOffsetTransformNode.examples'; -export const postOffsetTypeNode = defineNode('postOffsetTypeNode', { +export const postOffsetTransformNode = defineNode('postOffsetTransformNode', { docs: [ - 'After serialising the wrapped type, advance the cursor by `offset` bytes interpreted via the chosen strategy.', + 'After serialising the transformed type, advance the cursor by `offset` bytes interpreted via the chosen strategy.', '', - 'Since the offset is applied _after_ the wrapped type runs, this node is useful to move the cursor around once the wrapped type has been processed. See `preOffsetTypeNode` for the opposite behaviour.', + 'Since the offset is applied _after_ the transformed type runs, this transform is useful to move the cursor around once the transformed type has been processed. See `preOffsetTransformNode` for the opposite behaviour.', '', - 'The strategies below are illustrated against the following buffer: the `99` byte represents the encoded value of the wrapped type and the `FF` byte represents the next bytes to be encoded after it, in order to show the _post_ cursor position.', + 'The strategies below are illustrated against the following buffer: the `99` byte represents the encoded value of the transformed type and the `FF` byte represents the next bytes to be encoded after it, in order to show the _post_ cursor position.', '', '```', '0x00000099FF000000;', @@ -51,7 +51,7 @@ export const postOffsetTypeNode = defineNode('postOffsetTypeNode', { ' └-- Post-offset', '```', '', - '**`preOffset`** — the cursor is moved to the right of the pre-offset — i.e. where the wrapped type started — by the provided offset. A negative offset moves it to the left of the pre-offset instead.', + '**`preOffset`** — the cursor is moved to the right of the pre-offset — i.e. where the transformed type started — by the provided offset. A negative offset moves it to the left of the pre-offset instead.', '', '```', 'offset = 2', @@ -66,20 +66,17 @@ export const postOffsetTypeNode = defineNode('postOffsetTypeNode', { '```', '', '> [!IMPORTANT]', - '> Some type nodes affect the buffer that is available to us: depending on where we are in the type tree, we may not have access to the entire buffer.', - '> For instance, inside a `fixedSizeTypeNode`, the buffer is truncated or padded to match the provided fixed size once the wrapped content has been serialised — we are essentially "boxed" into a sub-buffer, and that sub-buffer is the one affected by the `absolute` strategy.', - '> The type nodes that create sub-buffers are: `fixedSizeTypeNode`, `sentinelTypeNode`, and `sizePrefixTypeNode`.', + '> Some transforms affect the buffer that is available to us: depending on where we are in the type tree, we may not have access to the entire buffer.', + '> For instance, under a `fixedSizeTransformNode`, the buffer is truncated or padded to match the provided fixed size once the transformed content has been serialised — we are essentially "boxed" into a sub-buffer, and that sub-buffer is the one affected by the `absolute` strategy.', + '> The transforms that create sub-buffers are: `fixedSizeTransformNode`, `sentinelTransformNode`, and `sizePrefixTransformNode`.', ], attributes: [ - attribute('offset', i64(), { - docs: ['The signed byte offset to apply after the wrapped type runs.'], + attribute('offset', byteOffset(), { + docs: ['The signed byte offset to apply after the transformed type runs.'], }), attribute('strategy', enumeration('postOffsetStrategy'), { docs: ['How the `offset` value is interpreted.'], }), - attribute('type', union('typeNode'), { - docs: ['The wrapped type whose serialisation is followed by the offset.'], - }), ], examples, }); diff --git a/src/spec/nodes/typeNodes/PreOffsetTypeNode.examples.ts b/src/spec/nodes/transformNodes/PreOffsetTransformNode.examples.ts similarity index 68% rename from src/spec/nodes/typeNodes/PreOffsetTypeNode.examples.ts rename to src/spec/nodes/transformNodes/PreOffsetTransformNode.examples.ts index dfa2658..6745fdc 100644 --- a/src/spec/nodes/typeNodes/PreOffsetTypeNode.examples.ts +++ b/src/spec/nodes/transformNodes/PreOffsetTransformNode.examples.ts @@ -6,7 +6,7 @@ export const examples: DocExamples = [ code( 'typescript', ` -preOffsetTypeNode(numberTypeNode('u32'), 2); +numberTypeNode('u32', { transforms: [preOffsetTransformNode(2)] }); `, ), ), @@ -15,7 +15,7 @@ preOffsetTypeNode(numberTypeNode('u32'), 2); code( 'typescript', ` -preOffsetTypeNode(numberTypeNode('u32'), -2, 'absolute'); +numberTypeNode('u32', { transforms: [preOffsetTransformNode(-2, 'absolute')] }); `, ), ), @@ -24,7 +24,7 @@ preOffsetTypeNode(numberTypeNode('u32'), -2, 'absolute'); code( 'typescript', ` -preOffsetTypeNode(numberTypeNode('u32'), 4, 'padded'); +numberTypeNode('u32', { transforms: [preOffsetTransformNode(4, 'padded')] }); // 42 => 0x000000002A000000 `, @@ -35,7 +35,7 @@ preOffsetTypeNode(numberTypeNode('u32'), 4, 'padded'); code( 'typescript', ` -tupleTypeNode([numberTypeNode('u32'), preOffsetTypeNode(numberTypeNode('u16'), -2)]); +tupleTypeNode([numberTypeNode('u32'), numberTypeNode('u16', { transforms: [preOffsetTransformNode(-2)] })]); // [1, 2] => 0x01000200 // [0xFFFFFFFF, 42] => 0xFFFF2A00 diff --git a/src/spec/nodes/typeNodes/PreOffsetTypeNode.ts b/src/spec/nodes/transformNodes/PreOffsetTransformNode.ts similarity index 61% rename from src/spec/nodes/typeNodes/PreOffsetTypeNode.ts rename to src/spec/nodes/transformNodes/PreOffsetTransformNode.ts index 39329ca..0eb6b42 100644 --- a/src/spec/nodes/typeNodes/PreOffsetTypeNode.ts +++ b/src/spec/nodes/transformNodes/PreOffsetTransformNode.ts @@ -1,13 +1,13 @@ -import { attribute, defineNode, enumeration, i64, union } from '../../../api'; -import { examples } from './PreOffsetTypeNode.examples'; +import { attribute, byteOffset, defineNode, enumeration } from '../../../api'; +import { examples } from './PreOffsetTransformNode.examples'; -export const preOffsetTypeNode = defineNode('preOffsetTypeNode', { +export const preOffsetTransformNode = defineNode('preOffsetTransformNode', { docs: [ - 'Before serialising the wrapped type, advance the cursor by `offset` bytes interpreted via the chosen strategy.', + 'Before serialising the transformed type, advance the cursor by `offset` bytes interpreted via the chosen strategy.', '', - 'Since the offset is applied _before_ the wrapped type runs, this node is useful to move the encoded value of the wrapped type itself. See `postOffsetTypeNode` for the opposite behaviour.', + 'Since the offset is applied _before_ the transformed type runs, this transform is useful to move the encoded value of the transformed type itself. See `postOffsetTransformNode` for the opposite behaviour.', '', - 'The strategies below are illustrated against the following buffer: the `99` byte represents some previously encoded value for reference and the `FF` byte represents the encoded value of the wrapped type, which moves as its pre-offset changes.', + 'The strategies below are illustrated against the following buffer: the `99` byte represents some previously encoded value for reference and the `FF` byte represents the encoded value of the transformed type, which moves as its pre-offset changes.', '', '```', '0x00000099FF000000;', @@ -51,20 +51,17 @@ export const preOffsetTypeNode = defineNode('preOffsetTypeNode', { '```', '', '> [!IMPORTANT]', - '> Some type nodes affect the buffer that is available to us: depending on where we are in the type tree, we may not have access to the entire buffer.', - '> For instance, inside a `fixedSizeTypeNode`, the buffer is truncated or padded to match the provided fixed size once the wrapped content has been serialised — we are essentially "boxed" into a sub-buffer, and that sub-buffer is the one affected by the `absolute` strategy.', - '> The type nodes that create sub-buffers are: `fixedSizeTypeNode`, `sentinelTypeNode`, and `sizePrefixTypeNode`.', + '> Some transforms affect the buffer that is available to us: depending on where we are in the type tree, we may not have access to the entire buffer.', + '> For instance, under a `fixedSizeTransformNode`, the buffer is truncated or padded to match the provided fixed size once the transformed content has been serialised — we are essentially "boxed" into a sub-buffer, and that sub-buffer is the one affected by the `absolute` strategy.', + '> The transforms that create sub-buffers are: `fixedSizeTransformNode`, `sentinelTransformNode`, and `sizePrefixTransformNode`.', ], attributes: [ - attribute('offset', i64(), { - docs: ['The signed byte offset to apply before the wrapped type runs.'], + attribute('offset', byteOffset(), { + docs: ['The signed byte offset to apply before the transformed type runs.'], }), attribute('strategy', enumeration('preOffsetStrategy'), { docs: ['How the `offset` value is interpreted.'], }), - attribute('type', union('typeNode'), { - docs: ['The wrapped type whose serialisation is preceded by the offset.'], - }), ], examples, }); diff --git a/src/spec/nodes/typeNodes/SentinelTypeNode.examples.ts b/src/spec/nodes/transformNodes/SentinelTransformNode.examples.ts similarity index 66% rename from src/spec/nodes/typeNodes/SentinelTypeNode.examples.ts rename to src/spec/nodes/transformNodes/SentinelTransformNode.examples.ts index 88b8a79..695a300 100644 --- a/src/spec/nodes/typeNodes/SentinelTypeNode.examples.ts +++ b/src/spec/nodes/transformNodes/SentinelTransformNode.examples.ts @@ -6,7 +6,9 @@ export const examples: DocExamples = [ code( 'typescript', ` -sentinelTypeNode(stringTypeNode('utf8'), constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ff'))); +stringTypeNode('utf8', { + transforms: [sentinelTransformNode(constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ff')))], +}); // Hello => 0x48656C6C6FFF `, diff --git a/src/spec/nodes/transformNodes/SentinelTransformNode.ts b/src/spec/nodes/transformNodes/SentinelTransformNode.ts new file mode 100644 index 0000000..ad639c5 --- /dev/null +++ b/src/spec/nodes/transformNodes/SentinelTransformNode.ts @@ -0,0 +1,19 @@ +import { attribute, defineNode, node } from '../../../api'; +import { examples } from './SentinelTransformNode.examples'; + +export const sentinelTransformNode = defineNode('sentinelTransformNode', { + docs: [ + 'Delimits the transformed type with a constant sentinel value written immediately after it.', + '', + 'When decoding, the transformed type is decoded until the sentinel value is encountered, at which point decoding stops and the sentinel is discarded.', + '', + '> [!IMPORTANT]', + '> For this transform to work, the sentinel value must never occur within the encoded bytes of the transformed type.', + ], + attributes: [ + attribute('sentinel', node('constantValueNode'), { + docs: ['The constant value written immediately after the transformed type to mark its end.'], + }), + ], + examples, +}); diff --git a/src/spec/nodes/typeNodes/SizePrefixTypeNode.examples.ts b/src/spec/nodes/transformNodes/SizePrefixTransformNode.examples.ts similarity index 76% rename from src/spec/nodes/typeNodes/SizePrefixTypeNode.examples.ts rename to src/spec/nodes/transformNodes/SizePrefixTransformNode.examples.ts index 5ef5206..9165c51 100644 --- a/src/spec/nodes/typeNodes/SizePrefixTypeNode.examples.ts +++ b/src/spec/nodes/transformNodes/SizePrefixTransformNode.examples.ts @@ -6,7 +6,7 @@ export const examples: DocExamples = [ code( 'typescript', ` -sizePrefixTypeNode(stringTypeNode('utf8'), numberTypeNode('u16')); +stringTypeNode('utf8', { transforms: [sizePrefixTransformNode(numberTypeNode('u16'))] }); // "" => 0x0000 // "Hello" => 0x050048656C6C6F diff --git a/src/spec/nodes/transformNodes/SizePrefixTransformNode.ts b/src/spec/nodes/transformNodes/SizePrefixTransformNode.ts new file mode 100644 index 0000000..ece4f77 --- /dev/null +++ b/src/spec/nodes/transformNodes/SizePrefixTransformNode.ts @@ -0,0 +1,15 @@ +import { attribute, defineNode, node } from '../../../api'; +import { examples } from './SizePrefixTransformNode.examples'; + +export const sizePrefixTransformNode = defineNode('sizePrefixTransformNode', { + docs: [ + 'Precedes the transformed type with a numeric prefix indicating its byte length.', + 'When decoding, the size is read first and determines how many bytes the transformed type may consume.', + ], + attributes: [ + attribute('prefix', node('numberTypeNode'), { + docs: ['The numeric type used as the size prefix.'], + }), + ], + examples, +}); diff --git a/src/spec/nodes/transformNodes/TransformNode.ts b/src/spec/nodes/transformNodes/TransformNode.ts new file mode 100644 index 0000000..9838878 --- /dev/null +++ b/src/spec/nodes/transformNodes/TransformNode.ts @@ -0,0 +1,23 @@ +/** + * Named unions for the transform-node category. + * + * - `transformNode` every modifier that can be applied to a type node's serialisation. + */ + +import { defineUnion } from '../../../api'; + +export const transformNodeUnion = defineUnion('transformNode', { + docs: [ + 'A modifier applied to the serialisation of the type node that carries it.', + 'Every type node has an optional `transforms` array. Transforms apply in array order, the first being the innermost: a `stringTypeNode` with `transforms: [sentinel, fixedSize]` first delimits the string with the sentinel, then fixes the total byte size — exactly the v1 nesting `fixedSizeTypeNode(sentinelTypeNode(stringTypeNode))` read inside-out.', + ], + members: [ + 'fixedSizeTransformNode', + 'hiddenPrefixTransformNode', + 'hiddenSuffixTransformNode', + 'postOffsetTransformNode', + 'preOffsetTransformNode', + 'sentinelTransformNode', + 'sizePrefixTransformNode', + ], +}); diff --git a/src/spec/nodes/transformNodes/index.ts b/src/spec/nodes/transformNodes/index.ts new file mode 100644 index 0000000..ab7ab7c --- /dev/null +++ b/src/spec/nodes/transformNodes/index.ts @@ -0,0 +1,33 @@ +import type { AttributeSpec } from '../../../api'; +import { array, optionalAttribute, union } from '../../../api'; +import { fixedSizeTransformNode } from './FixedSizeTransformNode'; +import { hiddenPrefixTransformNode } from './HiddenPrefixTransformNode'; +import { hiddenSuffixTransformNode } from './HiddenSuffixTransformNode'; +import { postOffsetTransformNode } from './PostOffsetTransformNode'; +import { preOffsetTransformNode } from './PreOffsetTransformNode'; +import { sentinelTransformNode } from './SentinelTransformNode'; +import { sizePrefixTransformNode } from './SizePrefixTransformNode'; +import { transformNodeUnion } from './TransformNode'; + +export const ALL_TRANSFORM_NODES = [ + fixedSizeTransformNode, + hiddenPrefixTransformNode, + hiddenSuffixTransformNode, + postOffsetTransformNode, + preOffsetTransformNode, + sentinelTransformNode, + sizePrefixTransformNode, +] as const; + +export const ALL_TRANSFORM_NODE_UNIONS = [transformNodeUnion] as const; + +/** + * The `transforms` attribute declared by every member of the `typeNode` + * union. Declared once here so all type nodes share the exact same shape + * and documentation. + */ +export function transformsAttribute(): AttributeSpec { + return optionalAttribute('transforms', array(union('transformNode')), { + docs: ['Transforms applied to the serialisation of this type, in order — the first is the innermost.'], + }); +} diff --git a/src/spec/nodes/typeNodes/AmountTypeNode.ts b/src/spec/nodes/typeNodes/AmountTypeNode.ts index 6ed93d9..63b6f82 100644 --- a/src/spec/nodes/typeNodes/AmountTypeNode.ts +++ b/src/spec/nodes/typeNodes/AmountTypeNode.ts @@ -1,4 +1,5 @@ -import { attribute, defineNode, nestedUnion, optionalAttribute, string, u32 } from '../../../api'; +import { attribute, defineNode, node, optionalAttribute, string, u32 } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './AmountTypeNode.examples'; export const amountTypeNode = defineNode('amountTypeNode', { @@ -9,16 +10,17 @@ export const amountTypeNode = defineNode('amountTypeNode', { attributes: [ attribute('decimals', u32(), { docs: [ - 'The number of decimal places the wrapped integer carries.', + 'The number of decimal places the inner integer carries.', 'For example, an integer value of 12345 with 2 decimal places represents 123.45.', ], }), optionalAttribute('unit', string(), { docs: ['The unit of the amount — e.g. "USD" or "%".'], }), - attribute('number', nestedUnion('nestedTypeNode', 'numberTypeNode'), { + attribute('number', node('numberTypeNode'), { docs: ['The number type the amount wraps.'], }), + transformsAttribute(), ], examples, }); diff --git a/src/spec/nodes/typeNodes/ArrayTypeNode.ts b/src/spec/nodes/typeNodes/ArrayTypeNode.ts index ecb1fe5..c6774ef 100644 --- a/src/spec/nodes/typeNodes/ArrayTypeNode.ts +++ b/src/spec/nodes/typeNodes/ArrayTypeNode.ts @@ -1,4 +1,5 @@ import { attribute, defineNode, union } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './ArrayTypeNode.examples'; export const arrayTypeNode = defineNode('arrayTypeNode', { @@ -12,6 +13,7 @@ export const arrayTypeNode = defineNode('arrayTypeNode', { attribute('count', union('countNode'), { docs: ['The strategy used to determine the number of items.'], }), + transformsAttribute(), ], examples, }); diff --git a/src/spec/nodes/typeNodes/BooleanTypeNode.ts b/src/spec/nodes/typeNodes/BooleanTypeNode.ts index e3ab3f2..48ced8b 100644 --- a/src/spec/nodes/typeNodes/BooleanTypeNode.ts +++ b/src/spec/nodes/typeNodes/BooleanTypeNode.ts @@ -1,15 +1,17 @@ -import { attribute, defineNode, nestedUnion } from '../../../api'; +import { attribute, defineNode, node } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './BooleanTypeNode.examples'; export const booleanTypeNode = defineNode('booleanTypeNode', { docs: [ - 'A boolean serialised as a numeric value. The wrapped number type determines the byte width.', + 'A boolean serialised as a numeric value. The inner number type determines the byte width.', 'A decoded number of `1` yields `true`; any other value yields `false`.', ], attributes: [ - attribute('size', nestedUnion('nestedTypeNode', 'numberTypeNode'), { + attribute('size', node('numberTypeNode'), { docs: ['The numeric type used to serialise the boolean.'], }), + transformsAttribute(), ], examples, }); diff --git a/src/spec/nodes/typeNodes/BytesTypeNode.ts b/src/spec/nodes/typeNodes/BytesTypeNode.ts index 41cd696..d8f3f30 100644 --- a/src/spec/nodes/typeNodes/BytesTypeNode.ts +++ b/src/spec/nodes/typeNodes/BytesTypeNode.ts @@ -1,10 +1,11 @@ import { defineNode } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './BytesTypeNode.examples'; export const bytesTypeNode = defineNode('bytesTypeNode', { docs: [ - 'A raw sequence of bytes. Typically used inside a fixed-size, size-prefixed, or sentinel-terminated wrapper.', + 'A raw sequence of bytes. Typically carries a fixed-size, size-prefix, or sentinel transform to bound its extent.', ], - attributes: [], + attributes: [transformsAttribute()], examples, }); diff --git a/src/spec/nodes/typeNodes/DateTimeTypeNode.ts b/src/spec/nodes/typeNodes/DateTimeTypeNode.ts index 0a25732..c16fbb8 100644 --- a/src/spec/nodes/typeNodes/DateTimeTypeNode.ts +++ b/src/spec/nodes/typeNodes/DateTimeTypeNode.ts @@ -1,14 +1,16 @@ -import { attribute, defineNode, nestedUnion } from '../../../api'; +import { attribute, defineNode, node } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './DateTimeTypeNode.examples'; export const dateTimeTypeNode = defineNode('dateTimeTypeNode', { docs: [ - 'A timestamp encoded as a number, typically seconds since the Unix epoch. The wrapped number type determines the byte width.', + 'A timestamp encoded as a number, typically seconds since the Unix epoch. The inner number type determines the byte width.', ], attributes: [ - attribute('number', nestedUnion('nestedTypeNode', 'numberTypeNode'), { + attribute('number', node('numberTypeNode'), { docs: ['The numeric type used to serialise the timestamp.'], }), + transformsAttribute(), ], examples, }); diff --git a/src/spec/nodes/typeNodes/EnumStructVariantTypeNode.ts b/src/spec/nodes/typeNodes/EnumStructVariantTypeNode.ts index 17caf66..993e335 100644 --- a/src/spec/nodes/typeNodes/EnumStructVariantTypeNode.ts +++ b/src/spec/nodes/typeNodes/EnumStructVariantTypeNode.ts @@ -1,4 +1,4 @@ -import { attribute, defineNode, nestedUnion, node, optionalAttribute, stringIdentifier, u32 } from '../../../api'; +import { attribute, defineNode, node, optionalAttribute, stringIdentifier, u32 } from '../../../api'; import { examples } from './EnumStructVariantTypeNode.examples'; export const enumStructVariantTypeNode = defineNode('enumStructVariantTypeNode', { @@ -12,7 +12,7 @@ export const enumStructVariantTypeNode = defineNode('enumStructVariantTypeNode', 'Explicit discriminator value. When omitted, the discriminator is the index of the variant in the enum, starting at 0.', ], }), - attribute('struct', nestedUnion('nestedTypeNode', 'structTypeNode'), { + attribute('struct', node('structTypeNode'), { docs: ['The struct of named fields carried by the variant.'], }), optionalAttribute('display', node('enumVariantDisplayNode'), { diff --git a/src/spec/nodes/typeNodes/EnumTupleVariantTypeNode.ts b/src/spec/nodes/typeNodes/EnumTupleVariantTypeNode.ts index f9fbe22..d592c31 100644 --- a/src/spec/nodes/typeNodes/EnumTupleVariantTypeNode.ts +++ b/src/spec/nodes/typeNodes/EnumTupleVariantTypeNode.ts @@ -1,4 +1,4 @@ -import { attribute, defineNode, nestedUnion, node, optionalAttribute, stringIdentifier, u32 } from '../../../api'; +import { attribute, defineNode, node, optionalAttribute, stringIdentifier, u32 } from '../../../api'; import { examples } from './EnumTupleVariantTypeNode.examples'; export const enumTupleVariantTypeNode = defineNode('enumTupleVariantTypeNode', { @@ -12,7 +12,7 @@ export const enumTupleVariantTypeNode = defineNode('enumTupleVariantTypeNode', { 'Explicit discriminator value. When omitted, the discriminator is the index of the variant in the enum, starting at 0.', ], }), - attribute('tuple', nestedUnion('nestedTypeNode', 'tupleTypeNode'), { + attribute('tuple', node('tupleTypeNode'), { docs: ['The tuple of positional fields carried by the variant.'], }), optionalAttribute('display', node('enumVariantDisplayNode'), { diff --git a/src/spec/nodes/typeNodes/EnumTypeNode.ts b/src/spec/nodes/typeNodes/EnumTypeNode.ts index 02e4c64..7d2f764 100644 --- a/src/spec/nodes/typeNodes/EnumTypeNode.ts +++ b/src/spec/nodes/typeNodes/EnumTypeNode.ts @@ -1,4 +1,5 @@ -import { array, attribute, defineNode, nestedUnion, union } from '../../../api'; +import { array, attribute, defineNode, node, union } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './EnumTypeNode.examples'; export const enumTypeNode = defineNode('enumTypeNode', { @@ -7,12 +8,13 @@ export const enumTypeNode = defineNode('enumTypeNode', { attribute('variants', array(union('enumVariantTypeNode')), { docs: ['The variants of the enum, in declaration order.'], }), - attribute('size', nestedUnion('nestedTypeNode', 'numberTypeNode'), { + attribute('size', node('numberTypeNode'), { docs: [ 'The numeric type used to serialise the discriminator.', 'The discriminator prepends the serialised variant payload to identify which variant was selected. By default it is the index of the variant (starting at 0), unless the variant provides its own custom discriminator value.', ], }), + transformsAttribute(), ], examples, }); diff --git a/src/spec/nodes/typeNodes/FixedSizeTypeNode.ts b/src/spec/nodes/typeNodes/FixedSizeTypeNode.ts deleted file mode 100644 index 31606bd..0000000 --- a/src/spec/nodes/typeNodes/FixedSizeTypeNode.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { attribute, defineNode, u64, union } from '../../../api'; -import { examples } from './FixedSizeTypeNode.examples'; - -export const fixedSizeTypeNode = defineNode('fixedSizeTypeNode', { - docs: ['Wraps another type and asserts a fixed total byte size. Padding or truncation is applied as needed.'], - attributes: [ - attribute('size', u64(), { - docs: ['The total byte size the wrapped type must occupy.'], - }), - attribute('type', union('typeNode'), { - docs: ['The wrapped type whose serialisation is constrained.'], - }), - ], - examples, -}); diff --git a/src/spec/nodes/typeNodes/HiddenPrefixTypeNode.examples.ts b/src/spec/nodes/typeNodes/HiddenPrefixTypeNode.examples.ts deleted file mode 100644 index 6738e8a..0000000 --- a/src/spec/nodes/typeNodes/HiddenPrefixTypeNode.examples.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { code, example, type DocExamples } from '../../../api'; - -export const examples: DocExamples = [ - example( - 'Create a hidden prefix type node from a type node and constant value nodes', - code( - 'typescript', - ` -const node = hiddenPrefixTypeNode(numberTypeNode('u32'), [ - constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff')), -]); -`, - ), - ), - example( - 'A number prefixed with 0xFFFF', - code( - 'typescript', - ` -hiddenPrefixTypeNode(numberTypeNode('u32'), [constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff'))]); - -// 42 => 0xFFFF2A000000 -`, - ), - ), - example( - 'A fixed UTF-8 string prefixed with "Hello"', - code( - 'typescript', - ` -hiddenPrefixTypeNode(fixedSizeTypeNode(stringTypeNode('utf8'), 10), [ - constantValueNode(stringTypeNode('utf8'), stringValueNode('Hello')), -]); - -// World => 0x48656C6C6F576F726C640000000000 -`, - ), - ), -]; diff --git a/src/spec/nodes/typeNodes/HiddenPrefixTypeNode.ts b/src/spec/nodes/typeNodes/HiddenPrefixTypeNode.ts deleted file mode 100644 index d66a3b5..0000000 --- a/src/spec/nodes/typeNodes/HiddenPrefixTypeNode.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { array, attribute, defineNode, node, union } from '../../../api'; -import { examples } from './HiddenPrefixTypeNode.examples'; - -export const hiddenPrefixTypeNode = defineNode('hiddenPrefixTypeNode', { - docs: [ - 'Prefixes another type with a list of constant values that are written and read but not surfaced as fields to consumers.', - 'When decoding, the prefixed constants are consumed and checked against their expected values before being discarded.', - ], - attributes: [ - attribute('type', union('typeNode'), { - docs: ['The wrapped type whose serialisation is preceded by the hidden prefix.'], - }), - attribute('prefix', array(node('constantValueNode')), { - docs: ['The constant values written before the wrapped type, in order.'], - }), - ], - examples, -}); diff --git a/src/spec/nodes/typeNodes/HiddenSuffixTypeNode.examples.ts b/src/spec/nodes/typeNodes/HiddenSuffixTypeNode.examples.ts deleted file mode 100644 index 97c8ef9..0000000 --- a/src/spec/nodes/typeNodes/HiddenSuffixTypeNode.examples.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { code, example, type DocExamples } from '../../../api'; - -export const examples: DocExamples = [ - example( - 'Create a hidden suffix type node from a type node and constant value nodes', - code( - 'typescript', - ` -const node = hiddenSuffixTypeNode(numberTypeNode('u32'), [ - constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff')), -]); -`, - ), - ), - example( - 'A number suffixed with 0xFFFF', - code( - 'typescript', - ` -hiddenSuffixTypeNode(numberTypeNode('u32'), [constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff'))]); - -// 42 => 0x2A000000FFFF -`, - ), - ), - example( - 'A fixed UTF-8 string suffixed with "Hello"', - code( - 'typescript', - ` -hiddenSuffixTypeNode(fixedSizeTypeNode(stringTypeNode('utf8'), 10), [ - constantValueNode(stringTypeNode('utf8'), stringValueNode('Hello')), -]); - -// World => 0x576F726C64000000000048656c6c6F -`, - ), - ), -]; diff --git a/src/spec/nodes/typeNodes/HiddenSuffixTypeNode.ts b/src/spec/nodes/typeNodes/HiddenSuffixTypeNode.ts deleted file mode 100644 index d70c208..0000000 --- a/src/spec/nodes/typeNodes/HiddenSuffixTypeNode.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { array, attribute, defineNode, node, union } from '../../../api'; -import { examples } from './HiddenSuffixTypeNode.examples'; - -export const hiddenSuffixTypeNode = defineNode('hiddenSuffixTypeNode', { - docs: [ - 'Suffixes another type with a list of constant values that are written and read but not surfaced as fields to consumers.', - 'When decoding, the suffixed constants are consumed and checked against their expected values before being discarded.', - ], - attributes: [ - attribute('type', union('typeNode'), { - docs: ['The wrapped type whose serialisation is followed by the hidden suffix.'], - }), - attribute('suffix', array(node('constantValueNode')), { - docs: ['The constant values written after the wrapped type, in order.'], - }), - ], - examples, -}); diff --git a/src/spec/nodes/typeNodes/MapTypeNode.examples.ts b/src/spec/nodes/typeNodes/MapTypeNode.examples.ts index b65dc4f..2d36a75 100644 --- a/src/spec/nodes/typeNodes/MapTypeNode.examples.ts +++ b/src/spec/nodes/typeNodes/MapTypeNode.examples.ts @@ -16,7 +16,7 @@ const node = mapTypeNode(publicKeyTypeNode(), numberTypeNode('u32'), prefixedCou 'typescript', ` mapTypeNode( - fixedSizeTypeNode(stringTypeNode('utf8'), 1), // Key: Single UTF-8 character. + stringTypeNode('utf8', { transforms: [fixedSizeTransformNode(1)] }), // Key: Single UTF-8 character. numberTypeNode('u16'), // Value: 16-bit unsigned integer. prefixedCountNode(numberTypeNode('u8')), // Count: map length is prefixed with a u8. ); diff --git a/src/spec/nodes/typeNodes/MapTypeNode.ts b/src/spec/nodes/typeNodes/MapTypeNode.ts index b99b264..5bbdaca 100644 --- a/src/spec/nodes/typeNodes/MapTypeNode.ts +++ b/src/spec/nodes/typeNodes/MapTypeNode.ts @@ -1,4 +1,5 @@ import { attribute, defineNode, union } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './MapTypeNode.examples'; export const mapTypeNode = defineNode('mapTypeNode', { @@ -17,6 +18,7 @@ export const mapTypeNode = defineNode('mapTypeNode', { attribute('count', union('countNode'), { docs: ['The strategy used to determine the number of entries.'], }), + transformsAttribute(), ], examples, }); diff --git a/src/spec/nodes/typeNodes/NumberTypeNode.ts b/src/spec/nodes/typeNodes/NumberTypeNode.ts index 2e7bf70..6fd8417 100644 --- a/src/spec/nodes/typeNodes/NumberTypeNode.ts +++ b/src/spec/nodes/typeNodes/NumberTypeNode.ts @@ -1,4 +1,5 @@ import { attribute, defineNode, enumeration, optionalAttribute, union } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './NumberTypeNode.examples'; export const numberTypeNode = defineNode('numberTypeNode', { @@ -13,6 +14,7 @@ export const numberTypeNode = defineNode('numberTypeNode', { optionalAttribute('display', union('numberDisplayNode'), { docs: ['Display metadata describing how the number is presented.'], }), + transformsAttribute(), ], examples, }); diff --git a/src/spec/nodes/typeNodes/OptionTypeNode.ts b/src/spec/nodes/typeNodes/OptionTypeNode.ts index fa957a9..bf9b8bd 100644 --- a/src/spec/nodes/typeNodes/OptionTypeNode.ts +++ b/src/spec/nodes/typeNodes/OptionTypeNode.ts @@ -1,4 +1,5 @@ -import { attribute, boolean, defineNode, nestedUnion, optionalAttribute, union } from '../../../api'; +import { attribute, boolean, defineNode, node, optionalAttribute, union } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './OptionTypeNode.examples'; export const optionTypeNode = defineNode('optionTypeNode', { @@ -13,12 +14,13 @@ export const optionTypeNode = defineNode('optionTypeNode', { attribute('item', union('typeNode'), { docs: ['The type carried by the option when present.'], }), - attribute('prefix', nestedUnion('nestedTypeNode', 'numberTypeNode'), { + attribute('prefix', node('numberTypeNode'), { docs: [ 'The numeric type used as the presence flag.', 'A prefix value of `1` means the item is present and follows the prefix; a value of `0` means the item is absent and nothing further is serialised.', ], }), + transformsAttribute(), ], examples, }); diff --git a/src/spec/nodes/typeNodes/PublicKeyTypeNode.ts b/src/spec/nodes/typeNodes/PublicKeyTypeNode.ts index cf58500..4500fb7 100644 --- a/src/spec/nodes/typeNodes/PublicKeyTypeNode.ts +++ b/src/spec/nodes/typeNodes/PublicKeyTypeNode.ts @@ -1,8 +1,9 @@ import { defineNode } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './PublicKeyTypeNode.examples'; export const publicKeyTypeNode = defineNode('publicKeyTypeNode', { docs: ['A 32-byte Solana public key.'], - attributes: [], + attributes: [transformsAttribute()], examples, }); diff --git a/src/spec/nodes/typeNodes/RemainderOptionTypeNode.ts b/src/spec/nodes/typeNodes/RemainderOptionTypeNode.ts index bb89beb..5b3f553 100644 --- a/src/spec/nodes/typeNodes/RemainderOptionTypeNode.ts +++ b/src/spec/nodes/typeNodes/RemainderOptionTypeNode.ts @@ -1,4 +1,5 @@ import { attribute, defineNode, union } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './RemainderOptionTypeNode.examples'; export const remainderOptionTypeNode = defineNode('remainderOptionTypeNode', { @@ -9,6 +10,7 @@ export const remainderOptionTypeNode = defineNode('remainderOptionTypeNode', { attribute('item', union('typeNode'), { docs: ['The type carried by the option when present.'], }), + transformsAttribute(), ], examples, }); diff --git a/src/spec/nodes/typeNodes/SentinelTypeNode.ts b/src/spec/nodes/typeNodes/SentinelTypeNode.ts deleted file mode 100644 index 7129f88..0000000 --- a/src/spec/nodes/typeNodes/SentinelTypeNode.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { attribute, defineNode, node, union } from '../../../api'; -import { examples } from './SentinelTypeNode.examples'; - -export const sentinelTypeNode = defineNode('sentinelTypeNode', { - docs: [ - 'Wraps another type and delimits it with a constant sentinel value written immediately after the wrapped type.', - '', - 'When decoding, the wrapped type is decoded until the sentinel value is encountered, at which point decoding stops and the sentinel is discarded.', - '', - '> [!IMPORTANT]', - '> For this node to work, the sentinel value must never occur within the encoded bytes of the wrapped type.', - ], - attributes: [ - attribute('type', union('typeNode'), { - docs: ['The wrapped type whose extent is delimited by the sentinel.'], - }), - attribute('sentinel', node('constantValueNode'), { - docs: ['The constant value written immediately after the wrapped type to mark its end.'], - }), - ], - examples, -}); diff --git a/src/spec/nodes/typeNodes/SetTypeNode.ts b/src/spec/nodes/typeNodes/SetTypeNode.ts index e034c83..2839754 100644 --- a/src/spec/nodes/typeNodes/SetTypeNode.ts +++ b/src/spec/nodes/typeNodes/SetTypeNode.ts @@ -1,4 +1,5 @@ import { attribute, defineNode, union } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './SetTypeNode.examples'; export const setTypeNode = defineNode('setTypeNode', { @@ -12,6 +13,7 @@ export const setTypeNode = defineNode('setTypeNode', { attribute('count', union('countNode'), { docs: ['The strategy used to determine the number of items.'], }), + transformsAttribute(), ], examples, }); diff --git a/src/spec/nodes/typeNodes/SizePrefixTypeNode.ts b/src/spec/nodes/typeNodes/SizePrefixTypeNode.ts deleted file mode 100644 index 49fcd04..0000000 --- a/src/spec/nodes/typeNodes/SizePrefixTypeNode.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { attribute, defineNode, nestedUnion, union } from '../../../api'; -import { examples } from './SizePrefixTypeNode.examples'; - -export const sizePrefixTypeNode = defineNode('sizePrefixTypeNode', { - docs: [ - 'Wraps another type with a numeric prefix indicating the byte length of the wrapped type.', - 'When decoding, the size is read first and determines how many bytes the wrapped type may consume.', - ], - attributes: [ - attribute('type', union('typeNode'), { - docs: ['The wrapped type whose serialisation is preceded by its size.'], - }), - attribute('prefix', nestedUnion('nestedTypeNode', 'numberTypeNode'), { - docs: ['The numeric type used as the size prefix.'], - }), - ], - examples, -}); diff --git a/src/spec/nodes/typeNodes/SolAmountTypeNode.ts b/src/spec/nodes/typeNodes/SolAmountTypeNode.ts index 91ff59c..f2d99d4 100644 --- a/src/spec/nodes/typeNodes/SolAmountTypeNode.ts +++ b/src/spec/nodes/typeNodes/SolAmountTypeNode.ts @@ -1,15 +1,17 @@ -import { attribute, defineNode, nestedUnion } from '../../../api'; +import { attribute, defineNode, node } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './SolAmountTypeNode.examples'; export const solAmountTypeNode = defineNode('solAmountTypeNode', { docs: [ - 'A SOL amount expressed in lamports under the wrapped numeric type.', + 'A SOL amount expressed in lamports under the inner numeric type.', 'Equivalent to an `amountTypeNode` with 9 decimals and `SOL` as the unit.', ], attributes: [ - attribute('number', nestedUnion('nestedTypeNode', 'numberTypeNode'), { + attribute('number', node('numberTypeNode'), { docs: ['The numeric type used to serialise the lamport amount.'], }), + transformsAttribute(), ], examples, }); diff --git a/src/spec/nodes/typeNodes/StringTypeNode.ts b/src/spec/nodes/typeNodes/StringTypeNode.ts index a976bb4..c8f25da 100644 --- a/src/spec/nodes/typeNodes/StringTypeNode.ts +++ b/src/spec/nodes/typeNodes/StringTypeNode.ts @@ -1,11 +1,12 @@ import { attribute, defineNode, enumeration, node, optionalAttribute } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './StringTypeNode.examples'; export const stringTypeNode = defineNode('stringTypeNode', { docs: [ 'A string value.', 'The encoding describes how its bytes are written.', - 'The byte length is determined by an enclosing wrapper such as `sizePrefixTypeNode` or `fixedSizeTypeNode`.', + 'The byte length is determined by a transform such as `sizePrefixTransformNode` or `fixedSizeTransformNode`.', ], attributes: [ attribute('encoding', enumeration('bytesEncoding'), { @@ -14,6 +15,7 @@ export const stringTypeNode = defineNode('stringTypeNode', { optionalAttribute('display', node('stringDisplayNode'), { docs: ['Display metadata describing how the string is presented.'], }), + transformsAttribute(), ], examples, }); diff --git a/src/spec/nodes/typeNodes/StructTypeNode.examples.ts b/src/spec/nodes/typeNodes/StructTypeNode.examples.ts index 375c1ad..cc5f35a 100644 --- a/src/spec/nodes/typeNodes/StructTypeNode.examples.ts +++ b/src/spec/nodes/typeNodes/StructTypeNode.examples.ts @@ -7,7 +7,7 @@ export const examples: DocExamples = [ 'typescript', ` structTypeNode([ - structFieldTypeNode({ name: 'name', type: fixedSizeTypeNode(stringTypeNode('utf8'), 10) }), + structFieldTypeNode({ name: 'name', type: stringTypeNode('utf8', { transforms: [fixedSizeTransformNode(10)] }) }), structFieldTypeNode({ name: 'age', type: numberTypeNode('u8') }), ]); diff --git a/src/spec/nodes/typeNodes/StructTypeNode.ts b/src/spec/nodes/typeNodes/StructTypeNode.ts index 0912f00..3aeaa4e 100644 --- a/src/spec/nodes/typeNodes/StructTypeNode.ts +++ b/src/spec/nodes/typeNodes/StructTypeNode.ts @@ -1,4 +1,5 @@ import { array, attribute, defineNode, node } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './StructTypeNode.examples'; export const structTypeNode = defineNode('structTypeNode', { @@ -9,6 +10,7 @@ export const structTypeNode = defineNode('structTypeNode', { attribute('fields', array(node('structFieldTypeNode')), { docs: ['The fields of the struct, in declaration order.'], }), + transformsAttribute(), ], examples, }); diff --git a/src/spec/nodes/typeNodes/TupleTypeNode.examples.ts b/src/spec/nodes/typeNodes/TupleTypeNode.examples.ts index 3666054..74c6cc8 100644 --- a/src/spec/nodes/typeNodes/TupleTypeNode.examples.ts +++ b/src/spec/nodes/typeNodes/TupleTypeNode.examples.ts @@ -6,7 +6,7 @@ export const examples: DocExamples = [ code( 'typescript', ` -tupleTypeNode([fixedSizeTypeNode(stringTypeNode('utf8'), 10), numberTypeNode('u8')]); +tupleTypeNode([stringTypeNode('utf8', { transforms: [fixedSizeTransformNode(10)] }), numberTypeNode('u8')]); // (Alice, 42) => 0x416C69636500000000002A `, diff --git a/src/spec/nodes/typeNodes/TupleTypeNode.ts b/src/spec/nodes/typeNodes/TupleTypeNode.ts index f526de2..dfd88c6 100644 --- a/src/spec/nodes/typeNodes/TupleTypeNode.ts +++ b/src/spec/nodes/typeNodes/TupleTypeNode.ts @@ -1,4 +1,5 @@ import { array, attribute, defineNode, union } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './TupleTypeNode.examples'; export const tupleTypeNode = defineNode('tupleTypeNode', { @@ -7,6 +8,7 @@ export const tupleTypeNode = defineNode('tupleTypeNode', { attribute('items', array(union('typeNode')), { docs: ['The type of each positional slot, in order.'], }), + transformsAttribute(), ], examples, }); diff --git a/src/spec/nodes/typeNodes/TypeNode.ts b/src/spec/nodes/typeNodes/TypeNode.ts index a536a1f..a4ba371 100644 --- a/src/spec/nodes/typeNodes/TypeNode.ts +++ b/src/spec/nodes/typeNodes/TypeNode.ts @@ -16,19 +16,12 @@ const STANDALONE_TYPE_NODE_KINDS = [ 'bytesTypeNode', 'dateTimeTypeNode', 'enumTypeNode', - 'fixedSizeTypeNode', - 'hiddenPrefixTypeNode', - 'hiddenSuffixTypeNode', 'mapTypeNode', 'numberTypeNode', 'optionTypeNode', - 'postOffsetTypeNode', - 'preOffsetTypeNode', 'publicKeyTypeNode', 'remainderOptionTypeNode', - 'sentinelTypeNode', 'setTypeNode', - 'sizePrefixTypeNode', 'solAmountTypeNode', 'stringTypeNode', 'structTypeNode', diff --git a/src/spec/nodes/typeNodes/ZeroableOptionTypeNode.ts b/src/spec/nodes/typeNodes/ZeroableOptionTypeNode.ts index 8bfcf4a..8d5cfe0 100644 --- a/src/spec/nodes/typeNodes/ZeroableOptionTypeNode.ts +++ b/src/spec/nodes/typeNodes/ZeroableOptionTypeNode.ts @@ -1,4 +1,5 @@ import { attribute, defineNode, node, optionalAttribute, union } from '../../../api'; +import { transformsAttribute } from '../transformNodes'; import { examples } from './ZeroableOptionTypeNode.examples'; export const zeroableOptionTypeNode = defineNode('zeroableOptionTypeNode', { @@ -12,6 +13,7 @@ export const zeroableOptionTypeNode = defineNode('zeroableOptionTypeNode', { 'The constant value that signals absence. When omitted, the all-zero byte pattern of the item type is used.', ], }), + transformsAttribute(), ], examples, }); diff --git a/src/spec/nodes/typeNodes/index.ts b/src/spec/nodes/typeNodes/index.ts index e577f91..322e2dc 100644 --- a/src/spec/nodes/typeNodes/index.ts +++ b/src/spec/nodes/typeNodes/index.ts @@ -7,19 +7,12 @@ import { enumEmptyVariantTypeNode } from './EnumEmptyVariantTypeNode'; import { enumStructVariantTypeNode } from './EnumStructVariantTypeNode'; import { enumTupleVariantTypeNode } from './EnumTupleVariantTypeNode'; import { enumTypeNode } from './EnumTypeNode'; -import { fixedSizeTypeNode } from './FixedSizeTypeNode'; -import { hiddenPrefixTypeNode } from './HiddenPrefixTypeNode'; -import { hiddenSuffixTypeNode } from './HiddenSuffixTypeNode'; import { mapTypeNode } from './MapTypeNode'; import { numberTypeNode } from './NumberTypeNode'; import { optionTypeNode } from './OptionTypeNode'; -import { postOffsetTypeNode } from './PostOffsetTypeNode'; -import { preOffsetTypeNode } from './PreOffsetTypeNode'; import { publicKeyTypeNode } from './PublicKeyTypeNode'; import { remainderOptionTypeNode } from './RemainderOptionTypeNode'; -import { sentinelTypeNode } from './SentinelTypeNode'; import { setTypeNode } from './SetTypeNode'; -import { sizePrefixTypeNode } from './SizePrefixTypeNode'; import { solAmountTypeNode } from './SolAmountTypeNode'; import { stringTypeNode } from './StringTypeNode'; import { structFieldTypeNode } from './StructFieldTypeNode'; @@ -38,19 +31,12 @@ export const ALL_TYPE_NODES = [ enumStructVariantTypeNode, enumTupleVariantTypeNode, enumTypeNode, - fixedSizeTypeNode, - hiddenPrefixTypeNode, - hiddenSuffixTypeNode, mapTypeNode, numberTypeNode, optionTypeNode, - postOffsetTypeNode, - preOffsetTypeNode, publicKeyTypeNode, remainderOptionTypeNode, - sentinelTypeNode, setTypeNode, - sizePrefixTypeNode, solAmountTypeNode, stringTypeNode, structFieldTypeNode, diff --git a/tests/docs/docs.test.ts b/tests/docs/docs.test.ts index 4568c66..aa526e3 100644 --- a/tests/docs/docs.test.ts +++ b/tests/docs/docs.test.ts @@ -140,13 +140,11 @@ describe('docs generation over the real spec', () => { } }); - it('renders the accountNode page with a Data/Children split and the nested-union data link', () => { + it('renders the accountNode page with a Data/Children split and the struct data link', () => { const content = pageOf(MODEL, 'node', 'accountNode').content; expect(content.startsWith('# AccountNode')).toBe(true); expect(content).toContain('### Data'); expect(content).toContain('### Children'); - // accountNode.data is a nestedUnion 'nestedTypeNode' - both arms are linked - expect(content).toContain('[`NestedTypeNode`]'); expect(content).toContain('[`StructTypeNode`]'); }); }); diff --git a/tests/spec.test.ts b/tests/spec.test.ts index 7091181..7ca5164 100644 --- a/tests/spec.test.ts +++ b/tests/spec.test.ts @@ -126,7 +126,7 @@ describe('spec — accountNode shape', () => { expect(attrNames).toEqual(['name', 'size', 'docs', 'data', 'pda', 'discriminators']); const data = account.attributes.find(a => a.name === 'data')!; - expect(data.type).toEqual({ alias: 'nestedTypeNode', kind: 'nestedUnion', name: 'structTypeNode' }); + expect(data.type).toEqual({ kind: 'node', name: 'structTypeNode' }); expect(data.optional).toBeUndefined(); expect(isChildAttribute(data.type)).toBe(true); @@ -151,11 +151,62 @@ describe('spec — accountNode shape', () => { }); describe('spec — typeNode union composition', () => { - it('preserves the nested-union structure', () => { + it('preserves the union-of-union structure', () => { const typeNode = getUnion('typeNode')!; expect(typeNode.members).toContainEqual({ kind: 'union', name: 'standaloneTypeNode' }); expect(typeNode.members).toContainEqual({ kind: 'node', name: 'definedTypeLinkNode' }); }); + + it('gives every typeNode union member a trailing transforms attribute, and no other node', () => { + const spec = getSpec(); + const standalone = getUnion('standaloneTypeNode')!; + const typeNodeKinds = new Set([ + ...standalone.members.flatMap(m => (m.kind === 'node' ? [m.name] : [])), + 'definedTypeLinkNode', + ]); + expect(typeNodeKinds.size).toBeGreaterThan(0); + for (const category of spec.categories) { + for (const n of category.nodes) { + const transforms = n.attributes.find(a => a.name === 'transforms'); + if (typeNodeKinds.has(n.kind)) { + expect(transforms, `node "${n.kind}" should declare transforms`).toBeDefined(); + expect(transforms!.optional).toBe(true); + expect(transforms!.type).toEqual({ kind: 'array', of: { kind: 'union', name: 'transformNode' } }); + expect( + n.attributes.at(-1)!.name, + `transforms should be the last declared attribute of "${n.kind}"`, + ).toBe('transforms'); + } else { + expect(transforms, `node "${n.kind}" should not declare transforms`).toBeUndefined(); + } + } + } + }); + + it('declares the seven transform nodes and no wrapper type nodes', () => { + const transformNode = getUnion('transformNode')!; + expect(transformNode.members.map(m => m.name).sort()).toEqual([ + 'fixedSizeTransformNode', + 'hiddenPrefixTransformNode', + 'hiddenSuffixTransformNode', + 'postOffsetTransformNode', + 'preOffsetTransformNode', + 'sentinelTransformNode', + 'sizePrefixTransformNode', + ]); + for (const wrapper of [ + 'fixedSizeTypeNode', + 'hiddenPrefixTypeNode', + 'hiddenSuffixTypeNode', + 'postOffsetTypeNode', + 'preOffsetTypeNode', + 'sentinelTypeNode', + 'sizePrefixTypeNode', + ]) { + expect(getNode(wrapper), `wrapper "${wrapper}" should no longer exist`).toBeUndefined(); + } + expect(getSpec().categories.every(c => c.nestedUnions.length === 0)).toBe(true); + }); }); describe('spec — programNode shape', () => {