constrainable 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.markdown ADDED
@@ -0,0 +1,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
+