specifind 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ module Specifind
2
+ VERSION = "0.1.0"
3
+ 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
@@ -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: