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 +4 -4
- data/README.md +250 -33
- data/keycase.gemspec +1 -1
- data/lib/keycase/camel_case.rb +54 -26
- data/lib/keycase/kebab_case.rb +51 -26
- data/lib/keycase/snake_case.rb +45 -26
- data/lib/keycase/support/acronyms.rb +30 -0
- data/lib/keycase/support/struct_instantiation.rb +112 -0
- data/lib/keycase/support/structure_keys.rb +20 -0
- data/lib/keycase/support/tokenizer.rb +3 -7
- data/lib/keycase/support/transformer/collision.rb +30 -0
- data/lib/keycase/support/transformer/only_normalization.rb +50 -0
- data/lib/keycase/support/transformer/structure_builders.rb +38 -0
- data/lib/keycase/support/transformer/visit.rb +33 -0
- data/lib/keycase/support/transformer.rb +60 -48
- data/lib/keycase/version.rb +1 -1
- data/lib/keycase.rb +8 -3
- metadata +9 -5
- data/lib/keycase/pascal_case.rb +0 -60
- data/lib/keycase/screaming_snake_case.rb +0 -60
- data/lib/keycase/train_case.rb +0 -60
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 39ff511337d13fc2e2c0118ee5f3d439f3f0be41eb83c86ead9fdee519454588
|
|
4
|
+
data.tar.gz: e13af5511466486ea3146f9f10c82b815b0879b0565e4590b84162e49cc73f52
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
|
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
|
|
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 |
|
|
72
|
-
| --- | --- | --- |
|
|
73
|
-
| `Keycase::CamelCase` | `
|
|
74
|
-
| `Keycase::
|
|
75
|
-
| `Keycase::
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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 `
|
|
83
|
-
Arrays are traversed so hashes inside arrays are converted.
|
|
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`
|
|
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
|
-
|
|
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`
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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"
|
data/lib/keycase/camel_case.rb
CHANGED
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
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::
|
|
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
|
-
|
|
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::
|
|
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::
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
data/lib/keycase/kebab_case.rb
CHANGED
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
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::
|
|
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
|
-
|
|
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::
|
|
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::
|
|
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
|