snaky_hash 2.0.1 → 2.0.3

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.
data/SECURITY.md CHANGED
@@ -4,7 +4,8 @@
4
4
 
5
5
  | Version | Supported | EOL | Post-EOL / Enterprise |
6
6
  |---------|-----------|---------|---------------------------------------|
7
- | 1.0.x | ✅ | 04/2023 | [Tidelift Subscription][tidelift-ref] |
7
+ | 2.0.x | ✅ | 04/2023 | [Tidelift Subscription][tidelift-ref] |
8
+ | 1.0.x | | | |
8
9
 
9
10
  ### EOL Policy
10
11
 
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SnakyHash
4
+ # Manages extensions that can modify values in a chain of transformations
5
+ #
6
+ # @example Adding and running an extension
7
+ # extensions = Extensions.new
8
+ # extensions.add(:upcase) { |value| value.to_s.upcase }
9
+ # extensions.run("hello") #=> "HELLO"
10
+ #
11
+ class Extensions
12
+ # Initializes a new Extensions instance with an empty extensions registry
13
+ def initialize
14
+ reset
15
+ end
16
+
17
+ # Reset the registry of extensions to an empty state
18
+ #
19
+ # @return [Hash] an empty hash of extensions
20
+ def reset
21
+ @extensions = {}
22
+ end
23
+
24
+ # Adds a new extension with the given name
25
+ #
26
+ # @param name [String, Symbol] the name of the extension
27
+ # @yield [value] block that will be called with a value to transform
28
+ # @raise [SnakyHash::Error] if an extension with the given name already exists
29
+ # @return [Proc] the added extension block
30
+ def add(name, &block)
31
+ if has?(name)
32
+ raise Error, "Extension already defined named '#{name}'"
33
+ end
34
+
35
+ @extensions[name.to_sym] = block
36
+ end
37
+
38
+ # Checks if an extension with the given name exists
39
+ #
40
+ # @param name [String, Symbol] the name of the extension to check
41
+ # @return [Boolean] true if the extension exists, false otherwise
42
+ def has?(name)
43
+ @extensions.key?(name.to_sym)
44
+ end
45
+
46
+ # Runs all registered extensions in sequence on the given value
47
+ #
48
+ # @param value [Object] the value to transform through all extensions
49
+ # @return [Object] the final transformed value after running all extensions
50
+ def run(value)
51
+ @extensions.each_value do |block|
52
+ value = block.call(value)
53
+ end
54
+ value
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module SnakyHash
6
+ # Provides JSON serialization and deserialization capabilities with extensible value transformation
7
+ #
8
+ # @example Basic usage
9
+ # class MyHash < Hashie::Mash
10
+ # extend SnakyHash::Serializer
11
+ # end
12
+ # hash = MyHash.load('{"key": "value"}')
13
+ # hash.dump #=> '{"key":"value"}'
14
+ #
15
+ module Serializer
16
+ class << self
17
+ # Extends the base class with serialization capabilities
18
+ #
19
+ # @param base [Class] the class being extended
20
+ # @return [void]
21
+ def extended(base)
22
+ extended_module = Modulizer.to_extended_mod
23
+ base.extend(extended_module)
24
+ base.include(ConvenienceInstanceMethods)
25
+ # :nocov:
26
+ # This will be run in CI on Ruby 2.3, but we only collect coverage from current Ruby
27
+ unless base.instance_methods.include?(:transform_values)
28
+ base.include(BackportedInstanceMethods)
29
+ end
30
+ # :nocov:
31
+ end
32
+ end
33
+
34
+ # Serializes a hash object to JSON
35
+ #
36
+ # @param obj [Hash] the hash to serialize
37
+ # @return [String] JSON string representation of the hash
38
+ def dump(obj)
39
+ hash = dump_hash(obj)
40
+ hash.to_json
41
+ end
42
+
43
+ # Deserializes a JSON string into a hash object
44
+ #
45
+ # @param raw_hash [String, nil] JSON string to deserialize
46
+ # @return [Hash] deserialized hash object
47
+ def load(raw_hash)
48
+ hash = JSON.parse(presence(raw_hash) || "{}")
49
+ load_hash(new(hash))
50
+ end
51
+
52
+ # Internal module for generating extension methods
53
+ module Modulizer
54
+ class << self
55
+ # Creates a new module with extension management methods
56
+ #
57
+ # @return [Module] a module containing extension management methods
58
+ def to_extended_mod
59
+ Module.new do
60
+ define_method :load_value_extensions do
61
+ @load_value_extensions ||= Extensions.new
62
+ end
63
+
64
+ define_method :load_extensions do
65
+ load_value_extensions
66
+ end
67
+
68
+ define_method :dump_value_extensions do
69
+ @dump_value_extensions ||= Extensions.new
70
+ end
71
+
72
+ define_method :dump_extensions do
73
+ dump_value_extensions
74
+ end
75
+
76
+ define_method :load_hash_extensions do
77
+ @load_hash_extensions ||= Extensions.new
78
+ end
79
+
80
+ define_method :dump_hash_extensions do
81
+ @dump_hash_extensions ||= Extensions.new
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ # Provides backported methods for older Ruby versions
89
+ module BackportedInstanceMethods
90
+ # :nocov:
91
+ # Transforms values of a hash using the given block
92
+ #
93
+ # @yield [Object] block to transform each value
94
+ # @return [Hash] new hash with transformed values
95
+ # @return [Enumerator] if no block given
96
+ # @note This will be run in CI on Ruby 2.3, but we only collect coverage from current Ruby
97
+ # Rails <= 5.2 had a transform_values method, which was added to Ruby in version 2.4.
98
+ # This method is a backport of that original Rails method for Ruby 2.2 and 2.3.
99
+ def transform_values(&block)
100
+ return enum_for(:transform_values) { size } unless block_given?
101
+ return {} if empty?
102
+ result = self.class.new
103
+ each do |key, value|
104
+ result[key] = yield(value)
105
+ end
106
+ result
107
+ end
108
+ # :nocov:
109
+ end
110
+
111
+ # Provides convenient instance methods for serialization
112
+ #
113
+ # @example Using convenience methods
114
+ # hash = MyHash.new(key: 'value')
115
+ # json = hash.dump #=> '{"key":"value"}'
116
+ module ConvenienceInstanceMethods
117
+ # Serializes the current hash instance to JSON
118
+ #
119
+ # @return [String] JSON string representation of the hash
120
+ def dump
121
+ self.class.dump(self)
122
+ end
123
+ end
124
+
125
+ private
126
+
127
+ # Checks if a value is blank (nil or empty string)
128
+ #
129
+ # @param value [Object] value to check
130
+ # @return [Boolean] true if value is blank
131
+ def blank?(value)
132
+ return true if value.nil?
133
+ return true if value.is_a?(String) && value.empty?
134
+
135
+ false
136
+ end
137
+
138
+ # Returns nil if value is blank, otherwise returns the value
139
+ #
140
+ # @param value [Object] value to check
141
+ # @return [Object, nil] the value or nil if blank
142
+ def presence(value)
143
+ blank?(value) ? nil : value
144
+ end
145
+
146
+ # Processes a hash for dumping, transforming its keys and/or values
147
+ #
148
+ # @param hash [Hash] hash to process
149
+ # @return [Hash] processed hash with transformed values
150
+ def dump_hash(hash)
151
+ dump_hash_extensions.run(self[hash]).transform_values do |value|
152
+ dump_value(value)
153
+ end
154
+ end
155
+
156
+ # Processes a single value for dumping
157
+ #
158
+ # @param value [Object] value to process
159
+ # @return [Object, nil] processed value
160
+ def dump_value(value)
161
+ if blank?(value)
162
+ return value
163
+ end
164
+
165
+ if value.is_a?(::Hash)
166
+ return dump_hash(value)
167
+ end
168
+
169
+ if value.is_a?(::Array)
170
+ return value.map { |v| dump_value(v) }.compact
171
+ end
172
+
173
+ dump_extensions.run(value)
174
+ end
175
+
176
+ # Processes a hash for loading, transforming its keys and/or values
177
+ #
178
+ # @param hash [Hash] hash to process
179
+ # @return [Hash] processed hash with transformed values
180
+ def load_hash(hash)
181
+ ran = load_hash_extensions.run(self[hash])
182
+ return load_value(ran) unless ran.is_a?(::Hash)
183
+
184
+ res = self[ran].transform_values do |value|
185
+ load_value(value)
186
+ end
187
+
188
+ # TODO: Drop this hack when dropping support for Ruby 2.6
189
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.7")
190
+ res
191
+ else
192
+ # :nocov:
193
+ # In Ruby <= 2.6 Hash#transform_values returned a new vanilla Hash,
194
+ # rather than a hash of the class being transformed.
195
+ self[res]
196
+ # :nocov:
197
+ end
198
+ end
199
+
200
+ # Processes a single value for loading
201
+ #
202
+ # @param value [Object] value to process
203
+ # @return [Object, nil] processed value
204
+ def load_value(value)
205
+ if blank?(value)
206
+ return value
207
+ end
208
+
209
+ if value.is_a?(::Hash)
210
+ return load_hash(value)
211
+ end
212
+
213
+ if value.is_a?(::Array)
214
+ return value.map { |v| load_value(v) }.compact
215
+ end
216
+
217
+ load_extensions.run(value)
218
+ end
219
+ end
220
+ end
@@ -1,69 +1,119 @@
1
1
  # This is a module-class hybrid.
2
2
  #
3
+ # A flexible key conversion system that supports both String and Symbol keys,
4
+ # with optional serialization capabilities.
5
+ #
6
+ # @example Basic usage with string keys
7
+ # class MyHash < Hashie::Mash
8
+ # include SnakyHash::Snake.new(key_type: :string)
9
+ # end
10
+ #
11
+ # @example Usage with symbol keys and serialization
12
+ # class MySerializableHash < Hashie::Mash
13
+ # include SnakyHash::Snake.new(key_type: :symbol, serializer: true)
14
+ # end
15
+ #
3
16
  # Hashie's standard SymbolizeKeys is similar to the functionality we want.
4
17
  # ... but not quite. We need to support both String (for oauth2) and Symbol keys (for oauth).
5
- # include Hashie::Extensions::Mash::SymbolizeKeys
18
+ # see: Hashie::Extensions::Mash::SymbolizeKeys
19
+ #
6
20
  module SnakyHash
21
+ # Creates a module that provides key conversion functionality when included
22
+ #
23
+ # @note Unlike Hashie::Mash, this implementation allows for both String and Symbol key types
7
24
  class Snake < Module
8
- def initialize(key_type: :string)
25
+ # Initialize a new Snake module
26
+ #
27
+ # @param key_type [Symbol] the type to convert keys to (:string or :symbol)
28
+ # @param serializer [Boolean] whether to include serialization capabilities
29
+ # @raise [ArgumentError] if key_type is not :string or :symbol
30
+ def initialize(key_type: :string, serializer: false)
9
31
  super()
10
32
  @key_type = key_type
33
+ @serializer = serializer
11
34
  end
12
35
 
36
+ # Includes appropriate conversion methods into the base class
37
+ #
38
+ # @param base [Class] the class including this module
39
+ # @return [void]
13
40
  def included(base)
14
41
  conversions_module = SnakyModulizer.to_mod(@key_type)
15
42
  base.include(conversions_module)
43
+ if @serializer
44
+ base.extend(SnakyHash::Serializer)
45
+ end
16
46
  end
17
47
 
48
+ # Internal module factory for creating key conversion functionality
18
49
  module SnakyModulizer
19
- def self.to_mod(key_type)
20
- Module.new do
21
- # Converts a key to a symbol, or a string, depending on key_type,
22
- # but only if it is able to be converted to a symbol,
23
- # and after underscoring it.
24
- #
25
- # @api private
26
- # @param [<K>] key the key to attempt convert to a symbol
27
- # @return [Symbol, K]
28
-
29
- case key_type
30
- when :string then
31
- define_method(:convert_key) { |key| key.respond_to?(:to_sym) ? underscore_string(key.to_s) : key }
32
- when :symbol then
33
- define_method(:convert_key) { |key| key.respond_to?(:to_sym) ? underscore_string(key.to_s).to_sym : key }
34
- else
35
- raise ArgumentError, "SnakyHash: Unhandled key_type: #{key_type}"
36
- end
37
-
38
- # Unlike its parent Mash, a SnakyHash::Snake will convert other
39
- # Hashie::Hash values to a SnakyHash::Snake when assigning
40
- # instead of respecting the existing subclass
41
- define_method :convert_value do |val, duping = false| #:nodoc:
42
- case val
43
- when self.class
44
- val.dup
45
- when ::Hash
46
- val = val.dup if duping
47
- self.class.new(val)
48
- when ::Array
49
- val.collect { |e| convert_value(e) }
50
+ class << self
51
+ # Creates a new module with key conversion methods based on the specified key type
52
+ #
53
+ # @param key_type [Symbol] the type to convert keys to (:string or :symbol)
54
+ # @return [Module] a new module with conversion methods
55
+ # @raise [ArgumentError] if key_type is not supported
56
+ def to_mod(key_type)
57
+ Module.new do
58
+ case key_type
59
+ when :string then
60
+ # Converts a key to a string if it is symbolizable, after underscoring
61
+ #
62
+ # @note checks for to_sym instead of to_s, because nearly everything responds_to?(:to_s)
63
+ # so respond_to?(:to_s) isn't very useful as a test, and would result in symbolizing integers
64
+ # amd it also provides parity between the :symbol behavior, and the :string behavior,
65
+ # regarding which keys get converted for a given version of Ruby.
66
+ #
67
+ # @param key [Object] the key to convert
68
+ # @return [String, Object] the converted key or original if not convertible
69
+ define_method(:convert_key) { |key| key.respond_to?(:to_sym) ? underscore_string(key.to_s) : key }
70
+ when :symbol then
71
+ # Converts a key to a symbol if possible, after underscoring
72
+ #
73
+ # @param key [Object] the key to convert
74
+ # @return [Symbol, Object] the converted key or original if not convertible
75
+ define_method(:convert_key) { |key| key.respond_to?(:to_sym) ? underscore_string(key.to_s).to_sym : key }
50
76
  else
51
- val
77
+ raise ArgumentError, "SnakyHash: Unhandled key_type: #{key_type}"
52
78
  end
53
- end
54
79
 
55
- # converts a camel_cased string to a underscore string
56
- # subs spaces with underscores, strips whitespace
57
- # Same way ActiveSupport does string.underscore
58
- define_method :underscore_string do |str|
59
- str.to_s.strip
60
- .tr(" ", "_")
61
- .gsub(/::/, "/")
62
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
63
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
64
- .tr("-", "_")
65
- .squeeze("_")
66
- .downcase
80
+ # Converts hash values to the appropriate type when assigning
81
+ #
82
+ # @param val [Object] the value to convert
83
+ # @param duping [Boolean] whether the value is being duplicated
84
+ # @return [Object] the converted value
85
+ define_method :convert_value do |val, duping = false| #:nodoc:
86
+ case val
87
+ when self.class
88
+ val.dup
89
+ when ::Hash
90
+ val = val.dup if duping
91
+ self.class.new(val)
92
+ when ::Array
93
+ val.collect { |e| convert_value(e) }
94
+ else
95
+ val
96
+ end
97
+ end
98
+
99
+ # Converts a string to underscore case
100
+ #
101
+ # @param str [String, #to_s] the string to convert
102
+ # @return [String] the underscored string
103
+ # @example
104
+ # underscore_string("CamelCase") #=> "camel_case"
105
+ # underscore_string("API::V1") #=> "api/v1"
106
+ # @note This is the same as ActiveSupport's String#underscore
107
+ define_method :underscore_string do |str|
108
+ str.to_s.strip.
109
+ tr(" ", "_").
110
+ gsub("::", "/").
111
+ gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
112
+ gsub(/([a-z\d])([A-Z])/, '\1_\2').
113
+ tr("-", "_").
114
+ squeeze("_").
115
+ downcase
116
+ end
67
117
  end
68
118
  end
69
119
  end
@@ -1,5 +1,13 @@
1
1
  module SnakyHash
2
+ # serializer is being introduced as an always disabled option for backwards compatibility.
3
+ # In snaky_hash v3 it will default to true.
4
+ # If you want to start using it immediately, reopen this class and add the Serializer module:
5
+ #
6
+ # SnakyHash::StringKeyed.class_eval do
7
+ # extend SnakyHash::Serializer
8
+ # end
9
+ #
2
10
  class StringKeyed < Hashie::Mash
3
- include SnakyHash::Snake.new(key_type: :string)
11
+ include SnakyHash::Snake.new(key_type: :string, serializer: false)
4
12
  end
5
13
  end
@@ -1,5 +1,13 @@
1
1
  module SnakyHash
2
+ # serializer is being introduced as an always disabled option for backwards compatibility.
3
+ # In snaky_hash v3 it will default to true.
4
+ # If you want to start using it immediately, reopen this class and add the Serializer module:
5
+ #
6
+ # SnakyHash::SymbolKeyed.class_eval do
7
+ # extend SnakyHash::Serializer
8
+ # end
9
+ #
2
10
  class SymbolKeyed < Hashie::Mash
3
- include SnakyHash::Snake.new(key_type: :symbol)
11
+ include SnakyHash::Snake.new(key_type: :symbol, serializer: false)
4
12
  end
5
13
  end
@@ -1,7 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SnakyHash
4
+ # Defines the version information for SnakyHash
5
+ #
6
+ # @api public
4
7
  module Version
5
- VERSION = "2.0.1".freeze
8
+ # Current version of SnakyHash
9
+ #
10
+ # @return [String] the current version in semantic versioning format
11
+ VERSION = "2.0.3"
6
12
  end
7
13
  end
data/lib/snaky_hash.rb CHANGED
@@ -5,14 +5,38 @@ require "hashie"
5
5
  require "version_gem"
6
6
 
7
7
  require_relative "snaky_hash/version"
8
+ require_relative "snaky_hash/extensions"
9
+ require_relative "snaky_hash/serializer"
8
10
  require_relative "snaky_hash/snake"
9
11
  require_relative "snaky_hash/string_keyed"
10
12
  require_relative "snaky_hash/symbol_keyed"
11
13
 
12
- # This is the namespace for this gem
14
+ # SnakyHash provides hash-like objects with automatic key conversion capabilities
15
+ #
16
+ # @example Using StringKeyed hash
17
+ # hash = SnakyHash::StringKeyed.new
18
+ # hash["camelCase"] = "value"
19
+ # hash["camel_case"] # => "value"
20
+ #
21
+ # @example Using SymbolKeyed hash
22
+ # hash = SnakyHash::SymbolKeyed.new
23
+ # hash["camelCase"] = "value"
24
+ # hash[:camel_case] # => "value"
25
+ #
26
+ # @see SnakyHash::StringKeyed
27
+ # @see SnakyHash::SymbolKeyed
28
+ # @see SnakyHash::Snake
13
29
  module SnakyHash
30
+ # Base error class for all SnakyHash errors
31
+ #
32
+ # @api public
33
+ class Error < StandardError
34
+ end
14
35
  end
15
36
 
37
+ # Enable version introspection via VersionGem
38
+ #
39
+ # @api private
16
40
  SnakyHash::Version.class_eval do
17
41
  extend VersionGem::Basic
18
42
  end
data.tar.gz.sig CHANGED
@@ -1,3 +1,3 @@
1
- ���:�I�3�د7��P�P�$;1 ��i��?-y�ˡ̔��V�\�峹n&śV� �}y�ڟ1
2
- ~�x=߷w�� #��T���3����+�����+1��>ؚ�fPns ��ZB "�6M_���o��
3
- �ʭ��(���Ѕ�UIYI{<͸.]J��؆`�.���Y��t1�����T���� 'Md��xA:�*G�2v����^�יYJ�����Jg��X���\�������FZz����@�U�:�oDs�o4٠�������(�7i��A0��E�jh�6�o�I�"�@���T��rK���b�A�IbCeϫ�)j8J/�8
1
+ ����
2
+ ��#�e��Ycr�\�$��bo�φ�ᔻw뇀�7�@��o>mi�
3
+ ��>�#+�qK�^���.�x�AY#�I&�d�!ԭKoc.+�߅"H}|�~�G�̣$0�㫏��< ���i��c&U��Shs�Ǖ�V�=!tsڼ-���;�(�DТIp})ʂ{]�%��"p��������XU��O�pر,w��ZR��.o�@�M3%[LB#��Œ˷-��%wC��?֔�X�+��DL��m����^Py\��Rxg;g���C���Y3,"�\����_��8�W١q^N ��SuY�)N /W��i|����xu�s9�Z�؍��2��