refinuri 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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