findable_by 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/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