keycase 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f9cd4f359e0bdcfad518f08a8bbb845060cb4d66d2d8a3738b68aa46c53f183e
4
- data.tar.gz: d0c79c9129587e65c2588efff8a969c5877ac802d1e1b818c2f9eaf2ff8bb114
3
+ metadata.gz: 39ff511337d13fc2e2c0118ee5f3d439f3f0be41eb83c86ead9fdee519454588
4
+ data.tar.gz: e13af5511466486ea3146f9f10c82b815b0879b0565e4590b84162e49cc73f52
5
5
  SHA512:
6
- metadata.gz: 606ecc4d4358942e9b5e00c4f5ac1c6dfda3fc0b548948b69c3d3fb30dfe935c6382c3964cb7ca4a737d8cad755c2d2db8ca55d0f40291faff1d3ab60072d2d6
7
- data.tar.gz: 376edfd2e6bc64564adb3b11ef63e658cc540d0c7759b6388bf84973e0fcee55b056b5d21ed01577c6346a0cdf01e13fdb2270ed195efced6e3e8d9436f048ae
6
+ metadata.gz: f9aba9bd010e999f6230da576f3785beac87369e83a8ddcda0b7ae6deb7aa32529f90cee48bf50fb7d5f18b81b195e8e9fdbc0bde1cb57ce926dce9843a9f4f1
7
+ data.tar.gz: b6782695576a300f13702c0a21be89b89631a35247d8628444b7f60c7fa8f74c14d3daa91224d9d22e709b373eec32b12c91cdd060fbb1c59746835e967fdab7
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Keycase
2
2
 
3
3
  This gem converts the case of strings, symbols, and keys of hash recursively.
4
- The convertible cases are camelCase, PascalCase, snake_case, etc.
4
+ The convertible cases are camelCase, PascalCase, snake_case, SCREAMING_SNAKE_CASE, kebab-case, and Train-Case.
5
5
 
6
6
  ## Installation
7
7
 
@@ -21,6 +21,43 @@ Or install it yourself as:
21
21
 
22
22
  ## Usage
23
23
 
24
+ ### Module functions (no `using`)
25
+
26
+ Call methods on `Keycase` when refinements are awkward (Rails, other gems, shared libraries).
27
+
28
+ ```rb
29
+ require "keycase"
30
+
31
+ Keycase.camel_case("user_id") # => "userId"
32
+ Keycase.camel_case(:user_id) # => :userId
33
+ Keycase.camel_case(42) # => 42
34
+
35
+ Keycase.camel_case("APIResponse", acronyms: %w[API HTTP ID])
36
+ # => "APIResponse"
37
+
38
+ Keycase.camel_case("user_id", capitalize: true)
39
+ # => "UserId"
40
+
41
+ Keycase.snake_case("userID", upcase: true)
42
+ # => "USER_ID"
43
+
44
+ Keycase.kebab_case("user_id", capitalize: true)
45
+ # => "User-Id"
46
+
47
+ Keycase.with_camel_case_keys({ "user_id" => 1, nested: { "item_count" => 2 } })
48
+ # => { "userId" => 1, :nested => { "itemCount" => 2 } }
49
+
50
+ Keycase.with_camel_case_keys(
51
+ { "API-Key" => 1, nested: { "http_status" => 200 } },
52
+ acronyms: %w[API HTTP],
53
+ )
54
+ # => { "APIKey" => 1, :nested => { "httpStatus" => 200 } }
55
+ ```
56
+
57
+ See [Interface](#interface) for the full list of `Keycase.*` methods and their refinement equivalents.
58
+
59
+ ### Refinements
60
+
24
61
  ```sh
25
62
  irb --context-mode=1
26
63
  ```
@@ -66,22 +103,32 @@ irb --context-mode=1
66
103
 
67
104
  ## Interface
68
105
 
69
- Keycase is provided as Ruby refinements. Enable the case conversion you want with `using`.
106
+ Keycase offers **module functions** on `Keycase` (no `using` required) and the same behavior as **refinements** when you `using Keycase::CamelCase` (and similarly for other case modules).
70
107
 
71
- | Refinement | String/Symbol conversion | Hash/Array key conversion |
72
- | --- | --- | --- |
73
- | `Keycase::CamelCase` | `to_camel_case` | `with_camel_case_keys` |
74
- | `Keycase::PascalCase` | `to_pascal_case` | `with_pascal_case_keys` |
75
- | `Keycase::SnakeCase` | `to_snake_case` | `with_snake_case_keys` |
76
- | `Keycase::ScreamingSnakeCase` | `to_screaming_snake_case` | `with_screaming_snake_case_keys` |
77
- | `Keycase::KebabCase` | `to_kebab_case` | `with_kebab_case_keys` |
78
- | `Keycase::TrainCase` | `to_train_case` | `with_train_case_keys` |
108
+ | Refinement | `Keycase` module function (scalar) | `Keycase` module function (Hash/Array keys) | Refinement instance methods |
109
+ | --- | --- | --- | --- |
110
+ | `Keycase::CamelCase` | `Keycase.camel_case` | `Keycase.with_camel_case_keys` | `to_camel_case` / `with_camel_case_keys` |
111
+ | `Keycase::SnakeCase` | `Keycase.snake_case` | `Keycase.with_snake_case_keys` | `to_snake_case` / `with_snake_case_keys` |
112
+ | `Keycase::KebabCase` | `Keycase.kebab_case` | `Keycase.with_kebab_case_keys` | `to_kebab_case` / `with_kebab_case_keys` |
113
+
114
+ `Keycase.camel_case` (and the other `Keycase.*` scalar methods) behave like refinement `to_*` for strings and symbols, and leave other objects unchanged.
115
+
116
+ Use options to select the former paired case variants:
117
+
118
+ - `Keycase.camel_case(..., capitalize: true)` converts to PascalCase.
119
+ - `Keycase.snake_case(..., upcase: true)` converts to SCREAMING_SNAKE_CASE.
120
+ - `Keycase.kebab_case(..., capitalize: true)` converts to Train-Case.
121
+
122
+ For **camelCase**, **PascalCase**, and **Train-Case** you can pass [`acronyms`](#acronyms-initialisms) so tokens such as `API`, `HTTP`, or `ID` keep the spelling you configure instead of being title-cased (for example `Api`, `Http`). Other case styles are unchanged by this option.
123
+
124
+ `Keycase.with_camel_case_keys` (and siblings) behave like `Hash#with_*_keys` and `Array#with_*_keys` after the matching `using`.
79
125
 
80
- `String#to_*` returns a converted string. `Symbol#to_*` returns a converted symbol. Other objects respond to these methods and return themselves unchanged.
126
+ `String#to_*` returns a converted string. `Symbol#to_*` returns a converted symbol. Other objects respond to these refinement-only methods by returning themselves unchanged.
81
127
 
82
- `Hash#with_*_keys` and `Array#with_*_keys` recursively convert only Hash keys.
83
- Arrays are traversed so hashes inside arrays are converted. Values that are not Hash or Array objects are returned unchanged.
128
+ `Hash#with_*_keys`, `Array#with_*_keys`, and `Struct#with_*_keys` recursively convert keys by default.
129
+ Arrays are traversed so hashes and structs inside arrays are converted. Other values are returned unchanged.
84
130
  Key type is preserved: string keys remain strings, and symbol keys remain symbols.
131
+ Optional [`recursive`](#traversal-scope-recursive-arrays), [`arrays`](#traversal-scope-recursive-arrays), and [`only`](#key-types-only) narrow **what** is traversed and **which** keys are converted.
85
132
 
86
133
  ```rb
87
134
  using Keycase::SnakeCase
@@ -104,13 +151,20 @@ using Keycase::SnakeCase
104
151
 
105
152
  ## Options
106
153
 
107
- All `with_*_keys` methods accept an options hash.
154
+ All structure key conversion methods (`Keycase.with_*_keys`, and `Hash#with_*_keys` / `Array#with_*_keys` after `using`) accept an options hash.
108
155
 
109
156
  | Option | Default | Description |
110
157
  | --- | --- | --- |
111
- | `max_depth` | `nil` | Maximum nested Hash/Array depth to traverse. `nil` means no depth limit. |
112
-
113
- Depth starts at `0` for the receiver itself. Each nested Hash or Array increases
158
+ | `max_depth` | `nil` | Maximum nested Hash/Array/Struct depth to traverse. `nil` means no depth limit. |
159
+ | `on_collision` | `:raise` | How to handle multiple source keys that convert to the same destination key in one Hash. See [Key collisions (`on_collision`)](#key-collisions-on_collision). |
160
+ | `recursive` | `true` | When `false`, hashes nested **under another hash** are not converted (see [Traversal scope](#traversal-scope-recursive-arrays)). |
161
+ | `arrays` | `true` | When `false`, array elements are not traversed (see [Traversal scope](#traversal-scope-recursive-arrays)). |
162
+ | `only` | `nil` | Restrict conversion to certain key types (`:string`, `:symbol`, or `String` / `Symbol`). See [Key types (`only`)](#key-types-only). |
163
+ | `acronyms` | `nil` | For `camel_case` and capitalized `kebab_case` key conversion only: list of strings treated as initialisms. See [Acronyms](#acronyms-initialisms). |
164
+ | `capitalize` | `false` | For `camel_case`, converts to PascalCase. For `kebab_case`, converts to Train-Case. |
165
+ | `upcase` | `false` | For `snake_case`, converts to SCREAMING_SNAKE_CASE. |
166
+
167
+ Depth starts at `0` for the receiver itself. Each nested Hash, Array, or Struct increases
114
168
  the depth by `1`. Leaf values are not counted.
115
169
 
116
170
  ```rb
@@ -123,14 +177,168 @@ using Keycase::CamelCase
123
177
  # raises Keycase::StructureTooDeepError
124
178
  ```
125
179
 
180
+ ### Traversal scope (`recursive`, `arrays`)
181
+
182
+ Use these when you want shallow conversion (for example only the outer hash or skipping arrays) without relying on `max_depth` alone.
183
+
184
+ - **`recursive`:** With `false`, **only hashes nested as values of another hash** are left untouched—same object as in the input for those inner hashes. Hashes that appear **as array elements** are still converted by default, because their parent is an Array, not a Hash.
185
+
186
+ ```rb
187
+ using Keycase::CamelCase
188
+
189
+ { outer_key: { inner_key: 1 } }.with_camel_case_keys(recursive: false)
190
+ # => { :outerKey => { :inner_key => 1 } }
191
+
192
+ { items: [{ nested_key: 1 }] }.with_camel_case_keys(recursive: false)
193
+ # => { :items => [{ :nestedKey => 1 }] }
194
+ ```
195
+
196
+ - **`arrays`:** With `false`, arrays are not walked: elements stay as-is inside a new outer Array.
197
+
198
+ ```rb
199
+ { items: [{ nested_key: 1 }] }.with_camel_case_keys(arrays: false)
200
+ # => { :items => [{ :nested_key => 1 }] }
201
+ ```
202
+
203
+ Combine both for “top-level hash keys only” when values include nested hashes and arrays you do not want to reshape:
204
+
205
+ ```rb
206
+ payload = { user_profile: { display_name: "Ada" }, tags: [{ tag_name: "ruby" }] }
207
+
208
+ payload.with_camel_case_keys(recursive: false, arrays: false)
209
+ # => { :userProfile => { :display_name => "Ada" }, :tags => [{ :tag_name => "ruby" }] }
210
+ ```
211
+
212
+ `recursive` and `arrays` must be exactly `true` or `false`; other values raise `ArgumentError`.
213
+
214
+ With `recursive: false`, **structs nested as members of another struct** are left untouched—the same object as in the input—mirroring hash-under-hash behavior. Structs inside hashes or arrays are still converted.
215
+
216
+ ### Structs
217
+
218
+ `Struct` member names are converted like hash keys. Nested hashes and arrays inside struct members are traversed by default.
219
+ If a member name converts to an empty name (for example `:[]=` or `:_`), its original name is preserved; its value is still traversed.
220
+
221
+ ```rb
222
+ using Keycase::CamelCase
223
+
224
+ Point = Struct.new(:foo_bar, :nested_hash, keyword_init: true)
225
+ point = Point.new(foo_bar: 1, nested_hash: { inner_key: 2 })
226
+
227
+ point.with_camel_case_keys
228
+ # => #<struct fooBar=1, nestedHash={ innerKey: 2 }>
229
+ ```
230
+
231
+ When member names change, a new anonymous `Struct` class is built (custom `Struct` subclasses are not preserved after rename).
232
+
233
+ ### Key types (`only`)
234
+
235
+ Pass `only:` as an array of `:string`, `:symbol`, and/or the classes `String` and `Symbol`. Only matching keys are passed through the case converter; other keys stay unchanged (same object).
236
+
237
+ Omit the option or pass `nil` for the usual behavior (string and symbol keys are converted). An **empty array** converts **no** keys.
238
+
239
+ ```rb
240
+ using Keycase::CamelCase
241
+
242
+ { "foo_bar" => 1, baz_qux: 2 }.with_camel_case_keys(only: [:string])
243
+ # => { "fooBar" => 1, :baz_qux => 2 }
244
+
245
+ { "foo_bar" => 1, baz_qux: 2 }.with_camel_case_keys(only: [:symbol])
246
+ # => { "foo_bar" => 1, :bazQux => 2 }
247
+ ```
248
+
249
+ Unsupported entries or a non-Array `only:` raise `ArgumentError`.
250
+
251
+ ### Key collisions (`on_collision`)
252
+
253
+ By default, if two keys in the same Hash map to the same key after conversion, Keycase raises `Keycase::KeyCollisionError` so data is never dropped without you noticing. For migrations or log shaping you can pick another strategy:
254
+
255
+ | `on_collision` | Behavior |
256
+ | --- | --- |
257
+ | `:raise` | **Default.** Raise `Keycase::KeyCollisionError`. |
258
+ | `:overwrite` | Keep the **last** entry in Ruby Hash iteration order (insertion order) for that destination key. |
259
+ | `:keep_first` | Keep the **first** entry; later colliding keys are ignored (their values are not traversed). |
260
+
261
+ Unknown values raise `ArgumentError`. The option applies **per Hash** at each nesting level.
262
+
263
+ ```rb
264
+ using Keycase::CamelCase
265
+
266
+ h = { :user_id => 1, :userID => 2 }
267
+
268
+ h.with_camel_case_keys
269
+ # raises Keycase::KeyCollisionError
270
+
271
+ h.with_camel_case_keys(on_collision: :overwrite)
272
+ # => { :userId => 2 }
273
+
274
+ h.with_camel_case_keys(on_collision: :keep_first)
275
+ # => { :userId => 1 }
276
+
277
+ Keycase.with_snake_case_keys({ "SomeKey" => 1, "someKey" => 2 }, on_collision: :keep_first)
278
+ # => { "some_key" => 1 }
279
+ ```
280
+
281
+ ### Acronyms (initialisms)
282
+
283
+ Use `acronyms` when you want multi-letter abbreviations to stay as you spell them (for example `API` instead of `Api`) after splitting a name into words.
284
+
285
+ Where it applies:
286
+
287
+ - `Keycase.camel_case` and `Keycase.kebab_case(..., capitalize: true)` accept a keyword: `acronyms:`.
288
+ - `Keycase.with_camel_case_keys` and `Keycase.with_kebab_case_keys(..., capitalize: true)` accept `acronyms:` in their **options hash** alongside `max_depth`, `on_collision`, `recursive`, `arrays`, and `only`.
289
+
290
+ Pass an array of strings. Matching is **case-insensitive** per word; the output uses the string from the array (the last occurrence wins if the same word appears twice with different casing). `nil` or omitting the option keeps the previous behavior.
291
+
292
+ **camelCase** lowercases the first character of the full name only when the **first** word is *not* one of the configured acronyms. That way `APIResponse` can stay `APIResponse`, while `myHTTPConnection` becomes `myHTTPConnection`.
293
+
294
+ **PascalCase** and **Train-Case** are selected with `capitalize: true`; they replace each matching word with the acronym spelling and title-Case other words as before.
295
+
296
+ snake_case, SCREAMING_SNAKE, and kebab_case do not use `acronyms`; they still normalize segments with `downcase` / `upcase` as usual.
297
+
298
+ ```rb
299
+ abbr = %w[API HTTP ID]
300
+
301
+ Keycase.camel_case("APIResponse", acronyms: abbr) # => "APIResponse"
302
+ Keycase.camel_case("HTTPResponseCode", acronyms: abbr, capitalize: true)
303
+ # => "HTTPResponseCode"
304
+ Keycase.kebab_case("HTTP-Response-Code", acronyms: abbr, capitalize: true)
305
+ # => "HTTP-Response-Code"
306
+
307
+ using Keycase::CamelCase
308
+ "userID".to_camel_case(acronyms: abbr) # => "userID"
309
+
310
+ Keycase.with_camel_case_keys({ "api_key" => 1 }, acronyms: abbr, capitalize: true)
311
+ # => { "APIKey" => 1 }
312
+ ```
313
+
314
+ ### Word splitting and character sets
315
+
316
+ Keycase splits names into words with `Keycase::Support::Tokenizer`, which collects runs of
317
+ **Unicode letters and numbers** (`\p{L}`, `\p{N}`) and inserts boundaries at common case
318
+ transitions in scripts that distinguish letter case (for example `userID` → `user`, `ID`;
319
+ `HTTPResponse` → `HTTP`, `Response`). Separators such as `-` and `_` between those runs are not
320
+ preserved as characters—only the letter and number pieces become words (for example
321
+ `API-Key` → `API`, `Key`; `ユーザー-user_id` → `ユーザー`, `user`, `id`).
322
+
323
+ **Characters outside `\p{L}` and `\p{N}`**—punctuation, symbols, emoji, and most whitespace—act as
324
+ separators and are not included in tokens. Scripts without cased letters (for example CJK) form a
325
+ single letter run unless you separate them with `-`, `_`, or similar. Mixed-script keys glued
326
+ without separators (for example Latin camelCase next to CJK text) are not split at the script
327
+ boundary.
328
+
329
+ Multilingual **Rails `I18n`** key paths that rely on punctuation or non-alphanumeric characters can
330
+ still behave unexpectedly after conversion, or make separate keys collide after conversion
331
+ (**`KeyCollisionError`** by default, or use [`on_collision`](#key-collisions-on_collision) to
332
+ overwrite or keep the first key) when you bulk-convert hashes.
333
+
126
334
  ## Errors
127
335
 
128
- `with_*_keys` raises Keycase-specific errors when recursive conversion cannot be completed without ambiguity or infinite traversal.
336
+ `Keycase.with_*_keys` and refinement `with_*_keys` raise Keycase-specific errors when recursive conversion cannot be completed without ambiguity or infinite traversal.
129
337
 
130
338
  | Error | Raised when |
131
339
  | --- | --- |
132
- | `Keycase::CircularStructureError` | A Hash or Array references itself through the current traversal path. |
133
- | `Keycase::KeyCollisionError` | Multiple source keys in the same Hash convert to the same destination key. |
340
+ | `Keycase::CircularStructureError` | A Hash, Array, or Struct references itself through the current traversal path. |
341
+ | `Keycase::KeyCollisionError` | Multiple source keys in the same Hash convert to the same destination key and `on_collision` is `:raise` (the default). |
134
342
  | `Keycase::StructureTooDeepError` | Traversal exceeds the supplied `max_depth`. |
135
343
 
136
344
  Circular references are rejected because recursive conversion could not finish.
@@ -145,20 +353,13 @@ hash.with_camel_case_keys
145
353
  # raises Keycase::CircularStructureError
146
354
  ```
147
355
 
148
- Key collisions are rejected so conversion never silently overwrites data. The check is performed per Hash after the keys are converted.
149
-
150
- ```rb
151
- using Keycase::CamelCase
152
-
153
- { :user_id => 1, :userID => 2 }.with_camel_case_keys
154
- # raises Keycase::KeyCollisionError
155
- ```
356
+ `Keycase::KeyCollisionError` is documented with examples under [Key collisions (`on_collision`)](#key-collisions-on_collision).
156
357
 
157
358
  ## Development
158
359
 
159
360
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests with the local Ruby version defined in `mise.toml` (currently Ruby 3.4.9). You can also run `bin/console` for an interactive prompt that will allow you to experiment.
160
361
 
161
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
362
+ To install this gem onto your local machine, run `bundle exec rake install`. To publish a version, use the GitHub Actions workflow described in [Releasing](#releasing).
162
363
 
163
364
  Tasks are defined in `mise.toml`. They run **RuboCop, and RSpec inside Docker** (via Docker Compose), so Docker must be available. Use these tasks for syntax checks and tests across supported Ruby versions.
164
365
 
@@ -187,14 +388,19 @@ To list all tasks and descriptions:
187
388
  mise tasks
188
389
  ```
189
390
 
190
- ## Release Authentication
391
+ ## Releasing
191
392
 
192
393
  Gem releases from GitHub Actions use RubyGems Trusted Publishing. No long-lived RubyGems API key or GitHub secret is required; GitHub Actions obtains a short-lived RubyGems API token through OIDC during the release job.
193
394
 
194
- Configure the trusted publisher once on RubyGems.org:
395
+ ### One-time setup
396
+
397
+ 1. Open the repository's [Settings → Environments](https://github.com/naoigcat/ruby-keycase/settings/environments) and create an environment named `release` if it does not exist. If deployment branch restrictions are enabled, allow `main`.
398
+ 2. Ensure repository or organization rules allow GitHub Actions to run the actions used in `.github/workflows/release.yml` and allow its `GITHUB_TOKEN` to create release tags (`v*`).
399
+
400
+ Configure the [trusted publisher](https://guides.rubygems.org/trusted-publishing/) on RubyGems.org:
195
401
 
196
402
  1. Log in to <https://rubygems.org> with an owner account for the `keycase` gem.
197
- 2. Open the `keycase` gem page and go to `Trusted publishers`.
403
+ 2. Open the [`keycase`](https://rubygems.org/gems/keycase) gem page and go to `Trusted publishers`.
198
404
  3. Create a GitHub Actions trusted publisher with these values:
199
405
 
200
406
  - Repository owner: `naoigcat`
@@ -202,7 +408,18 @@ Configure the trusted publisher once on RubyGems.org:
202
408
  - Workflow filename: `release.yml`
203
409
  - Environment name: `release`
204
410
 
205
- After this setup, run the `Release Gem` workflow manually from GitHub Actions. Enter the version already committed in `lib/keycase/version.rb`; the workflow verifies the version, creates the `v<version>` tag through Bundler's release task, and publishes the gem to RubyGems.org.
411
+ ### Publish 1.3.0
412
+
413
+ 1. Commit the changes, including `Keycase::VERSION = "1.3.0"` in `lib/keycase/version.rb`, and merge or push them to `main`. Confirm that the `CI` workflow passes for that commit.
414
+ 2. Open [Actions → Release Gem](https://github.com/naoigcat/ruby-keycase/actions/workflows/release.yml), click **Run workflow**, select `main`, and enter `1.3.0` for `version`. Click **Run workflow** to publish.
415
+ 3. If the `release` environment requires approval, approve the deployment in the workflow run.
416
+ 4. Confirm that the workflow succeeds, the `v1.3.0` tag exists on GitHub, and [keycase 1.3.0](https://rubygems.org/gems/keycase/versions/1.3.0) is available on RubyGems.org.
417
+
418
+ The workflow checks the version input, rejects an existing release tag, runs RSpec, and uses Bundler's release task to create the tag and publish the gem. Do not create `v1.3.0` manually before running it.
419
+
420
+ If publishing fails after the tag was pushed, check RubyGems.org before retrying. An existing tag makes this workflow fail on rerun. If the version is already published, do not delete or recreate its tag; if it is unpublished, resolve the failure and remove only the failed release's tag before rerunning.
421
+
422
+ For later releases, update `lib/keycase/version.rb` and substitute the new version in these steps.
206
423
 
207
424
  ## Contributing
208
425
 
data/keycase.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.summary = "Converts the case of strings, symbols, and keys of hash."
12
12
  spec.description = <<-DESCRIPTION
13
13
  This gem converts the case of strings, symbols, and keys of hash recursively.
14
- The convertible cases are camelCase, PascalCase, snake_case, etc.
14
+ The convertible cases are camelCase, snake_case, kebab-case.
15
15
  DESCRIPTION
16
16
  spec.homepage = "https://github.com/naoigcat/ruby-keycase"
17
17
  spec.license = "MIT"
@@ -1,14 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
- require_relative "support/transformer"
3
+ require_relative "support/acronyms"
4
+ require_relative "support/structure_keys"
6
5
  require_relative "support/tokenizer"
7
6
 
8
7
  module Keycase
9
8
  module CamelCase
9
+ def self.convert_string(str, acronyms: nil, capitalize: false)
10
+ words = Keycase::Support::Tokenizer.words(str)
11
+ acronyms_by_downcase = Keycase::Support::Acronyms.index(acronyms)
12
+ segments = words.map do |word|
13
+ Keycase::Support::Acronyms.segment(word, acronyms_by_downcase)
14
+ end
15
+ pascal = segments.join
16
+ return pascal if capitalize
17
+ return pascal if Keycase::Support::Acronyms.leading_acronym?(words.first, acronyms_by_downcase)
18
+
19
+ pascal.sub(/^./, &:downcase)
20
+ end
21
+
22
+ def self.convert(value, acronyms: nil, capitalize: false)
23
+ case value
24
+ when String then convert_string(value, acronyms: acronyms, capitalize: capitalize)
25
+ when Symbol then convert_string(value.to_s, acronyms: acronyms, capitalize: capitalize).to_sym
26
+ else value
27
+ end
28
+ end
29
+
30
+ def self.convert_keys(structure, options = {})
31
+ acronyms = options[:acronyms]
32
+ capitalize = options.fetch(:capitalize, false)
33
+ Keycase::Support::StructureKeys.transform(structure, options) do |key|
34
+ convert(key, acronyms: acronyms, capitalize: capitalize)
35
+ end
36
+ end
37
+
10
38
  refine Object do
11
- def to_camel_case
39
+ def to_camel_case(_options = {})
12
40
  self
13
41
  end
14
42
 
@@ -18,43 +46,43 @@ module Keycase
18
46
  end
19
47
 
20
48
  refine String do
21
- def to_camel_case
22
- Keycase::Support::Tokenizer.words(self).map do |word|
23
- word.capitalize
24
- end.join.sub(/^./, &:downcase)
49
+ def to_camel_case(acronyms: nil, capitalize: false)
50
+ Keycase::CamelCase.convert_string(self, acronyms: acronyms, capitalize: capitalize)
25
51
  end
26
52
  end
27
53
 
28
54
  refine Symbol do
29
- def to_camel_case
30
- to_s.to_camel_case.to_sym
55
+ def to_camel_case(acronyms: nil, capitalize: false)
56
+ Keycase::CamelCase.convert(self, acronyms: acronyms, capitalize: capitalize)
31
57
  end
32
58
  end
33
59
 
34
60
  refine Array do
35
61
  def with_camel_case_keys(options = {})
36
- Keycase::Support::Transformer.transform_array(
37
- self,
38
- ::Set.new,
39
- 0,
40
- options[:max_depth]
41
- ) do |key|
42
- key.to_camel_case
43
- end
62
+ Keycase::CamelCase.convert_keys(self, options)
44
63
  end
45
64
  end
46
65
 
47
66
  refine Hash do
48
67
  def with_camel_case_keys(options = {})
49
- Keycase::Support::Transformer.transform_hash(
50
- self,
51
- ::Set.new,
52
- 0,
53
- options[:max_depth]
54
- ) do |key|
55
- key.to_camel_case
56
- end
68
+ Keycase::CamelCase.convert_keys(self, options)
69
+ end
70
+ end
71
+
72
+ refine Struct do
73
+ def with_camel_case_keys(options = {})
74
+ Keycase::CamelCase.convert_keys(self, options)
57
75
  end
58
76
  end
59
77
  end
78
+
79
+ class << self
80
+ def camel_case(value, acronyms: nil, capitalize: false)
81
+ CamelCase.convert(value, acronyms: acronyms, capitalize: capitalize)
82
+ end
83
+
84
+ def with_camel_case_keys(value, options = {})
85
+ CamelCase.convert_keys(value, options)
86
+ end
87
+ end
60
88
  end
@@ -1,14 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
- require_relative "support/transformer"
3
+ require_relative "support/acronyms"
4
+ require_relative "support/structure_keys"
6
5
  require_relative "support/tokenizer"
7
6
 
8
7
  module Keycase
9
8
  module KebabCase
9
+ def self.convert_string(str, acronyms: nil, capitalize: false)
10
+ words = Keycase::Support::Tokenizer.words(str)
11
+ return words.map(&:downcase).join("-") unless capitalize
12
+
13
+ acronyms_by_downcase = Keycase::Support::Acronyms.index(acronyms)
14
+ words.map do |word|
15
+ Keycase::Support::Acronyms.segment(word, acronyms_by_downcase)
16
+ end.join("-")
17
+ end
18
+
19
+ def self.convert(value, acronyms: nil, capitalize: false)
20
+ case value
21
+ when String then convert_string(value, acronyms: acronyms, capitalize: capitalize)
22
+ when Symbol then convert_string(value.to_s, acronyms: acronyms, capitalize: capitalize).to_sym
23
+ else value
24
+ end
25
+ end
26
+
27
+ def self.convert_keys(structure, options = {})
28
+ acronyms = options[:acronyms]
29
+ capitalize = options.fetch(:capitalize, false)
30
+ Keycase::Support::StructureKeys.transform(structure, options) do |key|
31
+ convert(key, acronyms: acronyms, capitalize: capitalize)
32
+ end
33
+ end
34
+
10
35
  refine Object do
11
- def to_kebab_case
36
+ def to_kebab_case(_options = {})
12
37
  self
13
38
  end
14
39
 
@@ -18,43 +43,43 @@ module Keycase
18
43
  end
19
44
 
20
45
  refine String do
21
- def to_kebab_case
22
- Keycase::Support::Tokenizer.words(self).map do |word|
23
- word.downcase
24
- end.join("-")
46
+ def to_kebab_case(acronyms: nil, capitalize: false)
47
+ Keycase::KebabCase.convert_string(self, acronyms: acronyms, capitalize: capitalize)
25
48
  end
26
49
  end
27
50
 
28
51
  refine Symbol do
29
- def to_kebab_case
30
- to_s.to_kebab_case.to_sym
52
+ def to_kebab_case(acronyms: nil, capitalize: false)
53
+ Keycase::KebabCase.convert(self, acronyms: acronyms, capitalize: capitalize)
31
54
  end
32
55
  end
33
56
 
34
57
  refine Array do
35
58
  def with_kebab_case_keys(options = {})
36
- Keycase::Support::Transformer.transform_array(
37
- self,
38
- ::Set.new,
39
- 0,
40
- options[:max_depth]
41
- ) do |key|
42
- key.to_kebab_case
43
- end
59
+ Keycase::KebabCase.convert_keys(self, options)
44
60
  end
45
61
  end
46
62
 
47
63
  refine Hash do
48
64
  def with_kebab_case_keys(options = {})
49
- Keycase::Support::Transformer.transform_hash(
50
- self,
51
- ::Set.new,
52
- 0,
53
- options[:max_depth]
54
- ) do |key|
55
- key.to_kebab_case
56
- end
65
+ Keycase::KebabCase.convert_keys(self, options)
57
66
  end
58
67
  end
68
+
69
+ refine Struct do
70
+ def with_kebab_case_keys(options = {})
71
+ Keycase::KebabCase.convert_keys(self, options)
72
+ end
73
+ end
74
+ end
75
+
76
+ class << self
77
+ def kebab_case(value, acronyms: nil, capitalize: false)
78
+ KebabCase.convert(value, acronyms: acronyms, capitalize: capitalize)
79
+ end
80
+
81
+ def with_kebab_case_keys(value, options = {})
82
+ KebabCase.convert_keys(value, options)
83
+ end
59
84
  end
60
85
  end