diff --git a/codama-attributes/src/codama_directives/discriminator_directive.rs b/codama-attributes/src/codama_directives/discriminator_directive.rs index 18416c9..b11e988 100644 --- a/codama-attributes/src/codama_directives/discriminator_directive.rs +++ b/codama-attributes/src/codama_directives/discriminator_directive.rs @@ -35,8 +35,8 @@ impl DiscriminatorDirective { let mut encoding = SetOnce::::new("encoding").initial_value(BytesEncoding::Base16); let mut field = SetOnce::::new("field"); - let mut offset = SetOnce::::new("offset").initial_value(0); - let mut size = SetOnce::::new("size"); + let mut offset = SetOnce::::new("offset").initial_value(0); + let mut size = SetOnce::::new("size"); pl.each(|ref meta| match meta.path_str().as_str() { "bytes" => { if kind != DiscriminatorKind::Constant { diff --git a/codama-korok-visitors/src/identify_field_types_visitor.rs b/codama-korok-visitors/src/identify_field_types_visitor.rs index e0f0638..71c6a07 100644 --- a/codama-korok-visitors/src/identify_field_types_visitor.rs +++ b/codama-korok-visitors/src/identify_field_types_visitor.rs @@ -97,7 +97,7 @@ pub fn get_type_node(ty: &syn::Type) -> Option { } } syn::Type::Array(syn::TypeArray { elem, len, .. }) => { - let Ok(size) = len.as_unsigned_integer::() else { + let Ok(size) = len.as_unsigned_integer::() else { return None; }; get_type_node(elem) diff --git a/codama-nodes/src/count_nodes/count_node.rs b/codama-nodes/src/count_nodes/count_node.rs index cda97d2..1f10f0b 100644 --- a/codama-nodes/src/count_nodes/count_node.rs +++ b/codama-nodes/src/count_nodes/count_node.rs @@ -1,17 +1,6 @@ -use crate::{FixedCountNode, PrefixedCountNode, RemainderCountNode}; -use codama_nodes_derive::node_union; - -#[node_union] -pub enum CountNode { - Fixed(FixedCountNode), - Prefixed(PrefixedCountNode), - Remainder(RemainderCountNode), -} - #[cfg(test)] mod tests { - use super::*; - use crate::HasKind; + use crate::{CountNode, HasKind, RemainderCountNode}; #[test] fn kind() { diff --git a/codama-nodes/src/count_nodes/fixed_count_node.rs b/codama-nodes/src/count_nodes/fixed_count_node.rs index 63f509e..1e1b0dc 100644 --- a/codama-nodes/src/count_nodes/fixed_count_node.rs +++ b/codama-nodes/src/count_nodes/fixed_count_node.rs @@ -1,20 +1,7 @@ -use codama_nodes_derive::node; - -#[node] -#[derive(Copy)] -pub struct FixedCountNode { - // Data. - pub value: usize, -} - -impl From for crate::Node { - fn from(val: FixedCountNode) -> Self { - crate::Node::Count(val.into()) - } -} +use crate::FixedCountNode; impl FixedCountNode { - pub fn new(value: usize) -> Self { + pub fn new(value: u64) -> Self { Self { value } } } diff --git a/codama-nodes/src/count_nodes/mod.rs b/codama-nodes/src/count_nodes/mod.rs index e996af5..29e6396 100644 --- a/codama-nodes/src/count_nodes/mod.rs +++ b/codama-nodes/src/count_nodes/mod.rs @@ -2,8 +2,3 @@ mod count_node; mod fixed_count_node; mod prefixed_count_node; mod remainder_count_node; - -pub use count_node::*; -pub use fixed_count_node::*; -pub use prefixed_count_node::*; -pub use remainder_count_node::*; diff --git a/codama-nodes/src/count_nodes/prefixed_count_node.rs b/codama-nodes/src/count_nodes/prefixed_count_node.rs index fd01c9f..6957bf3 100644 --- a/codama-nodes/src/count_nodes/prefixed_count_node.rs +++ b/codama-nodes/src/count_nodes/prefixed_count_node.rs @@ -1,17 +1,4 @@ -use crate::{NestedTypeNode, NumberTypeNode}; -use codama_nodes_derive::node; - -#[node] -pub struct PrefixedCountNode { - // Data. - pub prefix: NestedTypeNode, -} - -impl From for crate::Node { - fn from(val: PrefixedCountNode) -> Self { - crate::Node::Count(val.into()) - } -} +use crate::{NestedTypeNode, NumberTypeNode, PrefixedCountNode}; impl PrefixedCountNode { pub fn new(prefix: T) -> Self diff --git a/codama-nodes/src/count_nodes/remainder_count_node.rs b/codama-nodes/src/count_nodes/remainder_count_node.rs index 29d2098..364243c 100644 --- a/codama-nodes/src/count_nodes/remainder_count_node.rs +++ b/codama-nodes/src/count_nodes/remainder_count_node.rs @@ -1,14 +1,4 @@ -use codama_nodes_derive::node; - -#[node] -#[derive(Copy, Default)] -pub struct RemainderCountNode {} - -impl From for crate::Node { - fn from(val: RemainderCountNode) -> Self { - crate::Node::Count(val.into()) - } -} +use crate::RemainderCountNode; impl RemainderCountNode { pub fn new() -> Self { diff --git a/codama-nodes/src/discriminator_nodes/constant_discriminator_node.rs b/codama-nodes/src/discriminator_nodes/constant_discriminator_node.rs index c4d5fe3..817b5bd 100644 --- a/codama-nodes/src/discriminator_nodes/constant_discriminator_node.rs +++ b/codama-nodes/src/discriminator_nodes/constant_discriminator_node.rs @@ -1,23 +1,7 @@ -use crate::ConstantValueNode; -use codama_nodes_derive::node; - -#[node] -pub struct ConstantDiscriminatorNode { - // Data. - pub offset: usize, - - // Children. - pub constant: ConstantValueNode, -} - -impl From for crate::Node { - fn from(val: ConstantDiscriminatorNode) -> Self { - crate::Node::Discriminator(val.into()) - } -} +use crate::{ConstantDiscriminatorNode, ConstantValueNode}; impl ConstantDiscriminatorNode { - pub fn new(constant: T, offset: usize) -> Self + pub fn new(constant: T, offset: u64) -> Self where T: Into, { @@ -30,9 +14,9 @@ impl ConstantDiscriminatorNode { #[cfg(test)] mod tests { - use crate::{Base16, NumberTypeNode, NumberValueNode, U32}; - - use super::*; + use crate::{ + Base16, ConstantDiscriminatorNode, ConstantValueNode, NumberTypeNode, NumberValueNode, U32, + }; #[test] fn new() { diff --git a/codama-nodes/src/discriminator_nodes/discriminator_node.rs b/codama-nodes/src/discriminator_nodes/discriminator_node.rs index a693467..b842b8a 100644 --- a/codama-nodes/src/discriminator_nodes/discriminator_node.rs +++ b/codama-nodes/src/discriminator_nodes/discriminator_node.rs @@ -1,17 +1,6 @@ -use crate::{ConstantDiscriminatorNode, FieldDiscriminatorNode, SizeDiscriminatorNode}; -use codama_nodes_derive::node_union; - -#[node_union] -pub enum DiscriminatorNode { - Constant(ConstantDiscriminatorNode), - Field(FieldDiscriminatorNode), - Size(SizeDiscriminatorNode), -} - #[cfg(test)] mod tests { - use super::*; - use crate::HasKind; + use crate::{DiscriminatorNode, HasKind, SizeDiscriminatorNode}; #[test] fn kind() { diff --git a/codama-nodes/src/discriminator_nodes/field_discriminator_node.rs b/codama-nodes/src/discriminator_nodes/field_discriminator_node.rs index a781029..cf5ccbb 100644 --- a/codama-nodes/src/discriminator_nodes/field_discriminator_node.rs +++ b/codama-nodes/src/discriminator_nodes/field_discriminator_node.rs @@ -1,21 +1,7 @@ -use crate::{CamelCaseString, HasName}; -use codama_nodes_derive::node; - -#[node] -pub struct FieldDiscriminatorNode { - // Data. - pub name: CamelCaseString, - pub offset: usize, -} - -impl From for crate::Node { - fn from(val: FieldDiscriminatorNode) -> Self { - crate::Node::Discriminator(val.into()) - } -} +use crate::{CamelCaseString, FieldDiscriminatorNode}; impl FieldDiscriminatorNode { - pub fn new(name: T, offset: usize) -> Self + pub fn new(name: T, offset: u64) -> Self where T: Into, { @@ -26,12 +12,6 @@ impl FieldDiscriminatorNode { } } -impl HasName for FieldDiscriminatorNode { - fn name(&self) -> &CamelCaseString { - &self.name - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/codama-nodes/src/discriminator_nodes/mod.rs b/codama-nodes/src/discriminator_nodes/mod.rs index bfa2c08..92d9619 100644 --- a/codama-nodes/src/discriminator_nodes/mod.rs +++ b/codama-nodes/src/discriminator_nodes/mod.rs @@ -2,8 +2,3 @@ mod constant_discriminator_node; mod discriminator_node; mod field_discriminator_node; mod size_discriminator_node; - -pub use constant_discriminator_node::*; -pub use discriminator_node::*; -pub use field_discriminator_node::*; -pub use size_discriminator_node::*; diff --git a/codama-nodes/src/discriminator_nodes/size_discriminator_node.rs b/codama-nodes/src/discriminator_nodes/size_discriminator_node.rs index 37a2904..c0a6e2c 100644 --- a/codama-nodes/src/discriminator_nodes/size_discriminator_node.rs +++ b/codama-nodes/src/discriminator_nodes/size_discriminator_node.rs @@ -1,20 +1,7 @@ -use codama_nodes_derive::node; - -#[node] -#[derive(Copy)] -pub struct SizeDiscriminatorNode { - // Data. - pub size: usize, -} - -impl From for crate::Node { - fn from(val: SizeDiscriminatorNode) -> Self { - crate::Node::Discriminator(val.into()) - } -} +use crate::SizeDiscriminatorNode; impl SizeDiscriminatorNode { - pub fn new(size: usize) -> Self { + pub fn new(size: u64) -> Self { Self { size } } } diff --git a/codama-nodes/src/generated/count_nodes/count_node.rs b/codama-nodes/src/generated/count_nodes/count_node.rs new file mode 100644 index 0000000..f0febf8 --- /dev/null +++ b/codama-nodes/src/generated/count_nodes/count_node.rs @@ -0,0 +1,9 @@ +use crate::{FixedCountNode, PrefixedCountNode, RemainderCountNode}; +use codama_nodes_derive::node_union; + +#[node_union] +pub enum CountNode { + Fixed(FixedCountNode), + Prefixed(PrefixedCountNode), + Remainder(RemainderCountNode), +} diff --git a/codama-nodes/src/generated/count_nodes/fixed_count_node.rs b/codama-nodes/src/generated/count_nodes/fixed_count_node.rs new file mode 100644 index 0000000..b947834 --- /dev/null +++ b/codama-nodes/src/generated/count_nodes/fixed_count_node.rs @@ -0,0 +1,14 @@ +use codama_nodes_derive::node; + +#[node] +#[derive(Copy)] +pub struct FixedCountNode { + // Data. + pub value: u64, +} + +impl From for crate::Node { + fn from(val: FixedCountNode) -> Self { + crate::Node::Count(val.into()) + } +} diff --git a/codama-nodes/src/generated/count_nodes/mod.rs b/codama-nodes/src/generated/count_nodes/mod.rs new file mode 100644 index 0000000..e996af5 --- /dev/null +++ b/codama-nodes/src/generated/count_nodes/mod.rs @@ -0,0 +1,9 @@ +mod count_node; +mod fixed_count_node; +mod prefixed_count_node; +mod remainder_count_node; + +pub use count_node::*; +pub use fixed_count_node::*; +pub use prefixed_count_node::*; +pub use remainder_count_node::*; diff --git a/codama-nodes/src/generated/count_nodes/prefixed_count_node.rs b/codama-nodes/src/generated/count_nodes/prefixed_count_node.rs new file mode 100644 index 0000000..bc5390d --- /dev/null +++ b/codama-nodes/src/generated/count_nodes/prefixed_count_node.rs @@ -0,0 +1,14 @@ +use crate::{NestedTypeNode, NumberTypeNode}; +use codama_nodes_derive::node; + +#[node] +pub struct PrefixedCountNode { + // Children. + pub prefix: NestedTypeNode, +} + +impl From for crate::Node { + fn from(val: PrefixedCountNode) -> Self { + crate::Node::Count(val.into()) + } +} diff --git a/codama-nodes/src/generated/count_nodes/remainder_count_node.rs b/codama-nodes/src/generated/count_nodes/remainder_count_node.rs new file mode 100644 index 0000000..e32e343 --- /dev/null +++ b/codama-nodes/src/generated/count_nodes/remainder_count_node.rs @@ -0,0 +1,11 @@ +use codama_nodes_derive::node; + +#[node] +#[derive(Copy, Default)] +pub struct RemainderCountNode {} + +impl From for crate::Node { + fn from(val: RemainderCountNode) -> Self { + crate::Node::Count(val.into()) + } +} diff --git a/codama-nodes/src/generated/discriminator_nodes/constant_discriminator_node.rs b/codama-nodes/src/generated/discriminator_nodes/constant_discriminator_node.rs new file mode 100644 index 0000000..f69ba47 --- /dev/null +++ b/codama-nodes/src/generated/discriminator_nodes/constant_discriminator_node.rs @@ -0,0 +1,17 @@ +use crate::ConstantValueNode; +use codama_nodes_derive::node; + +#[node] +pub struct ConstantDiscriminatorNode { + // Data. + pub offset: u64, + + // Children. + pub constant: ConstantValueNode, +} + +impl From for crate::Node { + fn from(val: ConstantDiscriminatorNode) -> Self { + crate::Node::Discriminator(val.into()) + } +} diff --git a/codama-nodes/src/generated/discriminator_nodes/discriminator_node.rs b/codama-nodes/src/generated/discriminator_nodes/discriminator_node.rs new file mode 100644 index 0000000..32cc1dc --- /dev/null +++ b/codama-nodes/src/generated/discriminator_nodes/discriminator_node.rs @@ -0,0 +1,9 @@ +use crate::{ConstantDiscriminatorNode, FieldDiscriminatorNode, SizeDiscriminatorNode}; +use codama_nodes_derive::node_union; + +#[node_union] +pub enum DiscriminatorNode { + Constant(ConstantDiscriminatorNode), + Field(FieldDiscriminatorNode), + Size(SizeDiscriminatorNode), +} diff --git a/codama-nodes/src/generated/discriminator_nodes/field_discriminator_node.rs b/codama-nodes/src/generated/discriminator_nodes/field_discriminator_node.rs new file mode 100644 index 0000000..e5d0633 --- /dev/null +++ b/codama-nodes/src/generated/discriminator_nodes/field_discriminator_node.rs @@ -0,0 +1,21 @@ +use crate::{CamelCaseString, HasName}; +use codama_nodes_derive::node; + +#[node] +pub struct FieldDiscriminatorNode { + // Data. + pub name: CamelCaseString, + pub offset: u64, +} + +impl From for crate::Node { + fn from(val: FieldDiscriminatorNode) -> Self { + crate::Node::Discriminator(val.into()) + } +} + +impl HasName for FieldDiscriminatorNode { + fn name(&self) -> &CamelCaseString { + &self.name + } +} diff --git a/codama-nodes/src/generated/discriminator_nodes/mod.rs b/codama-nodes/src/generated/discriminator_nodes/mod.rs new file mode 100644 index 0000000..bfa2c08 --- /dev/null +++ b/codama-nodes/src/generated/discriminator_nodes/mod.rs @@ -0,0 +1,9 @@ +mod constant_discriminator_node; +mod discriminator_node; +mod field_discriminator_node; +mod size_discriminator_node; + +pub use constant_discriminator_node::*; +pub use discriminator_node::*; +pub use field_discriminator_node::*; +pub use size_discriminator_node::*; diff --git a/codama-nodes/src/generated/discriminator_nodes/size_discriminator_node.rs b/codama-nodes/src/generated/discriminator_nodes/size_discriminator_node.rs new file mode 100644 index 0000000..cf134a0 --- /dev/null +++ b/codama-nodes/src/generated/discriminator_nodes/size_discriminator_node.rs @@ -0,0 +1,14 @@ +use codama_nodes_derive::node; + +#[node] +#[derive(Copy)] +pub struct SizeDiscriminatorNode { + // Data. + pub size: u64, +} + +impl From for crate::Node { + fn from(val: SizeDiscriminatorNode) -> Self { + crate::Node::Discriminator(val.into()) + } +} diff --git a/codama-nodes/src/generated/mod.rs b/codama-nodes/src/generated/mod.rs index a9cba95..177d7e8 100644 --- a/codama-nodes/src/generated/mod.rs +++ b/codama-nodes/src/generated/mod.rs @@ -1,3 +1,7 @@ +mod count_nodes; +mod discriminator_nodes; mod link_nodes; +pub use count_nodes::*; +pub use discriminator_nodes::*; pub use link_nodes::*; diff --git a/codama-nodes/src/lib.rs b/codama-nodes/src/lib.rs index a28f4d6..e0d6dbe 100644 --- a/codama-nodes/src/lib.rs +++ b/codama-nodes/src/lib.rs @@ -29,9 +29,7 @@ mod value_nodes; pub use account_node::*; pub use constant_node::*; pub use contextual_value_nodes::*; -pub use count_nodes::*; pub use defined_type_node::*; -pub use discriminator_nodes::*; pub use error_node::*; pub use event_node::*; pub use generated::*; diff --git a/codama-nodes/src/type_nodes/array_type_node.rs b/codama-nodes/src/type_nodes/array_type_node.rs index 2847b0b..cfc14e1 100644 --- a/codama-nodes/src/type_nodes/array_type_node.rs +++ b/codama-nodes/src/type_nodes/array_type_node.rs @@ -29,7 +29,7 @@ impl ArrayTypeNode { } } - pub fn fixed(item: T, value: usize) -> Self + pub fn fixed(item: T, value: u64) -> Self where T: Into, { diff --git a/codama-nodes/src/type_nodes/map_type_node.rs b/codama-nodes/src/type_nodes/map_type_node.rs index 9c5a962..4f8a4ab 100644 --- a/codama-nodes/src/type_nodes/map_type_node.rs +++ b/codama-nodes/src/type_nodes/map_type_node.rs @@ -32,7 +32,7 @@ impl MapTypeNode { } } - pub fn fixed(key: K, value: V, size: usize) -> Self + pub fn fixed(key: K, value: V, size: u64) -> Self where K: Into, V: Into, diff --git a/codama-nodes/src/type_nodes/set_type_node.rs b/codama-nodes/src/type_nodes/set_type_node.rs index af537a9..7cdda95 100644 --- a/codama-nodes/src/type_nodes/set_type_node.rs +++ b/codama-nodes/src/type_nodes/set_type_node.rs @@ -29,7 +29,7 @@ impl SetTypeNode { } } - pub fn fixed(item: T, value: usize) -> Self + pub fn fixed(item: T, value: u64) -> Self where T: Into, { diff --git a/spec-generators/src/defaults.ts b/spec-generators/src/defaults.ts index 189687e..bfb6530 100644 --- a/spec-generators/src/defaults.ts +++ b/spec-generators/src/defaults.ts @@ -22,7 +22,11 @@ export interface CategoryRouting { * v1. Categories absent from this map are not generated yet (today * that's every category except `link`). */ -export const CATEGORY_ROUTING: ReadonlyMap = new Map([['link', { nodeVariant: 'Link' }]]); +export const CATEGORY_ROUTING: ReadonlyMap = new Map([ + ['count', { nodeVariant: 'Count' }], + ['discriminator', { nodeVariant: 'Discriminator' }], + ['link', { nodeVariant: 'Link' }], +]); /** * Mapping from spec category name to the output subdirectory the diff --git a/spec-generators/src/fragments/attributeBodyLine.ts b/spec-generators/src/fragments/attributeBodyLine.ts index c18bd99..2eb3b4d 100644 --- a/spec-generators/src/fragments/attributeBodyLine.ts +++ b/spec-generators/src/fragments/attributeBodyLine.ts @@ -27,7 +27,15 @@ export function getAttributeBodyLineFragment(attr: AttributeSpec): Fragment { let typeFragment: Fragment; let serdeAttr: string; - if (isOptional) { + if (isDocsType(attr.type)) { + // `docs` attributes are kept as a bare `Docs` regardless of + // `optional`. `Docs` already wraps a `Vec` with a + // sensible `Default`/`is_default`, so wrapping it in + // `Option<…>` would be redundant and divergent from the + // hand-written convention. + typeFragment = inner; + serdeAttr = '#[serde(default, skip_serializing_if = "crate::is_default")]'; + } else if (isOptional) { if (isVecLike) { typeFragment = inner; serdeAttr = '#[serde(default, skip_serializing_if = "crate::is_default")]'; @@ -35,12 +43,6 @@ export function getAttributeBodyLineFragment(attr: AttributeSpec): Fragment { typeFragment = fragment`Option<${inner}>`; serdeAttr = '#[serde(skip_serializing_if = "crate::is_default")]'; } - } else if (isDocsType(attr.type)) { - // `docs` attributes are technically required by the spec but - // their absence-in-JSON case needs explicit defaulting. Match - // the hand-written convention. - typeFragment = inner; - serdeAttr = '#[serde(default, skip_serializing_if = "crate::is_default")]'; } else { typeFragment = inner; serdeAttr = ''; diff --git a/spec-generators/src/fragments/nodeStructFragment.ts b/spec-generators/src/fragments/nodeStructFragment.ts index c00a0e3..07a6813 100644 --- a/spec-generators/src/fragments/nodeStructFragment.ts +++ b/spec-generators/src/fragments/nodeStructFragment.ts @@ -1,24 +1,57 @@ import { pascalCase } from '@codama/fragments'; import { type Fragment, fragment, mergeFragments } from '@codama/fragments/rust'; -import { isChildAttribute, type AttributeSpec, type NodeSpec } from '@codama/spec'; +import { isChildAttribute, type AttributeSpec, type NodeSpec, type TypeExpr } from '@codama/spec'; import { getAttributeBodyLineFragment } from './attributeBodyLine'; import { use } from './helpers'; /** * Render the `#[node] pub struct XxxNode { … }` declaration for one - * spec node. Attributes are partitioned into "Data." (primitives, - * enumerations) and "Children." (node / union / nestedUnion refs, - * recursively through `array` / `tuple`) sections; within each - * section, fields appear in spec declaration order. Empty sections - * are omitted. + * spec node, plus any auto-derived `#[derive(...)]` line. + * + * Attributes are partitioned into "Data." (primitives, enumerations) + * and "Children." (node / union / nestedUnion refs, recursively + * through `array` / `tuple`) sections; within each section, fields + * appear in spec declaration order. Empty sections are omitted. + * + * Derive heuristic (verified against every `#[node]` struct in + * `codama-nodes`): + * + * - `Copy` when every attribute's type kind is a scalar + * (`integer` / `float` / `boolean` / `enumeration`); + * an empty struct also qualifies. + * - `Default` when the struct has no attributes at all. + * + * The handful of non-empty hand-written `Default` structs + * (`ProgramNode`, `InstructionNode`, `InstructionStatusNode`) are + * top-level builder types that keep their hand-written `Default` + * impl and aren't generated here. */ export function getNodeStructFragment(node: NodeSpec): Fragment { const structName = pascalCase(node.kind); const { data, children } = partitionAttributes(node); + const macros = fragment`#[${use('codama_nodes_derive::node')}]`; + const derives = buildDeriveFragment(node); + const header = derives === undefined ? macros : mergeFragments([macros, derives], parts => parts.join('\n')); + if (data.length === 0 && children.length === 0) { + return fragment`${header}\npub struct ${structName} {}`; + } const body = buildBody(data, children); - return fragment`#[${use('codama_nodes_derive::node')}]\npub struct ${structName} {\n${body}\n}`; + return fragment`${header}\npub struct ${structName} {\n${body}\n}`; +} + +const SCALAR_KINDS: ReadonlySet = new Set(['integer', 'float', 'boolean', 'enumeration']); + +function buildDeriveFragment(node: NodeSpec): Fragment | undefined { + const isEmpty = node.attributes.length === 0; + const isCopy = isEmpty || node.attributes.every(a => SCALAR_KINDS.has(a.type.kind)); + const isDefault = isEmpty; + const derives: string[] = []; + if (isCopy) derives.push('Copy'); + if (isDefault) derives.push('Default'); + if (derives.length === 0) return undefined; + return fragment`#[derive(${derives.join(', ')})]`; } interface PartitionedAttributes { diff --git a/spec-generators/src/unions.ts b/spec-generators/src/unions.ts index 0ab4843..a0df0d9 100644 --- a/spec-generators/src/unions.ts +++ b/spec-generators/src/unions.ts @@ -1,3 +1,4 @@ +import { pascalCase } from '@codama/fragments'; import type { NodeSpec, Spec, UnionSpec } from '@codama/spec'; /** @@ -10,11 +11,19 @@ const REGISTERED_UNION_PREFIX = 'registered'; /** * The spec unions in a category that the generator emits Rust enums - * for, sorted alphabetically by name for stable output. + * for. A union is "emittable" when it has a `registered` + * sibling in the same category — i.e. it's the category's main union + * (the standalone twin of a registered/dispatch union). Inline / + * synthetic unions (e.g. `constantPdaSeedValue`) don't have a twin + * and aren't emitted as Rust enums; they're either name-aliased to an + * existing type (via {@link UNION_NAME_OVERRIDES}) or handled + * bespoke. Sorted alphabetically by name for stable output. */ export function getEmittableUnions(category: Spec['categories'][number]): readonly UnionSpec[] { + const unionNames = new Set(category.unions.map(u => u.name)); return category.unions .filter(u => !u.name.startsWith(REGISTERED_UNION_PREFIX)) + .filter(u => unionNames.has(`${REGISTERED_UNION_PREFIX}${pascalCase(u.name)}`)) .toSorted((a, b) => a.name.localeCompare(b.name)); } diff --git a/spec-generators/test/fragments/attributeBodyLine.test.ts b/spec-generators/test/fragments/attributeBodyLine.test.ts index ba54e1c..2a4350b 100644 --- a/spec-generators/test/fragments/attributeBodyLine.test.ts +++ b/spec-generators/test/fragments/attributeBodyLine.test.ts @@ -23,11 +23,14 @@ describe('getAttributeBodyLineFragment', () => { ); }); - it('renders a docs attribute with default + skip_serializing_if and no Option wrap', () => { - const result = getAttributeBodyLineFragment(attribute('docs', docs())); - expect(result.content).toBe( - ['#[serde(default, skip_serializing_if = "crate::is_default")]', 'pub docs: Docs,'].join('\n'), - ); + it('renders a docs attribute as a bare `Docs` regardless of `optional`, never wrapping it in Option', () => { + // `Docs` already has a sensible `Default` (empty Vec) + `is_default`, + // so the spec's `optional: true` collapses to the same serde shape. + const required = getAttributeBodyLineFragment(attribute('docs', docs())); + const optional = getAttributeBodyLineFragment(optionalAttribute('docs', docs())); + const expected = ['#[serde(default, skip_serializing_if = "crate::is_default")]', 'pub docs: Docs,'].join('\n'); + expect(required.content).toBe(expected); + expect(optional.content).toBe(expected); }); it('escapes the Rust keyword `type` as `r#type` in field position', () => { diff --git a/spec-generators/test/fragments/nodeStructFragment.test.ts b/spec-generators/test/fragments/nodeStructFragment.test.ts index 1280399..9182c6c 100644 --- a/spec-generators/test/fragments/nodeStructFragment.test.ts +++ b/spec-generators/test/fragments/nodeStructFragment.test.ts @@ -1,4 +1,13 @@ -import { attribute, defineNode, node, optionalAttribute, stringIdentifier } from '@codama/spec/api'; +import { + attribute, + boolean, + defineNode, + enumeration, + node, + optionalAttribute, + stringIdentifier, + u64, +} from '@codama/spec/api'; import { describe, expect, it } from 'vitest'; import { getNodeStructFragment } from '../../src/fragments/nodeStructFragment'; @@ -57,4 +66,37 @@ describe('getNodeStructFragment', () => { expect(out).not.toContain('// Data.'); expect(out).toContain('// Children.'); }); + + it('emits an empty `{}` struct body with #[derive(Copy, Default)] for an attribute-less node', () => { + const spec = defineNode('remainderCountNode', { attributes: [] }); + const out = getNodeStructFragment(spec).content; + expect(out).toContain('#[derive(Copy, Default)]'); + expect(out).toContain('pub struct RemainderCountNode {}'); + expect(out).not.toContain('// Data.'); + expect(out).not.toContain('// Children.'); + }); + + it('derives Copy when every attribute is a scalar kind (integer / float / boolean / enumeration)', () => { + const spec = defineNode('fixedCountNode', { attributes: [attribute('value', u64())] }); + expect(getNodeStructFragment(spec).content).toContain('#[derive(Copy)]'); + const mixed = defineNode('numberTypeNode', { + attributes: [ + attribute('format', enumeration('numberFormat')), + attribute('endian', enumeration('endianness')), + attribute('signed', boolean()), + ], + }); + expect(getNodeStructFragment(mixed).content).toContain('#[derive(Copy)]'); + }); + + it('does NOT derive Copy when any attribute is a non-scalar (string / node / union / docs / …)', () => { + const withString = defineNode('fieldDiscriminatorNode', { + attributes: [attribute('name', stringIdentifier()), attribute('offset', u64())], + }); + expect(getNodeStructFragment(withString).content).not.toContain('#[derive('); + const withNode = defineNode('constantDiscriminatorNode', { + attributes: [attribute('offset', u64()), attribute('constant', node('constantValueNode'))], + }); + expect(getNodeStructFragment(withNode).content).not.toContain('#[derive('); + }); }); diff --git a/spec-generators/test/generate.test.ts b/spec-generators/test/generate.test.ts index 496cb36..2ed2659 100644 --- a/spec-generators/test/generate.test.ts +++ b/spec-generators/test/generate.test.ts @@ -34,9 +34,19 @@ describe('validateRenderOptions', () => { describe('getRenderMap', () => { const map = getRenderMap(getSpec(), options()); - it('emits one .rs file per link node, the link_node.rs union, link_nodes/mod.rs, and a root mod.rs', () => { + it('emits one .rs file per node, one per emittable union, a per-category mod.rs, and a root mod.rs', () => { const keys = [...map.keys()].toSorted(); expect(keys).toEqual([ + 'count_nodes/count_node.rs', + 'count_nodes/fixed_count_node.rs', + 'count_nodes/mod.rs', + 'count_nodes/prefixed_count_node.rs', + 'count_nodes/remainder_count_node.rs', + 'discriminator_nodes/constant_discriminator_node.rs', + 'discriminator_nodes/discriminator_node.rs', + 'discriminator_nodes/field_discriminator_node.rs', + 'discriminator_nodes/mod.rs', + 'discriminator_nodes/size_discriminator_node.rs', 'link_nodes/account_link_node.rs', 'link_nodes/defined_type_link_node.rs', 'link_nodes/instruction_account_link_node.rs', @@ -84,9 +94,59 @@ describe('getRenderMap', () => { expect(entry.content).toContain('pub use link_node::*;'); }); - it('emits a root mod.rs that re-exports the link_nodes subdirectory', () => { + it('emits a root mod.rs that re-exports every per-category subdirectory', () => { const entry = getFromRenderMap(map, 'mod.rs'); - expect(entry.content).toContain('mod link_nodes;'); - expect(entry.content).toContain('pub use link_nodes::*;'); + for (const dir of ['count_nodes', 'discriminator_nodes', 'link_nodes']) { + expect(entry.content).toContain(`mod ${dir};`); + expect(entry.content).toContain(`pub use ${dir}::*;`); + } + }); + + describe('count category', () => { + it('routes FixedCountNode through Node::Count and emits the u64 field plus #[derive(Copy)]', () => { + const entry = getFromRenderMap(map, 'count_nodes/fixed_count_node.rs'); + expect(entry.content).toContain('#[node]'); + expect(entry.content).toContain('#[derive(Copy)]'); + expect(entry.content).toContain('pub struct FixedCountNode {'); + expect(entry.content).toContain('pub value: u64,'); + expect(entry.content).toContain('impl From for crate::Node {'); + expect(entry.content).toContain('crate::Node::Count(val.into())'); + }); + + it('emits RemainderCountNode as an empty struct with #[derive(Copy, Default)]', () => { + const entry = getFromRenderMap(map, 'count_nodes/remainder_count_node.rs'); + expect(entry.content).toContain('#[derive(Copy, Default)]'); + expect(entry.content).toContain('pub struct RemainderCountNode {}'); + }); + + it('emits the CountNode union with Fixed/Prefixed/Remainder variants and no HasName impl', () => { + const entry = getFromRenderMap(map, 'count_nodes/count_node.rs'); + expect(entry.content).toContain('pub enum CountNode {'); + expect(entry.content).toContain('Fixed(FixedCountNode),'); + expect(entry.content).toContain('Prefixed(PrefixedCountNode),'); + expect(entry.content).toContain('Remainder(RemainderCountNode),'); + expect(entry.content).not.toContain('impl HasName for CountNode'); + }); + }); + + describe('discriminator category', () => { + it('routes through Node::Discriminator and emits u64 offset/size fields', () => { + const constant = getFromRenderMap(map, 'discriminator_nodes/constant_discriminator_node.rs'); + expect(constant.content).toContain('pub offset: u64,'); + expect(constant.content).toContain('crate::Node::Discriminator(val.into())'); + const size = getFromRenderMap(map, 'discriminator_nodes/size_discriminator_node.rs'); + expect(size.content).toContain('#[derive(Copy)]'); + expect(size.content).toContain('pub size: u64,'); + }); + + it('emits HasName only on FieldDiscriminatorNode (only member with a stringIdentifier name)', () => { + const field = getFromRenderMap(map, 'discriminator_nodes/field_discriminator_node.rs'); + expect(field.content).toContain('impl HasName for FieldDiscriminatorNode {'); + const constant = getFromRenderMap(map, 'discriminator_nodes/constant_discriminator_node.rs'); + expect(constant.content).not.toContain('impl HasName'); + const union = getFromRenderMap(map, 'discriminator_nodes/discriminator_node.rs'); + // Not every variant has a name -> no union HasName impl. + expect(union.content).not.toContain('impl HasName for DiscriminatorNode'); + }); }); }); diff --git a/spec-generators/test/unions.test.ts b/spec-generators/test/unions.test.ts index 0ec2f33..07906de 100644 --- a/spec-generators/test/unions.test.ts +++ b/spec-generators/test/unions.test.ts @@ -5,16 +5,24 @@ import { flattenNodeUnion, getEmittableUnions } from '../src/unions'; const spec = getSpec(); const linkCategory = spec.categories.find(c => c.name === 'link')!; +const pdaSeedCategory = spec.categories.find(c => c.name === 'pdaSeed')!; describe('getEmittableUnions', () => { - it('returns spec unions whose name does NOT start with `registered`, sorted alphabetically', () => { - const names = getEmittableUnions(linkCategory).map(u => u.name); - expect(names).toEqual(['linkNode']); + it('returns the category-main union (the standalone twin of a `registered…`), sorted alphabetically', () => { + expect(getEmittableUnions(linkCategory).map(u => u.name)).toEqual(['linkNode']); + expect(getEmittableUnions(pdaSeedCategory).map(u => u.name)).toEqual(['pdaSeedNode']); }); it('skips category-registry unions (`registered*`)', () => { - const names = getEmittableUnions(linkCategory).map(u => u.name); - expect(names).not.toContain('registeredLinkNode'); + expect(getEmittableUnions(linkCategory).map(u => u.name)).not.toContain('registeredLinkNode'); + }); + + it('skips inline / synthetic unions that lack a `registered` twin (e.g. constantPdaSeedValue)', () => { + // `constantPdaSeedValue` is the `constantPdaSeedNode.value` attribute's + // union; it has no `registered` twin (it's not a category + // dispatch enum) so the generator must not emit it as its own Rust + // enum. A later PR with inline-union support will handle it. + expect(getEmittableUnions(pdaSeedCategory).map(u => u.name)).not.toContain('constantPdaSeedValue'); }); });