Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/spicy-spoons-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@codama/renderers-rust': patch
---

Add options to configure how traits are rendered in Rust
118 changes: 109 additions & 9 deletions packages/renderers-rust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,112 @@ codama.accept(renderVisitor(pathToGeneratedFolder, options));

The `renderVisitor` accepts the following options.

| Name | Type | Default | Description |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `deleteFolderBeforeRendering` | `boolean` | `true` | Whether the base directory should be cleaned before generating new files. |
| `formatCode` | `boolean` | `false` | Whether we should use `cargo fmt` to format the generated code. When set to `true`, the `crateFolder` option must be provided. |
| `toolchain` | `string` | `"+stable"` | The toolchain to use when formatting the generated code. |
| `crateFolder` | `string` | none | The path to the root folder of the Rust crate. This option is required when `formatCode` is set to `true`. |
| `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. |
| `dependencyMap` | `Record<string, string>` | `{}` | A mapping between import aliases and their actual crate name or path in Rust. |
| `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. |
| Name | Type | Default | Description |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `deleteFolderBeforeRendering` | `boolean` | `true` | Whether the base directory should be cleaned before generating new files. |
| `formatCode` | `boolean` | `false` | Whether we should use `cargo fmt` to format the generated code. When set to `true`, the `crateFolder` option must be provided. |
| `toolchain` | `string` | `"+stable"` | The toolchain to use when formatting the generated code. |
| `crateFolder` | `string` | none | The path to the root folder of the Rust crate. This option is required when `formatCode` is set to `true`. |
| `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. |
| `dependencyMap` | `Record<string, string>` | `{}` | A mapping between import aliases and their actual crate name or path in Rust. |
| `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. |
| `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. |

## Trait Options

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.

### Default traits

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

micro-micro-nit:

Suggested change
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:
Using the `traitOptions` attribute, you may configure the default traits that will be applied to every generated Rust type. These default traits can be configured using 4 different attributes:


- `baseDefaults`: The default traits to implement for all types.
- `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 }`.
- `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 }`.
- `structDefaults`: The default traits to implement for all struct types, in addition to the `baseDefaults` traits.

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:

```ts
const traitOptions = {
baseDefaults: [
'borsh::BorshSerialize',
'borsh::BorshDeserialize',
'serde::Serialize',
'serde::Deserialize',
'Clone',
'Debug',
'Eq',
'PartialEq',
],
dataEnumDefaults: [],
scalarEnumDefaults: ['Copy', 'PartialOrd', 'Hash', 'num_derive::FromPrimitive'],
structDefaults: [],
};
Comment on lines +67 to +81
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a case to be made for the default being zero traits and zero scalar enum configurations, but I don't feel too strongly about it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had to keep them like that in order to avoid introducing breaking changes. This produces code in the exact same way we were before. But I think these are sensible defaults for most clients, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, it doesn't really hurt to just let people change the base defaults anyway, and avoid the breaking change.

```

### Overridden traits

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slick, I like it!


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:

```ts
const traitOptions = {
overrides: {
myCustomType: ['Clone', 'my::custom::Trait', 'my::custom::OtherTrait'],
myTypeWithNoTraits: [],
},
};
```

### Feature Flags

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this even more!


```ts
const traitOptions = {
featureFlags: { fruits: ['fruits::Apple', 'fruits::Banana'] },
};
```

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:

```rust
#[cfg(feature = "fruits", derive(fruits::Apple, fruits::Banana))]
```

By default, the `featureFlags` option is set to the following:

```ts
const traitOptions = {
featureFlags: { serde: ['serde::Serialize', 'serde::Deserialize'] },
};
```

### Using the Fully Qualified Name

By default, all traits are imported using the provided Fully Qualified Name which means their short name will be used within the `derive` attributes.

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`:

```ts
const traitOptions = {
useFullyQualifiedName: true,
};
```

Here is an example of rendered traits with this option set to `true` and `false` (which is the default):

```rust
// With `useFullyQualifiedName` set to `false` (default).
use serde::Serialize;
use serde::Deserialize;
// ...
#[derive(Serialize, Deserialize)]

// With `useFullyQualifiedName` set to `true`.
#[derive(serde::Serialize, serde::Deserialize)]
```

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.
9 changes: 7 additions & 2 deletions packages/renderers-rust/src/getRenderMapVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ import {
import { getTypeManifestVisitor } from './getTypeManifestVisitor';
import { ImportMap } from './ImportMap';
import { renderValueNode } from './renderValueNodeVisitor';
import { getImportFromFactory, LinkOverrides, render } from './utils';
import { getImportFromFactory, getTraitsFromNodeFactory, LinkOverrides, render, TraitOptions } from './utils';

export type GetRenderMapOptions = {
defaultTraitOverrides?: string[];
dependencyMap?: Record<string, string>;
linkOverrides?: LinkOverrides;
renderParentInstructions?: boolean;
traitOptions?: TraitOptions;
};

export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
Expand All @@ -42,7 +44,8 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
const renderParentInstructions = options.renderParentInstructions ?? false;
const dependencyMap = options.dependencyMap ?? {};
const getImportFrom = getImportFromFactory(options.linkOverrides ?? {});
const typeManifestVisitor = getTypeManifestVisitor({ getImportFrom });
const getTraitsFromNode = getTraitsFromNodeFactory(options.traitOptions);
const typeManifestVisitor = getTypeManifestVisitor({ getImportFrom, getTraitsFromNode });

return pipe(
staticVisitor(
Expand Down Expand Up @@ -147,6 +150,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
node.arguments.forEach(argument => {
const argumentVisitor = getTypeManifestVisitor({
getImportFrom,
getTraitsFromNode,
nestedStruct: true,
parentName: `${pascalCase(node.name)}InstructionData`,
});
Expand Down Expand Up @@ -187,6 +191,7 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}) {
const struct = structTypeNodeFromInstructionArgumentNodes(node.arguments);
const structVisitor = getTypeManifestVisitor({
getImportFrom,
getTraitsFromNode,
parentName: `${pascalCase(node.name)}InstructionData`,
});
const typeManifest = visit(struct, structVisitor);
Expand Down
59 changes: 18 additions & 41 deletions packages/renderers-rust/src/getTypeManifestVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE, CodamaError } from '@codama/
import {
arrayTypeNode,
CountNode,
definedTypeNode,
fixedCountNode,
isNode,
isScalarEnum,
NumberTypeNode,
numberTypeNode,
pascalCase,
Expand All @@ -17,7 +17,7 @@ import {
import { extendVisitor, mergeVisitor, pipe, visit } from '@codama/visitors-core';

import { ImportMap } from './ImportMap';
import { GetImportFromFunction, rustDocblock } from './utils';
import { GetImportFromFunction, GetTraitsFromNodeFunction, rustDocblock } from './utils';

export type TypeManifest = {
imports: ImportMap;
Expand All @@ -27,10 +27,11 @@ export type TypeManifest = {

export function getTypeManifestVisitor(options: {
getImportFrom: GetImportFromFunction;
getTraitsFromNode: GetTraitsFromNodeFunction;
nestedStruct?: boolean;
parentName?: string | null;
}) {
const { getImportFrom } = options;
const { getImportFrom, getTraitsFromNode } = options;
let parentName: string | null = options.parentName ?? null;
let nestedStruct: boolean = options.nestedStruct ?? false;
let inlineStruct: boolean = false;
Expand All @@ -50,14 +51,12 @@ export function getTypeManifestVisitor(options: {
visitAccount(account, { self }) {
parentName = pascalCase(account.name);
const manifest = visit(account.data, self);
manifest.imports.add(['borsh::BorshSerialize', 'borsh::BorshDeserialize']);
const traits = getTraitsFromNode(account);
manifest.imports.mergeWith(traits.imports);
parentName = null;
return {
...manifest,
type:
'#[derive(BorshSerialize, BorshDeserialize, Clone, Debug, Eq, PartialEq)]\n' +
'#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]\n' +
`${manifest.type}`,
type: traits.render + manifest.type,
};
},

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

if (isNode(definedType.type, 'enumTypeNode') && isScalarEnum(definedType.type)) {
traits.push('Copy', 'PartialOrd', 'Hash', 'FromPrimitive');
manifest.imports.add(['num_derive::FromPrimitive']);
}

const nestedStructs = manifest.nestedStructs.map(
struct =>
`#[derive(${traits.join(', ')})]\n` +
'#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]\n' +
`${struct}`,
);

if (!isNode(definedType.type, ['enumTypeNode', 'structTypeNode'])) {
if (nestedStructs.length > 0) {
manifest.imports.add(['borsh::BorshSerialize', 'borsh::BorshDeserialize']);
}
return {
...manifest,
nestedStructs,
type: `pub type ${pascalCase(definedType.name)} = ${manifest.type};`,
};
}
const renderedType = isNode(definedType.type, ['enumTypeNode', 'structTypeNode'])
? manifest.type
: `pub type ${pascalCase(definedType.name)} = ${manifest.type};`;

manifest.imports.add(['borsh::BorshSerialize', 'borsh::BorshDeserialize']);
return {
...manifest,
nestedStructs,
type:
`#[derive(${traits.join(', ')})]\n` +
'#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]\n' +
`${manifest.type}`,
};
return { ...manifest, type: `${traits.render}${renderedType}` };
},

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

if (nestedStruct) {
const nestedTraits = getTraitsFromNode(
definedTypeNode({ name: originalParentName, type: structType }),
);
mergedManifest.imports.mergeWith(nestedTraits.imports);
return {
...mergedManifest,
nestedStructs: [
...mergedManifest.nestedStructs,
`pub struct ${pascalCase(originalParentName)} {\n${fieldTypes}\n}`,
`${nestedTraits.render}pub struct ${pascalCase(originalParentName)} {\n${fieldTypes}\n}`,
],
type: pascalCase(originalParentName),
};
Expand Down
1 change: 1 addition & 0 deletions packages/renderers-rust/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './codecs';
export * from './linkOverrides';
export * from './render';
export * from './traitOptions';
Loading