Skip to content

Add SnakeCaseString to codama-nodes - #136

Closed
senzenn wants to merge 1 commit into
codama-idl:mainfrom
senzenn:feat/snake-case-string
Closed

senzenn wants to merge 1 commit into
codama-idl:mainfrom
senzenn:feat/snake-case-string

Conversation

@senzenn

@senzenn senzenn commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Adds SnakeCaseString to codama-nodes so #86 can drop its heck dependency. Suggested here: #86 (comment)

Same shape as CamelCaseString: same derives, new<T: AsRef<str>>, the String and &str conversions, Deref and AsRef, and the tests are laid out in the same order.

Word boundaries do not follow CamelCaseString, and that is the one thing worth a close look. to_camel_case treats a run of uppercase letters as a single word, so it turns tokenAMint into tokenAmint and SPLToken into spltoken. Carrying that into snake_case would give token_amint and spltoken, which are poor Rust identifiers, and tokenAMint / tokenBMint are common enough in real PDA seeds to matter. So an uppercase run splits before its last letter when a lowercase letter follows it:

input this PR to_camel_case reading
tokenAMint token_a_mint token_amint
SPLToken spl_token spltoken
poolNFTMint pool_nft_mint pool_nftmint
HTTPServer http_server httpserver
SCREAMING_SNAKE screaming_snake screaming_snake

Digits carry the case of whatever preceded them, so a digit ends a word only when an uppercase letter follows and the run started from lowercase. seed2Bump gives seed2_bump, while mint_2, this123 and 2Mint are left alone.

That combination matches heck::ToSnakeCase. I checked it by differential fuzzing against heck: 1,000,021 ASCII inputs over letters, digits, _, -, . and spaces plus an uppercase-heavy pass aimed at the acronym rule, zero mismatches, and to_snake_case is idempotent over the same corpus. A further 400,000 inputs over an alphabet of expanding-lowercase, titlecase, Greek and CJK characters turned up exactly one divergence class: heck lowercases a word-final Σ to ς, while this lowercases per character, as to_camel_case does, and gives σ. Every divergence in that run contained a Σ and none contained anything else, and it cannot reach a Rust identifier, so #86 can swap heck::ToSnakeCase for this without a change in behaviour.

One caveat on composition. Convert from the original string, not from a CamelCaseString. to_camel_case folds an uppercase run into one word and drops the separator before a digit, so by then tokenAMint is already tokenAmint and mint_2 is already mint2, and no snake-case pass can recover those boundaries. #86 is unaffected because seed names arrive as a raw String from the #[codama(seed(name = "..."))] attribute (SeedDirectiveType::Linked(String) and Variable { name: String, .. }), never as a node name. Anything that walks node names, which are all CamelCaseString, gets the collapsed reading. The doc comment says so.

Worth flagging for the record: this does not match snakeCase from @codama/fragments on uppercase runs, because that helper inserts a break before every uppercase letter and yields s_p_l_token and h_t_t_p_server. It does agree with the JS helper on the single-letter case, tokenAMint to token_a_mint. If you would rather this mirror the JS helper exactly, say so and I will switch it.

@senzenn

senzenn commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

@lorisleiva

@senzenn
senzenn force-pushed the feat/snake-case-string branch from 181c1c9 to b4c332f Compare September 2, 2026 07:24
@senzenn senzenn changed the title feat: add SnakeCaseString to codama-nodes Add SnakeCaseString to codama-nodes Sep 2, 2026
@senzenn
senzenn force-pushed the feat/snake-case-string branch 2 times, most recently from 6ed9288 to 9bf8642 Compare September 2, 2026 07:46
@lorisleiva

Copy link
Copy Markdown
Member

Hey, sorry for the late reply, Codama v2 is going to drop the casing requirement for identifiers so perhaps we shouldn't add more casing helpers here?

Same shape as CamelCaseString: same derives, new<T: AsRef<str>>, the String
and &str conversions, Deref and AsRef, and the tests in the same order.

Word boundaries deliberately do not follow CamelCaseString. to_camel_case
reads a run of uppercase letters as one word, turning tokenAMint into
tokenAmint and SPLToken into spltoken. In snake_case that would give
token_amint and spltoken, which make poor identifiers, and tokenAMint and
tokenBMint show up often enough in real PDA seeds to matter. So an uppercase
run splits before its last letter when a lowercase letter follows.

Digits carry the case of whatever preceded them, so a digit ends a word only
when an uppercase letter follows a lowercase run. seed2Bump gives seed2_bump
while mint_2, this123 and 2Mint are left alone.

That combination matches heck::ToSnakeCase, checked by differential fuzzing
over 1,000,021 ASCII inputs with zero mismatches. On a further 400,000
Unicode inputs the only divergence is a word-final Greek capital sigma, which
heck lowercases to the final form while this lowercases per character, as
to_camel_case does. So codama-idl#86 can drop the dependency.
@senzenn
senzenn force-pushed the feat/snake-case-string branch from 9bf8642 to 72e31f0 Compare September 2, 2026 10:52
@senzenn senzenn closed this Sep 2, 2026
@senzenn

senzenn commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Agreed, closing.

The point of this PR was to match CamelCaseString for consistency, but #129 drops the casing mandate and CamelCaseString is going with it, so adding a second casing helper to the node model is the wrong direction now. Landing it right before the 1.0.0 freeze would also mean maintaining it through 1.x while it's already obsolete in v2.

One piece is worth keeping separate though. #129 leaves casing to the renderers, and v2 widens the input: transferTokens, transfer_tokens and TransferTokens all become valid identifiers. A Rust codegen still has to normalize whatever it gets, which is what #86 wanted to_snake_case for. That belongs in codama-syn-helpers next to snake_case_ident, where #86 already had it.

If it saves #86 from pulling in heck, the function on this branch works as a private helper there. It matches heck::ToSnakeCase on ASCII, fuzzed over a million inputs with zero mismatches, and the only divergence I hit was a word-final Greek capital sigma. Up to whoever picks up #86.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants