sexy_scopes 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -6
  3. data/README.md +56 -7
  4. data/lib/arel/visitors_extensions.rb +89 -0
  5. data/lib/sexy_scopes.rb +33 -10
  6. data/lib/sexy_scopes/active_record.rb +0 -1
  7. data/lib/sexy_scopes/active_record/class_methods.rb +5 -16
  8. data/lib/sexy_scopes/active_record/dynamic_methods.rb +26 -21
  9. data/lib/sexy_scopes/arel.rb +10 -28
  10. data/lib/sexy_scopes/arel/expression_methods.rb +9 -0
  11. data/lib/sexy_scopes/arel/math.rb +28 -0
  12. data/lib/sexy_scopes/arel/nodes/regexp_matches.rb +12 -0
  13. data/lib/sexy_scopes/arel/predicate_methods.rb +13 -38
  14. data/lib/sexy_scopes/arel/predications.rb +67 -0
  15. data/lib/sexy_scopes/arel/typecasting.rb +21 -0
  16. data/lib/sexy_scopes/version.rb +1 -1
  17. metadata +53 -50
  18. data/.gitignore +0 -4
  19. data/.travis.yml +0 -19
  20. data/Gemfile +0 -2
  21. data/Rakefile +0 -13
  22. data/ci/Gemfile.ar-edge +0 -8
  23. data/ci/Gemfile.ar3.1 +0 -8
  24. data/ci/Gemfile.ar3.2 +0 -8
  25. data/lib/sexy_scopes/arel/boolean_methods.rb +0 -20
  26. data/lib/sexy_scopes/arel/math_methods.rb +0 -25
  27. data/lib/sexy_scopes/expression_wrappers.rb +0 -7
  28. data/lib/sexy_scopes/predicate_wrappers.rb +0 -6
  29. data/lib/sexy_scopes/wrappers.rb +0 -13
  30. data/sexy_scopes.gemspec +0 -32
  31. data/spec/active_record_spec.rb +0 -73
  32. data/spec/boolean_methods_spec.rb +0 -45
  33. data/spec/fixtures/models.rb +0 -2
  34. data/spec/fixtures/schema.rb +0 -9
  35. data/spec/matchers/be_extended_by.rb +0 -6
  36. data/spec/matchers/convert_to_sql.rb +0 -13
  37. data/spec/math_methods_spec.rb +0 -55
  38. data/spec/predicate_methods_spec.rb +0 -38
  39. data/spec/spec_helper.rb +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ce33d394e7388e54629f64eb7ffb6db752337088
4
- data.tar.gz: 08ed0052a528fba6aec3bdfc27ef53e36c881790
3
+ metadata.gz: ea9d2e3c3ca0c50058139d67222d97894fb0192c
4
+ data.tar.gz: aa1d5490fe8d78f9a3323a25bf10e78cdf072909
5
5
  SHA512:
6
- metadata.gz: fcf5492bd6d3717504dcf30371796781cd46d13ef5b9fc2de143925b93b6488141f9cf705348b20c0f77fe4e60dfc3b05b7bed8ce4a415a57407ecd381104d33
7
- data.tar.gz: 490a59156615aeef8e8e94bafb92877c20ebf30680bac27120e5883913ac34da5cf724c694e5d19ea758c17b6fe03ed5a4aa8678f0fa5f5d8edea2227e0a95b1
6
+ metadata.gz: 08a625a5ff5b18d1a0955efcb48bd66093a794382b4d86ee8ad8676fdf28ad34a8aaabd86fa3b8c403a3d328c2b1f06bf72139a12b871dfbeb2d449d64992bea
7
+ data.tar.gz: 5ec1351dc43eebefeed0defc5ea6da10a9d56cfb41f9812b59e57160f9ed18da9891a5ce0da1ee418b892bd3baef8d76789e461f6ad19e24164ee25613f0f757
data/CHANGELOG.md CHANGED
@@ -1,9 +1,24 @@
1
- ### 0.6.0
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
2
3
 
3
- - Drop support for Ruby < 1.9.2 and ActiveRecord < 3.1
4
- - Add support for JRuby and Rubinius
5
- - Integrate with Travis CI
4
+ ## 0.7.0 - 2014-07-23
6
5
 
7
- ### 0.5.1
6
+ ### Added
7
+ - Support for Regular Expressions with the `=~` and `!~` operators (PostgreSQL, MySQL & SQLite3)
8
+ - Support for ActiveRecord 4.1
9
+ - Support for Arel 6
10
+ - Integration with Travis CI (https://travis-ci.org/samleb/sexy_scopes)
8
11
 
9
- - Initial stable release
12
+ ### Fixed
13
+ - Existing class methods no longer overriden by dynamic method generation
14
+
15
+ ### Removed
16
+ - Support for Ruby 1.9.2
17
+
18
+ ## 0.6.0 - 2013-05-29
19
+
20
+ ### Added
21
+ - Support for JRuby and Rubinius
22
+
23
+ ### Removed
24
+ - Support for Ruby < 1.9.2 and ActiveRecord < 3.1
data/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  SexyScopes
2
2
  ==========
3
3
 
4
- [![Gem Version](https://badge.fury.io/rb/sexy_scopes.png)](https://rubygems.org/gems/sexy_scopes)
5
- [![Dependencies](https://gemnasium.com/samleb/sexy_scopes.png?travis)](https://gemnasium.com/samleb/sexy_scopes)
6
- [![Code Climate](https://codeclimate.com/github/samleb/sexy_scopes.png)](https://codeclimate.com/github/samleb/sexy_scopes)
7
- [![Build Status](https://api.travis-ci.org/samleb/sexy_scopes.png)](https://travis-ci.org/samleb/sexy_scopes)
8
- [![Coverage Status](https://coveralls.io/repos/samleb/sexy_scopes/badge.png)](https://coveralls.io/r/samleb/sexy_scopes)
4
+ [![Gem Version](http://img.shields.io/gem/v/sexy_scopes.svg?style=flat)](https://rubygems.org/gems/sexy_scopes)
5
+ [![Dependencies](https://img.shields.io/gemnasium/samleb/sexy_scopes.svg?style=flat)](https://gemnasium.com/samleb/sexy_scopes)
6
+ [![Code Climate](https://img.shields.io/codeclimate/github/samleb/sexy_scopes.svg?style=flat)](https://codeclimate.com/github/samleb/sexy_scopes)
7
+ [![Build Status](https://img.shields.io/travis/samleb/sexy_scopes.svg?style=flat)](https://travis-ci.org/samleb/sexy_scopes)
8
+ [![Coverage Status](https://img.shields.io/coveralls/samleb/sexy_scopes.svg?style=flat)](https://coveralls.io/r/samleb/sexy_scopes)
9
9
 
10
10
  **Write beautiful and expressive ActiveRecord scopes without SQL**
11
11
 
@@ -168,16 +168,65 @@ Add this line to your application's Gemfile:
168
168
 
169
169
  And then execute:
170
170
 
171
- $ bundle
171
+ bundle
172
172
 
173
173
  Or install it yourself as:
174
174
 
175
- $ gem install sexy_scopes
175
+ gem install sexy_scopes
176
176
 
177
177
  Then require it in your application code:
178
178
 
179
179
  require 'sexy_scopes'
180
180
 
181
+ Regular Expressions
182
+ -------------------
183
+
184
+ Did you know that most RDBMS come with pretty good support for regular expressions?
185
+
186
+ One reason they're quite unpopular in Rails applications is that their syntax is really different amongst
187
+ databases implementations.
188
+ Let's say you're using SQLite3 in development, and PostgreSQL in testing/production, well that's quite a good reason
189
+ not to use database-specific code, isn't it?
190
+
191
+ Once again, SexyScopes comes to the rescue:
192
+ The `=~` and `!~` operators when called with a regular expression will generate the SQL you don't want to know about.
193
+
194
+ ```ruby
195
+ predicate = User.username =~ /^john\b(.*\b)?doe$/i
196
+
197
+ # In development, using SQLite3:
198
+ predicate.to_sql
199
+ # => "users"."username" REGEXP '^john\b(.*\b)?doe$'
200
+
201
+ # In testing/production, using PostgreSQL
202
+ predicate.to_sql
203
+ # => "users"."username" ~* '^john\b(.*\b)?doe$'
204
+ ```
205
+
206
+ Now let's suppose that you want to give your admin a powerful regexp based search upon usernames, here's how
207
+ you could do it:
208
+
209
+ ```ruby
210
+ class Admin::UsersController
211
+ def index
212
+ @users = User.where(User.username =~ Regexp.compile(params[:query]))
213
+ respond_with @users
214
+ end
215
+ end
216
+ ```
217
+
218
+ Let's see what happens in our production logs (SQL included) when they try this new feature:
219
+
220
+ ```
221
+ Started GET "/admin/users?query=bob%7Calice" for xx.xx.xx.xx at 2014-03-31 16:00:50 +0200
222
+ Processing by Admin::UsersController#index as HTML
223
+ Parameters: {"query"=>"bob|alice"}
224
+ User Load (0.1ms) SELECT "users".* FROM "users" WHERE ("users"."username" ~ 'bob|alice')
225
+ ```
226
+
227
+ The proper SQL is generated, protected from SQL injection BTW, and from now on you have reusable code for
228
+ both you development and your production environment.
229
+
181
230
  Contributing
182
231
  ------------
183
232
 
@@ -0,0 +1,89 @@
1
+ require 'arel/visitors'
2
+
3
+ module Arel
4
+ module Visitors
5
+ class ToSql
6
+ private
7
+
8
+ def visit_Regexp(regexp, arg = nil)
9
+ source = SexyScopes.quote(regexp.source)
10
+ sexy_scopes_visit source, arg
11
+ end
12
+
13
+ # Arel >= 4.0
14
+ if instance_method(:visit).arity > 1
15
+ alias_method :sexy_scopes_visit, :visit
16
+ else
17
+ def sexy_scopes_visit(node, arg = nil)
18
+ visit(node)
19
+ end
20
+ end
21
+
22
+ # Arel >= 6.0
23
+ if instance_method(:accept).arity > 1
24
+ def reduce_visitor?
25
+ true
26
+ end
27
+ else
28
+ def reduce_visitor?
29
+ false
30
+ end
31
+ end
32
+ end
33
+
34
+ class MySQL
35
+ private
36
+
37
+ def visit_SexyScopes_Arel_Nodes_RegexpMatches(o, arg = nil)
38
+ regexp = o.right
39
+ right = SexyScopes.quote(regexp.source)
40
+ right = Arel::Nodes::Bin.new(right) unless regexp.casefold?
41
+ if reduce_visitor?
42
+ visit o.left, arg
43
+ arg << ' REGEXP '
44
+ visit right, arg
45
+ else
46
+ "#{sexy_scopes_visit o.left, arg} REGEXP #{visit right}"
47
+ end
48
+ end
49
+ end
50
+
51
+ class PostgreSQL
52
+ private
53
+
54
+ def visit_SexyScopes_Arel_Nodes_RegexpMatches(o, arg = nil)
55
+ regexp = o.right
56
+ operator = regexp.casefold? ? '~*' : '~'
57
+ right = SexyScopes.quote(regexp.source)
58
+ if reduce_visitor?
59
+ visit o.left, arg
60
+ arg << SPACE << operator << SPACE
61
+ visit right, arg
62
+ else
63
+ "#{sexy_scopes_visit o.left, arg} #{operator} #{visit right}"
64
+ end
65
+ end
66
+ end
67
+
68
+ class Oracle
69
+ private
70
+
71
+ def visit_SexyScopes_Arel_Nodes_RegexpMatches(o, arg = nil)
72
+ regexp = o.right
73
+ flags = regexp.casefold? ? 'i' : 'c'
74
+ flags << 'm' if regexp.options & Regexp::MULTILINE == Regexp::MULTILINE
75
+ if reduce_visitor?
76
+ arg << 'REGEXP_LIKE('
77
+ visit o.left, arg
78
+ arg << COMMA
79
+ visit regexp.source, arg
80
+ arg << COMMA
81
+ visit flags, arg
82
+ arg << ')'
83
+ else
84
+ "REGEXP_LIKE(#{sexy_scopes_visit o.left, arg}, #{visit regexp.source}, #{visit flags})"
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
data/lib/sexy_scopes.rb CHANGED
@@ -1,19 +1,42 @@
1
- require 'active_support/dependencies/autoload'
1
+ require 'active_support/lazy_load_hooks'
2
+ require 'sexy_scopes/version'
2
3
 
3
4
  module SexyScopes
4
- %w( Version VERSION ).each do |constant|
5
- autoload constant, 'sexy_scopes/version'
6
- end
7
-
8
- extend ActiveSupport::Autoload
5
+ autoload :Arel, 'sexy_scopes/arel'
9
6
 
10
- autoload :Wrappers
11
- autoload :ExpressionWrappers
12
- autoload :PredicateWrappers
7
+ class << self
8
+ AREL_6 = ::Arel::VERSION >= '6.0.0'
9
+
10
+ def extend_expression(expression)
11
+ expression.extend(Arel::ExpressionMethods)
12
+ end
13
+
14
+ def extend_predicate(predicate)
15
+ predicate.extend(Arel::PredicateMethods)
16
+ end
17
+
18
+ def arel_6?
19
+ AREL_6
20
+ end
21
+
22
+ if AREL_6
23
+ def quote(node, attribute = nil)
24
+ ::Arel::Nodes.build_quoted(node, attribute)
25
+ end
26
+ else
27
+ def quote(node, attribute = nil)
28
+ node
29
+ end
30
+ end
31
+
32
+ alias_method :type_cast, :quote
33
+ end
13
34
  end
14
35
 
15
36
  if defined? Rails::Railtie
16
37
  require 'sexy_scopes/railtie'
17
38
  else
18
- require 'sexy_scopes/active_record'
39
+ ActiveSupport.on_load :active_record do
40
+ require 'sexy_scopes/active_record'
41
+ end
19
42
  end
@@ -1,4 +1,3 @@
1
- require 'active_record'
2
1
  require 'sexy_scopes/active_record/class_methods'
3
2
  require 'sexy_scopes/active_record/dynamic_methods'
4
3
 
@@ -1,11 +1,6 @@
1
- require 'sexy_scopes/arel'
2
- require 'sexy_scopes/wrappers'
3
-
4
1
  module SexyScopes
5
2
  module ActiveRecord
6
3
  module ClassMethods
7
- include Wrappers
8
-
9
4
  # Creates and extends an Arel <tt>Attribute</tt> representing the table's column with
10
5
  # the given <tt>name</tt>.
11
6
  #
@@ -14,12 +9,11 @@ module SexyScopes
14
9
  # @note Please note that no exception is raised if no such column actually exists.
15
10
  #
16
11
  # @example
17
- # User.where(User.attribute(:score) > 1000)
18
- # # => SELECT "users".* FROM "users" WHERE ("users"."score" > 1000)
12
+ # where attribute(:score) > 1000
19
13
  #
20
14
  def attribute(name)
21
15
  attribute = arel_table[name]
22
- extend_expression(attribute)
16
+ SexyScopes.extend_expression(attribute)
23
17
  end
24
18
 
25
19
  # Creates and extends an Arel <tt>SqlLiteral</tt> instance for the given <tt>expression</tt>,
@@ -28,17 +22,12 @@ module SexyScopes
28
22
  # @param [String, #to_s] expression Any SQL expression.
29
23
  #
30
24
  # @example
31
- # def Circle.with_perimeter_smaller_than(perimeter)
32
- # where sql(2 * Math::PI) * radius < perimeter
33
- # end
34
- #
35
- # Circle.with_perimeter_smaller_than(20)
36
- # # => SELECT "circles".* FROM "circles" WHERE (6.283185307179586 * "circles"."radius" < 20)
25
+ # where sql('LENGTH(email)') > 200
37
26
  #
38
27
  def sql_literal(expression)
39
28
  ::Arel.sql(expression.to_s).tap do |literal|
40
- extend_expression(literal)
41
- extend_predicate(literal)
29
+ SexyScopes.extend_expression(literal)
30
+ SexyScopes.extend_predicate(literal)
42
31
  end
43
32
  end
44
33
  alias_method :sql, :sql_literal
@@ -2,11 +2,14 @@ module SexyScopes
2
2
  module ActiveRecord
3
3
  module DynamicMethods
4
4
  private
5
- # # @!visibility private
6
- def respond_to_missing?(method_name, include_private = false) # :nodoc:
5
+ def respond_to_missing?(method_name, *)
7
6
  # super currently resolve to Object#respond_to_missing? which return false,
8
7
  # but future version of ActiveRecord::Base might implement respond_to_missing?
9
- sexy_scopes_has_attribute?(method_name) || super
8
+ if @sexy_scopes_attribute_methods_generated
9
+ super
10
+ else
11
+ sexy_scopes_has_attribute?(method_name)
12
+ end
10
13
  end
11
14
 
12
15
  # Equivalent to calling {#attribute} with the missing method's <tt>name</tt> if the table
@@ -37,29 +40,31 @@ module SexyScopes
37
40
  # # In these cases you'll have to use `attribute` explicitely
38
41
  # User.attribute(:name)
39
42
  #
40
- def method_missing(name, *args)
41
- if sexy_scopes_has_attribute?(name)
42
- sexy_scopes_define_attribute_method(name)
43
- attribute(name)
44
- else
43
+ def method_missing(name, *args, &block)
44
+ if @sexy_scopes_attribute_methods_generated
45
45
  super
46
+ else
47
+ sexy_scopes_define_attribute_methods
48
+ send(name, *args, &block)
46
49
  end
47
50
  end
48
-
49
- def sexy_scopes_define_attribute_method(name)
50
- class_eval <<-EVAL, __FILE__, __LINE__ + 1
51
- def self.#{name} # def self.username
52
- attribute(:#{name}) # attribute(:username)
53
- end # end
54
- EVAL
51
+
52
+ def sexy_scopes_define_attribute_methods
53
+ @sexy_scopes_attribute_methods_generated = true
54
+ return unless sexy_scopes_is_table?
55
+ column_names.each do |name|
56
+ unless respond_to?(name, true)
57
+ define_singleton_method(name) { attribute(name) }
58
+ end
59
+ end
55
60
  end
56
-
61
+
57
62
  def sexy_scopes_has_attribute?(attribute_name)
58
- if equal?(::ActiveRecord::Base) || abstract_class? || !table_exists?
59
- false
60
- else
61
- column_names.include?(attribute_name.to_s)
62
- end
63
+ sexy_scopes_is_table? && column_names.include?(attribute_name.to_s)
64
+ end
65
+
66
+ def sexy_scopes_is_table?
67
+ !(equal?(::ActiveRecord::Base) || abstract_class?) && table_exists?
63
68
  end
64
69
  end
65
70
  end
@@ -1,34 +1,16 @@
1
+ require 'arel/visitors_extensions'
2
+
1
3
  module SexyScopes
2
4
  module Arel
3
- extend ActiveSupport::Autoload
4
-
5
- autoload :MathMethods
6
- autoload :PredicateMethods
7
- autoload :BooleanMethods
8
- end
9
- end
10
-
11
- module Arel
12
- module Nodes
13
- # <tt>Grouping</tt> nodes didn't include <tt>Arel::Predications</tt> before
14
- # {https://github.com/rails/arel/commit/c78227d9b219933f54cecefb99c72bb231fbb8f2 this commit}.
15
- #
16
- # Here they are included explicitely just in case they're missing.
17
- class Grouping
18
- include Arel::Predications
19
- end
5
+ autoload :ExpressionMethods, 'sexy_scopes/arel/expression_methods'
6
+ autoload :PredicateMethods, 'sexy_scopes/arel/predicate_methods'
20
7
 
21
- # As <tt>SqlLiteral</tt> could be any arbitrary SQL expression, include <tt>Arel::Math</tt>
22
- # to allow common mathematic operations.
23
- #
24
- # @see SexyScopes::ActiveRecord#sql_literal
25
- class SqlLiteral
26
- include Arel::Math
8
+ autoload :Predications, 'sexy_scopes/arel/predications'
9
+ autoload :Typecasting, 'sexy_scopes/arel/typecasting'
10
+ autoload :Math, 'sexy_scopes/arel/math'
11
+
12
+ module Nodes
13
+ autoload :RegexpMatches, 'sexy_scopes/arel/nodes/regexp_matches'
27
14
  end
28
15
  end
29
-
30
- # @!visibility private
31
- class SqlLiteral # :nodoc:
32
- include Arel::Math
33
- end
34
16
  end
@@ -0,0 +1,9 @@
1
+ module SexyScopes
2
+ module Arel
3
+ module ExpressionMethods
4
+ include ::Arel::Math
5
+ include Predications
6
+ include Math
7
+ end
8
+ end
9
+ end