facetious 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,4 @@
1
- Copyright (c) 2007 Clifford Heath
1
+ Copyright (c) 2013 Clifford Heath
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
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.txt')
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
-
@@ -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
@@ -1,8 +1,8 @@
1
1
  module Facetious #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 0
5
- TINY = 1
4
+ MINOR = 1
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: facetious
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors: