keycase 1.1.0 → 1.2.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: 9269ac3f6277ae7eec6635c9bb3a5187522ba0d2d7fb25544d7f1e4aa9fb76f6
4
- data.tar.gz: a4ba9ae8a5305896cb5f10775a19f77e7b7ea93d7699c54f628d4e93bc5d84ad
3
+ metadata.gz: f9cd4f359e0bdcfad518f08a8bbb845060cb4d66d2d8a3738b68aa46c53f183e
4
+ data.tar.gz: d0c79c9129587e65c2588efff8a969c5877ac802d1e1b818c2f9eaf2ff8bb114
5
5
  SHA512:
6
- metadata.gz: c3c1c1d9292ddcbf6105612cf83c6015701729dcd4735237426726bbd38ba73c69f637600d24d420957c386499818997a4d89402e6eadc4a32fcd089e30e47b5
7
- data.tar.gz: bf20fa1ce9b135f12a080c01bb2fcf55906cd66027273b31c29346a818c070d56471695d2a1b68c0fdd21cea5ca56f4d172410c3c0283d245386d43dac3d18a6
6
+ metadata.gz: 606ecc4d4358942e9b5e00c4f5ac1c6dfda3fc0b548948b69c3d3fb30dfe935c6382c3964cb7ca4a737d8cad755c2d2db8ca55d0f40291faff1d3ab60072d2d6
7
+ data.tar.gz: 376edfd2e6bc64564adb3b11ef63e658cc540d0c7759b6388bf84973e0fcee55b056b5d21ed01577c6346a0cdf01e13fdb2270ed195efced6e3e8d9436f048ae
data/README.md CHANGED
@@ -64,6 +64,96 @@ irb --context-mode=1
64
64
  }
65
65
  ```
66
66
 
67
+ ## Interface
68
+
69
+ Keycase is provided as Ruby refinements. Enable the case conversion you want with `using`.
70
+
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` |
79
+
80
+ `String#to_*` returns a converted string. `Symbol#to_*` returns a converted symbol. Other objects respond to these methods and return themselves unchanged.
81
+
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.
84
+ Key type is preserved: string keys remain strings, and symbol keys remain symbols.
85
+
86
+ ```rb
87
+ using Keycase::SnakeCase
88
+
89
+ "userID".to_snake_case
90
+ # => "user_id"
91
+
92
+ :userID.to_snake_case
93
+ # => :user_id
94
+
95
+ [
96
+ { "userID" => 1 },
97
+ { :createdAt => "2026-05-09" },
98
+ ].with_snake_case_keys
99
+ # => [
100
+ # { "user_id" => 1 },
101
+ # { :created_at => "2026-05-09" },
102
+ # ]
103
+ ```
104
+
105
+ ## Options
106
+
107
+ All `with_*_keys` methods accept an options hash.
108
+
109
+ | Option | Default | Description |
110
+ | --- | --- | --- |
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
114
+ the depth by `1`. Leaf values are not counted.
115
+
116
+ ```rb
117
+ using Keycase::CamelCase
118
+
119
+ { user: { profile_data: { display_name: "Alice" } } }.with_camel_case_keys(max_depth: 2)
120
+ # => { :user => { :profileData => { :displayName => "Alice" } } }
121
+
122
+ { user: { profile_data: { display_name: "Alice" } } }.with_camel_case_keys(max_depth: 1)
123
+ # raises Keycase::StructureTooDeepError
124
+ ```
125
+
126
+ ## Errors
127
+
128
+ `with_*_keys` raises Keycase-specific errors when recursive conversion cannot be completed without ambiguity or infinite traversal.
129
+
130
+ | Error | Raised when |
131
+ | --- | --- |
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. |
134
+ | `Keycase::StructureTooDeepError` | Traversal exceeds the supplied `max_depth`. |
135
+
136
+ Circular references are rejected because recursive conversion could not finish.
137
+ Reusing the same non-circular object from multiple places is supported; each path is converted into a separate result object.
138
+
139
+ ```rb
140
+ using Keycase::CamelCase
141
+
142
+ hash = {}
143
+ hash[:self_reference] = hash
144
+ hash.with_camel_case_keys
145
+ # raises Keycase::CircularStructureError
146
+ ```
147
+
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
+ ```
156
+
67
157
  ## Development
68
158
 
69
159
  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.
@@ -97,6 +187,23 @@ To list all tasks and descriptions:
97
187
  mise tasks
98
188
  ```
99
189
 
190
+ ## Release Authentication
191
+
192
+ 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
+
194
+ Configure the trusted publisher once on RubyGems.org:
195
+
196
+ 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`.
198
+ 3. Create a GitHub Actions trusted publisher with these values:
199
+
200
+ - Repository owner: `naoigcat`
201
+ - Repository name: `ruby-keycase`
202
+ - Workflow filename: `release.yml`
203
+ - Environment name: `release`
204
+
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.
206
+
100
207
  ## Contributing
101
208
 
102
209
  Bug reports and pull requests are welcome on GitHub at <https://github.com/naoigcat/ruby-keycase>.
@@ -2,7 +2,8 @@
2
2
 
3
3
  require "set"
4
4
 
5
- require_relative "recursive_transform/engine"
5
+ require_relative "support/transformer"
6
+ require_relative "support/tokenizer"
6
7
 
7
8
  module Keycase
8
9
  module CamelCase
@@ -18,17 +19,9 @@ module Keycase
18
19
 
19
20
  refine String do
20
21
  def to_camel_case
21
- gsub(/(?<=[A-Z])(?=[A-Z][a-z])/) do |_|
22
- "_"
23
- end.gsub(/(?<=[0-9a-z])(?=[A-Z])/) do |_|
24
- "_"
25
- end.gsub(/(?<=\b|\W|_)[0-9A-Za-z]+(?=\b|\W|_)/) do |matched|
26
- matched.capitalize
27
- end.sub(/^(?:\W|_)*([A-Z]+(?=[A-Z][0-9A-Za-z]|\d|$)|[A-Z][a-z])/) do |_|
28
- Regexp.last_match(1).downcase
29
- end.gsub(/(?:\b|\W|_)*([0-9A-Z])/) do |_|
30
- Regexp.last_match(1)
31
- end.gsub(/(?:\W|_)*$/, "")
22
+ Keycase::Support::Tokenizer.words(self).map do |word|
23
+ word.capitalize
24
+ end.join.sub(/^./, &:downcase)
32
25
  end
33
26
  end
34
27
 
@@ -40,7 +33,7 @@ module Keycase
40
33
 
41
34
  refine Array do
42
35
  def with_camel_case_keys(options = {})
43
- Keycase::RecursiveTransform::Engine.transform_array(
36
+ Keycase::Support::Transformer.transform_array(
44
37
  self,
45
38
  ::Set.new,
46
39
  0,
@@ -53,7 +46,7 @@ module Keycase
53
46
 
54
47
  refine Hash do
55
48
  def with_camel_case_keys(options = {})
56
- Keycase::RecursiveTransform::Engine.transform_hash(
49
+ Keycase::Support::Transformer.transform_hash(
57
50
  self,
58
51
  ::Set.new,
59
52
  0,
@@ -2,7 +2,8 @@
2
2
 
3
3
  require "set"
4
4
 
5
- require_relative "recursive_transform/engine"
5
+ require_relative "support/transformer"
6
+ require_relative "support/tokenizer"
6
7
 
7
8
  module Keycase
8
9
  module KebabCase
@@ -18,13 +19,9 @@ module Keycase
18
19
 
19
20
  refine String do
20
21
  def to_kebab_case
21
- gsub(/(?<=[A-Z])(?=[A-Z][a-z])/) do |_|
22
- "-"
23
- end.gsub(/(?<=[0-9a-z])(?=[A-Z])/) do |_|
24
- "-"
25
- end.gsub(/(?<=\b|\W|_)[0-9A-Za-z]+(?=\b|\W|_)/) do |matched|
26
- "-#{matched.downcase}"
27
- end.gsub(/(?:\W|_)+/, "-").gsub(/^(?:\W|_)*|(?:\W|_)*$/, "").downcase
22
+ Keycase::Support::Tokenizer.words(self).map do |word|
23
+ word.downcase
24
+ end.join("-")
28
25
  end
29
26
  end
30
27
 
@@ -36,7 +33,7 @@ module Keycase
36
33
 
37
34
  refine Array do
38
35
  def with_kebab_case_keys(options = {})
39
- Keycase::RecursiveTransform::Engine.transform_array(
36
+ Keycase::Support::Transformer.transform_array(
40
37
  self,
41
38
  ::Set.new,
42
39
  0,
@@ -49,7 +46,7 @@ module Keycase
49
46
 
50
47
  refine Hash do
51
48
  def with_kebab_case_keys(options = {})
52
- Keycase::RecursiveTransform::Engine.transform_hash(
49
+ Keycase::Support::Transformer.transform_hash(
53
50
  self,
54
51
  ::Set.new,
55
52
  0,
@@ -2,7 +2,8 @@
2
2
 
3
3
  require "set"
4
4
 
5
- require_relative "recursive_transform/engine"
5
+ require_relative "support/transformer"
6
+ require_relative "support/tokenizer"
6
7
 
7
8
  module Keycase
8
9
  module PascalCase
@@ -18,17 +19,9 @@ module Keycase
18
19
 
19
20
  refine String do
20
21
  def to_pascal_case
21
- gsub(/(?<=[A-Z])(?=[A-Z][a-z])/) do |_|
22
- "_"
23
- end.gsub(/(?<=[0-9a-z])(?=[A-Z])/) do |_|
24
- "_"
25
- end.gsub(/(?<=\b|\W|_)[0-9A-Za-z]+(?=\b|\W|_)/) do |matched|
26
- matched.capitalize
27
- end.sub(/^(?:\W|_)*([A-Z]+(?=[A-Z][0-9A-Za-z]|\d|$)|[A-Z][a-z])/) do |_|
28
- Regexp.last_match(1).capitalize
29
- end.gsub(/(?:\b|\W|_)*([0-9A-Z])/) do |_|
30
- Regexp.last_match(1)
31
- end.gsub(/(?:\W|_)*$/, "")
22
+ Keycase::Support::Tokenizer.words(self).map do |word|
23
+ word.capitalize
24
+ end.join
32
25
  end
33
26
  end
34
27
 
@@ -40,7 +33,7 @@ module Keycase
40
33
 
41
34
  refine Array do
42
35
  def with_pascal_case_keys(options = {})
43
- Keycase::RecursiveTransform::Engine.transform_array(
36
+ Keycase::Support::Transformer.transform_array(
44
37
  self,
45
38
  ::Set.new,
46
39
  0,
@@ -53,7 +46,7 @@ module Keycase
53
46
 
54
47
  refine Hash do
55
48
  def with_pascal_case_keys(options = {})
56
- Keycase::RecursiveTransform::Engine.transform_hash(
49
+ Keycase::Support::Transformer.transform_hash(
57
50
  self,
58
51
  ::Set.new,
59
52
  0,
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ require_relative "support/transformer"
6
+ require_relative "support/tokenizer"
7
+
8
+ module Keycase
9
+ module ScreamingSnakeCase
10
+ refine Object do
11
+ def to_screaming_snake_case
12
+ self
13
+ end
14
+
15
+ def with_screaming_snake_case_keys(_options = {})
16
+ self
17
+ end
18
+ end
19
+
20
+ refine String do
21
+ def to_screaming_snake_case
22
+ Keycase::Support::Tokenizer.words(self).map do |word|
23
+ word.upcase
24
+ end.join("_")
25
+ end
26
+ end
27
+
28
+ refine Symbol do
29
+ def to_screaming_snake_case
30
+ to_s.to_screaming_snake_case.to_sym
31
+ end
32
+ end
33
+
34
+ refine Array do
35
+ def with_screaming_snake_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_screaming_snake_case
43
+ end
44
+ end
45
+ end
46
+
47
+ refine Hash do
48
+ def with_screaming_snake_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_screaming_snake_case
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -2,7 +2,8 @@
2
2
 
3
3
  require "set"
4
4
 
5
- require_relative "recursive_transform/engine"
5
+ require_relative "support/transformer"
6
+ require_relative "support/tokenizer"
6
7
 
7
8
  module Keycase
8
9
  module SnakeCase
@@ -18,13 +19,9 @@ module Keycase
18
19
 
19
20
  refine String do
20
21
  def to_snake_case
21
- gsub(/(?<=[A-Z])(?=[A-Z][a-z])/) do |_|
22
- "_"
23
- end.gsub(/(?<=[0-9a-z])(?=[A-Z])/) do |_|
24
- "_"
25
- end.gsub(/(?<=\b|\W|_)[0-9A-Za-z]+(?=\b|\W|_)/) do |matched|
26
- "_#{matched.downcase}"
27
- end.gsub(/(?:\W|_)+/, "_").gsub(/^(?:\W|_)*|(?:\W|_)*$/, "").downcase
22
+ Keycase::Support::Tokenizer.words(self).map do |word|
23
+ word.downcase
24
+ end.join("_")
28
25
  end
29
26
  end
30
27
 
@@ -36,7 +33,7 @@ module Keycase
36
33
 
37
34
  refine Array do
38
35
  def with_snake_case_keys(options = {})
39
- Keycase::RecursiveTransform::Engine.transform_array(
36
+ Keycase::Support::Transformer.transform_array(
40
37
  self,
41
38
  ::Set.new,
42
39
  0,
@@ -49,7 +46,7 @@ module Keycase
49
46
 
50
47
  refine Hash do
51
48
  def with_snake_case_keys(options = {})
52
- Keycase::RecursiveTransform::Engine.transform_hash(
49
+ Keycase::Support::Transformer.transform_hash(
53
50
  self,
54
51
  ::Set.new,
55
52
  0,
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keycase
4
+ module Support
5
+ module Tokenizer
6
+ module_function
7
+
8
+ def words(value)
9
+ value
10
+ .gsub(/(?<=[A-Z])(?=[A-Z][a-z])/) do |_|
11
+ "_"
12
+ end
13
+ .gsub(/(?<=[0-9a-z])(?=[A-Z])/) do |_|
14
+ "_"
15
+ end
16
+ .scan(/[0-9A-Za-z]+/)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -3,8 +3,8 @@
3
3
  require_relative "errors"
4
4
 
5
5
  module Keycase
6
- module RecursiveTransform
7
- module Engine
6
+ module Support
7
+ module Transformer
8
8
  class << self
9
9
  def transform_hash(hash, visiting, depth, max_depth, &key_converter)
10
10
  check_depth!(depth, max_depth)
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ require_relative "support/transformer"
6
+ require_relative "support/tokenizer"
7
+
8
+ module Keycase
9
+ module TrainCase
10
+ refine Object do
11
+ def to_train_case
12
+ self
13
+ end
14
+
15
+ def with_train_case_keys(_options = {})
16
+ self
17
+ end
18
+ end
19
+
20
+ refine String do
21
+ def to_train_case
22
+ Keycase::Support::Tokenizer.words(self).map do |word|
23
+ word.capitalize
24
+ end.join("-")
25
+ end
26
+ end
27
+
28
+ refine Symbol do
29
+ def to_train_case
30
+ to_s.to_train_case.to_sym
31
+ end
32
+ end
33
+
34
+ refine Array do
35
+ def with_train_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_train_case
43
+ end
44
+ end
45
+ end
46
+
47
+ refine Hash do
48
+ def with_train_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_train_case
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Keycase
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.0"
5
5
  end
data/lib/keycase.rb CHANGED
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "keycase/version"
4
- require "keycase/recursive_transform/errors"
5
- require "keycase/recursive_transform/engine"
4
+ require "keycase/support/errors"
5
+ require "keycase/support/transformer"
6
+ require "keycase/support/tokenizer"
6
7
  require "keycase/camel_case"
7
8
  require "keycase/kebab_case"
8
9
  require "keycase/pascal_case"
10
+ require "keycase/screaming_snake_case"
9
11
  require "keycase/snake_case"
12
+ require "keycase/train_case"
10
13
 
11
14
  module Keycase
12
15
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keycase
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - naoigcat
@@ -25,9 +25,12 @@ files:
25
25
  - lib/keycase/camel_case.rb
26
26
  - lib/keycase/kebab_case.rb
27
27
  - lib/keycase/pascal_case.rb
28
- - lib/keycase/recursive_transform/engine.rb
29
- - lib/keycase/recursive_transform/errors.rb
28
+ - lib/keycase/screaming_snake_case.rb
30
29
  - lib/keycase/snake_case.rb
30
+ - lib/keycase/support/errors.rb
31
+ - lib/keycase/support/tokenizer.rb
32
+ - lib/keycase/support/transformer.rb
33
+ - lib/keycase/train_case.rb
31
34
  - lib/keycase/version.rb
32
35
  homepage: https://github.com/naoigcat/ruby-keycase
33
36
  licenses: