summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorepitron <chris@ill-logic.com>2014-04-06 09:17:27 -0400
committerdblock <dblock@dblock.org>2014-04-06 09:18:54 -0400
commit75e67454e4c30e7edcdc737ac48e38f7abcf4259 (patch)
tree94a382a3e21602d669d5b04213f156085bd54bd7
parent353bf534602d464f1924e3ec2a24232a2961a3a4 (diff)
downloadhashie-75e67454e4c30e7edcdc737ac48e38f7abcf4259.tar.gz
Added Hashie::Rash.
-rw-r--r--CHANGELOG.md1
-rw-r--r--README.md35
-rw-r--r--lib/hashie.rb1
-rw-r--r--lib/hashie/rash.rb119
-rw-r--r--spec/hashie/rash_spec.rb48
5 files changed, 203 insertions, 1 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 557d6c9..0b2879e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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).
diff --git a/README.md b/README.md
index e323c8f..d71f1b5 100644
--- a/README.md
+++ b/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)
diff --git a/lib/hashie.rb b/lib/hashie.rb
index 4975d3e..018fffc 100644
--- a/lib/hashie.rb
+++ b/lib/hashie.rb
@@ -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'
diff --git a/lib/hashie/rash.rb b/lib/hashie/rash.rb
new file mode 100644
index 0000000..d59de95
--- /dev/null
+++ b/lib/hashie/rash.rb
@@ -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
diff --git a/spec/hashie/rash_spec.rb b/spec/hashie/rash_spec.rb
new file mode 100644
index 0000000..33a237f
--- /dev/null
+++ b/spec/hashie/rash_spec.rb
@@ -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