specifind 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.md +55 -0
- data/Rakefile +37 -0
- data/lib/specifind/attribute_phrase.rb +101 -0
- data/lib/specifind/comparators/mysql2.rb +120 -0
- data/lib/specifind/comparators/postgre_sql.rb +119 -0
- data/lib/specifind/comparators/sq_lite3.rb +117 -0
- data/lib/specifind/comparators.rb +20 -0
- data/lib/specifind/method_builder.rb +144 -0
- data/lib/specifind/operator.rb +41 -0
- data/lib/specifind/type.rb +38 -0
- data/lib/specifind/version.rb +3 -0
- data/lib/specifind.rb +49 -0
- data/spec/config.yml +21 -0
- data/spec/spec.sqlite3 +0 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/specifind_spec.rb +120 -0
- metadata +104 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
Specifind
|
2
|
+
=========
|
3
|
+
|
4
|
+
[![Code Climate](https://codeclimate.com/github/maxwells/specifind.png)](https://codeclimate.com/github/maxwells/specifind)
|
5
|
+
[![Build Status](https://travis-ci.org/maxwells/specifind.png?branch=master)](https://travis-ci.org/maxwells/specifind)
|
6
|
+
|
7
|
+
Specifind offers advanced ActiveRecord dynamic find\_by_* methods that include comparators (like the grails ORM). Coupled with some solid SQL injection mitigation through strict verification of type and string escaping, your find methods will be much more readable. If an object of the wrong type (based on the type of the corresponding column of the db) is passed into a finder, it will raise an exception. Ruby 1.9.2 and above are supported
|
8
|
+
|
9
|
+
#### Installation
|
10
|
+
|
11
|
+
Include specifind in your Rails Gemfile
|
12
|
+
|
13
|
+
gem 'specifind'
|
14
|
+
|
15
|
+
and
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
#### Comparators
|
20
|
+
1. in\_list (list)
|
21
|
+
2. less\_than (value)
|
22
|
+
3. less\_than\_equals (value)
|
23
|
+
4. greater\_than (value)
|
24
|
+
5. greater\_than\_equals (value)
|
25
|
+
6. like (String) - as you'd enter it as SQL (eg. 'alpha%' for items starting with alpha)
|
26
|
+
7. ilike (String) - case insensitive like
|
27
|
+
8. not\_equal (value)
|
28
|
+
9. between (lower value, upper value)
|
29
|
+
10. is\_not\_null
|
30
|
+
11. is\_null
|
31
|
+
12. equals (value) - shorthand is to leave comparator blank (eg. find\_by\_name would mean find\_by\_name\_equals)
|
32
|
+
|
33
|
+
#### Usage
|
34
|
+
|
35
|
+
Here is an example person class:
|
36
|
+
|
37
|
+
class Person < ActiveRecord::Base
|
38
|
+
attr_accessible :age, :birthday, :name
|
39
|
+
end
|
40
|
+
|
41
|
+
Specifind is automatically included in ActiveRecord once it is added to your Gemfile. Given the three attributes above, here are some examples of methods you could use to slice your data:
|
42
|
+
|
43
|
+
`Person.find_by_age_greater_than_and_birthday_between 15, DateTime.new(1985, 1, 1), DateTime.new(1987,3,6)`
|
44
|
+
|
45
|
+
`Person.find_by_name_like '%a%'`
|
46
|
+
|
47
|
+
`Person.find_by_name_is_not_null`
|
48
|
+
|
49
|
+
#### Notes
|
50
|
+
1. Currently, the ilike comparator only works with mysql (to the best of my knowledge)
|
51
|
+
2. Unlike built in ActiveRecord methods, Specifind methods will always return a list, even if it only has one instance in it
|
52
|
+
|
53
|
+
### Todos
|
54
|
+
1. Implement boolean operator logic (eg. find\_by\_age\_greater\_than\_or\_name\_like)
|
55
|
+
2. Implement ability to use the same parameter multiple times (currently find_by_age_greater_than_and_age_less_than will choke)
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'Specifind'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
desc "Run all metrics"
|
25
|
+
task :metrics do
|
26
|
+
puts "Generating Metrics with metric_fu.\nCheck your browser for output."
|
27
|
+
`metric_fu -r`
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
Bundler::GemHelper.install_tasks
|
32
|
+
|
33
|
+
desc "Run all specs"
|
34
|
+
require 'rspec/core/rake_task'
|
35
|
+
RSpec::Core::RakeTask.new(:spec)
|
36
|
+
|
37
|
+
task :default => :spec
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Specifind
|
2
|
+
|
3
|
+
##
|
4
|
+
# The AttributePhrase class is a component of the {MethodBuilder} -> {QueryBuilder} -> SQL flow. See {MethodBuilder}
|
5
|
+
# for more detail on how that works. AttributePhrase objects have search parameters specific to the attribute
|
6
|
+
# (ie. name of attribute and value(s) associated with it), sometimes a {Comparator} (specifying how to query
|
7
|
+
# those attributes in relation to the provided values), and sometimes an {Operator} (specifying how to link)
|
8
|
+
# this attribute in the query to the next attribute (ie. 'and', 'or').
|
9
|
+
#
|
10
|
+
# Example AttributePhrase:
|
11
|
+
# - name: :first_name
|
12
|
+
# - type: :varchar
|
13
|
+
# - value: 'Steve'
|
14
|
+
# - comparator: Comparator matching 'equals'
|
15
|
+
# - operator: Operator matching 'and'
|
16
|
+
#
|
17
|
+
# will be used by the QueryBuilder, as delegated by MethodBuilder to construct a sql fragment that will find
|
18
|
+
# object ids that have a first_name property equal to 'Steve' and link it with whatever AttributePhrase follows
|
19
|
+
# with an and Operator.
|
20
|
+
#
|
21
|
+
class AttributePhrase
|
22
|
+
attr_accessor :name, :value, :type, :comparator, :operator
|
23
|
+
|
24
|
+
##
|
25
|
+
# Class factory method to build AttributePhrase objects from a String (which contains an attribute name and sometimes a comparator) and an Operator object.
|
26
|
+
# If no comparator is included in the string, it is assumed to mean equals. If no operator is included in the args, then the AttributePhrase is assumed
|
27
|
+
# to be the terminal part of the query
|
28
|
+
#
|
29
|
+
# @param [Hash] args requires [:string] and [:operator]. String is of form 'attribute(_comparator)' and operator is of type Operator or nil
|
30
|
+
def self.from_string_and_operator(args)
|
31
|
+
raise 'AttributePhrase.from_string_and_operator requires a hash with [:string]' unless args[:string]
|
32
|
+
comparator_patterns = Regexp.new Specifind.comparator.patterns.map{|c| c = "(#{c})"}.join '|'
|
33
|
+
attribute_with_comparator = args[:string].split comparator_patterns
|
34
|
+
name = attribute_with_comparator[0]
|
35
|
+
comparator = attribute_with_comparator[1] || '_equals'
|
36
|
+
AttributePhrase.new :name => name, :comparator => comparator, :operator => args[:operator]
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# A new instance of AttributePhrase
|
41
|
+
#
|
42
|
+
# @param [Hash] args requires [:name], specifying the name of the class's attribute that is being searched on. Can also take [:type], which will
|
43
|
+
# set the type of the object to any of (:boolean, :varchar, :datetime, :num). Can take [:value], which sets the value(s) to be queried (per the
|
44
|
+
# terms of the comparator). Can take [:comparator] and [:operator]. These values can also be set after creation time but before query building
|
45
|
+
# in case the values are not known by the instantiating object
|
46
|
+
def initialize(args)
|
47
|
+
@name = args[:name]
|
48
|
+
@type = args[:type] || nil
|
49
|
+
@value = args[:value] || nil
|
50
|
+
@comparator = Specifind.comparator.find(args[:comparator]) || nil
|
51
|
+
@operator = Operator.find(args[:operator]) || nil
|
52
|
+
end
|
53
|
+
|
54
|
+
def build_comparator_sql
|
55
|
+
comparator.build_sql
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Constructs a constructor call for this AttributePhrase object.
|
60
|
+
#
|
61
|
+
# Used in a special case by MethodBuilder, which needs to create a block of code (dynamically) to match the method passed. As such,
|
62
|
+
# we cannot pass a pointer to the object, so this method dehydrates it with directions for recreating itself.
|
63
|
+
def to_where
|
64
|
+
|
65
|
+
".where(\"#{@name} #{@comparator.to_where(@name, @type == :boolean)}\")"
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Converts this attribute to the parameters it requires (based on it's {Comparator}) for {MethodBuilder}.
|
70
|
+
#
|
71
|
+
# Example:
|
72
|
+
# An AttributePhrase object first_name equals 'Steve' will generate it's portion of the magic method signature
|
73
|
+
# search_by_first_name_equals(first_name_val), where first_name_val is the signature
|
74
|
+
#
|
75
|
+
# An AttributePhrase object with age between 15 and 25 will generate it's portion of the magic method signature
|
76
|
+
# search_by_age_between(age_bound_one, age_bound_two), where age_bound_one, age_bound_two is it's signature
|
77
|
+
def to_signature
|
78
|
+
@comparator ? @comparator.to_signature(@name) : nil
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Converts this attribute to the parameters that the search_by method in the Searchable concern takes
|
83
|
+
# and passes to QueryBuilder
|
84
|
+
def to_params
|
85
|
+
@comparator ? @comparator.to_params(@name) : nil
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_assertion
|
89
|
+
@comparator.to_type_test @name, @type
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# rearrange any parameter thats passed to necessary values
|
94
|
+
#
|
95
|
+
# example: sql lists are formatted as (val1, val2, val3), while ruby will export (strings) as ['val1', 'val2', 'val3']
|
96
|
+
def to_rearrangement
|
97
|
+
@comparator.to_rearrangement @name, @type
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Specifind
|
2
|
+
|
3
|
+
module Comparators
|
4
|
+
# Comparator holds the logic for each type of comparator that is use in {MethodBuilder} definition.
|
5
|
+
#
|
6
|
+
# The data are held in the class definition as [identifier (String), number of parameters (int), parameter suffixes (list of String), and sql creators (Procs)].
|
7
|
+
class Mysql2
|
8
|
+
@@comparators = []
|
9
|
+
attr_accessor :pattern, :num_params, :param_suffixes, :values, :sql_proc
|
10
|
+
|
11
|
+
# Getter for comparator list
|
12
|
+
def self.comparators
|
13
|
+
@@comparators
|
14
|
+
end
|
15
|
+
|
16
|
+
# Setter for comparator list
|
17
|
+
def self.comparators=(c)
|
18
|
+
@@comparators = c
|
19
|
+
end
|
20
|
+
|
21
|
+
# list of comparator's patterns
|
22
|
+
def self.patterns
|
23
|
+
a = []
|
24
|
+
comparators.each{|c| a << c.pattern}
|
25
|
+
a
|
26
|
+
end
|
27
|
+
|
28
|
+
# find and return a (new) comparator by it's identifying string
|
29
|
+
def self.find(s)
|
30
|
+
comparators.each{|c| return c.clone if c.pattern == s}
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
|
34
|
+
# From the data listed in the Comparator class definition, Comparator.generate_comparators constructs each type of Comparators
|
35
|
+
# and adds it to Comparator.comparators
|
36
|
+
def self.generate_comparators
|
37
|
+
self.comparators_data.each{|c| c = Mysql2.new :pattern => c[0], :num_params => c[1], :param_suffixes => c[2], :sql_proc => c[3] }
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.comparators_data
|
41
|
+
[
|
42
|
+
['_in_list', 1, %w(_list), Proc.new{|v| "in (#{v[0]})"}],
|
43
|
+
['_less_than_equals', 1, %w(_val), Proc.new{|v| "<= #{v[0]}"}],
|
44
|
+
['_less_than', 1, %w(_val), Proc.new{|v| "< #{v[0]}"}],
|
45
|
+
['_greater_than_equals', 1, %w(_val), Proc.new{|v| ">= #{v[0]}"}],
|
46
|
+
['_greater_than', 1, %w(_val), Proc.new{|v| "> #{v[0]}"}],
|
47
|
+
['_like', 1, %w(_val), Proc.new{|v| "like #{v[0]} collate #{@@encoding}_bin"}],
|
48
|
+
['_ilike', 1, %w(_val), Proc.new{|v| "like #{v[0]}"}],
|
49
|
+
['_not_equal', 1, %w(_val), Proc.new{|v| "!= #{v[0]}"}],
|
50
|
+
['_between', 2, %w(_bound_one _bound_two), Proc.new{|v| "between #{v[0]} and #{v[1]}"}],
|
51
|
+
['_is_not_null', 0, [], Proc.new{|v| "is not null"}],
|
52
|
+
['_is_null', 0, [], Proc.new{|v| "is null"}],
|
53
|
+
['_equals', 1, %w(_val), Proc.new{|v| "= #{v[0]}"}]
|
54
|
+
]
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize(args)
|
58
|
+
@pattern = args[:pattern]
|
59
|
+
@num_params = args[:num_params]
|
60
|
+
@param_suffixes = @num_params > 0 ? args[:param_suffixes] : []
|
61
|
+
@sql_proc = args[:sql_proc]
|
62
|
+
@@encoding = ActiveRecord::Base.connection.instance_variable_get('@config')[:encoding]
|
63
|
+
Mysql2.comparators << self
|
64
|
+
end
|
65
|
+
|
66
|
+
# Creates the values list for this comparator so that when it's build_sql method is called,
|
67
|
+
# the sql building proc will know how to build the appropriate where clause
|
68
|
+
def set_values(property_name, attribute_hash)
|
69
|
+
@values = []
|
70
|
+
param_suffixes.each{|s| @values << attribute_hash["#{property_name}#{s}".to_sym]} if num_params > 0
|
71
|
+
end
|
72
|
+
|
73
|
+
# builds sql (through @sql_proc) for values that corresponds with this type of comparator
|
74
|
+
def to_where(name, remove_quotes = false)
|
75
|
+
#raise 'values for comparator are not defined' if !values
|
76
|
+
response = @sql_proc.call param_suffixes.map{|p| "\#{#{name}#{p}}" }
|
77
|
+
response.delete! "'" if remove_quotes
|
78
|
+
response
|
79
|
+
end
|
80
|
+
|
81
|
+
# builds signature (the parameter definitions for a method) based on the type of suffixes
|
82
|
+
# that are required for this type of Comparator. See {AttributePhrase#to_signature} for further detail
|
83
|
+
def to_signature(name)
|
84
|
+
return '' if num_params == 0
|
85
|
+
param_suffixes.map{|p| "#{name}#{p}" }.join(',')
|
86
|
+
end
|
87
|
+
|
88
|
+
# builds hash component (the parameters for the search_by method) based on the type of suffixes
|
89
|
+
# that are required for this type of Comparator. See {AttributePhrase#to_params} for further detail
|
90
|
+
def to_params(name)
|
91
|
+
return '' if num_params == 0
|
92
|
+
param_suffixes.map { |p| ":#{name}#{p} => #{name}#{p}" }.join(',')
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_type_test(name, type)
|
96
|
+
out = ""
|
97
|
+
# assert that each variable associated with this comparator is of type passed.
|
98
|
+
# if its not a list
|
99
|
+
if @pattern != '_in_list'
|
100
|
+
param_suffixes.each do |p|
|
101
|
+
out += "#{name}#{p} = Type.assert_#{type}(#{name}#{p})\n"
|
102
|
+
end
|
103
|
+
else # is list - will only have one parameter, which is a list
|
104
|
+
out += "#{name}#{param_suffixes[0]}.each do |v|
|
105
|
+
v = Type.assert_#{type}(v)
|
106
|
+
end"
|
107
|
+
end
|
108
|
+
out
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_rearrangement(name, type)
|
112
|
+
out = ''
|
113
|
+
if @pattern == '_in_list'
|
114
|
+
out += "#{name}#{param_suffixes[0]} = #{name}#{param_suffixes[0]}.map{|el| '\"'+el+'\"'}.join ','"
|
115
|
+
end
|
116
|
+
out
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module Specifind
|
2
|
+
|
3
|
+
module Comparators
|
4
|
+
# Comparator holds the logic for each type of comparator that is use in {MethodBuilder} definition.
|
5
|
+
#
|
6
|
+
# The data are held in the class definition as [identifier (String), number of parameters (int), parameter suffixes (list of String), and sql creators (Procs)].
|
7
|
+
class PostgreSQL
|
8
|
+
@@comparators = []
|
9
|
+
attr_accessor :pattern, :num_params, :param_suffixes, :values, :sql_proc
|
10
|
+
|
11
|
+
# Getter for comparator list
|
12
|
+
def self.comparators
|
13
|
+
@@comparators
|
14
|
+
end
|
15
|
+
|
16
|
+
# Setter for comparator list
|
17
|
+
def self.comparators=(c)
|
18
|
+
@@comparators = c
|
19
|
+
end
|
20
|
+
|
21
|
+
# list of comparator's patterns
|
22
|
+
def self.patterns
|
23
|
+
a = []
|
24
|
+
comparators.each{|c| a << c.pattern}
|
25
|
+
a
|
26
|
+
end
|
27
|
+
|
28
|
+
# find and return a (new) comparator by it's identifying string
|
29
|
+
def self.find(s)
|
30
|
+
comparators.each{|c| return c.clone if c.pattern == s}
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
|
34
|
+
# From the data listed in the Comparator class definition, Comparator.generate_comparators constructs each type of Comparators
|
35
|
+
# and adds it to Comparator.comparators
|
36
|
+
def self.generate_comparators
|
37
|
+
self.comparators_data.each{|c| c = PostgreSQL.new :pattern => c[0], :num_params => c[1], :param_suffixes => c[2], :sql_proc => c[3] }
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.comparators_data
|
41
|
+
[
|
42
|
+
['_in_list', 1, %w(_list), Proc.new{|v| "in (#{v[0]})"}],
|
43
|
+
['_less_than_equals', 1, %w(_val), Proc.new{|v| "<= #{v[0]}"}],
|
44
|
+
['_less_than', 1, %w(_val), Proc.new{|v| "< #{v[0]}"}],
|
45
|
+
['_greater_than_equals', 1, %w(_val), Proc.new{|v| ">= #{v[0]}"}],
|
46
|
+
['_greater_than', 1, %w(_val), Proc.new{|v| "> #{v[0]}"}],
|
47
|
+
['_like', 1, %w(_val), Proc.new{|v| "like #{v[0]}"}],
|
48
|
+
['_ilike', 1, %w(_val), Proc.new{|v| "ilike #{v[0]}"}],
|
49
|
+
['_not_equal', 1, %w(_val), Proc.new{|v| "!= #{v[0]}"}],
|
50
|
+
['_between', 2, %w(_bound_one _bound_two), Proc.new{|v| "between #{v[0]} and #{v[1]}"}],
|
51
|
+
['_is_not_null', 0, [], Proc.new{|v| "is not null"}],
|
52
|
+
['_is_null', 0, [], Proc.new{|v| "is null"}],
|
53
|
+
['_equals', 1, %w(_val), Proc.new{|v| "= #{v[0]}"}]
|
54
|
+
]
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize(args)
|
58
|
+
@pattern = args[:pattern]
|
59
|
+
@num_params = args[:num_params]
|
60
|
+
@param_suffixes = @num_params > 0 ? args[:param_suffixes] : []
|
61
|
+
@sql_proc = args[:sql_proc]
|
62
|
+
PostgreSQL.comparators << self
|
63
|
+
end
|
64
|
+
|
65
|
+
# Creates the values list for this comparator so that when it's build_sql method is called,
|
66
|
+
# the sql building proc will know how to build the appropriate where clause
|
67
|
+
def set_values(property_name, attribute_hash)
|
68
|
+
@values = []
|
69
|
+
param_suffixes.each{|s| @values << attribute_hash["#{property_name}#{s}".to_sym]} if num_params > 0
|
70
|
+
end
|
71
|
+
|
72
|
+
# builds sql (through @sql_proc) for values that corresponds with this type of comparator
|
73
|
+
def to_where(name, remove_quotes = false)
|
74
|
+
#raise 'values for comparator are not defined' if !values
|
75
|
+
response = @sql_proc.call param_suffixes.map{|p| "\#{#{name}#{p}}" }
|
76
|
+
response.delete! "'" if remove_quotes
|
77
|
+
response
|
78
|
+
end
|
79
|
+
|
80
|
+
# builds signature (the parameter definitions for a method) based on the type of suffixes
|
81
|
+
# that are required for this type of Comparator. See {AttributePhrase#to_signature} for further detail
|
82
|
+
def to_signature(name)
|
83
|
+
return '' if num_params == 0
|
84
|
+
param_suffixes.map{|p| "#{name}#{p}" }.join(',')
|
85
|
+
end
|
86
|
+
|
87
|
+
# builds hash component (the parameters for the search_by method) based on the type of suffixes
|
88
|
+
# that are required for this type of Comparator. See {AttributePhrase#to_params} for further detail
|
89
|
+
def to_params(name)
|
90
|
+
return '' if num_params == 0
|
91
|
+
param_suffixes.map { |p| ":#{name}#{p} => #{name}#{p}" }.join(',')
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_type_test(name, type)
|
95
|
+
out = ""
|
96
|
+
# assert that each variable associated with this comparator is of type passed.
|
97
|
+
# if its not a list
|
98
|
+
if @pattern != '_in_list'
|
99
|
+
param_suffixes.each do |p|
|
100
|
+
out += "#{name}#{p} = Type.assert_#{type}(#{name}#{p})\n"
|
101
|
+
end
|
102
|
+
else # is list - will only have one parameter, which is a list
|
103
|
+
out += "#{name}#{param_suffixes[0]}.each do |v|
|
104
|
+
v = Type.assert_#{type}(v)
|
105
|
+
end"
|
106
|
+
end
|
107
|
+
out
|
108
|
+
end
|
109
|
+
|
110
|
+
def to_rearrangement(name, type)
|
111
|
+
out = ''
|
112
|
+
if @pattern == '_in_list'
|
113
|
+
out += "#{name}#{param_suffixes[0]} = #{name}#{param_suffixes[0]}.map{|el| \"'\"+el+\"'\"}.join ','"
|
114
|
+
end
|
115
|
+
out
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Specifind
|
2
|
+
|
3
|
+
module Comparators
|
4
|
+
# Comparator holds the logic for each type of comparator that is use in {MethodBuilder} definition.
|
5
|
+
#
|
6
|
+
# The data are held in the class definition as [identifier (String), number of parameters (int), parameter suffixes (list of String), and sql creators (Procs)].
|
7
|
+
class SQLite3
|
8
|
+
@@comparators = []
|
9
|
+
attr_accessor :pattern, :num_params, :param_suffixes, :values, :sql_proc
|
10
|
+
|
11
|
+
# Getter for comparator list
|
12
|
+
def self.comparators
|
13
|
+
@@comparators
|
14
|
+
end
|
15
|
+
|
16
|
+
# Setter for comparator list
|
17
|
+
def self.comparators=(c)
|
18
|
+
@@comparators = c
|
19
|
+
end
|
20
|
+
|
21
|
+
# list of comparator's patterns
|
22
|
+
def self.patterns
|
23
|
+
a = []
|
24
|
+
comparators.each{|c| a << c.pattern}
|
25
|
+
a
|
26
|
+
end
|
27
|
+
|
28
|
+
# find and return a (new) comparator by it's identifying string
|
29
|
+
def self.find(s)
|
30
|
+
comparators.each{|c| return c.clone if c.pattern == s}
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
|
34
|
+
# From the data listed in the Comparator class definition, Comparator.generate_comparators constructs each type of Comparators
|
35
|
+
# and adds it to Comparator.comparators
|
36
|
+
def self.generate_comparators
|
37
|
+
self.comparators_data.each{|c| c = SQLite3.new :pattern => c[0], :num_params => c[1], :param_suffixes => c[2], :sql_proc => c[3] }
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.comparators_data
|
41
|
+
[
|
42
|
+
['_in_list', 1, %w(_list), Proc.new{|v| "in (#{v[0]})"}],
|
43
|
+
['_less_than_equals', 1, %w(_val), Proc.new{|v| "<= #{v[0]}"}],
|
44
|
+
['_less_than', 1, %w(_val), Proc.new{|v| "< #{v[0]}"}],
|
45
|
+
['_greater_than_equals', 1, %w(_val), Proc.new{|v| ">= #{v[0]}"}],
|
46
|
+
['_greater_than', 1, %w(_val), Proc.new{|v| "> #{v[0]}"}],
|
47
|
+
['_like', 1, %w(_val), Proc.new{|v| "like #{v[0]}"}],
|
48
|
+
['_ilike', 1, %w(_val), Proc.new{|v| "like #{v[0]}"}],
|
49
|
+
['_not_equal', 1, %w(_val), Proc.new{|v| "!= #{v[0]}"}],
|
50
|
+
['_between', 2, %w(_bound_one _bound_two), Proc.new{|v| "between #{v[0]} and #{v[1]}"}],
|
51
|
+
['_is_not_null', 0, [], Proc.new{|v| "is not null"}],
|
52
|
+
['_is_null', 0, [], Proc.new{|v| "is null"}],
|
53
|
+
['_equals', 1, %w(_val), Proc.new{|v| "= #{v[0]}"}]
|
54
|
+
]
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize(args)
|
58
|
+
@pattern = args[:pattern]
|
59
|
+
@num_params = args[:num_params]
|
60
|
+
@param_suffixes = @num_params > 0 ? args[:param_suffixes] : []
|
61
|
+
@sql_proc = args[:sql_proc]
|
62
|
+
SQLite3.comparators << self
|
63
|
+
end
|
64
|
+
|
65
|
+
# Creates the values list for this comparator so that when it's build_sql method is called,
|
66
|
+
# the sql building proc will know how to build the appropriate where clause
|
67
|
+
def set_values(property_name, attribute_hash)
|
68
|
+
@values = []
|
69
|
+
param_suffixes.each{|s| @values << attribute_hash["#{property_name}#{s}".to_sym]} if num_params > 0
|
70
|
+
end
|
71
|
+
|
72
|
+
# builds sql (through @sql_proc) for values that corresponds with this type of comparator
|
73
|
+
def to_where(name, remove_quotes = false)
|
74
|
+
#raise 'values for comparator are not defined' if !values
|
75
|
+
response = @sql_proc.call param_suffixes.map{|p| "\#{#{name}#{p}}" }
|
76
|
+
response.delete! "'" if remove_quotes
|
77
|
+
response
|
78
|
+
end
|
79
|
+
|
80
|
+
# builds signature (the parameter definitions for a method) based on the type of suffixes
|
81
|
+
# that are required for this type of Comparator. See {AttributePhrase#to_signature} for further detail
|
82
|
+
def to_signature(name)
|
83
|
+
param_suffixes.map{|p| "#{name}#{p}" }.join(',')
|
84
|
+
end
|
85
|
+
|
86
|
+
# builds hash component (the parameters for the search_by method) based on the type of suffixes
|
87
|
+
# that are required for this type of Comparator. See {AttributePhrase#to_params} for further detail
|
88
|
+
def to_params(name)
|
89
|
+
param_suffixes.map { |p| ":#{name}#{p} => #{name}#{p}" }.join(',')
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_type_test(name, type)
|
93
|
+
out = ""
|
94
|
+
# assert that each variable associated with this comparator is of type passed.
|
95
|
+
# if its not a list
|
96
|
+
if @pattern != '_in_list'
|
97
|
+
param_suffixes.each do |p|
|
98
|
+
out += "#{name}#{p} = Type.assert_#{type}(#{name}#{p})\n"
|
99
|
+
end
|
100
|
+
else # is list - will only have one parameter, which is a list
|
101
|
+
out += "#{name}#{param_suffixes[0]}.each do |v|
|
102
|
+
v = Type.assert_#{type}(v)
|
103
|
+
end"
|
104
|
+
end
|
105
|
+
out
|
106
|
+
end
|
107
|
+
|
108
|
+
def to_rearrangement(name, type)
|
109
|
+
out = ''
|
110
|
+
if @pattern == '_in_list'
|
111
|
+
out += "#{name}#{param_suffixes[0]} = #{name}#{param_suffixes[0]}.map{|el| '\"'+el+'\"'}.join ','"
|
112
|
+
end
|
113
|
+
out
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Specifind
|
2
|
+
|
3
|
+
# Comparator holds the logic for each type of comparator that is use in {MethodBuilder} definition.
|
4
|
+
#
|
5
|
+
# The data are held in the class definition as [identifier (String), number of parameters (int), parameter suffixes (list of String), and sql creators (Procs)].
|
6
|
+
module Comparators
|
7
|
+
extend ActiveSupport::Autoload
|
8
|
+
|
9
|
+
autoload :SQLite3
|
10
|
+
autoload :Mysql2
|
11
|
+
autoload :PostgreSQL
|
12
|
+
|
13
|
+
def self.generate_comparators
|
14
|
+
SQLite3.generate_comparators
|
15
|
+
Mysql2.generate_comparators
|
16
|
+
PostgreSQL.generate_comparators
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
|
2
|
+
module Specifind
|
3
|
+
|
4
|
+
class MethodBuilder
|
5
|
+
@matchers = []
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_reader :matchers
|
9
|
+
|
10
|
+
def match(model, name)
|
11
|
+
klass = matchers.find { |k| name =~ k.pattern }
|
12
|
+
klass.new(model, name) if klass
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse_to_attributes(s)
|
16
|
+
operator_regexp = Regexp.new Operator.patterns.map{|s| s = "(#{s})"}.join '|'
|
17
|
+
hybrid_list = s.split(operator_regexp)
|
18
|
+
attribute_list = []
|
19
|
+
# it is now split into [field_name (+comparator)] + operator + [field_name (+comparator)] ...
|
20
|
+
|
21
|
+
attribute_list << AttributePhrase.from_string_and_operator(:string => hybrid_list.shift, :operator => hybrid_list.shift) while hybrid_list.length > 0
|
22
|
+
attribute_list
|
23
|
+
end
|
24
|
+
|
25
|
+
def pattern
|
26
|
+
/^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/
|
27
|
+
end
|
28
|
+
|
29
|
+
def prefix
|
30
|
+
raise NotImplementedError
|
31
|
+
end
|
32
|
+
|
33
|
+
def suffix
|
34
|
+
''
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :model, :name, :attribute_names, :attributes
|
39
|
+
|
40
|
+
def initialize(model, name)
|
41
|
+
@model = model
|
42
|
+
@name = name.to_s
|
43
|
+
match = @name.match(self.class.pattern)
|
44
|
+
@attributes = MethodBuilder.parse_to_attributes match[1]
|
45
|
+
@attribute_names = @attributes.map{ |a| a.name }
|
46
|
+
end
|
47
|
+
|
48
|
+
def merge_attribute_types(columns)
|
49
|
+
@attributes.each do |a|
|
50
|
+
columns.each do |c|
|
51
|
+
if a.name == c[:name]
|
52
|
+
a.type = c[:type]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def valid?
|
59
|
+
attribute_names.all? { |name| model.columns_hash[name] || model.reflect_on_aggregation(name.to_sym) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def define
|
63
|
+
assertions = ""
|
64
|
+
@attributes.each do |a|
|
65
|
+
assertions += a.to_assertion
|
66
|
+
end
|
67
|
+
rearrangements = ""
|
68
|
+
@attributes.each do |a|
|
69
|
+
rearrangements += a.to_rearrangement
|
70
|
+
end
|
71
|
+
model.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
72
|
+
def self.#{name}(#{signature})
|
73
|
+
#{assertions}
|
74
|
+
#{rearrangements}
|
75
|
+
#{body}
|
76
|
+
end
|
77
|
+
CODE
|
78
|
+
end
|
79
|
+
|
80
|
+
def body
|
81
|
+
raise NotImplementedError
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
module Finder
|
86
|
+
# Extended in activerecord-deprecated_finders
|
87
|
+
def body
|
88
|
+
result
|
89
|
+
end
|
90
|
+
|
91
|
+
# Extended in activerecord-deprecated_finders
|
92
|
+
def result
|
93
|
+
s = "#{model.name}"
|
94
|
+
@attributes.each do |a|
|
95
|
+
s += "#{a.to_where}"
|
96
|
+
end
|
97
|
+
s
|
98
|
+
end
|
99
|
+
|
100
|
+
# Extended in activerecord-deprecated_finders
|
101
|
+
def signature
|
102
|
+
attributes.map{|a| a.to_signature }.delete_if{|s| s == nil}.join(', ')
|
103
|
+
end
|
104
|
+
|
105
|
+
def attributes_hash
|
106
|
+
"{" + attribute_names.map { |name| ":#{name} => #{name}" }.join(',') + "}"
|
107
|
+
end
|
108
|
+
|
109
|
+
def finder
|
110
|
+
raise NotImplementedError
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class FindBy < MethodBuilder
|
115
|
+
MethodBuilder.matchers << self
|
116
|
+
include Finder
|
117
|
+
|
118
|
+
def self.prefix
|
119
|
+
"find_by"
|
120
|
+
end
|
121
|
+
|
122
|
+
def finder
|
123
|
+
"find_by"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class FindByBang < MethodBuilder
|
128
|
+
MethodBuilder.matchers << self
|
129
|
+
include Finder
|
130
|
+
|
131
|
+
def self.prefix
|
132
|
+
"find_by"
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.suffix
|
136
|
+
"!"
|
137
|
+
end
|
138
|
+
|
139
|
+
def finder
|
140
|
+
"find_by!"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Specifind
|
2
|
+
|
3
|
+
# Operator holds the logic for each type of operator that is use in {MethodBuilder} definition.
|
4
|
+
#
|
5
|
+
# The data are held in the class definition as a list of identifying Strings.
|
6
|
+
class Operator
|
7
|
+
@@operators_data = %w(_and_)
|
8
|
+
@@operators = []
|
9
|
+
attr_accessor :pattern
|
10
|
+
|
11
|
+
# list of operator objects
|
12
|
+
def self.operators
|
13
|
+
@@operators
|
14
|
+
end
|
15
|
+
|
16
|
+
# set list of operator objects
|
17
|
+
def self.operators=(o)
|
18
|
+
@@operators = o
|
19
|
+
end
|
20
|
+
|
21
|
+
# list of operator objects' patterns
|
22
|
+
def self.patterns
|
23
|
+
a = []
|
24
|
+
@@operators.each{|c| a << c.pattern}
|
25
|
+
a
|
26
|
+
end
|
27
|
+
|
28
|
+
# find an operator object based on identifying string
|
29
|
+
def self.find(s)
|
30
|
+
@@operators.each{|o| return o if o.pattern == s}
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(args)
|
35
|
+
@pattern = args[:pattern]
|
36
|
+
Operator.operators << self
|
37
|
+
end
|
38
|
+
|
39
|
+
@@operators_data.each{|o| o = Operator.new pattern: o}
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Specifind
|
2
|
+
class Type
|
3
|
+
|
4
|
+
def self.data
|
5
|
+
[
|
6
|
+
{ :type => 'binary', :allowed_classes => [String] },
|
7
|
+
{ :type => 'boolean', :allowed_classes => [TrueClass, FalseClass] },
|
8
|
+
{ :type => 'date', :allowed_classes => [Date, DateTime] },
|
9
|
+
{ :type => 'decimal', :allowed_classes => [Float, Fixnum] },
|
10
|
+
{ :type => 'float', :allowed_classes => [Float] },
|
11
|
+
{ :type => 'integer', :allowed_classes => [Fixnum] },
|
12
|
+
{ :type => 'string', :allowed_classes => [String] },
|
13
|
+
{ :type => 'text', :allowed_classes => [String] },
|
14
|
+
{ :type => 'time', :allowed_classes => [Time] },
|
15
|
+
{ :type => 'timestamp', :allowed_classes => [Date, DateTime] }
|
16
|
+
]
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.generate_methods
|
20
|
+
self.data.each do |h|
|
21
|
+
names = h[:allowed_classes].map{|c| c.name}.join ' or '
|
22
|
+
types = h[:allowed_classes].map{|c| "val.kind_of? #{c.name}"}.join ' or '
|
23
|
+
class_eval <<-CODE, __FILE__, __LINE__ + 1
|
24
|
+
def self.assert_#{h[:type]}(val)
|
25
|
+
raise "Dynamic finder required #{names}, \#{val.class} provided" unless #{types}
|
26
|
+
escape_values(val)
|
27
|
+
end
|
28
|
+
CODE
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.escape_values(val)
|
33
|
+
c = ActiveRecord::Base.connection
|
34
|
+
c.quote(val)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
data/lib/specifind.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
#require 'active_record'
|
2
|
+
|
3
|
+
module Specifind
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
extend ActiveSupport::Autoload
|
6
|
+
|
7
|
+
autoload :MethodBuilder
|
8
|
+
autoload :Comparators
|
9
|
+
autoload :Operator
|
10
|
+
autoload :AttributePhrase
|
11
|
+
autoload :Type
|
12
|
+
|
13
|
+
included do
|
14
|
+
Specifind::Comparators.generate_comparators
|
15
|
+
Type.generate_methods
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
|
20
|
+
def method_missing(name, *arguments, &block)
|
21
|
+
unless Specifind.comparator
|
22
|
+
active_record_adapter = ActiveRecord::Base.connection.class.name.split(':').last.gsub /Adapter/, ''
|
23
|
+
comparator = "Specifind::Comparators::#{active_record_adapter}".constantize
|
24
|
+
Specifind.comparator = "Specifind::Comparators::#{active_record_adapter}".constantize
|
25
|
+
end
|
26
|
+
|
27
|
+
match = MethodBuilder.match(self, name)
|
28
|
+
if match && match.valid?
|
29
|
+
types = self.columns.map{|c| {:name => c.name, :type => c.type}}
|
30
|
+
match.merge_attribute_types types
|
31
|
+
match.define
|
32
|
+
send(name, *arguments, &block)
|
33
|
+
else
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.comparator=(val)
|
40
|
+
@comparator = val
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.comparator
|
44
|
+
@comparator
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
ActiveRecord::Base.send(:include, Specifind)
|
data/spec/config.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
test:
|
2
|
+
adapter: mysql2
|
3
|
+
database: specifind_test
|
4
|
+
username: mlahey
|
5
|
+
password: karma559
|
6
|
+
encoding: utf8
|
7
|
+
|
8
|
+
test_sqlite:
|
9
|
+
adapter: sqlite3
|
10
|
+
database: spec/spec.sqlite3
|
11
|
+
|
12
|
+
test_mysql:
|
13
|
+
adapter: mysql2
|
14
|
+
database: specifind_test
|
15
|
+
username: root
|
16
|
+
encoding: utf8
|
17
|
+
|
18
|
+
test_postgres:
|
19
|
+
adapter: postgresql
|
20
|
+
database: specifind_test
|
21
|
+
username: postgres
|
data/spec/spec.sqlite3
ADDED
Binary file
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
cnf = YAML::load_file(File.join(File.dirname(File.expand_path(__FILE__)), 'config.yml'))
|
4
|
+
config_block = ENV['TEST_CONFIG'] || 'test'
|
5
|
+
cnf = cnf[config_block]
|
6
|
+
|
7
|
+
ActiveRecord::Base.establish_connection(cnf)
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
config.order = "random"
|
11
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
# ActiveRecord::Base.establish_connection(
|
5
|
+
# :adapter => 'sqlite3',
|
6
|
+
# :database => 'spec/spec.sqlite3'
|
7
|
+
# )
|
8
|
+
|
9
|
+
require 'specifind'
|
10
|
+
|
11
|
+
class Person < ActiveRecord::Base
|
12
|
+
attr_accessible :birthday, :name, :male
|
13
|
+
end
|
14
|
+
|
15
|
+
describe Person do
|
16
|
+
before(:all) do
|
17
|
+
|
18
|
+
ActiveRecord::Migration.class_eval do
|
19
|
+
create_table :people do |t|
|
20
|
+
t.integer :age
|
21
|
+
t.date :birthday
|
22
|
+
t.string :name
|
23
|
+
t.boolean :male
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
Person.create :name => 'Erin', :birthday => Date.new(1981, 5, 16), :male => false
|
28
|
+
Person.create :name => 'Aaron', :birthday => Date.new(1956, 8, 6), :male => true
|
29
|
+
Person.create :name => 'Niki', :birthday => Date.new(1992, 3, 29), :male => false
|
30
|
+
Person.create :name => 'Nick', :birthday => Date.new(1974, 11, 1), :male => true
|
31
|
+
Person.create :name => 'Dani', :birthday => Date.new(1987, 4, 9), :male => false
|
32
|
+
Person.create :name => 'Dan', :birthday => Date.new(2001, 1, 17), :male => true
|
33
|
+
Person.create :name => 'xxyxx', :birthday => nil, :male => false
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
after :all do
|
38
|
+
ActiveRecord::Migration.class_eval do
|
39
|
+
drop_table :people
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "Specifind Model" do
|
44
|
+
it "finds instances with in_list comparator" do
|
45
|
+
Person.find_by_name_in_list(['Erin', 'Aaron', 'Niki']).length.should == 3
|
46
|
+
end
|
47
|
+
|
48
|
+
it "finds instances with less_than comparator" do
|
49
|
+
list = Person.find_by_birthday_less_than(Date.new(1957,1,1))
|
50
|
+
list[0].name.should == 'Aaron'
|
51
|
+
end
|
52
|
+
|
53
|
+
it "finds instances with less_than_equals comparator" do
|
54
|
+
list = Person.find_by_birthday_less_than_equals(Date.new(1974, 11, 1))
|
55
|
+
list.length.should == 2
|
56
|
+
end
|
57
|
+
|
58
|
+
it "finds instances with greater_than comparator" do
|
59
|
+
list = Person.find_by_birthday_greater_than(Date.new(1992,3,29))
|
60
|
+
list[0].name.should == 'Dan'
|
61
|
+
end
|
62
|
+
|
63
|
+
it "finds instances with greater_than_equals comparator" do
|
64
|
+
list = Person.find_by_birthday_greater_than_equals(Date.new(1992,3,29))
|
65
|
+
list.length.should == 2
|
66
|
+
end
|
67
|
+
|
68
|
+
it "finds instances with like comparator" do
|
69
|
+
list = Person.find_by_name_like '%n%'
|
70
|
+
if ActiveRecord::Base.connection.class.name == "ActiveRecord::ConnectionAdapters::SQLite3Adapter"
|
71
|
+
list.length.should == 6 # sqlite3 uses case insensitive for like. no magic way to make case sensitive
|
72
|
+
else
|
73
|
+
list.length.should == 4 # Erin, Aaron, Dani, Dan
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
it "finds instances with ilike comparator" do
|
78
|
+
list = Person.find_by_name_ilike '%N%'
|
79
|
+
list.length.should == 6 # Erin, Aaron, Niki, Nick, Dani, Dan
|
80
|
+
end
|
81
|
+
|
82
|
+
it "finds instances with not_equal comparator" do
|
83
|
+
list = Person.find_by_name_not_equal 'Dan'
|
84
|
+
list.length.should == 6
|
85
|
+
list.each do |p|
|
86
|
+
p.name.should_not == 'Dan'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
it "finds instances with between comparator" do
|
91
|
+
list = Person.find_by_birthday_between Date.new(2000, 1, 1), Date.new(2002, 1, 1)
|
92
|
+
list.length.should == 1
|
93
|
+
list[0].name.should == 'Dan'
|
94
|
+
end
|
95
|
+
|
96
|
+
it "finds instances with is_not_null comparator" do
|
97
|
+
list = Person.find_by_birthday_is_not_null
|
98
|
+
list.length.should == 6
|
99
|
+
end
|
100
|
+
|
101
|
+
it "finds instances with is_null comparator" do
|
102
|
+
list = Person.find_by_birthday_is_null
|
103
|
+
list.length.should == 1
|
104
|
+
list[0].name.should == 'xxyxx'
|
105
|
+
end
|
106
|
+
|
107
|
+
it "finds instances with equals comparator" do
|
108
|
+
list = Person.find_by_male false
|
109
|
+
list.length.should == 4
|
110
|
+
end
|
111
|
+
|
112
|
+
it "works" do
|
113
|
+
end
|
114
|
+
it "verifies the type of objects passed in" do
|
115
|
+
expect {Person.find_by_age_equals 'max'}.to raise_error
|
116
|
+
expect {Person.find_by_birthday 'max'}.to raise_error
|
117
|
+
expect {Person.find_by_name_like 16}.to raise_error
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
metadata
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: specifind
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Max Lahey
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-07 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.2.9
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.2.9
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: sqlite3
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: Specifind offers advanced ActiveRecord dynamic find_by_* methods that
|
47
|
+
include comparators (like the grails ORM). Coupled with some solid SQL injection
|
48
|
+
mitigation through strict verification of type and string escaping, your find methods
|
49
|
+
will be much more readable. If an object of the wrong type (based on the type of
|
50
|
+
the corresponding column of the db) is passed into a finder, it will raise an exception.
|
51
|
+
Ruby 1.9.2 and above are supported
|
52
|
+
email:
|
53
|
+
- maxwellslahey@gmail.com
|
54
|
+
executables: []
|
55
|
+
extensions: []
|
56
|
+
extra_rdoc_files: []
|
57
|
+
files:
|
58
|
+
- lib/specifind/attribute_phrase.rb
|
59
|
+
- lib/specifind/comparators/mysql2.rb
|
60
|
+
- lib/specifind/comparators/postgre_sql.rb
|
61
|
+
- lib/specifind/comparators/sq_lite3.rb
|
62
|
+
- lib/specifind/comparators.rb
|
63
|
+
- lib/specifind/method_builder.rb
|
64
|
+
- lib/specifind/operator.rb
|
65
|
+
- lib/specifind/type.rb
|
66
|
+
- lib/specifind/version.rb
|
67
|
+
- lib/specifind.rb
|
68
|
+
- MIT-LICENSE
|
69
|
+
- Rakefile
|
70
|
+
- README.md
|
71
|
+
- spec/config.yml
|
72
|
+
- spec/spec.sqlite3
|
73
|
+
- spec/spec_helper.rb
|
74
|
+
- spec/specifind_spec.rb
|
75
|
+
homepage: http://maxwells.github.io
|
76
|
+
licenses: []
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options: []
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubyforge_project:
|
95
|
+
rubygems_version: 1.8.23
|
96
|
+
signing_key:
|
97
|
+
specification_version: 3
|
98
|
+
summary: Readble & Advanced ActiveRecord dynamic methods
|
99
|
+
test_files:
|
100
|
+
- spec/config.yml
|
101
|
+
- spec/spec.sqlite3
|
102
|
+
- spec/spec_helper.rb
|
103
|
+
- spec/specifind_spec.rb
|
104
|
+
has_rdoc:
|