constrainable 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown ADDED
@@ -0,0 +1,72 @@
1
+ # Constrainable
2
+
3
+ Simple filtering for ActiveRecord. Sanitizes simple and readable query parameters -great for building APIs & HTML filters.
4
+
5
+ ## Straight to the point. Examples:
6
+
7
+ Let's asume we have a model called Post, defined as:
8
+ Post(id: integer, title: string, body: string, author_id: integer, category: string, created_at: datetime, updated_at: datetime)
9
+
10
+ In the simplest possible case you can define a few attributes and start filtering:
11
+
12
+ class Post < ActiveRecord::Base
13
+
14
+ constrainable do
15
+ fields :id, :author_id
16
+ end
17
+
18
+ end
19
+
20
+ # Examle request:
21
+ # GET /posts?where[id][not_eq]=1&where[author_id][eq]=2
22
+ # Params:
23
+ # "where" => { "id" => { "not_eq" => "1" }, "author_id" => { "eq" => "2" } }
24
+
25
+ Post.constrain(params[:where])
26
+ # => SELECT posts.* FROM posts WHERE id != 1 AND author_id = 2
27
+
28
+ By default, only *eq* and *not_eq* operations are enabled, but there are plenty more:
29
+
30
+ class Post < ActiveRecord::Base
31
+
32
+ constrainable do
33
+ fields :id, :author_id, :with => [:in, :not_in, :gt, :gteq, :lt, :lteq]
34
+ fields :created_at, :with => [:between]
35
+ end
36
+
37
+ end
38
+
39
+ # Example request (various notations are accepted):
40
+ # GET /posts?
41
+ # where[id][not_in]=1|2|3|4&
42
+ # where[author_id][in][]=1&
43
+ # where[author_id][in][]=2&
44
+ # where[created_at][between]=2011-01-01...2011-02-01
45
+
46
+ Want to *alias* a column? Try this:
47
+
48
+ class Post < ActiveRecord::Base
49
+
50
+ constrainable do
51
+ timestamp :created, :using => :created_at, :with => [:lt, :lte, :between]
52
+ end
53
+
54
+ end
55
+ # Example request:
56
+ # GET /posts?where[created][lt]=2011-01-01
57
+
58
+ What about associations?
59
+
60
+ class Post < ActiveRecord::Base
61
+ belongs_to :author
62
+
63
+ constrainable do
64
+ string :author_name, :using => lambda { Author.arel_table[:name] }, :with => [:matches], :scope => lambda { includes(:author) }
65
+ end
66
+ end
67
+ # Example request:
68
+ # GET /posts?where[author][matches]=%tom%
69
+
70
+ Post.constrain(params[:where])
71
+ # => SELECT posts.* FROM posts LEFT OUTER JOIN authors ON authors.id = posts.author_id WHERE authors.name LIKE '%tom%'
72
+
@@ -0,0 +1,59 @@
1
+ # Abstract field type
2
+ class Bsm::Constrainable::Field::Base
3
+ DEFAULT_OPERATORS = [:eq, :not_eq, :gt, :gteq, :lt, :lteq, :between].freeze
4
+
5
+ class_inheritable_accessor :operators, :defaults, :instance_reader => false, :instance_writer => false
6
+ self.operators = DEFAULT_OPERATORS.dup
7
+ self.defaults = [:eq, :not_eq]
8
+
9
+ # Returns the field type/kind, e.g. <tt>:string</tt> or <tt>:integer</tt>
10
+ def self.kind
11
+ @kind ||= name.demodulize.underscore.to_sym
12
+ end
13
+
14
+ attr_reader :name, :operators, :attribute, :scope
15
+
16
+ # Accepts a name and options. Valid options are:
17
+ # * <tt>:using</tt> - a Symbol or a Proc pointing to a DB column, optional (uses name by default)
18
+ # * <tt>:with</tt> - a list of operators to use
19
+ # * <tt>:scope</tt> - a Proc containing additonal scopes
20
+ #
21
+ # Examples:
22
+ #
23
+ # Field::Integer.new :id
24
+ # Field::Integer.new :uid, :using => :id
25
+ # Field::Integer.new :uid, :using => proc { Model.scoped.table[:col_name] }
26
+ # Field::String.new :name, :with => [:matches]
27
+ # Field::String.new :author, :with => [:matches], :using => proc { Author.scoped.table[:name] }, :scope => proc { includes(:author) }
28
+ #
29
+ def initialize(name, options = {})
30
+ @name = name.to_s
31
+ @attribute = options[:using] || name
32
+ @operators = Set.new(self.class.operators & Array.wrap(options[:with]))
33
+ @operators = Set.new(self.class.defaults) if @operators.empty?
34
+ @scope = options[:scope]
35
+ end
36
+
37
+ # Merge params into a relation
38
+ def merge(relation, params)
39
+ params.slice(*operators).each do |operator, value|
40
+ operation = Bsm::Constrainable::Operation.new(operator, value, relation, self)
41
+ next if operation.clause.nil?
42
+
43
+ relation = relation.instance_eval(&scope) if scope
44
+ relation = relation.where(operation.clause)
45
+ end
46
+ relation
47
+ end
48
+
49
+ def convert(value)
50
+ value.is_a?(Array) ? value.map {|v| convert(v) } : _convert(value)
51
+ end
52
+
53
+ protected
54
+
55
+ def _convert(value)
56
+ value.to_s
57
+ end
58
+
59
+ end
@@ -0,0 +1,46 @@
1
+ module Bsm::Constrainable::Field
2
+ class Number < Base
3
+ self.operators += [:in, :not_in]
4
+
5
+ protected
6
+
7
+ def _convert(v)
8
+ Float(v) rescue nil
9
+ end
10
+ end
11
+
12
+ class Integer < Number
13
+ protected
14
+
15
+ def _convert(v)
16
+ result = super
17
+ result ? result.to_i : nil
18
+ end
19
+ end
20
+
21
+ class Decimal < Number
22
+ end
23
+
24
+ class String < Base
25
+ self.operators = [:eq, :not_eq]
26
+ end
27
+
28
+ class Timestamp < Base
29
+ protected
30
+
31
+ def _convert(v)
32
+ v.to_time(:utc) rescue nil
33
+ end
34
+ end
35
+
36
+ class Datetime < Timestamp
37
+ end
38
+
39
+ class Date < Base
40
+ protected
41
+
42
+ def _convert(v)
43
+ v.to_date rescue nil
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ module Bsm::Constrainable::Field
2
+ extend Bsm::Constrainable::Registry
3
+
4
+ autoload :Base, 'bsm/constrainable/field/base'
5
+ autoload :Number, 'bsm/constrainable/field/common'
6
+ autoload :Integer, 'bsm/constrainable/field/common'
7
+ autoload :Decimal, 'bsm/constrainable/field/common'
8
+ autoload :String, 'bsm/constrainable/field/common'
9
+ autoload :Timestamp,'bsm/constrainable/field/common'
10
+ autoload :Datetime, 'bsm/constrainable/field/common'
11
+ autoload :Date, 'bsm/constrainable/field/common'
12
+
13
+ register self::Number
14
+ register self::Integer
15
+ register self::Decimal
16
+ register self::String
17
+ register self::Timestamp
18
+ register self::Datetime
19
+ register self::Date
20
+ end
@@ -0,0 +1,51 @@
1
+ class Bsm::Constrainable::FilterSet < Hash
2
+ include ::Bsm::Constrainable::Util
3
+
4
+ attr_reader :schema
5
+
6
+ def initialize(schema, params = {})
7
+ @schema = schema
8
+
9
+ normalized_hash(params).slice(*schema.keys).each do |name, part|
10
+ update name => part.symbolize_keys if part.is_a?(Hash)
11
+ end
12
+ end
13
+
14
+ def merge(relation)
15
+ each do |name, part|
16
+ schema[name].each do |field|
17
+ relation = field.merge(relation, part)
18
+ end
19
+ end
20
+ relation
21
+ end
22
+
23
+ def respond_to?(sym, *)
24
+ name, operator = sym.to_s.sub(NAME_OP, ''), $1
25
+ super || (operator.nil? && schema.key?(name)) || valid_operator?(name, operator)
26
+ end
27
+
28
+ private
29
+ NAME_OP = /\[(\w+)\]$/.freeze
30
+
31
+ def method_missing(sym, *args)
32
+ name, operator = sym.to_s.sub(NAME_OP, ''), $1
33
+
34
+ if (operator.nil? && schema.key?(name))
35
+ self[name]
36
+ elsif valid_operator?(name, operator)
37
+ self[name].try(:[], operator.to_sym)
38
+ else
39
+ super
40
+ end
41
+ end
42
+
43
+ def valid_operator?(name, operator)
44
+ return false unless operator.present? && schema.key?(name.to_s)
45
+
46
+ schema[name].any? do |field|
47
+ field.operators.include?(operator.to_sym)
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,39 @@
1
+ # Includable module, for ActiveRecord::Base
2
+ module Bsm::Constrainable::Model
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_inheritable_accessor :_constrainable
7
+ self._constrainable = {}
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ # Constraint definition for a model. Example:
13
+ #
14
+ # class Post < ActiveRecord::Base
15
+ #
16
+ # constrainable do
17
+ # # Add your default constraints
18
+ # end
19
+ #
20
+ # constrainable :custom do
21
+ # # Add your custom constraints
22
+ # end
23
+ #
24
+ # end
25
+ #
26
+ def constrainable(name = nil, &block)
27
+ name = name.present? ? name.to_sym : :default
28
+ _constrainable[name] ||= Bsm::Constrainable::Schema.new(self)
29
+ _constrainable[name.to_sym].instance_eval(&block) if block
30
+ _constrainable[name]
31
+ end
32
+
33
+ # Delegator to Relation#constrain
34
+ def constrain(*args)
35
+ relation.constrain(*args)
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,55 @@
1
+ class Bsm::Constrainable::Operation::Base
2
+ extend ActiveSupport::Memoizable
3
+
4
+ def self.kind
5
+ name.demodulize.underscore.to_sym
6
+ end
7
+
8
+ attr_reader :value, :field, :relation
9
+
10
+ def initialize(value, relation, field)
11
+ @value = value
12
+ @relation = relation
13
+ @field = field
14
+ end
15
+
16
+ def parsed
17
+ value.to_s
18
+ end
19
+
20
+ def valid?
21
+ !invalid?
22
+ end
23
+
24
+ def invalid?
25
+ wrap = Array.wrap(normalized)
26
+ wrap.empty? || wrap.any?(&:nil?)
27
+ end
28
+
29
+ def clause
30
+ valid? ? _clause : nil
31
+ end
32
+ memoize :clause
33
+
34
+ protected
35
+
36
+ def _clause
37
+ attribute.send self.class.kind, normalized
38
+ end
39
+
40
+ def normalized
41
+ field.convert(parsed)
42
+ end
43
+ memoize :normalized
44
+
45
+ def attribute
46
+ case field.attribute
47
+ when Proc
48
+ field.attribute.call(relation)
49
+ else
50
+ relation.table[field.attribute]
51
+ end
52
+ end
53
+ memoize :attribute
54
+
55
+ end
@@ -0,0 +1,21 @@
1
+ module Bsm::Constrainable::Operation
2
+ class Between < Base
3
+
4
+ def parsed
5
+ result = case value
6
+ when /^ *(.+?) *\.{2,} *(.+?) *$/
7
+ [$1, $2]
8
+ else
9
+ value
10
+ end
11
+ result.is_a?(Array) && result.size == 2 ? result.map(&:to_s) : nil
12
+ end
13
+
14
+ protected
15
+
16
+ def _clause
17
+ attribute.gteq(normalized.first).and(attribute.lteq(normalized.last))
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ module Bsm::Constrainable::Operation
2
+ class Collection < Base
3
+
4
+ def parsed
5
+ Bsm::Constrainable::Util.normalized_array(value)
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ module Bsm::Constrainable::Operation
2
+ class Eq < Base
3
+ end
4
+ class NotEq < Base
5
+ end
6
+ class Gt < Base
7
+ end
8
+ class Gteq < Base
9
+ end
10
+ class Lt < Base
11
+ end
12
+ class Lteq < Base
13
+ end
14
+ class Matches < Base
15
+ end
16
+ end
@@ -0,0 +1,4 @@
1
+ module Bsm::Constrainable::Operation
2
+ class In < Collection
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Bsm::Constrainable::Operation
2
+ class NotIn < Collection
3
+ end
4
+ end
@@ -0,0 +1,27 @@
1
+ module Bsm::Constrainable::Operation
2
+ extend Bsm::Constrainable::Registry
3
+
4
+ autoload :Base, 'bsm/constrainable/operation/base'
5
+ autoload :Collection, 'bsm/constrainable/operation/collection'
6
+ autoload :Eq, 'bsm/constrainable/operation/common'
7
+ autoload :NotEq, 'bsm/constrainable/operation/common'
8
+ autoload :Gt, 'bsm/constrainable/operation/common'
9
+ autoload :Lt, 'bsm/constrainable/operation/common'
10
+ autoload :Gteq, 'bsm/constrainable/operation/common'
11
+ autoload :Lteq, 'bsm/constrainable/operation/common'
12
+ autoload :Matches, 'bsm/constrainable/operation/common'
13
+ autoload :In, 'bsm/constrainable/operation/in'
14
+ autoload :NotIn, 'bsm/constrainable/operation/not_in'
15
+ autoload :Between, 'bsm/constrainable/operation/between'
16
+
17
+ register self::Eq
18
+ register self::NotEq
19
+ register self::In
20
+ register self::NotIn
21
+ register self::Gt
22
+ register self::Lt
23
+ register self::Gteq
24
+ register self::Lteq
25
+ register self::Matches
26
+ register self::Between
27
+ end
@@ -0,0 +1,25 @@
1
+ module Bsm::Constrainable::Registry
2
+
3
+ # Returns the current registry Hash
4
+ def registry
5
+ @registry ||= {}
6
+ end
7
+
8
+ # Register a class
9
+ def register(klass)
10
+ raise ArgumentError, "Already registered kind: #{klass.kind}" if registered?(klass.kind)
11
+ registry[klass.kind] = klass
12
+ end
13
+
14
+ # Create a new object of a certain kind.
15
+ def new(kind, *args)
16
+ raise ArgumentError, "Invalid kind #{kind}" unless registered?(kind)
17
+ registry[kind.to_sym].new(*args)
18
+ end
19
+
20
+ # Returns true if kind was already registered, else false
21
+ def registered?(kind)
22
+ registry.key?(kind.to_sym)
23
+ end
24
+
25
+ end
@@ -0,0 +1,28 @@
1
+ # Extension for ActiveRecord::Relation
2
+ module Bsm::Constrainable::Relation
3
+
4
+ # Apply contraints. Example:
5
+ #
6
+ # Post.constrain("created_at" => { "lt" => "2011-01-01" }})
7
+ #
8
+ # # Use "custom" constraints
9
+ # Post.constrain(:custom, "created_at" => { "lt" => "2011-01-01" }})
10
+ #
11
+ # # Combine it with relations & scopes
12
+ # Post.archived.includes(:author).constrain(params[:where]).paginate :page => 1
13
+ #
14
+ def constrain(*args)
15
+ scope = args.first.is_a?(Symbol) ? args.shift : nil
16
+ filters = args.last
17
+
18
+ case filters
19
+ when Bsm::Constrainable::FilterSet
20
+ filters.merge(self)
21
+ when Hash
22
+ klass.constrainable(scope).filter(filters).merge(self)
23
+ else
24
+ self
25
+ end
26
+ end
27
+
28
+ end
@@ -0,0 +1,84 @@
1
+ # Schema definition.
2
+ class Bsm::Constrainable::Schema < Hash
3
+ Field = ::Bsm::Constrainable::Field
4
+
5
+ def initialize(klass)
6
+ @klass = klass
7
+ super()
8
+ end
9
+
10
+ # Define multiple constrainable columns. Expects 1-n column names (must be
11
+ # real columns) and an options Hash. Examples:
12
+ #
13
+ # fields :id
14
+ # fields :id, :author_id
15
+ # fields :id, :author_id, :with => [:in, :not_in]
16
+ #
17
+ def fields(*names)
18
+ options = names.extract_options!
19
+ names.map(&:to_s).each do |name|
20
+ column = @klass.columns_hash[name]
21
+ raise ArgumentError, "Invalid field #{name}" unless column
22
+ raise ArgumentError, "Invalid field type #{column.type}" unless Field.registered?(column.type)
23
+ match name, options.merge(:as => column.type)
24
+ end
25
+ end
26
+
27
+ # Define constrainable names (don't have to be real columns). Expects 1-n
28
+ # names and an options Hash. Options, must specify the type via <tt>:as</tt>.
29
+ # Examples:
30
+ #
31
+ # # One match
32
+ # match :id, :as => :number
33
+ #
34
+ # # Multiple matches
35
+ # match :id, :author_id, :as => :integer, :with => [:in, :not_in]
36
+ #
37
+ # # Use a specific column
38
+ # match :created, :using => :created_at, :as => :timestamp, :with => [:lt, :between]
39
+ #
40
+ # # Complex example, using an attribute from another table, and ensure it's included (LEFT OUTER JOIN)
41
+ # match :author, :using => proc { Author.scope.table[:name] }, :scope => { includes(:author) }, :as => :string, :with => [:eq, :matches]
42
+ #
43
+ # There are also several short-cutrs available. Examples:
44
+ #
45
+ # timestamp :created, :using => :created_at, :with => [:lt, :between]
46
+ # number :id, :author_id
47
+ # string :title, :with => [:eq, :matches]
48
+ #
49
+ def match(*names)
50
+ options = names.extract_options!
51
+ kind = options.delete(:as)
52
+ names.map(&:to_s).each do |name|
53
+ self[name] ||= []
54
+ self[name] << Field.new(kind, name, options)
55
+ end
56
+ end
57
+ alias_method :field, :match
58
+
59
+ # Creates a FilterSet object for given params. Filter-sets can be used to
60
+ # constrain relations as well as e.g. in forms. Example:
61
+ #
62
+ # filters = Post.constrainable.filter(params[:where])
63
+ # Post.archived.constrain(filters).limit(100)
64
+ #
65
+ def filter(params = nil)
66
+ Bsm::Constrainable::FilterSet.new self, params
67
+ end
68
+
69
+ def respond_to?(sym)
70
+ super || Field.registered?(sym)
71
+ end
72
+
73
+ protected
74
+
75
+ def method_missing(sym, *args)
76
+ if Field.registered?(sym)
77
+ opts = args.extract_options!.merge(:as => sym)
78
+ match(*(args << opts))
79
+ else
80
+ super
81
+ end
82
+ end
83
+
84
+ end
@@ -0,0 +1,15 @@
1
+ module Bsm::Constrainable::Util
2
+ extend self
3
+
4
+ def normalized_hash(hash)
5
+ hash.is_a?(Hash) ? hash.stringify_keys : {}
6
+ end
7
+
8
+ def normalized_array(array)
9
+ array = array.keys if array.is_a?(Hash)
10
+ Array.wrap(array).map do |item|
11
+ item.to_s.split('|')
12
+ end.flatten.reject(&:blank?).uniq
13
+ end
14
+
15
+ end
@@ -0,0 +1,22 @@
1
+ require "active_support/core_ext"
2
+
3
+ module Bsm # @private
4
+ module Constrainable # @private
5
+ autoload :Util, "bsm/constrainable/util"
6
+ autoload :Model, "bsm/constrainable/model"
7
+ autoload :Relation, "bsm/constrainable/relation"
8
+ autoload :Schema, "bsm/constrainable/schema"
9
+ autoload :Registry, "bsm/constrainable/registry"
10
+ autoload :Field, "bsm/constrainable/field"
11
+ autoload :Operation, "bsm/constrainable/operation"
12
+ autoload :FilterSet, "bsm/constrainable/filter_set"
13
+ end
14
+ end
15
+
16
+ ActiveRecord::Base.class_eval do
17
+ include Bsm::Constrainable::Model
18
+ end
19
+
20
+ ActiveRecord::Relation.class_eval do
21
+ include Bsm::Constrainable::Relation
22
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: constrainable
3
+ version: !ruby/object:Gem::Version
4
+ hash: 19
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 3
9
+ - 0
10
+ version: 0.3.0
11
+ platform: ruby
12
+ authors:
13
+ - Dimitrij Denissenko
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-07-01 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: abstract
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: activerecord
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ hash: 7
44
+ segments:
45
+ - 3
46
+ - 0
47
+ - 0
48
+ version: 3.0.0
49
+ type: :runtime
50
+ version_requirements: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ name: activesupport
53
+ prerelease: false
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ~>
58
+ - !ruby/object:Gem::Version
59
+ hash: 7
60
+ segments:
61
+ - 3
62
+ - 0
63
+ - 0
64
+ version: 3.0.0
65
+ type: :runtime
66
+ version_requirements: *id003
67
+ description: Sanitizes simple and readable query parameters -great for building APIs & HTML filters
68
+ email: dimitrij@blacksquaremedia.com
69
+ executables: []
70
+
71
+ extensions: []
72
+
73
+ extra_rdoc_files: []
74
+
75
+ files:
76
+ - README.markdown
77
+ - lib/bsm/constrainable/field/common.rb
78
+ - lib/bsm/constrainable/field/base.rb
79
+ - lib/bsm/constrainable/field.rb
80
+ - lib/bsm/constrainable/filter_set.rb
81
+ - lib/bsm/constrainable/relation.rb
82
+ - lib/bsm/constrainable/registry.rb
83
+ - lib/bsm/constrainable/schema.rb
84
+ - lib/bsm/constrainable/util.rb
85
+ - lib/bsm/constrainable/operation.rb
86
+ - lib/bsm/constrainable/model.rb
87
+ - lib/bsm/constrainable/operation/common.rb
88
+ - lib/bsm/constrainable/operation/in.rb
89
+ - lib/bsm/constrainable/operation/base.rb
90
+ - lib/bsm/constrainable/operation/between.rb
91
+ - lib/bsm/constrainable/operation/collection.rb
92
+ - lib/bsm/constrainable/operation/not_in.rb
93
+ - lib/bsm/constrainable.rb
94
+ has_rdoc: true
95
+ homepage: https://github.com/bsm/constrainable
96
+ licenses: []
97
+
98
+ post_install_message:
99
+ rdoc_options: []
100
+
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ hash: 57
109
+ segments:
110
+ - 1
111
+ - 8
112
+ - 7
113
+ version: 1.8.7
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ hash: 23
120
+ segments:
121
+ - 1
122
+ - 3
123
+ - 6
124
+ version: 1.3.6
125
+ requirements: []
126
+
127
+ rubyforge_project:
128
+ rubygems_version: 1.6.2
129
+ signing_key:
130
+ specification_version: 3
131
+ summary: Simple filtering for ActiveRecord
132
+ test_files: []
133
+