keycase 1.0.2 → 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: 6e6f493b80f7e3db6e7ef62af37c153849e924bc8bdb297fc112758f9d41147e
4
- data.tar.gz: 6b45dfb8ec00157c8e098fb6e9b227206d1aa6804d0b8408afa6c03d983cc853
3
+ metadata.gz: f9cd4f359e0bdcfad518f08a8bbb845060cb4d66d2d8a3738b68aa46c53f183e
4
+ data.tar.gz: d0c79c9129587e65c2588efff8a969c5877ac802d1e1b818c2f9eaf2ff8bb114
5
5
  SHA512:
6
- metadata.gz: a1f3a2dd6e435edb695b3de39c0fbc13bf746ab9bcf55a2206f8e8cbdc484b862d8aae3813352107ae855a2ba3b4f1cc505daca6a7ab17eff19fc29435d9b30f
7
- data.tar.gz: 37480590ce74499c8ccc9c3fc55611187f311b966ecb48da8de70b26ffa2fab0af89bc04ef50e27de88b60a5430b037552bdca5b3ed4e4dee68201e517781f99
6
+ metadata.gz: 606ecc4d4358942e9b5e00c4f5ac1c6dfda3fc0b548948b69c3d3fb30dfe935c6382c3964cb7ca4a737d8cad755c2d2db8ca55d0f40291faff1d3ab60072d2d6
7
+ data.tar.gz: 376edfd2e6bc64564adb3b11ef63e658cc540d0c7759b6388bf84973e0fcee55b056b5d21ed01577c6346a0cdf01e13fdb2270ed195efced6e3e8d9436f048ae
data/README.md CHANGED
@@ -64,12 +64,146 @@ 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
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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.
70
160
 
71
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).
72
162
 
163
+ 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
+
165
+ Run **RuboCop** on every supported Ruby image (2.3 through 4.0):
166
+
167
+ ```sh
168
+ mise run -j 1 rubocop
169
+ ```
170
+
171
+ Run **RSpec** the same way:
172
+
173
+ ```sh
174
+ mise run -j 1 rspec
175
+ ```
176
+
177
+ These commands execute the version-specific tasks in order (`rubocop23` … `rubocop40`, `rspec23` … `rspec40`). To run against a **single** Ruby version, use the matching task name, for example:
178
+
179
+ ```sh
180
+ mise run rubocop34
181
+ mise run rspec34
182
+ ```
183
+
184
+ To list all tasks and descriptions:
185
+
186
+ ```sh
187
+ mise tasks
188
+ ```
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
+
73
207
  ## Contributing
74
208
 
75
209
  Bug reports and pull requests are welcome on GitHub at <https://github.com/naoigcat/ruby-keycase>.
data/keycase.gemspec CHANGED
@@ -9,22 +9,22 @@ Gem::Specification.new do |spec|
9
9
  spec.email = ["17925623+naoigcat@users.noreply.github.com"]
10
10
 
11
11
  spec.summary = "Converts the case of strings, symbols, and keys of hash."
12
- spec.description = <<~DESCRIPTION
12
+ spec.description = <<-DESCRIPTION
13
13
  This gem converts the case of strings, symbols, and keys of hash recursively.
14
14
  The convertible cases are camelCase, PascalCase, snake_case, etc.
15
15
  DESCRIPTION
16
16
  spec.homepage = "https://github.com/naoigcat/ruby-keycase"
17
17
  spec.license = "MIT"
18
- spec.required_ruby_version = Gem::Requirement.new(">= 2.0.0")
18
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
19
19
 
20
20
  # Specify which files should be added to the gem when it is released.
21
21
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.require_paths = ["lib"]
22
23
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ `git ls-files -z`.split("\x0").select do |f|
25
+ f.match(%r{^lib/|^LICENSE\.md$|^README\.md$|^keycase\.gemspec$})
26
+ end
24
27
  end
25
- spec.bindir = "exe"
26
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
- spec.require_paths = ["lib"]
28
28
 
29
29
  spec.metadata["homepage_uri"] = spec.homepage
30
30
  spec.metadata["source_code_uri"] = spec.homepage
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
5
+ require_relative "support/transformer"
6
+ require_relative "support/tokenizer"
7
+
3
8
  module Keycase
4
9
  module CamelCase
5
10
  refine Object do
@@ -7,22 +12,16 @@ module Keycase
7
12
  self
8
13
  end
9
14
 
10
- def with_camel_case_keys
15
+ def with_camel_case_keys(_options = {})
11
16
  self
12
17
  end
13
18
  end
14
19
 
15
20
  refine String do
16
21
  def to_camel_case
17
- gsub(/(?<=[0-9a-z])(?=[A-Z])/) do |_|
18
- "_"
19
- end.gsub(/(?<=\b|\W|_)[0-9A-Za-z]+(?=\b|\W|_)/) do |matched| # rubocop:disable Style/SymbolProc
20
- matched.capitalize
21
- end.sub(/^(?:\W|_)*([A-Z]+(?=[A-Z][0-9A-Za-z]|\d|$)|[A-Z][a-z])/) do |_|
22
- Regexp.last_match(1).downcase
23
- end.gsub(/(?:\b|\W|_)*([0-9A-Z])/) do |_|
24
- Regexp.last_match(1)
25
- end.gsub(/(?:\W|_)*$/, "")
22
+ Keycase::Support::Tokenizer.words(self).map do |word|
23
+ word.capitalize
24
+ end.join.sub(/^./, &:downcase)
26
25
  end
27
26
  end
28
27
 
@@ -33,17 +32,27 @@ module Keycase
33
32
  end
34
33
 
35
34
  refine Array do
36
- def with_camel_case_keys
37
- map do |value| # rubocop:disable Style/SymbolProc
38
- value.with_camel_case_keys
35
+ 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
39
43
  end
40
44
  end
41
45
  end
42
46
 
43
47
  refine Hash do
44
- def with_camel_case_keys
45
- each_with_object({}) do |(key, value), memo|
46
- memo[key.to_camel_case] = value.with_camel_case_keys
48
+ 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
47
56
  end
48
57
  end
49
58
  end
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
5
+ require_relative "support/transformer"
6
+ require_relative "support/tokenizer"
7
+
3
8
  module Keycase
4
9
  module KebabCase
5
10
  refine Object do
@@ -7,18 +12,16 @@ module Keycase
7
12
  self
8
13
  end
9
14
 
10
- def with_kebab_case_keys
15
+ def with_kebab_case_keys(_options = {})
11
16
  self
12
17
  end
13
18
  end
14
19
 
15
20
  refine String do
16
21
  def to_kebab_case
17
- gsub(/(?<=[0-9a-z])(?=[A-Z])/) do |_|
18
- "-"
19
- end.gsub(/(?<=\b|\W|_)[0-9A-Za-z]+(?=\b|\W|_)/) do |matched|
20
- "-#{matched.downcase}"
21
- end.gsub(/(?:\W|_)+/, "-").gsub(/^(?:\W|_)*|(?:\W|_)*$/, "").downcase
22
+ Keycase::Support::Tokenizer.words(self).map do |word|
23
+ word.downcase
24
+ end.join("-")
22
25
  end
23
26
  end
24
27
 
@@ -29,17 +32,27 @@ module Keycase
29
32
  end
30
33
 
31
34
  refine Array do
32
- def with_kebab_case_keys
33
- map do |value| # rubocop:disable Style/SymbolProc
34
- value.with_kebab_case_keys
35
+ 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
35
43
  end
36
44
  end
37
45
  end
38
46
 
39
47
  refine Hash do
40
- def with_kebab_case_keys
41
- each_with_object({}) do |(key, value), memo|
42
- memo[key.to_kebab_case] = value.with_kebab_case_keys
48
+ 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
43
56
  end
44
57
  end
45
58
  end
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
5
+ require_relative "support/transformer"
6
+ require_relative "support/tokenizer"
7
+
3
8
  module Keycase
4
9
  module PascalCase
5
10
  refine Object do
@@ -7,22 +12,16 @@ module Keycase
7
12
  self
8
13
  end
9
14
 
10
- def with_pascal_case_keys
15
+ def with_pascal_case_keys(_options = {})
11
16
  self
12
17
  end
13
18
  end
14
19
 
15
20
  refine String do
16
21
  def to_pascal_case
17
- gsub(/(?<=[0-9a-z])(?=[A-Z])/) do |_|
18
- "_"
19
- end.gsub(/(?<=\b|\W|_)[0-9A-Za-z]+(?=\b|\W|_)/) do |matched| # rubocop:disable Style/SymbolProc
20
- matched.capitalize
21
- end.sub(/^(?:\W|_)*([A-Z]+(?=[A-Z][0-9A-Za-z]|\d|$)|[A-Z][a-z])/) do |_|
22
- Regexp.last_match(1).capitalize
23
- end.gsub(/(?:\b|\W|_)*([0-9A-Z])/) do |_|
24
- Regexp.last_match(1)
25
- end.gsub(/(?:\W|_)*$/, "")
22
+ Keycase::Support::Tokenizer.words(self).map do |word|
23
+ word.capitalize
24
+ end.join
26
25
  end
27
26
  end
28
27
 
@@ -33,17 +32,27 @@ module Keycase
33
32
  end
34
33
 
35
34
  refine Array do
36
- def with_pascal_case_keys
37
- map do |value| # rubocop:disable Style/SymbolProc
38
- value.with_pascal_case_keys
35
+ def with_pascal_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_pascal_case
39
43
  end
40
44
  end
41
45
  end
42
46
 
43
47
  refine Hash do
44
- def with_pascal_case_keys
45
- each_with_object({}) do |(key, value), memo|
46
- memo[key.to_pascal_case] = value.with_pascal_case_keys
48
+ def with_pascal_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_pascal_case
47
56
  end
48
57
  end
49
58
  end
@@ -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
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
5
+ require_relative "support/transformer"
6
+ require_relative "support/tokenizer"
7
+
3
8
  module Keycase
4
9
  module SnakeCase
5
10
  refine Object do
@@ -7,18 +12,16 @@ module Keycase
7
12
  self
8
13
  end
9
14
 
10
- def with_snake_case_keys
15
+ def with_snake_case_keys(_options = {})
11
16
  self
12
17
  end
13
18
  end
14
19
 
15
20
  refine String do
16
21
  def to_snake_case
17
- gsub(/(?<=[0-9a-z])(?=[A-Z])/) do |_|
18
- "_"
19
- end.gsub(/(?<=\b|\W|_)[0-9A-Za-z]+(?=\b|\W|_)/) do |matched|
20
- "_#{matched.downcase}"
21
- end.gsub(/(?:\W|_)+/, "_").gsub(/^(?:\W|_)*|(?:\W|_)*$/, "").downcase
22
+ Keycase::Support::Tokenizer.words(self).map do |word|
23
+ word.downcase
24
+ end.join("_")
22
25
  end
23
26
  end
24
27
 
@@ -29,17 +32,27 @@ module Keycase
29
32
  end
30
33
 
31
34
  refine Array do
32
- def with_snake_case_keys
33
- map do |value| # rubocop:disable Style/SymbolProc
34
- value.with_snake_case_keys
35
+ def with_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_snake_case
35
43
  end
36
44
  end
37
45
  end
38
46
 
39
47
  refine Hash do
40
- def with_snake_case_keys
41
- each_with_object({}) do |(key, value), memo|
42
- memo[key.to_snake_case] = value.with_snake_case_keys
48
+ def with_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_snake_case
43
56
  end
44
57
  end
45
58
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keycase
4
+ # Signals that recursive key conversion cannot finish because the input graph loops.
5
+ class CircularStructureError < StandardError; end
6
+
7
+ # Signals that conversion would overwrite data by mapping multiple source keys to one key.
8
+ class KeyCollisionError < StandardError; end
9
+
10
+ # Signals that the caller's depth limit rejected input that is too deeply nested.
11
+ class StructureTooDeepError < StandardError; end
12
+ end
@@ -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
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module Keycase
6
+ module Support
7
+ module Transformer
8
+ class << self
9
+ def transform_hash(hash, visiting, depth, max_depth, &key_converter)
10
+ check_depth!(depth, max_depth)
11
+
12
+ oid = hash.object_id
13
+ raise CircularStructureError, "Keycase detected a circular reference in a Hash" if visiting.include?(oid)
14
+
15
+ visiting.add(oid)
16
+ begin
17
+ hash.each_with_object({}) do |(key, value), memo|
18
+ new_key = key_converter.call(key)
19
+ if memo.key?(new_key)
20
+ message = "Keycase detected a key collision: #{key.inspect} converted to " \
21
+ "#{new_key.inspect}, which already exists in the transformed hash"
22
+ raise KeyCollisionError, message
23
+ end
24
+
25
+ memo[new_key] = transform_value(
26
+ value,
27
+ visiting,
28
+ depth + 1,
29
+ max_depth,
30
+ &key_converter
31
+ )
32
+ end
33
+ ensure
34
+ visiting.delete(oid)
35
+ end
36
+ end
37
+
38
+ def transform_array(array, visiting, depth, max_depth, &key_converter)
39
+ check_depth!(depth, max_depth)
40
+
41
+ oid = array.object_id
42
+ raise CircularStructureError, "Keycase detected a circular reference in an Array" if visiting.include?(oid)
43
+
44
+ visiting.add(oid)
45
+ begin
46
+ array.map do |element|
47
+ transform_value(
48
+ element,
49
+ visiting,
50
+ depth + 1,
51
+ max_depth,
52
+ &key_converter
53
+ )
54
+ end
55
+ ensure
56
+ visiting.delete(oid)
57
+ end
58
+ end
59
+
60
+ def transform_value(value, visiting, depth, max_depth, &key_converter)
61
+ case value
62
+ when Hash
63
+ transform_hash(value, visiting, depth, max_depth, &key_converter)
64
+ when Array
65
+ transform_array(value, visiting, depth, max_depth, &key_converter)
66
+ else
67
+ value
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def check_depth!(depth, max_depth)
74
+ return if max_depth.nil?
75
+
76
+ raise StructureTooDeepError, "Keycase nesting exceeds max_depth (#{max_depth})" if depth > max_depth
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -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.0.2"
4
+ VERSION = "1.2.0"
5
5
  end
data/lib/keycase.rb CHANGED
@@ -1,10 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "keycase/version"
4
+ require "keycase/support/errors"
5
+ require "keycase/support/transformer"
6
+ require "keycase/support/tokenizer"
4
7
  require "keycase/camel_case"
5
8
  require "keycase/kebab_case"
6
9
  require "keycase/pascal_case"
10
+ require "keycase/screaming_snake_case"
7
11
  require "keycase/snake_case"
12
+ require "keycase/train_case"
8
13
 
9
14
  module Keycase
10
15
  end
metadata CHANGED
@@ -1,39 +1,36 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keycase
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - naoigcat
8
- autorequire:
9
- bindir: exe
8
+ bindir: bin
10
9
  cert_chain: []
11
- date: 2022-12-09 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
- description: |
14
- This gem converts the case of strings, symbols, and keys of hash recursively.
15
- The convertible cases are camelCase, PascalCase, snake_case, etc.
12
+ description: |2
13
+ This gem converts the case of strings, symbols, and keys of hash recursively.
14
+ The convertible cases are camelCase, PascalCase, snake_case, etc.
16
15
  email:
17
16
  - 17925623+naoigcat@users.noreply.github.com
18
17
  executables: []
19
18
  extensions: []
20
19
  extra_rdoc_files: []
21
20
  files:
22
- - ".gitignore"
23
- - ".rspec"
24
- - ".rubocop.yml"
25
- - Gemfile
26
21
  - LICENSE.md
27
22
  - README.md
28
- - Rakefile
29
- - bin/console
30
- - bin/setup
31
23
  - keycase.gemspec
32
24
  - lib/keycase.rb
33
25
  - lib/keycase/camel_case.rb
34
26
  - lib/keycase/kebab_case.rb
35
27
  - lib/keycase/pascal_case.rb
28
+ - lib/keycase/screaming_snake_case.rb
36
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
37
34
  - lib/keycase/version.rb
38
35
  homepage: https://github.com/naoigcat/ruby-keycase
39
36
  licenses:
@@ -42,7 +39,6 @@ metadata:
42
39
  homepage_uri: https://github.com/naoigcat/ruby-keycase
43
40
  source_code_uri: https://github.com/naoigcat/ruby-keycase
44
41
  rubygems_mfa_required: 'true'
45
- post_install_message:
46
42
  rdoc_options: []
47
43
  require_paths:
48
44
  - lib
@@ -50,15 +46,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
50
46
  requirements:
51
47
  - - ">="
52
48
  - !ruby/object:Gem::Version
53
- version: 2.0.0
49
+ version: 2.3.0
54
50
  required_rubygems_version: !ruby/object:Gem::Requirement
55
51
  requirements:
56
52
  - - ">="
57
53
  - !ruby/object:Gem::Version
58
54
  version: '0'
59
55
  requirements: []
60
- rubygems_version: 3.3.23
61
- signing_key:
56
+ rubygems_version: 3.6.9
62
57
  specification_version: 4
63
58
  summary: Converts the case of strings, symbols, and keys of hash.
64
59
  test_files: []
data/.gitignore DELETED
@@ -1,14 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
6
- /pkg/
7
- /spec/reports/
8
- /tmp/
9
-
10
- # rspec failure tracking
11
- .rspec_status
12
-
13
- # bundler
14
- Gemfile.lock
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
data/.rubocop.yml DELETED
@@ -1,39 +0,0 @@
1
- require:
2
- - rubocop-rake
3
- - rubocop-rspec
4
-
5
- AllCops:
6
- TargetRubyVersion: 2.7
7
- DisplayCopNames: true
8
- DisplayStyleGuide: true
9
- NewCops: enable
10
-
11
- Gemspec/RequiredRubyVersion:
12
- Enabled: false
13
-
14
- Metrics/BlockLength:
15
- Enabled: false
16
-
17
- Metrics/MethodLength:
18
- Enabled: false
19
-
20
- Naming/VariableNumber:
21
- Enabled: false
22
-
23
- Style/Documentation:
24
- Enabled: false
25
-
26
- Style/MultilineBlockChain:
27
- Enabled: false
28
-
29
- Style/StringLiterals:
30
- EnforcedStyle: double_quotes
31
-
32
- Style/StringLiteralsInInterpolation:
33
- EnforcedStyle: double_quotes
34
-
35
- RSpec/ExampleLength:
36
- Enabled: false
37
-
38
- RSpec/MultipleExpectations:
39
- Enabled: false
data/Gemfile DELETED
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source "https://rubygems.org"
4
-
5
- gemspec
6
-
7
- gem "rake", "~> 12.0"
8
- gem "rspec", "~> 3.0"
9
- gem "rubocop"
10
- gem "rubocop-rake"
11
- gem "rubocop-rspec"
data/Rakefile DELETED
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
- require "rubocop/rake_task"
6
-
7
- RSpec::Core::RakeTask.new(:spec)
8
- RuboCop::RakeTask.new
9
-
10
- task default: :spec
data/bin/console DELETED
@@ -1,15 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require "bundler/setup"
5
- require "keycase"
6
-
7
- # You can add fixtures and/or initialization code here to make experimenting
8
- # with your gem easier. You can also use a different console, if you like.
9
-
10
- # (If you use this, don't forget to add pry to your Gemfile!)
11
- # require "pry"
12
- # Pry.start
13
-
14
- require "irb"
15
- IRB.start(__FILE__)
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here