findable_by 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown ADDED
@@ -0,0 +1,46 @@
1
+ findable_by
2
+ =
3
+
4
+ findable_by is a simple Rails 3 plugin to improve a way how you search for records. You can pass the search form parameters directly to the find_with method in your relation, and the plugin will take care to build a correct query in a safer way.
5
+
6
+ See below how it's works.
7
+
8
+ Install:
9
+ -
10
+
11
+ In your Gemfile:
12
+
13
+ gem "findable_by"
14
+
15
+ Usage
16
+ -
17
+
18
+ class Person < ActiveRecord::Base
19
+ scope :gender, proc { |value| where(:gender => value) }
20
+
21
+ findable_by :first_name, :using => :like
22
+ findable_by :last_name, :using => UpcaseFinder
23
+ findable_by :primary_contact_id, :using => :equals
24
+ findable_by :number1_fan, :using => proc { |attribute, value| where("1=1") }
25
+ findable_by :gender, :using => proc { |attribute, value| gender(value) }
26
+ findable_by :age, :using => :between, :with => [ :min, :max ]
27
+ findable_by :birth_date, :using => :between, :with => [ :min, :max ]
28
+ end
29
+
30
+ And now you can search:
31
+
32
+ relation = Person.scoped
33
+ relation = relation.find_with(params[:search])
34
+ relation.all
35
+
36
+ TODO
37
+ -
38
+
39
+ README and some documentation.
40
+
41
+ Bugs and Feedback
42
+ -
43
+
44
+ http://github.com/thiagomoretto/findable_by/issues
45
+
46
+ MIT License. Copyright 2010.
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require "findable_by"
@@ -0,0 +1,5 @@
1
+ class BetweenFinder < Finder
2
+ def build_condition(relation, attribute, value, params)
3
+ relation.where([ "#{relation.table_name}.#{attribute} BETWEEN ? AND ?", value.first, value.last ])
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class EqualsFinder < Finder
2
+ def build_condition(relation, attribute, value, params)
3
+ relation.where(attribute => value)
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class LikeFinder < Finder
2
+ def build_condition(relation, attribute, value, params)
3
+ relation.where([ "UPPER(#{relation.table_name}.#{attribute}) LIKE '%' || UPPER(?) || '%'", value ])
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ class ProcFinder < Finder
2
+ attr_accessor :proc
3
+ def initialize(options, proc)
4
+ super(options)
5
+ self.proc = proc
6
+ end
7
+
8
+ def build_condition(relation, attribute, value, params)
9
+ relation & proc.call(attribute, value)
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module FindableBy
2
+ VERSION = "0.1.0".freeze
3
+ end
@@ -0,0 +1,92 @@
1
+ module FindableBy
2
+ module Relation
3
+ def find_with(params)
4
+ relation = self.dup
5
+ params.each do |attribute, value|
6
+ finder = self._finders[attribute]
7
+
8
+ if not finder.blank?
9
+ if not finder.options[:with].blank?
10
+ # value is replaced in this case.
11
+ value = _fb_override_value(attribute, finder.options[:with], params)
12
+ end
13
+
14
+ if _fb_is_value_ok(value)
15
+ relation = finder.send(:build_condition, relation, attribute, value, params)
16
+ end
17
+ end
18
+ end if not params.blank?
19
+
20
+ relation
21
+ end
22
+
23
+ private
24
+ def _fb_is_value_ok(value)
25
+ not value.blank? or (value.is_a?(Hash) and value.all?)
26
+ end
27
+
28
+ def _fb_override_value(attribute, with_opts, params)
29
+ case with_opts
30
+ when Symbol
31
+ raise NotImplementedError
32
+ when Array
33
+ value = with_opts.collect{ |wp| params[attribute][wp] }
34
+ end
35
+ value
36
+ end
37
+ end
38
+
39
+ module ActsAsFindable
40
+ extend ActiveSupport::Concern
41
+
42
+ included do
43
+ class_attribute :_finders
44
+ self._finders = HashWithIndifferentAccess.new{ |h,k| h[k] = [] }
45
+ end
46
+
47
+ private
48
+ module ClassMethods
49
+ def findable_by(*attr_names)
50
+ options = attr_names.extract_options!
51
+ options.merge!(:attributes => attr_names.flatten)
52
+ options[:using] = :equals if not options.key?(:using)
53
+ options[:attributes].each do |attribute|
54
+ using_what = options[:using]
55
+
56
+ if using_what.is_a?(Class)
57
+ self._finders[attribute] = using_what.new(options)
58
+ elsif using_what.is_a?(Proc)
59
+ self._finders[attribute] = ProcFinder.new(options, using_what)
60
+ else
61
+ self._finders[attribute] = Kernel.const_get("#{options[:using]}_finder".classify).new(options)
62
+ end
63
+ end
64
+ end
65
+
66
+ def inherited(base)
67
+ dup = _finders.dup
68
+ base._finders = dup.each{ |k,v| dup[k] = v.dup }
69
+ super
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ class Finder
76
+ attr_accessor :options
77
+ def initialize(options)
78
+ self.options = options
79
+ end
80
+
81
+ def build_condition(relation, attribute, value, params)
82
+ raise NotImplementedError
83
+ end
84
+ end
85
+
86
+ Dir[File.dirname(__FILE__) + "/findable_by/finders/*.rb"].sort.each do |path|
87
+ filename = File.basename(path)
88
+ require "findable_by/finders/#{filename}"
89
+ end
90
+
91
+ ActiveRecord::Base.send(:include, FindableBy::ActsAsFindable)
92
+ ActiveRecord::Relation.send(:include, FindableBy::Relation)
@@ -0,0 +1,9 @@
1
+ print "Using native SQLite3\n"
2
+ require 'logger'
3
+ ActiveRecord::Base.logger = Logger.new("debug.log")
4
+
5
+ class SqliteError < StandardError
6
+ end
7
+
8
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
9
+ load("schema/schema.rb")
@@ -0,0 +1,62 @@
1
+ require 'test_helper'
2
+
3
+ require 'support/models'
4
+
5
+ class FindableByTest < ActiveRecord::TestCase
6
+
7
+ test "should generate a SQL to find a person by first name using like" do
8
+ with_scoped(Person) do |person|
9
+ assert_not_nil person.find_with(:first_name => "OMG!").to_sql
10
+ end
11
+ end
12
+
13
+ test "should generate a SQL to find a person using a proc" do
14
+ with_scoped(Person) do |person|
15
+ assert_not_nil person.find_with(:number1_fan => "Don't care").to_sql
16
+ end
17
+ end
18
+
19
+ test "should generate a SQL to find a person using a proc calling a scope" do
20
+ with_scoped(Person) do |person|
21
+ sql = person.find_with("first_name" => "OMG!", :gender => 'M').to_sql
22
+ assert_not_nil sql =~ /first_name/ and sql =~ /gender/
23
+ end
24
+ end
25
+
26
+ test "should generate a SQL to find a person calling 'find_with' chained" do
27
+ with_scoped(Person) do |person|
28
+ assert_not_nil person.find_with(:first_name => "OMG!").find_with(:gender => 'F').to_sql
29
+ end
30
+ end
31
+
32
+ test "should generate a SQL to find a person by primary_contact_id using equals" do
33
+ with_scoped(Person) do |person|
34
+ assert_not_nil person.find_with(:primary_contact_id => 1).to_sql
35
+ end
36
+ end
37
+
38
+ test "should generate a SQL to find a person using a custom finder" do
39
+ with_scoped(Person) do |person|
40
+ assert person.find_with(:last_name => "omg").to_sql =~ /OMG/
41
+ end
42
+ end
43
+
44
+ test "should generate a SQL to find people by age range" do
45
+ with_scoped(Person) do |person|
46
+ assert person.find_with(:age => { :min => 20, :max => 45 }).to_sql =~ /BETWEEN 20 AND 45/
47
+ end
48
+ end
49
+
50
+ test "should generate a SQL to find people by birth date range" do
51
+ with_scoped(Person) do |person|
52
+ params, params[:birth_date] = {}, {}
53
+ params[:birth_date][:min], params[:birth_date][:max] = Time.now - 1.day, Time.now + 1.day
54
+ assert person.find_with(params).to_sql =~ /BETWEEN/
55
+ end
56
+ end
57
+ private
58
+ def with_scoped(model, &block)
59
+ block.call(model.send(:scoped))
60
+ end
61
+
62
+ end
@@ -0,0 +1,13 @@
1
+ ActiveRecord::Schema.define do
2
+
3
+ create_table :people, :force => true do |t|
4
+ t.string :first_name, :null => false
5
+ t.string :last_name
6
+ t.references :primary_contact
7
+ t.string :gender, :limit => 1
8
+ t.references :number1_fan
9
+ t.integer :age, :limit => 3
10
+ t.integer :lock_version, :null => false, :default => 0
11
+ end
12
+
13
+ end
@@ -0,0 +1,20 @@
1
+ class UpcaseFinder < Finder
2
+ def build_condition(relation, attribute, value, params)
3
+ relation.where(attribute => value.upcase!)
4
+ end
5
+ end
6
+
7
+ class Person < ActiveRecord::Base
8
+ scope :gender, proc { |value| where(:gender => 'F') }
9
+
10
+ findable_by :first_name, :using => :like
11
+ findable_by :last_name, :using => UpcaseFinder
12
+ findable_by :primary_contact_id, :using => :equals
13
+ findable_by :number1_fan, :using => proc { |attribute, value| where("1=1") }
14
+ findable_by :gender, :using => proc { |attribute, value| gender(value) }
15
+ findable_by :age, :using => :between, :with => [ :min, :max ]
16
+ findable_by :birth_date, :using => :between, :with => [ :min, :max ]
17
+
18
+ # TODO: findable_by :gender, :through => :gender # uses scope :gender
19
+ # TODO: findable_by :gender # uses (:using => :equals)
20
+ end
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler.setup
5
+
6
+ require 'test/unit'
7
+ require 'mocha'
8
+
9
+ require 'active_model'
10
+ require 'active_record'
11
+ require 'action_controller'
12
+
13
+ $:.unshift File.expand_path("../../lib", __FILE__)
14
+ require 'findable_by'
15
+ require 'connection/native_sqlite3/in_memory_connection'
16
+
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: findable_by
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Thiago Moretto
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-11-12 00:00:00 -03:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: Even easier to find records!
23
+ email: thiago@moretto.eng.br
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files:
29
+ - README.markdown
30
+ files:
31
+ - init.rb
32
+ - lib/findable_by.rb
33
+ - lib/findable_by/finders/between_finder.rb
34
+ - lib/findable_by/finders/equals_finder.rb
35
+ - lib/findable_by/finders/like_finder.rb
36
+ - lib/findable_by/finders/proc_finder.rb
37
+ - lib/findable_by/version.rb
38
+ - README.markdown
39
+ - test/connection/native_sqlite3/in_memory_connection.rb
40
+ - test/findable_by_test.rb
41
+ - test/schema/schema.rb
42
+ - test/support/models.rb
43
+ - test/test_helper.rb
44
+ has_rdoc: true
45
+ homepage: http://github.com/thiagomoretto/findable_by
46
+ licenses: []
47
+
48
+ post_install_message:
49
+ rdoc_options:
50
+ - --charset=UTF-8
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ hash: 3
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ hash: 3
68
+ segments:
69
+ - 0
70
+ version: "0"
71
+ requirements: []
72
+
73
+ rubyforge_project:
74
+ rubygems_version: 1.3.7
75
+ signing_key:
76
+ specification_version: 3
77
+ summary: Improving a way how you find records!
78
+ test_files:
79
+ - test/connection/native_sqlite3/in_memory_connection.rb
80
+ - test/findable_by_test.rb
81
+ - test/schema/schema.rb
82
+ - test/support/models.rb
83
+ - test/test_helper.rb