constrainable 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|