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 +72 -0
- data/lib/bsm/constrainable/field/base.rb +59 -0
- data/lib/bsm/constrainable/field/common.rb +46 -0
- data/lib/bsm/constrainable/field.rb +20 -0
- data/lib/bsm/constrainable/filter_set.rb +51 -0
- data/lib/bsm/constrainable/model.rb +39 -0
- data/lib/bsm/constrainable/operation/base.rb +55 -0
- data/lib/bsm/constrainable/operation/between.rb +21 -0
- data/lib/bsm/constrainable/operation/collection.rb +9 -0
- data/lib/bsm/constrainable/operation/common.rb +16 -0
- data/lib/bsm/constrainable/operation/in.rb +4 -0
- data/lib/bsm/constrainable/operation/not_in.rb +4 -0
- data/lib/bsm/constrainable/operation.rb +27 -0
- data/lib/bsm/constrainable/registry.rb +25 -0
- data/lib/bsm/constrainable/relation.rb +28 -0
- data/lib/bsm/constrainable/schema.rb +84 -0
- data/lib/bsm/constrainable/util.rb +15 -0
- data/lib/bsm/constrainable.rb +22 -0
- metadata +133 -0
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,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
|
+
|