Skip to content

Commit d4736da

Browse files
authored
Add options to configure how traits are rendered in Rust (#242)
* Add traitOptions types and helper functions * Fix typo * Add test for default enum traits * Add test for default struct traits * Add test for default aliases traits * Add more tests * Add base and overridden traits tests * Add feature flag tests * Add FQN tests * Start readme * Sanitise the overrides option * Finish readme * Finish all trait line with a line jump It makes it easier to plug into generated code * Use traitOptions when visiting account nodes * Use traitOptions when visiting defined type nodes * Split enumDefault into scalar and data enums * Remove aliasDefaults * Add changeset * Fix things * Update readme * Update tests * Simplify nested struct trait rendering * Apply feedback
1 parent 281d6c9 commit d4736da

File tree

7 files changed

+665
-52
lines changed

7 files changed

+665
-52
lines changed

.changeset/spicy-spoons-worry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@codama/renderers-rust': patch
3+
---
4+
5+
Add options to configure how traits are rendered in Rust

packages/renderers-rust/README.md

Lines changed: 111 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,114 @@ codama.accept(renderVisitor(pathToGeneratedFolder, options));
3737

3838
The `renderVisitor` accepts the following options.
3939

40-
| Name | Type | Default | Description |
41-
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
42-
| `deleteFolderBeforeRendering` | `boolean` | `true` | Whether the base directory should be cleaned before generating new files. |
43-
| `formatCode` | `boolean` | `false` | Whether we should use `cargo fmt` to format the generated code. When set to `true`, the `crateFolder` option must be provided. |
44-
| `toolchain` | `string` | `"+stable"` | The toolchain to use when formatting the generated code. |
45-
| `crateFolder` | `string` | none | The path to the root folder of the Rust crate. This option is required when `formatCode` is set to `true`. |
46-
| `linkOverrides` | `Record<'accounts' \| 'definedTypes' \| 'instructions' \| 'pdas' \| 'programs' \| 'resolvers', Record<string, string>>` | `{}` | A object that overrides the import path of link nodes. For instance, `{ definedTypes: { counter: 'hooked' } }` uses the `hooked` folder to import any link node referring to the `counter` type. |
47-
| `dependencyMap` | `Record<string, string>` | `{}` | A mapping between import aliases and their actual crate name or path in Rust. |
48-
| `renderParentInstructions` | `boolean` | `false` | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered. |
40+
| Name | Type | Default | Description |
41+
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
42+
| `deleteFolderBeforeRendering` | `boolean` | `true` | Whether the base directory should be cleaned before generating new files. |
43+
| `formatCode` | `boolean` | `false` | Whether we should use `cargo fmt` to format the generated code. When set to `true`, the `crateFolder` option must be provided. |
44+
| `toolchain` | `string` | `"+stable"` | The toolchain to use when formatting the generated code. |
45+
| `crateFolder` | `string` | none | The path to the root folder of the Rust crate. This option is required when `formatCode` is set to `true`. |
46+
| `linkOverrides` | `Record<'accounts' \| 'definedTypes' \| 'instructions' \| 'pdas' \| 'programs' \| 'resolvers', Record<string, string>>` | `{}` | A object that overrides the import path of link nodes. For instance, `{ definedTypes: { counter: 'hooked' } }` uses the `hooked` folder to import any link node referring to the `counter` type. |
47+
| `dependencyMap` | `Record<string, string>` | `{}` | A mapping between import aliases and their actual crate name or path in Rust. |
48+
| `renderParentInstructions` | `boolean` | `false` | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered. |
49+
| `traitOptions` | [`TraitOptions`](#trait-options) | `DEFAULT_TRAIT_OPTIONS` | A set of options that can be used to configure how traits are rendered for every Rust types. See [documentation below](#trait-options) for more information. |
50+
51+
## Trait Options
52+
53+
The Rust renderer provides sensible default traits when generating the various Rust types you client will use. However, you may wish to configure these traits to better suit your needs. The `traitOptions` attribute is here to help you with that. Let's see the various settings it provides.
54+
55+
### Default traits
56+
57+
Using the `traitOptions` attribute, you may configure the default traits that will be applied to every Rust type. These default traits can be configured using 4 different attributes:
58+
59+
- `baseDefaults`: The default traits to implement for all types.
60+
- `dataEnumDefaults`: The default traits to implement for all data enum types, in addition to the `baseDefaults` traits. Data enums are enums with at least one non-unit variant — e.g. `pub enum Command { Write(String), Quit }`.
61+
- `scalarEnumDefaults`: The default traits to implement for all scalar enum types, in addition to the `baseDefaults` traits. Scalar enums are enums with unit variants only — e.g. `pub enum Feedback { Good, Bad }`.
62+
- `structDefaults`: The default traits to implement for all struct types, in addition to the `baseDefaults` traits.
63+
64+
Note that you must provide the fully qualified name of the traits you provide (e.g. `serde::Serialize`). Here are the default values for these attributes:
65+
66+
```ts
67+
const traitOptions = {
68+
baseDefaults: [
69+
'borsh::BorshSerialize',
70+
'borsh::BorshDeserialize',
71+
'serde::Serialize',
72+
'serde::Deserialize',
73+
'Clone',
74+
'Debug',
75+
'Eq',
76+
'PartialEq',
77+
],
78+
dataEnumDefaults: [],
79+
scalarEnumDefaults: ['Copy', 'PartialOrd', 'Hash', 'num_derive::FromPrimitive'],
80+
structDefaults: [],
81+
};
82+
```
83+
84+
### Overridden traits
85+
86+
In addition to configure the default traits, you may also override the traits for specific types. This will completely replace the default traits for the given type. To do so, you may use the `overrides` attribute of the `traitOptions` object.
87+
88+
This attribute is a map where the keys are the names of the types you want to override, and the values are the traits you want to apply to these types. Here is an example:
89+
90+
```ts
91+
const traitOptions = {
92+
overrides: {
93+
myCustomType: ['Clone', 'my::custom::Trait', 'my::custom::OtherTrait'],
94+
myTypeWithNoTraits: [],
95+
},
96+
};
97+
```
98+
99+
### Feature Flags
100+
101+
You may also configure which traits should be rendered under a feature flag by using the `featureFlags` attribute. This attribute is a map where the keys are feature flag names and the values are the traits that should be rendered under that feature flag. Here is an example:
102+
103+
```ts
104+
const traitOptions = {
105+
featureFlags: { fruits: ['fruits::Apple', 'fruits::Banana'] },
106+
};
107+
```
108+
109+
Now, if at any point, we encounter a `fruits::Apple` or `fruits::Banana` trait to be rendered (either as default traits or as overridden traits), they will be rendered under the `fruits` feature flag. For instance:
110+
111+
```rust
112+
#[cfg_attr(feature = "fruits", derive(fruits::Apple, fruits::Banana))]
113+
```
114+
115+
By default, the `featureFlags` option is set to the following:
116+
117+
```ts
118+
const traitOptions = {
119+
featureFlags: { serde: ['serde::Serialize', 'serde::Deserialize'] },
120+
};
121+
```
122+
123+
Note that for feature flags to be effective, they must be added to the `Cargo.toml` file of the generated Rust client.
124+
125+
### Using the Fully Qualified Name
126+
127+
By default, all traits are imported using the provided Fully Qualified Name which means their short name will be used within the `derive` attributes.
128+
129+
However, you may want to avoid importing these traits and use the Fully Qualified Name directly in the generated code. To do so, you may use the `useFullyQualifiedName` attribute of the `traitOptions` object by setting it to `true`:
130+
131+
```ts
132+
const traitOptions = {
133+
useFullyQualifiedName: true,
134+
};
135+
```
136+
137+
Here is an example of rendered traits with this option set to `true` and `false` (which is the default):
138+
139+
```rust
140+
// With `useFullyQualifiedName` set to `false` (default).
141+
use serde::Serialize;
142+
use serde::Deserialize;
143+
// ...
144+
#[derive(Serialize, Deserialize)]
145+
146+
// With `useFullyQualifiedName` set to `true`.
147+
#[derive(serde::Serialize, serde::Deserialize)]
148+
```
149+
150+
Note that any trait rendered under a feature flag will always use the Fully Qualified Name in order to ensure we only reference the trait when the feature is enabled.

packages/renderers-rust/src/getRenderMapVisitor.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ import {
2727
import { getTypeManifestVisitor } from './getTypeManifestVisitor';
2828
import { ImportMap } from './ImportMap';
2929
import { renderValueNode } from './renderValueNodeVisitor';
30-
import { getImportFromFactory, LinkOverrides, render } from './utils';
30+
import { getImportFromFactory, getTraitsFromNodeFactory, LinkOverrides, render, TraitOptions } from './utils';
3131

3232
export type GetRenderMapOptions = {
33+
defaultTraitOverrides?: string[];
3334
dependencyMap?: Record<string, string>;
3435
linkOverrides?: LinkOverrides;
3536
renderParentInstructions?: boolean;
37+
traitOptions?: TraitOptions;
3638
};
3739

3840
export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
@@ -42,7 +44,8 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
4244
const renderParentInstructions = options.renderParentInstructions ?? false;
4345
const dependencyMap = options.dependencyMap ?? {};
4446
const getImportFrom = getImportFromFactory(options.linkOverrides ?? {});
45-
const typeManifestVisitor = getTypeManifestVisitor({ getImportFrom });
47+
const getTraitsFromNode = getTraitsFromNodeFactory(options.traitOptions);
48+
const typeManifestVisitor = getTypeManifestVisitor({ getImportFrom, getTraitsFromNode });
4649

4750
return pipe(
4851
staticVisitor(
@@ -147,6 +150,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
147150
node.arguments.forEach(argument => {
148151
const argumentVisitor = getTypeManifestVisitor({
149152
getImportFrom,
153+
getTraitsFromNode,
150154
nestedStruct: true,
151155
parentName: `${pascalCase(node.name)}InstructionData`,
152156
});
@@ -187,6 +191,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
187191
const struct = structTypeNodeFromInstructionArgumentNodes(node.arguments);
188192
const structVisitor = getTypeManifestVisitor({
189193
getImportFrom,
194+
getTraitsFromNode,
190195
parentName: `${pascalCase(node.name)}InstructionData`,
191196
});
192197
const typeManifest = visit(struct, structVisitor);

packages/renderers-rust/src/getTypeManifestVisitor.ts

Lines changed: 18 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE, CodamaError } from '@codama/
22
import {
33
arrayTypeNode,
44
CountNode,
5+
definedTypeNode,
56
fixedCountNode,
67
isNode,
7-
isScalarEnum,
88
NumberTypeNode,
99
numberTypeNode,
1010
pascalCase,
@@ -17,7 +17,7 @@ import {
1717
import { extendVisitor, mergeVisitor, pipe, visit } from '@codama/visitors-core';
1818

1919
import { ImportMap } from './ImportMap';
20-
import { GetImportFromFunction, rustDocblock } from './utils';
20+
import { GetImportFromFunction, GetTraitsFromNodeFunction, rustDocblock } from './utils';
2121

2222
export type TypeManifest = {
2323
imports: ImportMap;
@@ -27,10 +27,11 @@ export type TypeManifest = {
2727

2828
export function getTypeManifestVisitor(options: {
2929
getImportFrom: GetImportFromFunction;
30+
getTraitsFromNode: GetTraitsFromNodeFunction;
3031
nestedStruct?: boolean;
3132
parentName?: string | null;
3233
}) {
33-
const { getImportFrom } = options;
34+
const { getImportFrom, getTraitsFromNode } = options;
3435
let parentName: string | null = options.parentName ?? null;
3536
let nestedStruct: boolean = options.nestedStruct ?? false;
3637
let inlineStruct: boolean = false;
@@ -50,14 +51,12 @@ export function getTypeManifestVisitor(options: {
5051
visitAccount(account, { self }) {
5152
parentName = pascalCase(account.name);
5253
const manifest = visit(account.data, self);
53-
manifest.imports.add(['borsh::BorshSerialize', 'borsh::BorshDeserialize']);
54+
const traits = getTraitsFromNode(account);
55+
manifest.imports.mergeWith(traits.imports);
5456
parentName = null;
5557
return {
5658
...manifest,
57-
type:
58-
'#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)]\n' +
59-
'#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]\n' +
60-
`${manifest.type}`,
59+
type: traits.render + manifest.type,
6160
};
6261
},
6362

@@ -141,41 +140,15 @@ export function getTypeManifestVisitor(options: {
141140
visitDefinedType(definedType, { self }) {
142141
parentName = pascalCase(definedType.name);
143142
const manifest = visit(definedType.type, self);
143+
const traits = getTraitsFromNode(definedType);
144+
manifest.imports.mergeWith(traits.imports);
144145
parentName = null;
145-
const traits = ['BorshSerialize', 'BorshDeserialize', 'Clone', 'Debug', 'Eq', 'PartialEq'];
146146

147-
if (isNode(definedType.type, 'enumTypeNode') && isScalarEnum(definedType.type)) {
148-
traits.push('Copy', 'PartialOrd', 'Hash', 'FromPrimitive');
149-
manifest.imports.add(['num_derive::FromPrimitive']);
150-
}
151-
152-
const nestedStructs = manifest.nestedStructs.map(
153-
struct =>
154-
`#[derive(${traits.join(', ')})]\n` +
155-
'#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]\n' +
156-
`${struct}`,
157-
);
158-
159-
if (!isNode(definedType.type, ['enumTypeNode', 'structTypeNode'])) {
160-
if (nestedStructs.length > 0) {
161-
manifest.imports.add(['borsh::BorshSerialize', 'borsh::BorshDeserialize']);
162-
}
163-
return {
164-
...manifest,
165-
nestedStructs,
166-
type: `pub type ${pascalCase(definedType.name)} = ${manifest.type};`,
167-
};
168-
}
147+
const renderedType = isNode(definedType.type, ['enumTypeNode', 'structTypeNode'])
148+
? manifest.type
149+
: `pub type ${pascalCase(definedType.name)} = ${manifest.type};`;
169150

170-
manifest.imports.add(['borsh::BorshSerialize', 'borsh::BorshDeserialize']);
171-
return {
172-
...manifest,
173-
nestedStructs,
174-
type:
175-
`#[derive(${traits.join(', ')})]\n` +
176-
'#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]\n' +
177-
`${manifest.type}`,
178-
};
151+
return { ...manifest, type: `${traits.render}${renderedType}` };
179152
},
180153

181154
visitDefinedTypeLink(node) {
@@ -450,11 +423,15 @@ export function getTypeManifestVisitor(options: {
450423
const mergedManifest = mergeManifests(fields);
451424

452425
if (nestedStruct) {
426+
const nestedTraits = getTraitsFromNode(
427+
definedTypeNode({ name: originalParentName, type: structType }),
428+
);
429+
mergedManifest.imports.mergeWith(nestedTraits.imports);
453430
return {
454431
...mergedManifest,
455432
nestedStructs: [
456433
...mergedManifest.nestedStructs,
457-
`pub struct ${pascalCase(originalParentName)} {\n${fieldTypes}\n}`,
434+
`${nestedTraits.render}pub struct ${pascalCase(originalParentName)} {\n${fieldTypes}\n}`,
458435
],
459436
type: pascalCase(originalParentName),
460437
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './codecs';
22
export * from './linkOverrides';
33
export * from './render';
4+
export * from './traitOptions';

0 commit comments

Comments
 (0)