refinuri 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Chris Kalafarski
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # Refinuri
2
+
3
+ Refinuri provides two primary functions related to querying and filtering data:
4
+
5
+ + a simple way to produce pretty, meaningful URLs, even with complex query strings
6
+ + a standardized, extensible interface to filtering metadata
7
+
8
+ ## Basics
9
+
10
+ @filters = Refinuri::Parser.parse_url('name:apple,banana,cherry;price:0-5;age:7-')
11
+ # sets up a new FilterSet based on the filter part of a URL
12
+
13
+ Product.filtered(@filters)
14
+ # automatically applies all the filters to an ActiveRecord object just as though each were it's own #where()
15
+
16
+ ## In practice
17
+
18
+ ### Pretty URLs
19
+
20
+ A 'traditional' style URL with standard query strings such as
21
+
22
+ craigslist.org/search/housing?cat=loft&minPrice=500&maxPrice=1000&cats=true&dogs=true&bedrooms=2
23
+
24
+ Will become
25
+
26
+ craigslist.org/search/housing/type:loft;price:500-1000;pets:cats,dogs;bedrooms:2/
27
+
28
+ ### Internal interface to filters
29
+
30
+ Filters are stored within a FilterSet, a Hash-like object that allows for easy access to properly-typed data and merging in changes to the set of filters. Filters and changes are passed into FilterSet as hashes.
31
+
32
+ >> base_filters = { :name => ['apple','banana','cherry'], :price => 0..5, :weight => '4..' }
33
+ >> set = FilterSet.new(base_filters)
34
+
35
+ The filters can be exposed through
36
+
37
+ >> set[:name].value # => ['apple','banana','cherry']
38
+ >> set.to_url # => 'name:apple,banana,cherry;price:0-5;weight:4+'
39
+ >> set.to_h # => { :name => ['apple','banana','cherry'], :price => 0..5, :weight => '4..' }
40
+
41
+ Changes are merged into an existing filter set, either implicitly or explicitly as CRUD functions
42
+
43
+ # implicit changes generally update existing values
44
+ >> changes = { :name => 'dewberry' }
45
+ >> set.merge!(changes)
46
+ >> set[:name].value # => ['apple','banana','cherry','dewberry']
47
+
48
+ # explicilty stated CRUD functions
49
+ >> more_changes = { :update => { :name => ['eggplant','fig] }, :destroy => { :price => nil } }
50
+ >> set.merge!(more_changes)
51
+ >> set[:name].value # => ['apple','banana','cherry','dewberry','eggplant','fig']
52
+ >> set[:price] # => nil
53
+
54
+ ## Benefits
55
+
56
+ Not only does Refinuri improve the readability and length of URLs, but behind the scenes each piece of filtering data is being handled as the appropriate data type automatically. A range of prices is an actual Range, a list of brands is an Array, etc.
57
+
58
+ This is useful both because it allows you to hand the queries off to ActiveRecord very easily, and also because the data can change very freely. A filter can change from an Integer to a Range with very little overhear need to change things on the backend.
59
+
60
+ The hope is that with most of the heavy lifting being done automatically and in a consistent way, it will be much easier to actually implement a user-facing interface that allows for these more advanced controls than are generally being offered.
61
+
62
+ ## Usage
63
+
64
+ ### Installation
65
+
66
+ $ sudo gem install refinuri
67
+
68
+ ### Common API aspects
69
+
70
+ #### URL parsing
71
+
72
+ >> Refinuri::Parser.parse_url('name:apple,banana,cherry')
73
+ => #<Refinuri::Base::FilterSet:0x1003ad300>
74
+
75
+ #### Generating URL string
76
+
77
+ >> @filter_set.to_url
78
+ => 'name:apple,banana,cherry'
79
+
80
+ #### Available data types
81
+
82
+ Currently supported datatypes include:
83
+
84
+ ##### Arrays
85
+
86
+ # in URLs an array is represented as
87
+ key:item1,item2,item3
88
+
89
+ # which are parsed as standard Ruby arrays, available through the FilterSet
90
+ @set[:key] => ['item1','item2','item3']
91
+
92
+ ##### Bounded ranges
93
+
94
+ # in URLs a range is represented as
95
+ key:10-20
96
+
97
+ # which are parsed as standard Ruby ranges, available through the FilterSet
98
+ @set[:key] => 10..20
99
+
100
+ (Support for both inclusive and exclusive ranges is not yet included)
101
+
102
+ ##### Unbounded ranges
103
+
104
+ Unbounded ranges are a non-standard datatype, which allows for a convenient way of defining only an upper- or lower-bound on a range.
105
+
106
+ Lower-bound ranges
107
+
108
+ # represented in URLs as
109
+ key:10+
110
+
111
+ # are parsed into strings in the format
112
+ @set[:key] => '10..'
113
+
114
+ # and provides a convenience method for use in an ActiveRecord #where
115
+ @set[:key].to_db => 'key >= 10'
116
+
117
+ Upper-bound ranges
118
+
119
+ # represented in URLs as
120
+ key:10-
121
+
122
+ # are parsed into strings in the format
123
+ @set[:key] => '..10'
124
+
125
+ # and provides a convenience method for use in an ActiveRecord #where
126
+ @set[:key].to_db => 'key <= 10'
127
+
128
+ #### Creating and modifying FilterSets
129
+
130
+ New filters a populated with a hash
131
+
132
+ >> Refinuri::Base::FilterSet.new({ :name => ['apple','banana','cherry'] })
133
+
134
+ Filters are modified with #merge! by passing in a hash of changes.
135
+
136
+ Changes that are not explicitly stated as a CRUD function are assumed to be UPDATEs. UPDATEs will create a new filter if it does not already exist, or will add unique values to an array, or replace a range. CREATE will either create a new filter or completely replace an existing filter.
137
+
138
+ # implicit changes, treated as UPDATE
139
+ >> @filter.merge!({ :name => ['dewberry','eggplant'] })
140
+ # @filter[:name].value => ['apple','banana','cherry','dewberry','eggplant']
141
+
142
+ # explicit UPDATE
143
+ >> @filter.merge!({ :update => { :name => ['dewberry','eggplant'] } })
144
+ # @filter[:name].value => ['apple','banana','cherry','dewberry','eggplant']
145
+
146
+ # explicit CREATE
147
+ >> @filter.merge!({ :creaet => { :name => ['dewberry','eggplant'] } })
148
+ # @filter[:name].value => ['dewberry','eggplant']
149
+
150
+ # explicit DELETE
151
+ >> @filter.merge!({ :delete => { :name => nil } })
152
+ # @filter[:name] => nil
153
+
154
+ #### Using filters with ActiveRecord
155
+
156
+ Each data type has a #to_db method which returns a value suitable for use as the argument for ActiveRecord's #where method
157
+
158
+ @set = Refinuri::Base::FilterSet.new({ :price => 10..20, :age => '10..' })
159
+
160
+ Product.where(@set[:price].to_db)
161
+ # is equivalent to
162
+ Product.where(:price => 10..20)
163
+
164
+ and
165
+ Product.where(@set[:age].to_db)
166
+ # is equivalent to
167
+ Product.where('age >= 10')
168
+
169
+ If you would like to simply query an ActiveRecord object against all the filters in a FilterSet, the #filtered() method comes in handy
170
+
171
+ Product.filtered(@set)
172
+ # is equivalent to
173
+ Product.where(:price => 10..20).where('age >= 10')
174
+
175
+ #### Helpers
176
+
177
+ # creates a link to the current view, adding 'apple' the the :name filter,
178
+ # or returning an unchanged link if 'apple' is already in the :name filter
179
+ >> filter_with_link("Apple", { :name => 'apple' })
180
+
181
+ # toggles the value 'apple' within the name filter, adding it if is not
182
+ # in the set, or removing it if it is
183
+ >> toggle_filter_with_link("Apple", { :name => 'apple' })
184
+
185
+ ### Example Application
186
+ *coming soon...*
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "refinuri"
8
+ gem.summary = %Q{Helps clean up complex URLs with filtering query string, like you may find in an online store}
9
+ gem.description = %Q{Helps clean up complex URLs with filtering query string, like you may find in an online store}
10
+ gem.email = "chris@farski.com"
11
+ gem.homepage = "http://github.com/farski/refinuri"
12
+ gem.authors = ["Chris Kalafarski"]
13
+ gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:test) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/test_*.rb'
25
+ test.verbose = true
26
+ end
27
+
28
+ begin
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |test|
31
+ test.libs << 'test'
32
+ test.pattern = 'test/**/test_*.rb'
33
+ test.verbose = true
34
+ end
35
+ rescue LoadError
36
+ task :rcov do
37
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
+ end
39
+ end
40
+
41
+ task :test => :check_dependencies
42
+
43
+ task :default => :test
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "refinuri #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.5.0
@@ -0,0 +1,110 @@
1
+ module Refinuri
2
+ module Base
3
+ class FilterSet
4
+ attr_reader :filters
5
+ def initialize(hash)
6
+ raise ArgumentError, "Argument must be a Hash", caller unless hash.is_a?(Hash)
7
+ @filters = Hash.new
8
+ modify_filters(hash)
9
+ end
10
+
11
+ def merge!(hash)
12
+ hash[:update] ||= Hash.new
13
+ hash[:update].merge!(extract_implicit_filters(hash))
14
+ hash.each { |key,value| modify_filters(value,key) }
15
+ return self
16
+ end
17
+
18
+ def [](filter_name)
19
+ @filters[filter_name]# && @filters[filter_name].value
20
+ end
21
+
22
+ def to_h
23
+ filters = Hash.new
24
+ @filters.each { |key,value| filters[key] = value.value }
25
+ return filters
26
+ end
27
+
28
+ def to_url
29
+ string_array = Array.new
30
+ @filters.each do |name,filter_obj|
31
+ string_array << "#{name}:#{filter_obj.to_s}"
32
+ end
33
+ return string_array.join(';')
34
+ end
35
+
36
+ private
37
+ def modify_filters(hash, function = :create)
38
+ case function
39
+ when :create then create_filters(hash)
40
+ when :update then update_filters(hash)
41
+ when :delete then delete_filters(hash)
42
+ end
43
+ end
44
+
45
+ def create_filters(hash)
46
+ hash.each do |key,value|
47
+ delete_filters({ key => value }) if @filters.has_key?(key)
48
+ @filters[key] = case value
49
+ when Array then Filters::Array.new(key,value)
50
+ when Range then Filters::Range.new(key,value)
51
+ when String
52
+ case value
53
+ when /\d\.\./ then Filters::UnboundedRange.new(key,value)
54
+ when /\.\.\d/ then Filters::UnboundedRange.new(key,value)
55
+ else Filters::Array.new(key,[value])
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ def update_filters(hash)
62
+ hash.each do |key,value|
63
+ if @filters.has_key?(key)
64
+ @filters[key].update(value)
65
+ else
66
+ create_filters({ key => value })
67
+ end
68
+ end
69
+ end
70
+
71
+ def delete_filters(hash)
72
+ hash.each do |key,value|
73
+ if @filters.has_key?(key)
74
+ @filters[key].delete(value)
75
+ @filters.delete(key) if @filters[key].value.nil? || @filters[key].value.empty?
76
+ end
77
+ end
78
+ end
79
+
80
+ def extract_implicit_filters(hash)
81
+ implicits = Hash.new
82
+ hash.each do |key,value|
83
+ implicits[key] = value unless [:create,:read,:update,:delete].include?(key)
84
+ end
85
+ return implicits
86
+ end
87
+ # end of private methods
88
+ end
89
+
90
+ class Filter
91
+ attr_reader :name, :value
92
+ def initialize(name,value)
93
+ @name = name
94
+ @value = value
95
+ end
96
+
97
+ def update(value)
98
+ @value = value
99
+ end
100
+
101
+ def delete(value)
102
+ @value = nil
103
+ end
104
+
105
+ def to_db
106
+ { @name => @value }
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,40 @@
1
+ module Refinuri
2
+ module Filters
3
+ class Array < Base::Filter
4
+ def update(value)
5
+ (@value << [value]).flatten!.uniq!
6
+ end
7
+
8
+ def delete(value)
9
+ [value].flatten.each { |v| @value.delete(v) }
10
+ end
11
+
12
+ def to_s
13
+ value.join(',')
14
+ end
15
+ end
16
+
17
+ class Range < Base::Filter
18
+ def to_s
19
+ Utilities.transcode_range(@value)
20
+ end
21
+ end
22
+
23
+ class UnboundedRange < Base::Filter
24
+ def to_s
25
+ Utilities.transcode_unbounded_range(@value)
26
+ end
27
+
28
+ def numeric_value
29
+ @value.sub(/\.{2,3}/, '')
30
+ end
31
+
32
+ def to_db
33
+ case @value
34
+ when /^\.\./ then "#{@name} <= #{self.numeric_value}"
35
+ when /\.\.$/ then "#{@name} >= #{self.numeric_value}"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,27 @@
1
+ module Refinuri
2
+ module Helpers
3
+ def filter_with_link(name, filters, options = nil, html_options = nil)
4
+ options = options || params
5
+ options[:filters] = @filters.merge!(filters).to_url
6
+ link_to(name,options,html_options)
7
+ end
8
+
9
+ # TODO needs to get cleaned up
10
+ def toggle_filter_with_link(name, toggle_filter, options = nil, html_options = nil)
11
+ options = options || params
12
+
13
+ key_to_toggle = toggle_filter.first[0]
14
+ if @filters.filters[key_to_toggle]
15
+ crud = case @filters.filters[key_to_toggle].value
16
+ when Array then (@filters.filters[key_to_toggle].value.include?(toggle_filter.first[1]) ? :delete : :update)
17
+ else :delete
18
+ end
19
+ else
20
+ crud = :update
21
+ end
22
+
23
+ options[:filters] = @filters.merge!({ crud => toggle_filter }).to_url
24
+ link_to(name,options,html_options)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ module Refinuri
2
+ module Parser
3
+ def self.parse_url(string)
4
+ Refinuri::Base::FilterSet.new(extract_filters(string))
5
+ end
6
+
7
+ private
8
+ def self.extract_filters(string)
9
+ string.split(';').inject(Hash.new) do |filters, str|
10
+ filters.merge(parse_filter(str))
11
+ end
12
+ end
13
+
14
+ def self.parse_filter(string)
15
+ name, value_string = string.split(':')
16
+ value = normalize_filter_value(value_string)
17
+ return { name.to_sym => value }
18
+ end
19
+
20
+ def self.normalize_filter_value(string)
21
+ case string
22
+ when /^(\d+(\.\d+)?)-(\d+(\.\d+)?)$/ then Utilities.transcode_range(string) #range
23
+ when /,/ then string.split(',') # array
24
+ when /\+$/ then Utilities.transcode_unbounded_range(string) #greater than
25
+ when /\-$/ then Utilities.transcode_unbounded_range(string) #less than
26
+ else string
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ module Refinuri
2
+ module Query
3
+ def filtered(filterset)
4
+ filtered_self = self
5
+
6
+ if filterset
7
+ filterset.filters.each do |name,filter_obj|
8
+ filtered_self = filtered_self.where(filter_obj.to_db)
9
+ end
10
+ end
11
+
12
+ return filtered_self.scoped
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ module Refinuri
2
+ module Utilities
3
+ def self.transcode_range(range_or_string)
4
+ case range_or_string
5
+ when Range then "#{range_or_string.first}-#{range_or_string.last}"
6
+ when String then instance_eval(range_or_string.sub(/-/,'..'))
7
+ end
8
+ end
9
+
10
+ def self.transcode_unbounded_range(range)
11
+ case range
12
+ when /\+$/ then range.chop.concat('..')
13
+ when /\-$/ then "..".concat(range.chop)
14
+ when /\.{2}$/ then range.chop.chop.concat('+')
15
+ when /^\.{2}/ then range.reverse.chop.chop.reverse.concat('-')
16
+ end
17
+ end
18
+ end
19
+ end
data/lib/refinuri.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'refinuri/base'
2
+ require 'refinuri/utilities'
3
+ require 'refinuri/filters'
4
+ require 'refinuri/parser'
5
+ require 'refinuri/helpers'
6
+ require 'refinuri/query'
7
+
8
+ module Refinuri
9
+ include Refinuri::Base
10
+ include Refinuri::Utilities
11
+ include Refinuri::Filters
12
+ include Refinuri::Helpers
13
+ include Refinuri::Parser
14
+ include Refinuri::Query
15
+ end
16
+
17
+ ActionView::Base.send :include, Refinuri::Helpers
18
+ ActiveRecord::Base.send :extend, Refinuri::Query
data/refinuri.gemspec ADDED
@@ -0,0 +1,71 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{refinuri}
8
+ s.version = "0.5.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Chris Kalafarski"]
12
+ s.date = %q{2010-02-07}
13
+ s.description = %q{Helps clean up complex URLs with filtering query string, like you may find in an online store}
14
+ s.email = %q{chris@farski.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.md"
18
+ ]
19
+ s.files = [
20
+ ".gitignore",
21
+ "LICENSE",
22
+ "README.md",
23
+ "Rakefile",
24
+ "VERSION",
25
+ "lib/refinuri.rb",
26
+ "lib/refinuri/base.rb",
27
+ "lib/refinuri/filters.rb",
28
+ "lib/refinuri/helpers.rb",
29
+ "lib/refinuri/parser.rb",
30
+ "lib/refinuri/query.rb",
31
+ "lib/refinuri/utilities.rb",
32
+ "refinuri.gemspec",
33
+ "test/helper.rb",
34
+ "test/test_array_filters.rb",
35
+ "test/test_helpers.rb",
36
+ "test/test_parser.rb",
37
+ "test/test_range_filters.rb",
38
+ "test/test_refinuri.rb",
39
+ "test/test_unbounded_range_filters.rb",
40
+ "test/test_utilties.rb"
41
+ ]
42
+ s.homepage = %q{http://github.com/farski/refinuri}
43
+ s.rdoc_options = ["--charset=UTF-8"]
44
+ s.require_paths = ["lib"]
45
+ s.rubygems_version = %q{1.3.5}
46
+ s.summary = %q{Helps clean up complex URLs with filtering query string, like you may find in an online store}
47
+ s.test_files = [
48
+ "test/helper.rb",
49
+ "test/test_array_filters.rb",
50
+ "test/test_helpers.rb",
51
+ "test/test_parser.rb",
52
+ "test/test_range_filters.rb",
53
+ "test/test_refinuri.rb",
54
+ "test/test_unbounded_range_filters.rb",
55
+ "test/test_utilties.rb"
56
+ ]
57
+
58
+ if s.respond_to? :specification_version then
59
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
60
+ s.specification_version = 3
61
+
62
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
63
+ s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
64
+ else
65
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
66
+ end
67
+ else
68
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
69
+ end
70
+ end
71
+
data/test/helper.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'refinuri'
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,164 @@
1
+ require 'helper'
2
+
3
+ class TestArrayFilters < Test::Unit::TestCase
4
+ context "When modifying a FilterSet that started as a string" do
5
+ setup do
6
+ @set = Refinuri::Base::FilterSet.new({:name=>"apple"})
7
+ end
8
+
9
+ context "and running UPDATE actions on the original filter" do
10
+
11
+ should "allow implicit additions as a string" do
12
+ assert_equal ["apple","banana"], @set.merge!({ :name => 'banana' }).filters[:name].value
13
+ end
14
+
15
+ should "allow implicit additions as an array" do
16
+ assert_equal ["apple","banana"], @set.merge!({ :name => ['banana'] }).filters[:name].value
17
+ assert_equal ["apple","banana","cherry"], @set.merge!({ :name => ['banana','cherry'] }).filters[:name].value
18
+ end
19
+
20
+ should "allow explicit additions as a string" do
21
+ assert_equal ["apple","banana"], @set.merge!({ :update => { :name => 'banana' } }).filters[:name].value
22
+ end
23
+
24
+ should "allow explicit additions as an array" do
25
+ assert_equal ["apple","banana"], @set.merge!({ :update => { :name => ['banana'] } }).filters[:name].value
26
+ assert_equal ["apple","banana","cherry"], @set.merge!({ :update => { :name => ['banana','cherry'] } }).filters[:name].value
27
+ end
28
+ end
29
+
30
+ context "and running CREATE actions on the original filter" do
31
+ should "replace the original filter" do
32
+ assert_equal ["banana"], @set.merge!({ :create => { :name => 'banana' } }).filters[:name].value
33
+ assert_equal ["banana"], @set.merge!({ :create => { :name => ['banana'] } }).filters[:name].value
34
+ assert_equal ["banana",'cherry'], @set.merge!({ :create => { :name => ['banana','cherry'] } }).filters[:name].value
35
+ end
36
+ end
37
+
38
+ context "and running UPDATE actions on a new filter" do
39
+ should "default to CREATE for the new filter and be able to return the original filter and the new filter" do
40
+ assert_equal ["apple"], @set.merge!({ :update => { :color => 'red' } }).filters[:name].value
41
+ assert_equal ["red"], @set.merge!({ :update => { :color => 'red' } }).filters[:color].value
42
+ assert_equal ["red"], @set.merge!({ :update => { :color => ['red'] } }).filters[:color].value
43
+ assert_equal ["red",'yellow'], @set.merge!({ :update => { :color => ['red','yellow'] } }).filters[:color].value
44
+ end
45
+ end
46
+
47
+ context "and running UPDATE actions on a new filter and the original filter" do
48
+ should "default to CREATE for the new filter and be able to return the UPDATEd filter and the new filter" do
49
+ assert_equal ["apple","banana"], @set.merge!({ :update => { :color => 'red', :name => 'banana' } }).filters[:name].value
50
+ assert_equal ["red"], @set.merge!({ :update => { :color => 'red', :name => 'banana' } }).filters[:color].value
51
+ end
52
+ end
53
+
54
+ context "and running CREATE actions on the original filter and a new filter" do
55
+ should "replace the original filter" do
56
+ assert_equal ["banana"], @set.merge!({ :create => { :name => 'banana', :color => ['red'] } }).filters[:name].value
57
+ assert_equal ["red"], @set.merge!({ :create => { :name => 'banana', :color => ['red'] } }).filters[:color].value
58
+ end
59
+ end
60
+
61
+ context "and running simultaneous explicit CREATE and UPDATE actions" do
62
+ should "be able to update the original filter and create a new one" do
63
+ assert_equal ['apple',"banana"], @set.merge!({ :update => { :name => 'banana' }, :create => { :color => 'red' } }).filters[:name].value
64
+ assert_equal ['red'], @set.merge!({ :update => { :name => 'banana' }, :create => { :color => 'red' } }).filters[:color].value
65
+ end
66
+
67
+ should "be able to replace the original filter and UPDATE (default to create) a new one" do
68
+ assert_equal ['banana'], @set.merge!({ :create => { :name => 'banana' }, :update => { :color => 'red' } }).filters[:name].value
69
+ assert_equal ['red'], @set.merge!({ :create => { :name => 'banana' }, :update => { :color => 'red' } }).filters[:color].value
70
+ end
71
+ end
72
+ end
73
+
74
+ context "When modifying a FilterSet that started as an array" do
75
+ setup do
76
+ @set = Refinuri::Base::FilterSet.new({:name=>["apple"]})
77
+ end
78
+
79
+ context "and running UPDATE actions on the original filter" do
80
+
81
+ should "allow implicit additions as a string" do
82
+ assert_equal ["apple","banana"], @set.merge!({ :name => 'banana' }).filters[:name].value
83
+ end
84
+
85
+ should "allow implicit additions as an array" do
86
+ assert_equal ["apple","banana"], @set.merge!({ :name => ['banana'] }).filters[:name].value
87
+ assert_equal ["apple","banana","cherry"], @set.merge!({ :name => ['banana','cherry'] }).filters[:name].value
88
+ end
89
+
90
+ should "allow explicit additions as a string" do
91
+ assert_equal ["apple","banana"], @set.merge!({ :update => { :name => 'banana' } }).filters[:name].value
92
+ end
93
+
94
+ should "allow explicit additions as an array" do
95
+ assert_equal ["apple","banana"], @set.merge!({ :update => { :name => ['banana'] } }).filters[:name].value
96
+ assert_equal ["apple","banana","cherry"], @set.merge!({ :update => { :name => ['banana','cherry'] } }).filters[:name].value
97
+ end
98
+ end
99
+
100
+ context "and running CREATE actions on the original filter" do
101
+ should "replace the original filter" do
102
+ assert_equal ["banana"], @set.merge!({ :create => { :name => 'banana' } }).filters[:name].value
103
+ assert_equal ["banana"], @set.merge!({ :create => { :name => ['banana'] } }).filters[:name].value
104
+ assert_equal ["banana",'cherry'], @set.merge!({ :create => { :name => ['banana','cherry'] } }).filters[:name].value
105
+ end
106
+ end
107
+
108
+ context "and running UPDATE actions on a new filter" do
109
+ should "default to CREATE for the new filter and be able to return the original filter and the new filter" do
110
+ assert_equal ["apple"], @set.merge!({ :update => { :color => 'red' } }).filters[:name].value
111
+ assert_equal ["red"], @set.merge!({ :update => { :color => 'red' } }).filters[:color].value
112
+ assert_equal ["red"], @set.merge!({ :update => { :color => ['red'] } }).filters[:color].value
113
+ assert_equal ["red",'yellow'], @set.merge!({ :update => { :color => ['red','yellow'] } }).filters[:color].value
114
+ end
115
+ end
116
+
117
+ context "and running UPDATE actions on a new filter and the original filter" do
118
+ should "default to CREATE for the new filter and be able to return the UPDATEd filter and the new filter" do
119
+ assert_equal ["apple","banana"], @set.merge!({ :update => { :color => 'red', :name => 'banana' } }).filters[:name].value
120
+ assert_equal ["red"], @set.merge!({ :update => { :color => 'red', :name => 'banana' } }).filters[:color].value
121
+ end
122
+ end
123
+
124
+ context "and running CREATE actions on the original filter and a new filter" do
125
+ should "replace the original filter" do
126
+ assert_equal ["banana"], @set.merge!({ :create => { :name => 'banana', :color => ['red'] } }).filters[:name].value
127
+ assert_equal ["red"], @set.merge!({ :create => { :name => 'banana', :color => ['red'] } }).filters[:color].value
128
+ end
129
+ end
130
+
131
+ context "and running simultaneous explicit CREATE and UPDATE actions" do
132
+ should "be able to update the original filter and create a new one" do
133
+ assert_equal ['apple',"banana"], @set.merge!({ :update => { :name => 'banana' }, :create => { :color => 'red' } }).filters[:name].value
134
+ assert_equal ['red'], @set.merge!({ :update => { :name => 'banana' }, :create => { :color => 'red' } }).filters[:color].value
135
+ end
136
+
137
+ should "be able to replace the original filter and UPDATE (default to create) a new one" do
138
+ assert_equal ['banana'], @set.merge!({ :create => { :name => 'banana' }, :update => { :color => 'red' } }).filters[:name].value
139
+ assert_equal ['red'], @set.merge!({ :create => { :name => 'banana' }, :update => { :color => 'red' } }).filters[:color].value
140
+ end
141
+ end
142
+ end
143
+
144
+ context "When DELETEing items from an Array filter" do
145
+ setup do
146
+ @set = Refinuri::Base::FilterSet.new({:name=>["apple","banana","cherry"]})
147
+ end
148
+
149
+ should "remove only the item given, when only one item is given" do
150
+ assert_equal ['banana','cherry'], @set.merge!(:delete => { :name => 'apple' }).filters[:name].value
151
+ assert_equal ['banana','cherry'], @set.merge!(:delete => { :name => ['apple'] }).filters[:name].value
152
+ end
153
+
154
+ should "remove only the items given, when only multiple items are given" do
155
+ assert_equal ['cherry'], @set.merge!(:delete => { :name => ['apple','banana'] }).filters[:name].value
156
+ end
157
+
158
+ should "purge the filter entirely when there are no items left" do
159
+ assert_equal ['banana','cherry'], @set.merge!(:delete => { :name => ['apple'] }).filters[:name].value
160
+ assert_equal ['cherry'], @set.merge!(:delete => { :name => ['apple','banana'] }).filters[:name].value
161
+ assert_nil @set.merge!(:delete => { :name => ['apple','banana','cherry'] }).filters[:name]
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,13 @@
1
+ require 'helper'
2
+
3
+ class TestHelpers < Test::Unit::TestCase
4
+ context "The filter_with_link help" do
5
+ setup do
6
+ params = { :controller => 'items', :action => 'index', :id => 1, :filters => "name:apple,banana,cherry;price:0-1;weight:4+;spots:-4" }
7
+ end
8
+
9
+ # should "return like a normal link_to " do
10
+ # assert true
11
+ # end
12
+ end
13
+ end
@@ -0,0 +1,39 @@
1
+ require 'helper'
2
+
3
+ class TestParser < Test::Unit::TestCase
4
+ context "The URL :filters parser" do
5
+ setup do
6
+ @filter_string = "name:apple,banana,cherry;price:0-1;weight:4+;spots:4-"
7
+ @filter_string_with_single_value = "name:apple"
8
+ end
9
+
10
+ should "parse the names as an array" do
11
+ assert_equal ['apple','banana','cherry'], Refinuri::Parser.parse_url(@filter_string).filters[:name].value
12
+ assert_equal ['apple'], Refinuri::Parser.parse_url(@filter_string_with_single_value).filters[:name].value
13
+ end
14
+
15
+ should "parse the price as a range" do
16
+ assert_equal 0..1, Refinuri::Parser.parse_url(@filter_string).filters[:price].value
17
+ assert_kind_of Range, Refinuri::Parser.parse_url(@filter_string).filters[:price].value
18
+ end
19
+
20
+ should "parse the weight as a string" do
21
+ assert_equal '4..', Refinuri::Parser.parse_url(@filter_string).filters[:weight].value
22
+ end
23
+
24
+ should "parse the spots as a string" do
25
+ assert_equal '..4', Refinuri::Parser.parse_url(@filter_string).filters[:spots].value
26
+ end
27
+ end
28
+
29
+ context "The object parser" do
30
+ setup do
31
+ @set = Refinuri::Base::FilterSet.new({ :name => ['apple','banana', 'cherry'], :price => 0..1, :weight => '4..' })
32
+ end
33
+
34
+ should "return a nicely formed URL string" do
35
+ # TODO this needs to get tested based on inclusion, not an exact string match
36
+ # assert_equal 'price:0-1;name:apple,banana,cherry;weight:4+', @set.to_url
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,36 @@
1
+ require 'helper'
2
+
3
+ class TestRangeFilters < Test::Unit::TestCase
4
+ context "When modifying a Range" do
5
+ setup do
6
+ @set = Refinuri::Base::FilterSet.new({:price=>0..1})
7
+ end
8
+
9
+ should "update the value with an implicit update" do
10
+ assert_equal 1..2, @set.merge!({ :price => 1..2 }).filters[:price].value
11
+ end
12
+
13
+ should "update the value with an explicit update" do
14
+ assert_equal 1..2, @set.merge!({ :update => { :price => 1..2 } }).filters[:price].value
15
+ end
16
+
17
+ should "replace the value with an explicit create" do
18
+ assert_equal 1..2, @set.merge!({ :create => { :price => 1..2 } }).filters[:price].value
19
+ end
20
+
21
+ should "purge the filter when deleted" do
22
+ assert_nil @set.merge!({ :delete => { :price => 1..2 } }).filters[:price]
23
+ assert_nil @set.merge!({ :delete => { :price => nil } }).filters[:price]
24
+ end
25
+ end
26
+
27
+ context "When outputting values for use in ActiveRecord queries" do
28
+ setup do
29
+ @range = Refinuri::Base::FilterSet.new({:price=>10..20})
30
+ end
31
+
32
+ should "output a hash suitable for #where" do
33
+ assert_equal({ :price => 10..20 }, @range[:price].to_db)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,52 @@
1
+ require 'helper'
2
+
3
+ class TestRefinuri < Test::Unit::TestCase
4
+ context "The FilterSet class" do
5
+ should "raise an error if it's initialized with anything but a Hash" do
6
+ assert_raise ArgumentError do
7
+ Refinuri::Base::FilterSet.new(String.new)
8
+ end
9
+ end
10
+
11
+ should "initialize if it's given a basic hash" do
12
+ assert Refinuri::Base::FilterSet.new({:key => 'value'})
13
+ end
14
+
15
+ context "when given an Array or a non-special String" do
16
+ should "be able to regurgitate the array as the filter value" do
17
+ assert_equal ['value'], Refinuri::Base::FilterSet.new({:key => ['value']}).filters[:key].value
18
+ end
19
+
20
+ should "return an array even when the input is a string" do
21
+ assert_equal ['value'], Refinuri::Base::FilterSet.new({:key => 'value'}).filters[:key].value
22
+ end
23
+ end
24
+
25
+ context "when given a Range" do
26
+ should "return a Range as the filter value" do
27
+ assert_kind_of Range, Refinuri::Base::FilterSet.new({:key => 0..1}).filters[:key].value
28
+ assert_kind_of Range, Refinuri::Base::FilterSet.new({:key => 0...1}).filters[:key].value
29
+ end
30
+ end
31
+
32
+ context "when asked for standard output" do
33
+ setup do
34
+ @hsh = {:name => ['apple','banana'], :price => 0..1, :weight => '4..'}
35
+ @set = Refinuri::Base::FilterSet.new(@hsh)
36
+ end
37
+
38
+ should "return a normal looking Hash" do
39
+ assert_equal @hsh, @set.to_h
40
+ end
41
+
42
+ should "return the values of individual filters upon request" do
43
+ assert_equal ['apple','banana'], @set[:name].value
44
+ assert_equal 0..1, @set[:price].value
45
+ end
46
+
47
+ should "return nil if the request filter doesn't exist" do
48
+ assert_nil @set[:nofilter]
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,15 @@
1
+ require 'helper'
2
+
3
+ class TestUnboundedRangeFilters < Test::Unit::TestCase
4
+ context "When outputting values for use in ActiveRecord queries" do
5
+ setup do
6
+ @lower = Refinuri::Base::FilterSet.new({:price=>'10..'})
7
+ @upper = Refinuri::Base::FilterSet.new({:price=>'..10'})
8
+ end
9
+
10
+ should "output a string suitable for #where" do
11
+ assert_equal "price >= 10", @lower[:price].to_db
12
+ assert_equal "price <= 10", @upper[:price].to_db
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,52 @@
1
+ require 'helper'
2
+
3
+ class TestUtilities < Test::Unit::TestCase
4
+ context "The Refinuri utilities" do
5
+ context "when dealing with Ranges or Range-like strings" do
6
+ should "transcode from a legit Range to a string with a hyphen" do
7
+ assert_equal "0-1", Refinuri::Utilities.transcode_range(0..1)
8
+ assert_equal "10.5-20.5", Refinuri::Utilities.transcode_range(10.5..20.5)
9
+ end
10
+
11
+ should "transcode from a string with a hyphen to a legit Range" do
12
+ assert_equal 0..1, Refinuri::Utilities.transcode_range('0-1')
13
+ assert_equal 10.5..20.5, Refinuri::Utilities.transcode_range('10.5-20.5')
14
+ end
15
+ end
16
+
17
+ context "when dealing with an UnboundedRange-like string" do
18
+ context "with a lower bound" do
19
+ should "transcode from X.. notation to URL-friendly X+ notation" do
20
+ assert_equal '1+', Refinuri::Utilities.transcode_unbounded_range('1..')
21
+ assert_equal '-1+', Refinuri::Utilities.transcode_unbounded_range('-1..')
22
+ assert_equal '1.0+', Refinuri::Utilities.transcode_unbounded_range('1.0..')
23
+ assert_equal '-1.0+', Refinuri::Utilities.transcode_unbounded_range('-1.0..')
24
+ # TODO check if a decimal point can/should be URL encoded
25
+ end
26
+
27
+ should "transcode from X+ notation to X.. notation" do
28
+ assert_equal '1..', Refinuri::Utilities.transcode_unbounded_range('1+')
29
+ assert_equal '1.0..', Refinuri::Utilities.transcode_unbounded_range('1.0+')
30
+ assert_equal '-1..', Refinuri::Utilities.transcode_unbounded_range('-1+')
31
+ assert_equal '-1.0..', Refinuri::Utilities.transcode_unbounded_range('-1.0+')
32
+ end
33
+ end
34
+
35
+ context "with an upper bound" do
36
+ should "transcode from ..X notation to URL-friendly X- notation" do
37
+ assert_equal '1-', Refinuri::Utilities.transcode_unbounded_range('..1')
38
+ assert_equal '1.0-', Refinuri::Utilities.transcode_unbounded_range('..1.0')
39
+ assert_equal '-1-', Refinuri::Utilities.transcode_unbounded_range('..-1')
40
+ assert_equal '-1.0-', Refinuri::Utilities.transcode_unbounded_range('..-1.0')
41
+ end
42
+
43
+ should "transcode from X- notation to ..X notation" do
44
+ assert_equal '..1', Refinuri::Utilities.transcode_unbounded_range('1-')
45
+ assert_equal '..1.0', Refinuri::Utilities.transcode_unbounded_range('1.0-')
46
+ assert_equal '..-1', Refinuri::Utilities.transcode_unbounded_range('-1-')
47
+ assert_equal '..-1.0', Refinuri::Utilities.transcode_unbounded_range('-1.0-')
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: refinuri
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Chris Kalafarski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-02-07 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: thoughtbot-shoulda
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: Helps clean up complex URLs with filtering query string, like you may find in an online store
26
+ email: chris@farski.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - LICENSE
33
+ - README.md
34
+ files:
35
+ - .gitignore
36
+ - LICENSE
37
+ - README.md
38
+ - Rakefile
39
+ - VERSION
40
+ - lib/refinuri.rb
41
+ - lib/refinuri/base.rb
42
+ - lib/refinuri/filters.rb
43
+ - lib/refinuri/helpers.rb
44
+ - lib/refinuri/parser.rb
45
+ - lib/refinuri/query.rb
46
+ - lib/refinuri/utilities.rb
47
+ - refinuri.gemspec
48
+ - test/helper.rb
49
+ - test/test_array_filters.rb
50
+ - test/test_helpers.rb
51
+ - test/test_parser.rb
52
+ - test/test_range_filters.rb
53
+ - test/test_refinuri.rb
54
+ - test/test_unbounded_range_filters.rb
55
+ - test/test_utilties.rb
56
+ has_rdoc: true
57
+ homepage: http://github.com/farski/refinuri
58
+ licenses: []
59
+
60
+ post_install_message:
61
+ rdoc_options:
62
+ - --charset=UTF-8
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ version:
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: "0"
76
+ version:
77
+ requirements: []
78
+
79
+ rubyforge_project:
80
+ rubygems_version: 1.3.5
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: Helps clean up complex URLs with filtering query string, like you may find in an online store
84
+ test_files:
85
+ - test/helper.rb
86
+ - test/test_array_filters.rb
87
+ - test/test_helpers.rb
88
+ - test/test_parser.rb
89
+ - test/test_range_filters.rb
90
+ - test/test_refinuri.rb
91
+ - test/test_unbounded_range_filters.rb
92
+ - test/test_utilties.rb