facetious 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/License.txt +1 -1
- data/README.md +51 -0
- data/Rakefile +13 -2
- data/lib/facetious.rb +151 -0
- data/lib/facetious/version.rb +2 -2
- metadata +1 -1
data/License.txt
CHANGED
data/README.md
CHANGED
@@ -0,0 +1,51 @@
|
|
1
|
+
# Facetious
|
2
|
+
|
3
|
+
A Faceted search extension for ActiveRecord
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'facetious'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install facetious
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
require 'facetious'
|
23
|
+
|
24
|
+
class MyRecord < ActiveRecord::Base
|
25
|
+
facet :options
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
Options are:
|
30
|
+
* :field_name
|
31
|
+
* :data_type
|
32
|
+
* :facet_title
|
33
|
+
* :where
|
34
|
+
=> "SQL where fragment"
|
35
|
+
|
36
|
+
Data Types are:
|
37
|
+
* :string
|
38
|
+
* :integer
|
39
|
+
* :date
|
40
|
+
* :strings
|
41
|
+
* :integers
|
42
|
+
* :my_custom_type => {}
|
43
|
+
|
44
|
+
## Contributing
|
45
|
+
|
46
|
+
1. Fork it
|
47
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
48
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
49
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
50
|
+
5. Create new Pull Request
|
51
|
+
|
data/Rakefile
CHANGED
@@ -5,10 +5,22 @@ require "bundler/gem_tasks"
|
|
5
5
|
Rake::RDocTask.new do |rdoc|
|
6
6
|
rdoc.rdoc_dir = 'rdoc'
|
7
7
|
rdoc.title = "facetious #{Facetious::VERSION::STRING}"
|
8
|
-
rdoc.rdoc_files.include('README.
|
8
|
+
rdoc.rdoc_files.include('README.md')
|
9
9
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
10
10
|
end
|
11
11
|
|
12
|
+
task :default => :test
|
13
|
+
task :spec => :test
|
14
|
+
|
15
|
+
require 'rake/testtask'
|
16
|
+
desc 'Test the facetious gem'
|
17
|
+
Rake::TestTask.new(:test) do |t|
|
18
|
+
t.libs << 'lib'
|
19
|
+
t.libs << 'spec'
|
20
|
+
t.pattern = 'spec/**/*_spec.rb'
|
21
|
+
t.verbose = true
|
22
|
+
end
|
23
|
+
|
12
24
|
#desc 'Generate website files'
|
13
25
|
#task :website_generate do
|
14
26
|
# sh %q{ruby script/txt2html website/index.txt > website/index.html}
|
@@ -20,4 +32,3 @@ end
|
|
20
32
|
# ENV['RSYNC_PASSWORD'] = rfconfig['password']
|
21
33
|
# sh %{rsync -aCv website #{rfconfig['username']}@rubyforge.org:/var/www/gforge-projects/polyglot}
|
22
34
|
#end
|
23
|
-
|
data/lib/facetious.rb
CHANGED
@@ -2,4 +2,155 @@
|
|
2
2
|
require 'active_record'
|
3
3
|
|
4
4
|
module Facetious #:nodoc:
|
5
|
+
Facet = Struct.new(:name, :field_name, :title, :data_type, :where) do
|
6
|
+
ValueConverter = {
|
7
|
+
string:
|
8
|
+
proc {|value| "'" + value.gsub(/'/, "''") + "'" },
|
9
|
+
integer:
|
10
|
+
proc {|value| Integer(value).to_s },
|
11
|
+
date:
|
12
|
+
proc {|value|
|
13
|
+
raise "REVISIT: Don't use DATE VALUE before implementing it"
|
14
|
+
value
|
15
|
+
},
|
16
|
+
integers:
|
17
|
+
proc {|value|
|
18
|
+
'(' + (value-['']).map{|i| Integer(i).to_s}.join(',') + ')'
|
19
|
+
},
|
20
|
+
strings:
|
21
|
+
proc {|value|
|
22
|
+
'(' + (value-['']).map{|str| sql_value(:string, str)}.join(',') + ')'
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
def sql_value data_type, value
|
27
|
+
conversion_proc = ValueConverter[data_type]
|
28
|
+
raise "facet data type #{data_type} is not recognised" unless conversion_proc
|
29
|
+
conversion_proc.call(value)
|
30
|
+
end
|
31
|
+
|
32
|
+
def condition_for value
|
33
|
+
name = field_name.to_s
|
34
|
+
# puts "condition_for facet #{name}, value #{value.inspect}"
|
35
|
+
conditions =
|
36
|
+
case value
|
37
|
+
when /\A\s*-\s*\Z/
|
38
|
+
if where =~ /\s*\(?\s*EXISTS\b/i
|
39
|
+
nil # No condition will be used, rather the EXISTS will be negated.
|
40
|
+
else
|
41
|
+
'('+name+' IS NULL OR '+name+" = '')"
|
42
|
+
end
|
43
|
+
when /\A\s*\*\s*\Z/
|
44
|
+
"#{name} != ''" # Which also means IS NOT NULL, incidentally
|
45
|
+
when /,/
|
46
|
+
'('+
|
47
|
+
value.split(/,/).map(&:strip).map do |alternate|
|
48
|
+
condition_for alternate
|
49
|
+
end.join(' OR ') +
|
50
|
+
')'
|
51
|
+
when /\A(>=?)(.*)/ # Greater than
|
52
|
+
name + " #{$1} " + sql_value(data_type, $2)
|
53
|
+
when /\A(<=?)(.*)/ # Less than
|
54
|
+
name + " #{$1} " + sql_value(data_type, $2)
|
55
|
+
when /\A~(.*)/ # Near this value
|
56
|
+
# name + ...
|
57
|
+
when /\A(.*)\.\.(.*)\z/ # Between
|
58
|
+
name + " >= " + sql_value(data_type, $1) + " AND " +
|
59
|
+
name + " <= " + sql_value(data_type, $2)
|
60
|
+
when /%/
|
61
|
+
name + " LIKE " + sql_value(data_type, value)
|
62
|
+
when Array
|
63
|
+
'(' + value.map{|v| condition_for v }*' OR ' + ')'
|
64
|
+
when Range
|
65
|
+
name + " >= " + sql_value(data_type, value.begin.to_s) + " AND " +
|
66
|
+
name + " <= " + sql_value(data_type, value.end.to_s)
|
67
|
+
else # Equals
|
68
|
+
if [:integers, :strings].include?(data_type)
|
69
|
+
name + " IN " + sql_value(data_type, value.to_s)
|
70
|
+
else
|
71
|
+
name + " = " + sql_value(data_type, value.to_s)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
if sql = where
|
76
|
+
if sql.include?('?')
|
77
|
+
if conditions
|
78
|
+
sql.gsub(/\?/, " AND "+conditions)
|
79
|
+
else # Make an EXISTS clause into NOT EXISTS (see above)
|
80
|
+
'NOT '+sql.gsub(/\?/, '')
|
81
|
+
end
|
82
|
+
else
|
83
|
+
if sql.match /{{.*}}/
|
84
|
+
name + sql.gsub(/{{.*}}/, sql_value(data_type, value))
|
85
|
+
else
|
86
|
+
sql + "WHERE\t"+conditions
|
87
|
+
end
|
88
|
+
end
|
89
|
+
else
|
90
|
+
conditions
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# These methods are made available on all ActiveRecord classes:
|
96
|
+
module BaseClassMethods
|
97
|
+
def facet *args
|
98
|
+
respond_to?(:facets) or extend FacetedClassMethods
|
99
|
+
|
100
|
+
facet_name = args.first.is_a?(Hash) ? args[-1][:name] : args.shift
|
101
|
+
unless facet_name
|
102
|
+
raise "Usage: facet :field_name, :title => 'Field Name', :data_type => :string (etc), :where => 'SQL'"
|
103
|
+
end
|
104
|
+
|
105
|
+
field_name = (args.first.is_a?(Hash) ? args[-1][:field_name] : args.shift) || facet_name
|
106
|
+
title = (args.first.is_a?(Hash) ? args[-1][:title] : args.shift) || facet_name
|
107
|
+
data_type = (args.first.is_a?(Hash) ? args[-1][:data_type] : args.shift) || :string
|
108
|
+
where = args.first.is_a?(Hash) ? args[-1][:where] : args.shift
|
109
|
+
f = self.facets[facet_name] = Facet.new(facet_name, field_name, title, data_type, where)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# These methods are made available on faceted ActiveRecord classes:
|
114
|
+
module FacetedClassMethods
|
115
|
+
def where_clause_for_facets facet_values_hash
|
116
|
+
facet_values_hash.map do |facet_name, value|
|
117
|
+
facet = facets[facet_name.to_sym] or raise "#{self.name} has no search facet #{facet_name}"
|
118
|
+
facet.condition_for value
|
119
|
+
end*" AND "
|
120
|
+
end
|
121
|
+
|
122
|
+
def find_by_facets facet_values_hash
|
123
|
+
self.class.where(where_clause_for_facets facet_values_hash)
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
# def some_faceted_class_private_method; end
|
128
|
+
|
129
|
+
def self.extended other
|
130
|
+
other.instance_exec do
|
131
|
+
include Facetious # Add the instance methods
|
132
|
+
self.class_attribute :facets
|
133
|
+
self.facets ||= {}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# These methods will be publicly visible on faceted ActiveRecord instances:
|
139
|
+
# def some_faceted_instance; end
|
140
|
+
end
|
141
|
+
|
142
|
+
class ActiveRecord::Base
|
143
|
+
extend Facetious::BaseClassMethods
|
144
|
+
end
|
145
|
+
|
146
|
+
if $0 == __FILE__
|
147
|
+
class Foo < ActiveRecord::Base #:nodoc:
|
148
|
+
facet :fred
|
149
|
+
facet :fly, :data_type => :integer
|
150
|
+
facet :nerk, :field_name => 'other_table.nerk', :where => "EXISTS(SELECT * FROM other_table WHERE foos.fk = other_table.id ?)"
|
151
|
+
end
|
152
|
+
|
153
|
+
puts Foo.where_clause_for_facets :fred => [2069, 3000..3999], :fly => 100
|
154
|
+
puts Foo.where_clause_for_facets :fred => "2069, 3000..3999", :fly => ">=100"
|
155
|
+
puts Foo.where_clause_for_facets :fly => ">=100", :nerk => '100..200'
|
5
156
|
end
|
data/lib/facetious/version.rb
CHANGED