Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Mash::UnderscoreKeys Extensions #566

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -13,6 +13,7 @@ Any violations of this scheme are considered to be bugs.
### Added

* Your contribution here.
* [#566](https://github.com/hashie/hashie/pull/566): Added `Mash::UnderscoreKeys` extensions for conversion of all keys to underscore - [@arianf](https://github.com/arianf)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just missing a period.


### Changed

Expand Down
25 changes: 25 additions & 0 deletions README.md
Expand Up @@ -35,6 +35,7 @@
- [PermissiveRespondTo](#permissiverespondto)
- [SafeAssignment](#safeassignment)
- [SymbolizeKeys](#symbolizekeys)
- [UnderscoreKeys](#underscorekeys)
- [DefineAccessors](#defineaccessors)
- [Dash](#dash)
- [Potential Gotchas](#potential-gotchas)
Expand Down Expand Up @@ -783,6 +784,30 @@ 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.

### UnderscoreKeys
This extension can be mixed into a Mash to change the default behavior of converting keys to be underscore. After mixing this extension into a Mash, the Mash will convert all string keys to underscore. It can be useful to use with external source hashes, which maybe contain hyphens or CamelCase.

```ruby
class UnderscoreMash < ::Hashie::Mash
include Hashie::Extensions::Mash::UnderscoreKeys
end

mash = UnderscoreMash.new
mash.updatedAt = 'Today' #=> 'Today'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: Can we lowercase today so that it matches the below tomorrow pls.

mash.updatedAt #=> 'Today'
mash[:updated_at] #=> 'Today'
mash['updated_at'] #=> 'Today'
mash.updated_at #=> 'Today'
mash.to_hash #=> {"updated_at"=>true}
```

The other benefit is hashes that have hyphens can be accessed with methods
```ruby
mash = UnderscoreMash.new('created-at': 'tomorrow') #=> {"created_at"=>"tomorrow"}

mash.created_at #=> 'tomorrow'
```

### DefineAccessors

This extension can be mixed into a Mash so it makes it behave like `OpenStruct`. It reduces the overhead of `method_missing?` magic by lazily defining field accessors when they're requested.
Expand Down
1 change: 1 addition & 0 deletions lib/hashie.rb
Expand Up @@ -50,6 +50,7 @@ module Mash
autoload :SafeAssignment, 'hashie/extensions/mash/safe_assignment'
autoload :SymbolizeKeys, 'hashie/extensions/mash/symbolize_keys'
autoload :DefineAccessors, 'hashie/extensions/mash/define_accessors'
autoload :UnderscoreKeys, 'hashie/extensions/mash/underscore_keys'
end

module Array
Expand Down
59 changes: 59 additions & 0 deletions lib/hashie/extensions/mash/underscore_keys.rb
@@ -0,0 +1,59 @@
module Hashie
module Extensions
module Mash
# Overrides the indifferent access of a Mash to keep keys in
# underscore format.
#
# @example
# class UnderscoreMash < ::Hashie::Mash
# include Hashie::Extensions::Mash::UnderscoreKeys
# end
#
# mash = UnderscoreMash.new(symbolKey: { dataFrom: { java: true, javaScript: true } })
# mash.symbol_key.data_from.java #=> true
# mash.symbolKey.dataFrom.java_script #=> true
module UnderscoreKeys
ACRONYMS = {}

def self.included(base)
raise ArgumentError, "#{base} must descent from Hashie::Mash" unless base <= Hashie::Mash
end

private

# Makes an underscored, lowercase form from the expression in the string.
# Also converts spaces and colons to underscore
#
# @param [String, Symbol] camel_cased_word to be pocessed
# @return [String] underscored string
def _underscore(camel_cased_word)
# check if there is work to be done, if not early exit
return camel_cased_word.to_s unless /[A-Z\-: ]/.match?(camel_cased_word)

acronym_regex = ACRONYMS.empty? ? /(?=a)b/ : /#{ACRONYMS.values.join("|")}/
acronyms_underscore_regex = /(?:(?<=([A-Za-z\d]))|\b)(#{acronym_regex})(?=\b|[^a-z])/

word = camel_cased_word.gsub(acronyms_underscore_regex) do
"#{::Regexp.last_match(1) && '_'}#{::Regexp.last_match(2).downcase}"
end

word.gsub!(/([A-Z]+)(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) do
(::Regexp.last_match(1) || ::Regexp.last_match(2)) << '_'
end

word.tr!('- ', '_')
word.downcase!
word
end

# Ensures all keys are underscore formatting.
#
# @param [Object, String, Symbol] key the key to access.
# @return [Object] the value assigned to the key.
def convert_key(key)
_underscore(key.to_s) if key.is_a?(String) || key.is_a?(Symbol)
end
end
end
end
end
2 changes: 1 addition & 1 deletion spec/hashie/extensions/autoload_spec.rb
Expand Up @@ -2,7 +2,7 @@
require 'hashie'

describe Hashie::Extensions do
describe 'autloads constants' do
describe 'autoloads constants' do
it { is_expected.to be_const_defined(:MethodAccess) }
it { is_expected.to be_const_defined(:Coercion) }
it { is_expected.to be_const_defined(:DeepMerge) }
Expand Down
110 changes: 110 additions & 0 deletions spec/hashie/extensions/mash/underscrore_keys_spec.rb
@@ -0,0 +1,110 @@
require 'spec_helper'

RSpec.describe Hashie::Extensions::Mash::UnderscoreKeys, :aggregate_failures do
let(:underscore_mash) do
Class.new(Hashie::Mash) do
include Hashie::Extensions::Mash::UnderscoreKeys
end
end

it 'allows access to keys via original name' do
original = {
dataFrom: { swift: true, javaScript: true },
DataSource: { GitHub: true },
'created-at': 'today'
}

mash = underscore_mash.new(original)

expect(mash.dataFrom.swift).to be(true)
expect(mash[:dataFrom][:swift]).to be(true)
expect(mash['dataFrom']['swift']).to be(true)

expect(mash.dataFrom.javaScript).to be(true)
expect(mash[:dataFrom][:javaScript]).to be(true)
expect(mash['dataFrom']['javaScript']).to be(true)

expect(mash.DataSource.GitHub).to be(true)
expect(mash[:DataSource][:GitHub]).to be(true)
expect(mash['DataSource']['GitHub']).to be(true)

expect(mash.send('created-at')).to eq('today')
expect(mash[:'created-at']).to eq('today')
expect(mash['created-at']).to eq('today')
end

it 'allows access to underscore key names' do
original = {
dataFrom: { swift: true, javaScript: true },
DataSource: { GitHub: true },
'created-at': 'today'
}

mash = underscore_mash.new(original)

expect(mash.data_from.swift).to be(true)
expect(mash[:data_from][:swift]).to be(true)
expect(mash['data_from']['swift']).to be(true)

expect(mash.data_from.java_script).to be(true)
expect(mash[:data_from][:java_script]).to be(true)
expect(mash['data_from']['java_script']).to be(true)

expect(mash.data_source.git_hub).to be(true)
expect(mash[:data_source][:git_hub]).to be(true)
expect(mash['data_source']['git_hub']).to be(true)

expect(mash.created_at).to eq('today')
expect(mash[:'created-at']).to eq('today')
expect(mash['created-at']).to eq('today')
end

it 'allows mixing and matching of underscore and camelCase' do
original = {
dataFrom: { java: true, javaScript: true },
DataSource: { GitHub: true },
'created-at': 'today'
}

mash = underscore_mash.new(original)

expect(mash.dataFrom.java_script).to be(true)
expect(mash[:data_from][:javaScript]).to be(true)
expect(mash['dataFrom']['java_script']).to be(true)

expect(mash.DataSource.git_hub).to be(true)
expect(mash[:data_source][:GitHub]).to be(true)
expect(mash['DataSource']['git_hub']).to be(true)
end

it 'converts spaces to underscore' do
original = { 'hashie mashie': 'mashie hashie' }

mash = underscore_mash.new(original)

expect(mash.hashie_mashie).to eq('mashie hashie')
end

it 'converts spaces to underscore' do
original = { 'hashie mashie?': true }

mash = underscore_mash.new(original)

expect(mash.hashie_mashie?).to be(true)
end

context 'when setting acronyms override' do
before do
Hashie::Extensions::Mash::UnderscoreKeys::ACRONYMS[:GH] = :GitHub
end

it 'does not break apart with underscore, but does downcase' do
original = { GitHub: true }

mash = underscore_mash.new(original)

expect(mash.github).to be(true)
expect(mash.git_hub).to be(nil)
end
end
end