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.
@@ -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: