feat: support for more String transformation actions#1558
Conversation
Closes open-circle#1324. valibot has toLowerCase/toUpperCase but no actions for the common case conversions (camelCase, snake_case, kebab case, PascalCase) needed when converting between API (snake_case) and frontend (camelCase) naming, building URL slugs, etc. Add four transformation actions: toCamelCase (hello_world to helloWorld), toSnakeCase (helloWorld to hello_world), toKebabCase (helloWorld to hello world), toPascalCase (hello_world to HelloWorld).
WalkthroughCase transformation actions now use a shared Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@library/src/actions/index.ts`:
- Line 108: Remove the redundant duplicate export of ./toCamelCase/index.ts from
the barrel exports in library/src/actions/index.ts, keeping the existing single
re-export unchanged.
In `@library/src/actions/toCamelCase/toCamelCase.ts`:
- Around line 4-12: Update splitWords to split acronym-to-word boundaries before
applying the existing lowercase-or-digit-to-uppercase replacement, so inputs
like parseURLValue produce separate URL and Value tokens. Preserve the existing
separator handling and ensure the shared tokenization yields correct results for
toCamelCase, toSnakeCase, toKebabCase, and toPascalCase.
- Around line 53-149: Remove the ToSnakeCaseAction, toSnakeCase,
ToKebabCaseAction, toKebabCase, ToPascalCaseAction, and toPascalCase
declarations from this module, leaving the toCamelCase implementation intact.
Rely on their dedicated modules and existing exports from the actions index so
each case action has a single public definition.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e1af0518-19c5-4713-ac57-2efec33b86aa
📒 Files selected for processing (3)
library/src/actions/index.tslibrary/src/actions/toCamelCase/index.tslibrary/src/actions/toCamelCase/toCamelCase.ts
| export * from './toDate/index.ts'; | ||
| export * from './toKebabCase/index.ts'; | ||
| export * from './toLowerCase/index.ts'; | ||
| export * from './toCamelCase/index.ts'; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Duplicate re-export of toCamelCase/index.ts.
Line 104 already exports ./toCamelCase/index.ts; line 108 duplicates it verbatim. The line-range summary claims this was "a previously missing re-export," but the annotated file shows the export already existed — this is a redundant duplicate, not a new addition, and likely a leftover from the file-organization mixup in toCamelCase.ts (see companion comment there).
🧹 Proposed fix
export * from './toDate/index.ts';
export * from './toKebabCase/index.ts';
export * from './toLowerCase/index.ts';
-export * from './toCamelCase/index.ts';
export * from './toMaxValue/index.ts';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export * from './toCamelCase/index.ts'; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@library/src/actions/index.ts` at line 108, Remove the redundant duplicate
export of ./toCamelCase/index.ts from the barrel exports in
library/src/actions/index.ts, keeping the existing single re-export unchanged.
| function splitWords(value: string): string[] { | ||
| // Split on non-alphanumeric boundaries and existing case/separator breaks. | ||
| return value | ||
| .replace(/([a-z0-9])([A-Z])/g, '$1 $2') | ||
| .replace(/[_\-\s]+/g, ' ') | ||
| .split(' ') | ||
| .map((part) => part.trim()) | ||
| .filter((part) => part.length > 0); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Does valibot's toCamelCase/toSnakeCase/toKebabCase/toPascalCase splitWords implementation split acronym boundaries like "parseURLValue" into "parse Url Value"?
💡 Result:
Yes, Valibot's string case transformation actions (toCamelCase, toSnakeCase, toKebabCase, and toPascalCase) do split words based on acronym boundaries [1][2][3][4]. According to the official documentation, these actions treat acronym runs as distinct segments that are normalized to lowercase [2][3][4]. For example, the input parseURLValue is processed as parse, URL, and Value, resulting in outputs like [3][4]: - toCamelCase: parseUrlValue [3] - toSnakeCase: parse_url_value [4] - toKebabCase: parse-url-value [2] - toPascalCase: ParseUrlValue [5] The implementation uses a shared tokenizer utility that identifies word boundaries by underscores (_), hyphens (-), ASCII whitespace, and both case and acronym changes [1][2][3][6]. Digits remain attached to the preceding token (e.g., item2Name becomes item2-name) [2][3][4].
Citations:
- 1: https://valibot.dev/blog/valibot-v1.4-release-notes/
- 2: https://valibot.dev/api/toKebabCase/
- 3: https://valibot.dev/api/toCamelCase/
- 4: https://valibot.dev/api/toSnakeCase/
- 5: https://valibot.dev/api/toPascalCase/
- 6: feat: add built-in string case transformation actions (
toCamelCase,toSnakeCase,toKebabCase,toPascalCase) #1457
🏁 Script executed:
#!/bin/bash
set -euo pipefail
git ls-files 'library/src/actions/toCamelCase/*' 'library/src/actions/*Case*' 'library/src/**/to*Case*' | sed -n '1,120p'Repository: open-circle/valibot
Length of output: 1328
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the implementation and tests with line numbers.
sed -n '1,220p' library/src/actions/toCamelCase/toCamelCase.ts
printf '\n--- TESTS ---\n'
sed -n '1,240p' library/src/actions/toCamelCase/toCamelCase.test.tsRepository: open-circle/valibot
Length of output: 6673
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import re
def split_words(value: str):
return [
part.strip()
for part in re.sub(r'([a-z0-9])([A-Z])', r'\1 \2', value)
.replace('_', ' ')
.replace('-', ' ')
.split(' ')
if part.strip()
]
def camel(value: str):
words = split_words(value)
return ''.join(
w.lower() if i == 0 else w[:1].upper() + w[1:].lower()
for i, w in enumerate(words)
)
for s in ["parseURLValue", "helloWorld", "item2Name", "HELLO_WORLD", "--foo__bar--", "foo\tbar\nbaz"]:
print(s, "=>", split_words(s), "=>", camel(s))
PYRepository: open-circle/valibot
Length of output: 441
Split acronym boundaries in splitWords
Add the acronym split before the lowercase→uppercase rule, otherwise parseURLValue becomes parseUrlvalue and the same bug affects toSnakeCase, toKebabCase, and toPascalCase.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@library/src/actions/toCamelCase/toCamelCase.ts` around lines 4 - 12, Update
splitWords to split acronym-to-word boundaries before applying the existing
lowercase-or-digit-to-uppercase replacement, so inputs like parseURLValue
produce separate URL and Value tokens. Preserve the existing separator handling
and ensure the shared tokenization yields correct results for toCamelCase,
toSnakeCase, toKebabCase, and toPascalCase.
| /** | ||
| * To snake case action interface. | ||
| */ | ||
| export interface ToSnakeCaseAction | ||
| extends BaseTransformation<string, string, never> { | ||
| readonly type: 'to_snake_case'; | ||
| readonly reference: typeof toSnakeCase; | ||
| } | ||
|
|
||
| /** | ||
| * Creates a to snake case transformation action. | ||
| * | ||
| * Converts the input to snake_case, e.g. `"helloWorld"` → `"hello_world"`, | ||
| * `"foo-bar"` → `"foo_bar"`. See issue #1324. | ||
| * | ||
| * @returns A to snake case action. | ||
| */ | ||
| // @__NO_SIDE_EFFECTS__ | ||
| export function toSnakeCase(): ToSnakeCaseAction { | ||
| return { | ||
| kind: 'transformation', | ||
| type: 'to_snake_case', | ||
| reference: toSnakeCase, | ||
| async: false, | ||
| '~run'(dataset) { | ||
| const words = splitWords(dataset.value); | ||
| dataset.value = words.map((w) => w.toLowerCase()).join('_'); | ||
| return dataset; | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * To kebab case action interface. | ||
| */ | ||
| export interface ToKebabCaseAction | ||
| extends BaseTransformation<string, string, never> { | ||
| readonly type: 'to_kebab_case'; | ||
| readonly reference: typeof toKebabCase; | ||
| } | ||
|
|
||
| /** | ||
| * Creates a to kebab case transformation action. | ||
| * | ||
| * Converts the input to kebab-case, e.g. `"helloWorld"` → `"hello-world"`, | ||
| * `"foo_bar"` → `"foo-bar"`. See issue #1324. | ||
| * | ||
| * @returns A to kebab case action. | ||
| */ | ||
| // @__NO_SIDE_EFFECTS__ | ||
| export function toKebabCase(): ToKebabCaseAction { | ||
| return { | ||
| kind: 'transformation', | ||
| type: 'to_kebab_case', | ||
| reference: toKebabCase, | ||
| async: false, | ||
| '~run'(dataset) { | ||
| const words = splitWords(dataset.value); | ||
| dataset.value = words.map((w) => w.toLowerCase()).join('-'); | ||
| return dataset; | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * To pascal case action interface. | ||
| */ | ||
| export interface ToPascalCaseAction | ||
| extends BaseTransformation<string, string, never> { | ||
| readonly type: 'to_pascal_case'; | ||
| readonly reference: typeof toPascalCase; | ||
| } | ||
|
|
||
| /** | ||
| * Creates a to pascal case transformation action. | ||
| * | ||
| * Converts the input to PascalCase, e.g. `"hello_world"` → `"HelloWorld"`, | ||
| * `"foo-bar"` → `"FooBar"`. See issue #1324. | ||
| * | ||
| * @returns A to pascal case action. | ||
| */ | ||
| // @__NO_SIDE_EFFECTS__ | ||
| export function toPascalCase(): ToPascalCaseAction { | ||
| return { | ||
| kind: 'transformation', | ||
| type: 'to_pascal_case', | ||
| reference: toPascalCase, | ||
| async: false, | ||
| '~run'(dataset) { | ||
| const words = splitWords(dataset.value); | ||
| dataset.value = words | ||
| .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()) | ||
| .join(''); | ||
| return dataset; | ||
| }, | ||
| }; | ||
| } No newline at end of file |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check whether dedicated action files for toSnakeCase/toKebabCase/toPascalCase already exist
fd -a 'toSnakeCase.ts|toKebabCase.ts|toPascalCase.ts' library/src/actions
echo "---"
cat -n library/src/actions/toSnakeCase/toSnakeCase.ts 2>/dev/null
echo "---"
cat -n library/src/actions/toKebabCase/toKebabCase.ts 2>/dev/null
echo "---"
cat -n library/src/actions/toPascalCase/toPascalCase.ts 2>/dev/nullRepository: open-circle/valibot
Length of output: 4741
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '--- toCamelCase.ts ---\n'
cat -n library/src/actions/toCamelCase/toCamelCase.ts
printf '\n--- toCamelCase/index.ts ---\n'
cat -n library/src/actions/toCamelCase/index.ts
printf '\n--- actions/index.ts ---\n'
cat -n library/src/actions/index.ts
printf '\n--- exports referencing toSnakeCase/toKebabCase/toPascalCase ---\n'
rg -n "toSnakeCase|toKebabCase|toPascalCase" library/src/actions -g '!**/*.d.ts'Repository: open-circle/valibot
Length of output: 248
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '--- library/src/actions/toCamelCase/index.ts ---\n'
cat -n library/src/actions/toCamelCase/index.ts
printf '\n--- library/src/actions/index.ts ---\n'
cat -n library/src/actions/index.ts
printf '\n--- library/src/actions/toSnakeCase/index.ts ---\n'
cat -n library/src/actions/toSnakeCase/index.ts
printf '\n--- library/src/actions/toKebabCase/index.ts ---\n'
cat -n library/src/actions/toKebabCase/index.ts
printf '\n--- library/src/actions/toPascalCase/index.ts ---\n'
cat -n library/src/actions/toPascalCase/index.tsRepository: open-circle/valibot
Length of output: 248
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- library/src/actions/toCamelCase/toCamelCase.ts ---'
cat -n library/src/actions/toCamelCase/toCamelCase.ts
echo
echo '--- library/src/actions/toCamelCase/index.ts ---'
cat -n library/src/actions/toCamelCase/index.ts
echo
echo '--- library/src/actions/index.ts ---'
cat -n library/src/actions/index.ts
echo
echo '--- library/src/actions/toSnakeCase/index.ts ---'
cat -n library/src/actions/toSnakeCase/index.ts
echo
echo '--- library/src/actions/toKebabCase/index.ts ---'
cat -n library/src/actions/toKebabCase/index.ts
echo
echo '--- library/src/actions/toPascalCase/index.ts ---'
cat -n library/src/actions/toPascalCase/index.tsRepository: open-circle/valibot
Length of output: 11039
Remove the duplicate case actions from library/src/actions/toCamelCase/toCamelCase.ts
toSnakeCase, toKebabCase, and toPascalCase already have dedicated modules and are re-exported from library/src/actions/index.ts; keeping them here makes those names ambiguous in the aggregate barrel and drops them from the public export surface.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@library/src/actions/toCamelCase/toCamelCase.ts` around lines 53 - 149, Remove
the ToSnakeCaseAction, toSnakeCase, ToKebabCaseAction, toKebabCase,
ToPascalCaseAction, and toPascalCase declarations from this module, leaving the
toCamelCase implementation intact. Rely on their dedicated modules and existing
exports from the actions index so each case action has a single public
definition.
Source: Coding guidelines
There was a problem hiding this comment.
2 issues found across 3 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="library/src/actions/toCamelCase/toCamelCase.ts">
<violation number="1" location="library/src/actions/toCamelCase/toCamelCase.ts:4">
P2: Reimplementing case-splitting logic in a local splitWords() helper instead of reusing the shared `_formatCase` util (still used by the sibling toSnakeCase/toKebabCase/toPascalCase actions) duplicates tested logic and risks behavioral drift between the case-conversion actions.</violation>
<violation number="2" location="library/src/actions/toCamelCase/toCamelCase.ts:7">
P2: `splitWords` doesn't split on uppercase acronym boundaries (e.g., `URLValue` → stays as one word). This means inputs with acronym runs like `parseURLValue` are not split into their constituent words. The old `toCamelCase` implementation handled this correctly (`parseURLValue` → `parseUrlValue`), but the new shared `splitWords` function produces `"parseUrlvalue"` instead, losing the capital V in `Value`. This affects all four conversion actions. Consider adding a regex like `/([A-Z]+)([A-Z][a-z])/g` to split between uppercase runs that transition to a capitalized word.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| function splitWords(value: string): string[] { | ||
| // Split on non-alphanumeric boundaries and existing case/separator breaks. | ||
| return value | ||
| .replace(/([a-z0-9])([A-Z])/g, '$1 $2') |
There was a problem hiding this comment.
P2: splitWords doesn't split on uppercase acronym boundaries (e.g., URLValue → stays as one word). This means inputs with acronym runs like parseURLValue are not split into their constituent words. The old toCamelCase implementation handled this correctly (parseURLValue → parseUrlValue), but the new shared splitWords function produces "parseUrlvalue" instead, losing the capital V in Value. This affects all four conversion actions. Consider adding a regex like /([A-Z]+)([A-Z][a-z])/g to split between uppercase runs that transition to a capitalized word.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At library/src/actions/toCamelCase/toCamelCase.ts, line 7:
<comment>`splitWords` doesn't split on uppercase acronym boundaries (e.g., `URLValue` → stays as one word). This means inputs with acronym runs like `parseURLValue` are not split into their constituent words. The old `toCamelCase` implementation handled this correctly (`parseURLValue` → `parseUrlValue`), but the new shared `splitWords` function produces `"parseUrlvalue"` instead, losing the capital V in `Value`. This affects all four conversion actions. Consider adding a regex like `/([A-Z]+)([A-Z][a-z])/g` to split between uppercase runs that transition to a capitalized word.</comment>
<file context>
@@ -1,36 +1,33 @@
+function splitWords(value: string): string[] {
+ // Split on non-alphanumeric boundaries and existing case/separator breaks.
+ return value
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
+ .replace(/[_\-\s]+/g, ' ')
+ .split(' ')
</file context>
| import { _formatCase } from '../../utils/index.ts'; | ||
|
|
||
| /** Splits a string into words for case conversion. */ | ||
| function splitWords(value: string): string[] { |
There was a problem hiding this comment.
P2: Reimplementing case-splitting logic in a local splitWords() helper instead of reusing the shared _formatCase util (still used by the sibling toSnakeCase/toKebabCase/toPascalCase actions) duplicates tested logic and risks behavioral drift between the case-conversion actions.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At library/src/actions/toCamelCase/toCamelCase.ts, line 4:
<comment>Reimplementing case-splitting logic in a local splitWords() helper instead of reusing the shared `_formatCase` util (still used by the sibling toSnakeCase/toKebabCase/toPascalCase actions) duplicates tested logic and risks behavioral drift between the case-conversion actions.</comment>
<file context>
@@ -1,36 +1,33 @@
-import { _formatCase } from '../../utils/index.ts';
+
+/** Splits a string into words for case conversion. */
+function splitWords(value: string): string[] {
+ // Split on non-alphanumeric boundaries and existing case/separator breaks.
+ return value
</file context>
Closes #1324.
valibot has toLowerCase/toUpperCase but no actions for the common case conversions (camelCase, snake_case, kebab case, PascalCase) needed when converting between API (snake_case) and frontend (camelCase) naming, building URL slugs, etc. Add four transformation actions: toCamelCase (hello_world to helloWorld), toSnakeCase (helloWorld to hello_world), toKebabCase (helloWorld to hello world), toPascalCase (hello_world to HelloWorld).
Summary by CodeRabbit