Skip to content

danielmiladinov/camel-snake-kebab-java

Repository files navigation

camel-snake-kebab-java

High-performance word case converter. Java engine with a thin Clojure wrapper providing a drop-in API replacement for camel-snake-kebab 0.4.x.

Clojars Project

Motivation

If profile or heap-dump analysis shows camel-snake-kebab on your hot path, consider this drop-in replacement. The original Clojure implementation allocates 15-25 objects per conversion (boxed keyword vectors, subvec per loop iteration, transient vectors, .substring per word, lazy sequences). This library replaces the inner loop with a single-pass Java algorithm that produces 3 allocations per conversion:

  • byte[] for fast character classification,
  • StringBuilder, for building up the transformation result,
  • String the final String.

Benchmark Results

Measured with a custom warm-up + timing harness (5,000 iterations, JIT-warmed) on the same JVM, comparing original camel-snake-kebab 0.4.3 to this library.

Benchmark Original New Speedup
->kebab-case (string, "fooBarBaz") 3,247 ns 393 ns 8.3x
->camelCase (string, "foo-bar-baz") 2,165 ns 938 ns 2.3x
->snake_case (string, "fooBarBaz") 1,528 ns 151 ns 10.1x
->PascalCase (string, "foo-bar-baz") 1,826 ns 161 ns 11.3x
->SCREAMING_SNAKE_CASE ("fooBarBaz") 1,324 ns 173 ns 7.7x
->Camel_Snake_Case ("fooBarBaz") 1,740 ns 174 ns 10.0x
->HTTP-Header-Case ("x-ssl-cipher") 1,919 ns 290 ns 6.6x
->kebab-case (long string, 61 chars) 10,229 ns 1,013 ns 10.1x
->kebab-case-keyword ("objectId") 1,234 ns 216 ns 5.7x
Batch 20 keys (keyword) 25,723 ns 2,341 ns 11.0x
Round-trip kebab→camel→kebab (20 keys) 72,964 ns 5,668 ns 12.9x
transform-keys (nested map, 156 keys) 274,720 ns 30,794 ns 8.9x

Installation

deps.edn

net.clojars.danielmiladinov/camel-snake-kebab-java {:mvn/version "1.0.0"}

project.clj

[net.clojars.danielmiladinov/camel-snake-kebab-java "1.0.0"]

Usage

Drop-in replacement

Replace camel-snake-kebab.core with camel-snake-kebab-java.core and camel-snake-kebab.extras with camel-snake-kebab-java.extras. All function names and signatures are identical.

;; Before
(ns my.app
  (:require [camel-snake-kebab.core :as csk]
            [camel-snake-kebab.extras :as cske]))

;; After
(ns my.app
  (:require [camel-snake-kebab-java.core :as csk]
            [camel-snake-kebab-java.extras :as cske]))

All existing call sites work unchanged:

(csk/->kebab-case "fooBarBaz")                                 ;=> "foo-bar-baz"
(csk/->camelCase :foo-bar)                                     ;=> :fooBar
(csk/->kebab-case-keyword "objectId")                          ;=> :object-id
(csk/->SCREAMING_SNAKE_CASE "fooBar")                          ;=> "FOO_BAR"
(csk/->HTTP-Header-Case "x-ssl-cipher")                        ;=> "X-SSL-Cipher"

(csk/->kebab-case "foo.bar" :separator \.)                     ;=> "foo-bar"

(cske/transform-keys csk/->kebab-case-keyword
                     {"firstName" "John" "lastName" "Doe"})    ;=> {:first-name "John" :last-name "Doe"}

Custom conversions

convert-case accepts arbitrary Clojure functions, matching the original signature:

(require '[clojure.string :as str])

(csk/convert-case str/capitalize str/capitalize " " "foo-bar") ;=> "Foo Bar"

Available functions

Type-preserving (returns same type as input — string, keyword, or symbol):

  • ->kebab-case
  • ->snake_case
  • ->camelCase
  • ->PascalCase
  • ->SCREAMING_SNAKE_CASE
  • ->Camel_Snake_Case
  • ->HTTP-Header-Case

Type-converting (always returns the specified type):

  • ->kebab-case-keyword, ->kebab-case-string, ->kebab-case-symbol
  • ->snake_case_keyword, ->snake_case_string, ->snake_case_symbol
  • ->camelCaseKeyword, ->camelCaseString, ->camelCaseSymbol
  • ->PascalCaseKeyword, ->PascalCaseString, ->PascalCaseSymbol
  • ->SCREAMING_SNAKE_CASE_KEYWORD, ->SCREAMING_SNAKE_CASE_STRING, ->SCREAMING_SNAKE_CASE_SYMBOL
  • ->Camel_Snake_Case_Keyword, ->Camel_Snake_Case_String, ->Camel_Snake_Case_Symbol
  • ->HTTP-Header-Case-Keyword, ->HTTP-Header-Case-String, ->HTTP-Header-Case-Symbol

Architecture

┌────────────────────────────────────────────────────────┐
│  Clojure (core.clj / extras.clj)                       │
│  - alter-name: type dispatch (string/keyword/symbol)   │
│  - convert-case: custom fn support via WordSplitter    │
│  - transform-keys: postwalk over nested maps           │
├──────────────────────────────────────────────────────-─┤
│  Java (CaseConverter.java / WordSplitter.java)         │
│  - Single-pass split + transform + join                │
│  - ASCII fast-path character classification            │
└────────────────────────────────────────────────────────┘

How it works

  1. Character classification: An ASCII lookup table (byte[128]) classifies each character as LOWER, UPPER, NUMBER, WHITESPACE, or OTHER in a single array access. Non-ASCII falls back to the same rules as the original library.

  2. Single-pass conversion: computeGeneric walks the input once, detecting word boundaries using the same rules as camel-snake-kebab's generic-split, and writes transformed characters directly into a pre-sized StringBuilder. No intermediate collections, no substring allocations, no lazy sequences.

ClojureScript

This library is JVM-only. Consumer repos with .cljc files should continue using the original camel-snake-kebab for ClojureScript via reader conditionals:

(ns my.shared
   (:require #?(:clj  [camel-snake-kebab-java.core :as csk]
                :cljs [camel-snake-kebab.core :as csk])))

Requirements

  • JDK 21+
  • Clojure 1.12+
  • Clojure CLI (clojure/clj)
  • Leiningen (for lein jar, lein deploy)

License

Apache License 2.0 (see LICENSE file).

About

A drop-in replacement for https://github.com/clj-commons/camel-snake-kebab, rewritten in Java for higher performance

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors