deep_hash_transformer 2.0.0 → 2.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: a7f171270350f94fdce54754d2cd72e7832b50d96fd92255e7c0ccf2b4a4460b
4
- data.tar.gz: bfbe82ac975f7a38f21a26ffd969fb226592ec67e4d54c6009213ad56883e146
3
+ metadata.gz: 7e757436383ce13e17ee8e2b68e48d419e2f9f3c2e57fcfa76503e723ecde5e0
4
+ data.tar.gz: 5508770653b53402c6be53f9a3b1c0ea42a899b42760c2844565c3befa4bc54c
5
5
  SHA512:
6
- metadata.gz: 3c2b9b9c03df98e7407c1ff2e365b2b49a5e200380b1a3747be28d3bcdb8832be8c59c5b94db2753b93809549d7841da2bf1e0a9d6bb425c1c9f545802bea830
7
- data.tar.gz: cab7709f4bf6dbb2a3c9d692c0d3bb4af83b4ce318a020624ad7151c7e1d71f40dde2dec2df51326d79e0f9ae7f2a97c72ba11c65c38467e3e617d26bb572d1f
6
+ metadata.gz: 02c165dda7044c595af36f476fe4234dfca724654e4384d971c183f14dadab6a1f9782f58dd5736f2b82962b7811a41fd51350b2a755ef62cfe684b259dd705d
7
+ data.tar.gz: 41ed65816b738a755fdfbf9e97c0c6ef408fa022a16230d504b0acb4f744ef10279f98954b3eb1c22eaf7af552a9ef5a40c61b75e68cfe4f723f444ebd7e9987
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DeepHashTransformer
4
+ class Blank
5
+ BLANK_STRING = /\A[[:space:]]*\z/.freeze
6
+
7
+ def self.call(value)
8
+ new(value).blank?
9
+ end
10
+
11
+ def initialize(value)
12
+ @value = value
13
+ end
14
+
15
+ def blank?
16
+ return true unless value
17
+ return value.blank? if value.respond_to?(:blank?)
18
+ return BLANK_STRING.match?(value) if value.is_a?(String)
19
+ return value.empty? if value.respond_to?(:empty?)
20
+
21
+ false
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :value
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './blank'
4
+
5
+ class DeepHashTransformer
6
+ module CollectionOperation
7
+ class << self
8
+ def compact(val)
9
+ case val
10
+ when Array, Hash
11
+ val.compact
12
+ else
13
+ val
14
+ end
15
+ end
16
+
17
+ def compact_blank(val)
18
+ case val
19
+ when Array
20
+ val.reject { |elem| Blank.call(elem) }
21
+ when Hash
22
+ val.reject { |_, v| Blank.call(v) }
23
+ else
24
+ val
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DeepHashTransformer
4
+ module ElementOperation
5
+ class << self
6
+ def camel_case(val)
7
+ pascal_case(val)
8
+ .sub(/^[A-Z]/, &:downcase)
9
+ end
10
+
11
+ def dasherize(val)
12
+ stringify(val)
13
+ .tr('_', '-')
14
+ end
15
+
16
+ def identity(val)
17
+ val
18
+ end
19
+
20
+ def pascal_case(val)
21
+ stringify(val)
22
+ .split(/(?=[A-Z])|[-_]/)
23
+ .map(&:capitalize)
24
+ .join
25
+ end
26
+
27
+ def stringify(val)
28
+ val.to_s
29
+ end
30
+
31
+ def symbolize(val)
32
+ val.to_sym
33
+ end
34
+
35
+ def underscore(val)
36
+ stringify(val)
37
+ .tr('-', '_')
38
+ end
39
+
40
+ def snake_case(val)
41
+ stringify(val)
42
+ .gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
43
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
44
+ .tr('-', '_')
45
+ .downcase
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class DeepHashTransformer
4
- VERSION = '2.0.0'
4
+ VERSION = '2.2.0'
5
5
  end
@@ -1,25 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'deep_hash_transformer/collection_operation'
4
+ require 'deep_hash_transformer/element_operation'
3
5
  require 'deep_hash_transformer/version'
4
6
 
5
7
  class DeepHashTransformer
6
- OPS = {
7
- dasherize: ->(val) { val.to_s.tr('_', '-') },
8
- identity: ->(val) { val },
9
- stringify: ->(val) { val.to_s },
10
- symbolize: ->(val) { val.to_sym },
11
- underscore: ->(val) { val.to_s.tr('-', '_') }
12
- }.freeze
8
+ ELEMENT_OPS = %i[
9
+ camel_case
10
+ dasherize
11
+ identity
12
+ pascal_case
13
+ snake_case
14
+ stringify
15
+ symbolize
16
+ underscore
17
+ ].freeze
18
+
19
+ COLLECTION_OPS = %i[
20
+ compact
21
+ compact_blank
22
+ ].freeze
23
+
24
+ OPS = ELEMENT_OPS + COLLECTION_OPS
13
25
 
14
26
  def initialize(hash)
15
27
  @hash = hash
16
28
  end
17
29
 
18
30
  def tr(*ops)
31
+ unknown_transformations = ops.map(&:to_s) - OPS.map(&:to_s)
32
+ if unknown_transformations.any?
33
+ raise(
34
+ ArgumentError, "unknown transformation(s): #{unknown_transformations.join(',')}"
35
+ )
36
+ end
37
+
19
38
  transform_value(hash, ops)
20
39
  end
21
40
 
22
- OPS.each_key do |operation|
41
+ OPS.each do |operation|
23
42
  define_method(operation) { tr(operation) }
24
43
  end
25
44
 
@@ -27,20 +46,30 @@ class DeepHashTransformer
27
46
 
28
47
  attr_reader :hash
29
48
 
30
- def transform_value(value, ops)
31
- case value
32
- when Array
33
- value.map { |e| transform_value(e, ops) }
34
- when Hash
35
- value.map { |k, v| [transform_key(k, ops), transform_value(v, ops)] }.to_h
36
- else
37
- value
49
+ def transform_collection(collection, ops)
50
+ ops.inject(collection) do |c, op|
51
+ COLLECTION_OPS.include?(op) ? CollectionOperation.public_send(op, c) : c
38
52
  end
39
53
  end
40
54
 
55
+ def transform_value(value, ops)
56
+ collection = case value
57
+ when Array
58
+ value.map { |e| transform_value(e, ops) }
59
+ when Hash
60
+ value.map { |k, v| [transform_key(k, ops), transform_value(v, ops)] }.to_h
61
+ else
62
+ value
63
+ end
64
+
65
+ transform_collection(collection, ops)
66
+ end
67
+
41
68
  def transform_key(key, ops)
42
69
  return key unless [String, Symbol].include?(key.class)
43
70
 
44
- ops.inject(key) { |k, op| OPS.fetch(op).call(k) }
71
+ ops.inject(key) do |k, op|
72
+ ELEMENT_OPS.include?(op) ? ElementOperation.public_send(op, k) : k
73
+ end
45
74
  end
46
75
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe DeepHashTransformer::Blank do
4
+ describe '.call' do
5
+ subject { described_class.call(value) }
6
+
7
+ presents = [true, 1, 0, 'any', [nil], { A: nil }]
8
+ blanks = [nil, false, '', ' ', [], {}]
9
+
10
+ presents.each do |value|
11
+ context "with #{value.inspect}" do
12
+ let(:value) { value }
13
+
14
+ it { is_expected.to be false }
15
+ end
16
+ end
17
+
18
+ blanks.each do |value|
19
+ context "with #{value.inspect}" do
20
+ let(:value) { value }
21
+
22
+ it { is_expected.to be true }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe DeepHashTransformer::CollectionOperation do
4
+ examples = [
5
+ { a: 'b' }, { a: nil }, { a: ' ' }, [:foo], ['', nil]
6
+ ]
7
+
8
+ subject do
9
+ examples.map { |value| described_class.public_send(method, value) }
10
+ end
11
+
12
+ describe '.compact' do
13
+ let(:method) { :compact }
14
+
15
+ expected_output = [
16
+ { a: 'b' }, {}, { a: ' ' }, [:foo], ['']
17
+ ]
18
+
19
+ it { is_expected.to eq(expected_output) }
20
+ end
21
+
22
+ describe '.compact_blank' do
23
+ let(:method) { :compact_blank }
24
+
25
+ expected_output = [
26
+ { a: 'b' }, {}, {}, [:foo], []
27
+ ]
28
+
29
+ it { is_expected.to eq(expected_output) }
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe DeepHashTransformer::ElementOperation do
4
+ examples = [
5
+ :symbol, 'string', 'camelCase', 'dashed-key', 'PascalCase', 'under_scored'
6
+ ]
7
+
8
+ subject do
9
+ examples.map { |value| described_class.public_send(method, value) }
10
+ end
11
+
12
+ # rubocop:disable Layout/HashAlignment, Style/SymbolArray, Style/WordArray
13
+ expected_output = {
14
+ camel_case: ['symbol', 'string', 'camelCase', 'dashedKey', 'pascalCase', 'underScored'],
15
+ dasherize: ['symbol', 'string', 'camelCase', 'dashed-key', 'PascalCase', 'under-scored'],
16
+ identity: [:symbol, 'string', 'camelCase', 'dashed-key', 'PascalCase', 'under_scored'],
17
+ pascal_case: ['Symbol', 'String', 'CamelCase', 'DashedKey', 'PascalCase', 'UnderScored'],
18
+ snake_case: ['symbol', 'string', 'camel_case', 'dashed_key', 'pascal_case', 'under_scored'],
19
+ stringify: ['symbol', 'string', 'camelCase', 'dashed-key', 'PascalCase', 'under_scored'],
20
+ symbolize: [:symbol, :string, :camelCase, :'dashed-key', :PascalCase, :under_scored],
21
+ underscore: ['symbol', 'string', 'camelCase', 'dashed_key', 'PascalCase', 'under_scored']
22
+ }
23
+ # rubocop:enable Layout/HashAlignment, Style/SymbolArray, Style/WordArray
24
+
25
+ DeepHashTransformer::ELEMENT_OPS.each do |method|
26
+ describe "##{method}" do
27
+ let(:method) { method }
28
+
29
+ it { is_expected.to eq(expected_output[method]) }
30
+ end
31
+ end
32
+ end
@@ -1,87 +1,128 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RSpec.describe DeepHashTransformer do
4
- subject do
5
- described_class.new(
6
- Integer => 123,
7
- :foobar => { bar: 'bar' },
8
- 'aa_zz' => [{ 'bar' => :bar, 'a-z' => 'a-z' }]
9
- )
10
- end
4
+ subject(:operation) { described_class.new(example) }
5
+
6
+ let(:example) { { foo: 'bar' } }
11
7
 
12
- describe '#tr with `:underscore, :symbolize`' do
13
- subject { super().tr(:underscore, :symbolize) }
8
+ describe 'autogenerated methods' do
9
+ DeepHashTransformer::ELEMENT_OPS.each do |method|
10
+ describe ".#{method}" do
11
+ it "delegates to `Operation.#{method}`" do
12
+ expect(
13
+ DeepHashTransformer::ElementOperation
14
+ ).to receive(method).with(:foo).once # rubocop:disable RSpec/MessageSpies
14
15
 
15
- it do
16
- expect(subject).to eq( # rubocop:disable RSpec/NamedSubject
17
- Integer => 123,
18
- :foobar => { bar: 'bar' },
19
- :aa_zz => [{ bar: :bar, a_z: 'a-z' }]
20
- )
16
+ operation.public_send(method)
17
+ end
18
+ end
21
19
  end
22
20
  end
23
21
 
24
- describe '#dasherize' do
25
- subject { super().dasherize }
22
+ describe '#tr' do
23
+ context 'with `:snake_case, :symbolize`' do
24
+ subject { super().tr(:snake_case, :symbolize) }
26
25
 
27
- it do
28
- expect(subject).to eq( # rubocop:disable RSpec/NamedSubject
29
- Integer => 123,
30
- 'foobar' => { 'bar' => 'bar' },
31
- 'aa-zz' => [{ 'bar' => :bar, 'a-z' => 'a-z' }]
32
- )
26
+ let(:example) { { 'FooBar' => 'baz' } }
27
+
28
+ it { is_expected.to eq(foo_bar: 'baz') }
33
29
  end
34
- end
35
30
 
36
- describe '#identity' do
37
- subject { super().identity }
31
+ context 'with an unknown transformation' do
32
+ subject(:unknown_ops) { operation.tr(:unknown) }
38
33
 
39
- it do
40
- expect(subject).to eq( # rubocop:disable RSpec/NamedSubject
41
- Integer => 123,
42
- :foobar => { bar: 'bar' },
43
- 'aa_zz' => [{ 'bar' => :bar, 'a-z' => 'a-z' }]
44
- )
34
+ it 'raise an exception' do
35
+ expect { unknown_ops }.to raise_error(ArgumentError, /unknown/)
36
+ end
45
37
  end
46
- end
47
38
 
48
- describe '#stringify' do
49
- subject { super().stringify }
39
+ context 'with a complex, nested example' do
40
+ subject { super().tr(:camel_case, :symbolize) }
41
+
42
+ let(:example) do
43
+ {
44
+ Integer => 123,
45
+ :symbol => { foo_bar: 'bar' },
46
+ 'string' => { 'foo_bar' => 123 },
47
+ 'nested-array' => [
48
+ {
49
+ 'camelCased' => 'camelCased',
50
+ 'dashed-key' => 'dashed-key',
51
+ 'PascalCased' => 'PascalCased',
52
+ 'under_scored' => 'under_scored'
53
+ }
54
+ ]
55
+ }
56
+ end
50
57
 
51
- it do
52
- expect(subject).to eq( # rubocop:disable RSpec/NamedSubject
53
- Integer => 123,
54
- 'foobar' => { 'bar' => 'bar' },
55
- 'aa_zz' => [{ 'bar' => :bar, 'a-z' => 'a-z' }]
56
- )
58
+ it do # rubocop:disable RSpec/ExampleLength
59
+ is_expected.to eq( # rubocop:disable RSpec/ImplicitSubject
60
+ Integer => 123,
61
+ :symbol => { fooBar: 'bar' },
62
+ :string => { fooBar: 123 },
63
+ :nestedArray => [
64
+ {
65
+ camelCased: 'camelCased',
66
+ dashedKey: 'dashed-key',
67
+ pascalCased: 'PascalCased',
68
+ underScored: 'under_scored'
69
+ }
70
+ ]
71
+ )
72
+ end
57
73
  end
58
- end
59
74
 
60
- describe '#symbolize' do
61
- subject { super().symbolize }
75
+ context 'with nil values' do
76
+ subject { super().tr(:compact, :stringify) }
62
77
 
63
- it do
64
- expect(subject).to eq( # rubocop:disable RSpec/NamedSubject
65
- Integer => 123,
66
- :foobar => { bar: 'bar' },
67
- :aa_zz => [{ bar: :bar, 'a-z': 'a-z' }]
68
- )
78
+ let(:example) do
79
+ { a: { b: ['', nil, :value], c: '', d: nil, e: true, f: false, g: 123, h: [''] } }
80
+ end
81
+
82
+ it do # rubocop:disable RSpec/ExampleLength
83
+ is_expected.to eq( # rubocop:disable RSpec/ImplicitSubject
84
+ 'a' => {
85
+ 'b' => ['', :value],
86
+ 'c' => '',
87
+ 'e' => true,
88
+ 'f' => false,
89
+ 'g' => 123,
90
+ 'h' => ['']
91
+ }
92
+ )
93
+ end
69
94
  end
70
- end
71
95
 
72
- describe '#underscore' do
73
- subject { super().underscore }
96
+ context 'with blank values' do
97
+ subject { super().tr(:compact_blank, :stringify) }
98
+
99
+ let(:example) do
100
+ { a: { b: ['', nil, :value], c: '', d: nil, e: true, f: false, g: 123, h: [''] } }
101
+ end
102
+
103
+ it do # rubocop:disable RSpec/ExampleLength
104
+ is_expected.to eq( # rubocop:disable RSpec/ImplicitSubject
105
+ 'a' => {
106
+ 'b' => [:value],
107
+ 'e' => true,
108
+ 'g' => 123
109
+ }
110
+ )
111
+ end
112
+ end
113
+
114
+ context 'with deeply nested blank values' do
115
+ subject { super().tr(:compact_blank) }
116
+
117
+ let(:example) do
118
+ { a: { b: [nil, { c: nil }] } }
119
+ end
74
120
 
75
- it do
76
- expect(subject).to eq( # rubocop:disable RSpec/NamedSubject
77
- Integer => 123,
78
- 'foobar' => { 'bar' => 'bar' },
79
- 'aa_zz' => [{ 'bar' => :bar, 'a_z' => 'a-z' }]
80
- )
121
+ it { is_expected.to eq({}) }
81
122
  end
82
123
  end
83
124
 
84
125
  it 'has a version number' do
85
- expect(DeepHashTransformer::VERSION).not_to be nil
126
+ expect(DeepHashTransformer::VERSION).not_to be_nil
86
127
  end
87
128
  end
data/spec/spec_helper.rb CHANGED
@@ -1,10 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bundler/setup'
4
- require 'coveralls'
5
4
  require 'deep_hash_transformer'
5
+ require 'deep_hash_transformer/blank'
6
+ require 'deep_hash_transformer/collection_operation'
7
+ require 'deep_hash_transformer/element_operation'
8
+ require 'simplecov'
6
9
 
7
- Coveralls.wear!
10
+ SimpleCov.start do
11
+ if ENV['CI']
12
+ require 'simplecov-lcov'
13
+
14
+ SimpleCov::Formatter::LcovFormatter.config do |c|
15
+ c.report_with_single_file = true
16
+ c.single_report_path = 'coverage/lcov.info'
17
+ end
18
+
19
+ formatter SimpleCov::Formatter::LcovFormatter
20
+ end
21
+
22
+ add_filter %w[version.rb initializer.rb]
23
+ end
8
24
 
9
25
  RSpec.configure do |config|
10
26
  # Enable flags like --only-failures and --next-failure
metadata CHANGED
@@ -1,100 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deep_hash_transformer
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin Spickermann
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-28 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: coveralls
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: rake
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: rspec
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: rubocop
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: rubocop-performance
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: rubocop-rspec
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- description: " DeepHashTransformer helps to transform deeply nested hash structures\n"
11
+ date: 2023-05-16 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: " DeepHashTransformer helps to transform keys in deeply nested hash
14
+ structures\n"
98
15
  email:
99
16
  - spickermann@gmail.com
100
17
  executables: []
@@ -103,13 +20,20 @@ extra_rdoc_files: []
103
20
  files:
104
21
  - MIT-LICENSE
105
22
  - lib/deep_hash_transformer.rb
23
+ - lib/deep_hash_transformer/blank.rb
24
+ - lib/deep_hash_transformer/collection_operation.rb
25
+ - lib/deep_hash_transformer/element_operation.rb
106
26
  - lib/deep_hash_transformer/version.rb
27
+ - spec/deep_hash_transformer/blank_spec.rb
28
+ - spec/deep_hash_transformer/collection_operation_spec.rb
29
+ - spec/deep_hash_transformer/element_operation_spec.rb
107
30
  - spec/deep_hash_transformer_spec.rb
108
31
  - spec/spec_helper.rb
109
32
  homepage: https://github.com/spickermann/deep_hash_transformer
110
33
  licenses:
111
34
  - MIT
112
- metadata: {}
35
+ metadata:
36
+ rubygems_mfa_required: 'true'
113
37
  post_install_message:
114
38
  rdoc_options: []
115
39
  require_paths:
@@ -125,7 +49,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
49
  - !ruby/object:Gem::Version
126
50
  version: '0'
127
51
  requirements: []
128
- rubygems_version: 3.2.3
52
+ rubygems_version: 3.4.10
129
53
  signing_key:
130
54
  specification_version: 4
131
55
  summary: Transforms deeply nested hash structure