diff options
author | epitron <chris@ill-logic.com> | 2014-04-06 09:17:27 -0400 |
---|---|---|
committer | dblock <dblock@dblock.org> | 2014-04-06 09:18:54 -0400 |
commit | 75e67454e4c30e7edcdc737ac48e38f7abcf4259 (patch) | |
tree | 94a382a3e21602d669d5b04213f156085bd54bd7 | |
parent | 353bf534602d464f1924e3ec2a24232a2961a3a4 (diff) | |
download | hashie-75e67454e4c30e7edcdc737ac48e38f7abcf4259.tar.gz |
Added Hashie::Rash.
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | README.md | 35 | ||||
-rw-r--r-- | lib/hashie.rb | 1 | ||||
-rw-r--r-- | lib/hashie/rash.rb | 119 | ||||
-rw-r--r-- | spec/hashie/rash_spec.rb | 48 |
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). @@ -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 |