Added Hashie::Rash.
This commit is contained in:
parent
353bf53460
commit
75e67454e4
|
@ -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).
|
||||
|
|
35
README.md
35
README.md
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue