Add Hashie::Extensions::Mash::SymbolizeKeys

We often have requests to make Mash symbolize keys by default. Since
Hashie is used across so many different version of Ruby, we have been
hesitant to make this behavior the default. However, there are valid use
cases for wanting symbol keys.

To satisfy both the needs of those on older Rubies and the needs of
those who want symbol keys, this extension gives the end-user the
ability to toggle on symbolized keys in their Mash subclasses. By adding
this ability, we can wait to implement the symbol keys as a default for
a while longer.

See #341, #342 for more information. This is a half-measure toward the
implementation of #342 (which makes Mash symbolize keys by default).
This commit is contained in:
Michael Herold 2017-02-19 23:31:10 -06:00
parent cc359a22b6
commit 5fa3ebc132
No known key found for this signature in database
GPG Key ID: 0325A44E1EA06F99
5 changed files with 106 additions and 0 deletions

View File

@ -12,6 +12,7 @@ scheme are considered to be bugs.
### Added
* [#412](https://github.com/intridea/hashie/pull/412): Added a Hashie::Extensions::Mash::SymbolizeKeys extension that overrides the default stringification behavior for keys - [@michaelherold](https://github.com/michaelherold).
* Your contribution here.
### Changed

View File

@ -561,6 +561,33 @@ safe_mash.zip = 'Test' # => ArgumentError
safe_mash[:zip] = 'test' # => still ArgumentError
```
### Mash Extension:: SymbolizeKeys
This extension can be mixed into a Mash to change the default behavior of converting keys to strings. After mixing this extension into a Mash, the Mash will convert all keys to symbols.
```ruby
class SymbolizedMash < ::Hashie::Mash
include Hashie::Extensions::Mash::SymbolizeKeys
end
symbol_mash = SymbolizedMash.new
symbol_mash['test'] = 'value'
symbol_mash.test #=> 'value'
symbol_mash.to_h #=> {test: 'value'}
```
There is a major benefit and coupled with a major trade-off to this decision (at least on older Rubies). As a benefit, by using symbols as keys, you will be able to use the implicit conversion of a Mash via the `#to_hash` method to destructure (or splat) the contents of a Mash out to a block. This can be handy for doing iterations through the Mash's keys and values, as follows:
```ruby
symbol_mash = SymbolizedMash.new(id: 123, name: 'Rey')
symbol_mash.each do |key, value|
# key is :id, then :name
# value is 123, then 'Rey'
end
```
However, on Rubies less than 2.0, this means that every key you send to the Mash will generate a symbol. Since symbols are not garbage-collected on older versions of Ruby, this can cause a slow memory leak when using a symbolized Mash with data generated from user input.
## Dash
Dash is an extended Hash that has a discrete set of defined properties and only those properties may be set on the hash. Additionally, you can set defaults for each property. You can also flag a property as required. Required properties will raise an exception if unset. Another option is message for required properties, which allow you to add custom messages for required property.

View File

@ -45,6 +45,7 @@ module Hashie
module Mash
autoload :SafeAssignment, 'hashie/extensions/mash/safe_assignment'
autoload :SymbolizeKeys, 'hashie/extensions/mash/symbolize_keys'
end
module Array

View File

@ -0,0 +1,38 @@
module Hashie
module Extensions
module Mash
# Overrides Mash's default behavior of converting keys to strings
#
# @example
# class LazyResponse < Hashie::Mash
# include Hashie::Extensions::Mash::SymbolizedKeys
# end
#
# response = LazyResponse.new("id" => 123, "name" => "Rey").to_h
# #=> {id: 123, name: "Rey"}
#
# @api public
module SymbolizeKeys
# Hook for being included in a class
#
# @api private
# @return [void]
# @raise [ArgumentError] when the base class isn't a Mash
def self.included(base)
fail ArgumentError, "#{base} must descent from Hashie::Mash" unless base <= Hashie::Mash
end
private
# Converts a key to a symbol
#
# @api private
# @param [String, Symbol] key the key to convert to a symbol
# @return [void]
def convert_key(key)
key.to_sym
end
end
end
end
end

View File

@ -0,0 +1,39 @@
require 'spec_helper'
RSpec.describe Hashie::Extensions::Mash::SymbolizeKeys do
it 'raises an error when included in a class that is not a Mash' do
expect do
Class.new do
include Hashie::Extensions::Mash::SymbolizeKeys
end
end.to raise_error(ArgumentError)
end
it 'symbolizes all keys in the Mash' do
my_mash = Class.new(Hashie::Mash) do
include Hashie::Extensions::Mash::SymbolizeKeys
end
expect(my_mash.new('test' => 'value').to_h).to eq(test: 'value')
end
context 'implicit to_hash on double splat' do
let(:destructure) { ->(**opts) { opts } }
let(:my_mash) do
Class.new(Hashie::Mash) do
include Hashie::Extensions::Mash::SymbolizeKeys
end
end
let(:instance) { my_mash.new('outer' => { 'inner' => 42 }, 'testing' => [1, 2, 3]) }
subject { destructure.call(instance) }
it 'is converted on method calls' do
expect(subject).to eq(outer: { inner: 42 }, testing: [1, 2, 3])
end
it 'is converted on explicit operator call' do
expect(**instance).to eq(outer: { inner: 42 }, testing: [1, 2, 3])
end
end
end