snaky_hash 2.0.1 → 2.0.2

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.
@@ -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,177 @@
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
+ # :nocov:
25
+ # This will be run in CI on Ruby 2.3, but we only collect coverage from current Ruby
26
+ unless base.instance_methods.include?(:transform_values)
27
+ base.include(BackportedInstanceMethods)
28
+ end
29
+ # :nocov:
30
+ end
31
+ end
32
+
33
+ # Serializes a hash object to JSON
34
+ #
35
+ # @param obj [Hash] the hash to serialize
36
+ # @return [String] JSON string representation of the hash
37
+ def dump(obj)
38
+ hash = dump_hash(obj)
39
+ hash.to_json
40
+ end
41
+
42
+ # Deserializes a JSON string into a hash object
43
+ #
44
+ # @param raw_hash [String, nil] JSON string to deserialize
45
+ # @return [Hash] deserialized hash object
46
+ def load(raw_hash)
47
+ hash = JSON.parse(presence(raw_hash) || "{}")
48
+ hash = load_value(new(hash))
49
+ 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_extensions do
61
+ @load_extensions ||= Extensions.new
62
+ end
63
+
64
+ define_method :dump_extensions do
65
+ @dump_extensions ||= Extensions.new
66
+ end
67
+
68
+ define_method :load_hash_extensions do
69
+ @load_hash_extensions ||= Extensions.new
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ # Provides backported methods for older Ruby versions
77
+ module BackportedInstanceMethods
78
+ # :nocov:
79
+ # Transforms values of a hash using the given block
80
+ #
81
+ # @yield [Object] block to transform each value
82
+ # @return [Hash] new hash with transformed values
83
+ # @return [Enumerator] if no block given
84
+ # @note This will be run in CI on Ruby 2.3, but we only collect coverage from current Ruby
85
+ # Rails <= 5.2 had a transform_values method, which was added to Ruby in version 2.4.
86
+ # This method is a backport of that original Rails method for Ruby 2.2 and 2.3.
87
+ def transform_values(&block)
88
+ return enum_for(:transform_values) { size } unless block_given?
89
+ return {} if empty?
90
+ result = self.class.new
91
+ each do |key, value|
92
+ result[key] = yield(value)
93
+ end
94
+ result
95
+ end
96
+ # :nocov:
97
+ end
98
+
99
+ private
100
+
101
+ # Checks if a value is blank (nil or empty string)
102
+ #
103
+ # @param value [Object] value to check
104
+ # @return [Boolean] true if value is blank
105
+ def blank?(value)
106
+ return true if value.nil?
107
+ return true if value.is_a?(String) && value.empty?
108
+
109
+ false
110
+ end
111
+
112
+ # Returns nil if value is blank, otherwise returns the value
113
+ #
114
+ # @param value [Object] value to check
115
+ # @return [Object, nil] the value or nil if blank
116
+ def presence(value)
117
+ blank?(value) ? nil : value
118
+ end
119
+
120
+ # Processes a hash for dumping, transforming its values
121
+ #
122
+ # @param hash [Hash] hash to process
123
+ # @return [Hash] processed hash with transformed values
124
+ def dump_hash(hash)
125
+ hash = self[hash].transform_values do |value|
126
+ dump_value(value)
127
+ end
128
+ hash.reject { |_, v| blank?(v) }
129
+ end
130
+
131
+ # Processes a single value for dumping
132
+ #
133
+ # @param value [Object] value to process
134
+ # @return [Object, nil] processed value
135
+ def dump_value(value)
136
+ if blank?(value)
137
+ return
138
+ end
139
+
140
+ if value.is_a?(::Hash)
141
+ return dump_hash(value)
142
+ end
143
+
144
+ if value.is_a?(::Array)
145
+ return value.map { |v| dump_value(v) }.compact
146
+ end
147
+
148
+ dump_extensions.run(value)
149
+ end
150
+
151
+ # Processes a hash for loading, transforming its values
152
+ #
153
+ # @param hash [Hash] hash to process
154
+ # @return [Hash] processed hash with transformed values
155
+ def load_hash(hash)
156
+ hash.transform_values do |value|
157
+ load_value(value)
158
+ end
159
+ end
160
+
161
+ # Processes a single value for loading
162
+ #
163
+ # @param value [Object] value to process
164
+ # @return [Object] processed value
165
+ def load_value(value)
166
+ if value.is_a?(::Hash)
167
+ hash = load_hash_extensions.run(new(value))
168
+ return load_hash(new(hash)) if hash.is_a?(::Hash)
169
+ return load_value(hash)
170
+ end
171
+
172
+ return value.map { |v| load_value(v) } if value.is_a?(Array)
173
+
174
+ load_extensions.run(value)
175
+ end
176
+ end
177
+ end
@@ -1,69 +1,114 @@
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 possible, after underscoring
61
+ #
62
+ # @param key [Object] the key to convert
63
+ # @return [String, Object] the converted key or original if not convertible
64
+ define_method(:convert_key) { |key| key.respond_to?(:to_sym) ? underscore_string(key.to_s) : key }
65
+ when :symbol then
66
+ # Converts a key to a symbol if possible, after underscoring
67
+ #
68
+ # @param key [Object] the key to convert
69
+ # @return [Symbol, Object] the converted key or original if not convertible
70
+ define_method(:convert_key) { |key| key.respond_to?(:to_sym) ? underscore_string(key.to_s).to_sym : key }
50
71
  else
51
- val
72
+ raise ArgumentError, "SnakyHash: Unhandled key_type: #{key_type}"
52
73
  end
53
- end
54
74
 
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
75
+ # Converts hash values to the appropriate type when assigning
76
+ #
77
+ # @param val [Object] the value to convert
78
+ # @param duping [Boolean] whether the value is being duplicated
79
+ # @return [Object] the converted value
80
+ define_method :convert_value do |val, duping = false| #:nodoc:
81
+ case val
82
+ when self.class
83
+ val.dup
84
+ when ::Hash
85
+ val = val.dup if duping
86
+ self.class.new(val)
87
+ when ::Array
88
+ val.collect { |e| convert_value(e) }
89
+ else
90
+ val
91
+ end
92
+ end
93
+
94
+ # Converts a string to underscore case
95
+ #
96
+ # @param str [String, #to_s] the string to convert
97
+ # @return [String] the underscored string
98
+ # @example
99
+ # underscore_string("CamelCase") #=> "camel_case"
100
+ # underscore_string("API::V1") #=> "api/v1"
101
+ # @note This is the same as ActiveSupport's String#underscore
102
+ define_method :underscore_string do |str|
103
+ str.to_s.strip.
104
+ tr(" ", "_").
105
+ gsub("::", "/").
106
+ gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
107
+ gsub(/([a-z\d])([A-Z])/, '\1_\2').
108
+ tr("-", "_").
109
+ squeeze("_").
110
+ downcase
111
+ end
67
112
  end
68
113
  end
69
114
  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.2"
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
Binary file