facetious 0.0.1 → 0.1.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/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