Added Hashie::Rash.

This commit is contained in:
epitron 2014-04-06 09:17:27 -04:00 committed by dblock
parent 353bf53460
commit 75e67454e4
5 changed files with 203 additions and 1 deletions

View File

@ -3,6 +3,7 @@
* [#134](https://github.com/intridea/hashie/pull/134): Add deep_fetch extension for nested access - [@tylerdooling](https://github.com/tylerdooling).
* Removed support for Ruby 1.8.7 - [@dblock](https://github.com/dblock).
* Ruby style now enforced with Rubocop - [@dblock](https://github.com/dblock).
* [#138](https://github.com/intridea/hashie/pull/138): Added Hashie#Rash, a hash whose keys can be regular expressions or ranges - [@epitron](https://github.com/epitron).
* [#136](https://github.com/intridea/hashie/issues/136): Removed Hashie::Extensions::Structure - [@markiz](https://github.com/markiz).
* [#107](https://github.com/intridea/hashie/pull/107): Fixed excessive value conversions, poor performance of deep merge in Hashie::Mash - [@davemitchell](https://github.com/dblock), [@dblock](https://github.com/dblock).
* [#69](https://github.com/intridea/hashie/issues/69): Fixed assigning multiple properties in Hashie::Trash - [@einzige](https://github.com/einzige).

View File

@ -258,7 +258,7 @@ Essentially, a Clash is a generalized way to provide much of the same
kind of "chainability" that libraries like Arel or Rails 2.x's named_scopes
provide.
### Example
### Example:
```ruby
c = Hashie::Clash.new
@ -277,6 +277,39 @@ c.where(abc: 'def').where(hgi: 123)
c # => { where: { abc: 'def', hgi: 123 } }
```
## Rash
Rash is a Hash whose keys can be Regexps or Ranges, which will map many input keys to a value.
A good use case for the Rash is an URL router for a web framework, where URLs need to be mapped to actions; the Rash's keys match URL patterns, while the values call the action which handles the URL.
If the Rash's value is a `proc`, the `proc` will be automatically called with the regexp's MatchData (matched groups) as a block argument.
### Example:
```ruby
# Mapping names to appropriate greetings
greeting = Hashie::Rash.new( /^Mr./ => "Hello sir!", /^Mrs./ => "Evening, madame." )
greeting["Mr. Steve Austin"] #=> "Hello sir!"
greeting["Mrs. Steve Austin"] #=> "Evening, madame."
# Mapping statements to saucy retorts
mapper = Hashie::Rash.new(
/I like (.+)/ => proc { |m| "Who DOESN'T like #{m[1]}?!" },
/Get off my (.+)!/ => proc { |m| "Forget your #{m[1]}, old man!" }
)
mapper["I like traffic lights"] #=> "Who DOESN'T like traffic lights?!"
mapper["Get off my lawn!"] #=> "Forget your lawn, old man!"
```
### Auto-optimized
**Note:** The Rash is automatically optimized every 500 accesses
(which means that it sorts the list of Regexps, putting the most frequently matched ones at the beginning).
If this value is too low or too high for your needs, you can tune it by setting: `rash.optimize_every = n`.
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md)

View File

@ -6,6 +6,7 @@ module Hashie
autoload :Mash, 'hashie/mash'
autoload :PrettyInspect, 'hashie/hash_extensions'
autoload :Trash, 'hashie/trash'
autoload :Rash, 'hashie/rash'
module Extensions
autoload :Coercion, 'hashie/extensions/coercion'

119
lib/hashie/rash.rb Normal file
View File

@ -0,0 +1,119 @@
module Hashie
#
# Rash is a Hash whose keys can be Regexps, or Ranges, which will
# match many input keys.
#
# A good use case for this class is routing URLs in a web framework.
# The Rash's keys match URL patterns, and the values specify actions
# which can handle the URL. When the Rash's value is proc, the proc
# will be automatically called with the regexp's matched groups as
# block arguments.
#
# Usage example:
#
# greeting = Hashie::Rash.new( /^Mr./ => "Hello sir!", /^Mrs./ => "Evening, madame." )
# greeting["Mr. Steve Austin"] #=> "Hello sir!"
# greeting["Mrs. Steve Austin"] #=> "Evening, madame."
#
# Note: The Rash is automatically optimized every 500 accesses
# (Regexps get sorted by how often they get matched).
# If this is too low or too high, you can tune it by
# setting: `rash.optimize_every = n`
#
class Rash
attr_accessor :optimize_every
def initialize(initial = {})
@hash = {}
@regexes = []
@ranges = []
@regex_counts = Hash.new(0)
@optimize_every = 500
@lookups = 0
update(initial)
end
def update(other)
other.each do |key, value|
self[key] = value
end
self
end
def []=(key, value)
case key
when Regexp
# key = normalize_regex(key) # this used to just do: /#{regexp}/
@regexes << key
when Range
@ranges << key
end
@hash[key] = value
end
#
# Return the first thing that matches the key.
#
def [](key)
all(key).first
end
#
# Return everything that matches the query.
#
def all(query)
return to_enum(:all, query) unless block_given?
if @hash.include? query
yield @hash[query]
return
end
case query
when String
optimize_if_necessary!
# see if any of the regexps match the string
@regexes.each do |regex|
match = regex.match(query)
if match
@regex_counts[regex] += 1
value = @hash[regex]
if value.respond_to? :call
yield value.call(match)
else
yield value
end
end
end
when Integer
# see if any of the ranges match the integer
@ranges.each do |range|
yield @hash[range] if range.include? query
end
when Regexp
# Reverse operation: `rash[/regexp/]` returns all the hash's string keys which match the regexp
@hash.each do |key, val|
yield val if key.is_a?(String) && query =~ key
end
end
end
def method_missing(*args, &block)
@hash.send(*args, &block)
end
private
def optimize_if_necessary!
if (@lookups += 1) >= @optimize_every
@regexes = @regex_counts.sort_by { |regex, count| -count }.map { |regex, count| regex }
@lookups = 0
end
end
end
end

48
spec/hashie/rash_spec.rb Normal file
View File

@ -0,0 +1,48 @@
require 'spec_helper'
describe Hashie::Rash do
attr_accessor :r
before :each do
@r = Hashie::Rash.new(
/hello/ => 'hello',
/world/ => 'world',
'other' => 'whee',
true => false,
1 => 'awesome',
1..1000 => 'rangey',
# /.+/ => "EVERYTHING"
)
end
it 'should lookup strings' do
r['other'].should eq 'whee'
r['well hello there'].should eq 'hello'
r['the world is round'].should eq 'world'
r.all('hello world').sort.should eq %w(hello world)
end
it 'should lookup regexps' do
r[/other/].should eq 'whee'
end
it 'should lookup other objects' do
r[true].should eq false
r[1].should eq 'awesome'
end
it 'should lookup numbers from ranges' do
@r[250].should eq 'rangey'
@r[999].should eq 'rangey'
@r[1000].should eq 'rangey'
@r[1001].should be_nil
end
it 'should call values which are procs' do
r = Hashie::Rash.new(/(ello)/ => proc { |m| m[1] })
r['hello'].should eq 'ello'
r['ffffff'].should be_nil
end
end