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 +21 -0
- data/LICENSE +20 -0
- data/README.md +186 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/lib/refinuri/base.rb +110 -0
- data/lib/refinuri/filters.rb +40 -0
- data/lib/refinuri/helpers.rb +27 -0
- data/lib/refinuri/parser.rb +30 -0
- data/lib/refinuri/query.rb +15 -0
- data/lib/refinuri/utilities.rb +19 -0
- data/lib/refinuri.rb +18 -0
- data/refinuri.gemspec +71 -0
- data/test/helper.rb +10 -0
- data/test/test_array_filters.rb +164 -0
- data/test/test_helpers.rb +13 -0
- data/test/test_parser.rb +39 -0
- data/test/test_range_filters.rb +36 -0
- data/test/test_refinuri.rb +52 -0
- data/test/test_unbounded_range_filters.rb +15 -0
- data/test/test_utilties.rb +52 -0
- metadata +92 -0
data/.gitignore
ADDED
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,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
|
data/test/test_parser.rb
ADDED
@@ -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
|