Permalink
Browse files

Added form-usable filter-set. Streamlined processes. Added gemspec

  • Loading branch information...
1 parent 2dc2870 commit bf86c57b8a70a775fec7f8334a3c960eeae9c474 @dim dim committed Jul 1, 2011
View
@@ -1,13 +1,11 @@
source "http://rubygems.org"
-gem "activesupport"
-gem "activemodel"
-gem "abstract"
+gemspec
group :test do
- gem "activerecord"
gem "rspec"
gem "rspec-rails", :require => false
gem "sqlite3-ruby"
gem "shoulda-matchers"
+ gem "actionpack", :require => false
end
View
@@ -1,3 +1,11 @@
+PATH
+ remote: .
+ specs:
+ constrainable (0.3.0)
+ abstract
+ activerecord (~> 3.0.0)
+ activesupport (~> 3.0.0)
+
GEM
remote: http://rubygems.org/
specs:
@@ -65,10 +73,8 @@ PLATFORMS
ruby
DEPENDENCIES
- abstract
- activemodel
- activerecord
- activesupport
+ actionpack
+ constrainable!
rspec
rspec-rails
shoulda-matchers
View
@@ -0,0 +1,22 @@
+# -*- encoding: utf-8 -*-
+Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.required_ruby_version = '>= 1.8.7'
+ s.required_rubygems_version = ">= 1.3.6"
+
+ s.name = "constrainable"
+ s.summary = "Simple filtering for ActiveRecord"
+ s.description = "Sanitizes simple and readable query parameters -great for building APIs & HTML filters"
+ s.version = '0.3.0'
+
+ s.authors = ["Dimitrij Denissenko"]
+ s.email = "dimitrij@blacksquaremedia.com"
+ s.homepage = "https://github.com/bsm/constrainable"
+
+ s.require_path = 'lib'
+ s.files = Dir['LICENSE', 'README.markdown', 'lib/**/*']
+
+ s.add_dependency "abstract"
+ s.add_dependency "activerecord", "~> 3.0.0"
+ s.add_dependency "activesupport", "~> 3.0.0"
+end
View
@@ -9,6 +9,7 @@ module Constrainable # @private
autoload :Registry, "bsm/constrainable/registry"
autoload :Field, "bsm/constrainable/field"
autoload :Operation, "bsm/constrainable/operation"
+ autoload :FilterSet, "bsm/constrainable/filter_set"
end
end
@@ -0,0 +1,51 @@
+class Bsm::Constrainable::FilterSet < Hash
+ include ::Bsm::Constrainable::Util
+
+ attr_reader :schema
+
+ def initialize(schema, params = {})
+ @schema = schema
+
+ normalized_hash(params).slice(*schema.keys).each do |name, part|
+ update name => part.symbolize_keys if part.is_a?(Hash)
+ end
+ end
+
+ def merge(relation)
+ each do |name, part|
+ schema[name].each do |field|
+ relation = field.merge(relation, part)
+ end
+ end
+ relation
+ end
+
+ def respond_to?(sym, *)
+ name, operator = sym.to_s.sub(NAME_OP, ''), $1
+ super || (operator.nil? && schema.key?(name)) || valid_operator?(name, operator)
+ end
+
+ private
+ NAME_OP = /\[(\w+)\]$/.freeze
+
+ def method_missing(sym, *args)
+ name, operator = sym.to_s.sub(NAME_OP, ''), $1
+
+ if (operator.nil? && schema.key?(name))
+ self[name]
+ elsif valid_operator?(name, operator)
+ self[name].try(:[], operator.to_sym)
+ else
+ super
+ end
+ end
+
+ def valid_operator?(name, operator)
+ return false unless operator.present? && schema.key?(name.to_s)
+
+ schema[name].any? do |field|
+ field.operators.include?(operator.to_sym)
+ end
+ end
+
+end
@@ -12,12 +12,15 @@ module ClassMethods
# Constraint definition for a model. Example:
#
# class Post < ActiveRecord::Base
+ #
# constrainable do
# # Add your default constraints
# end
+ #
# constrainable :custom do
# # Add your custom constraints
# end
+ #
# end
#
def constrainable(name = nil, &block)
@@ -27,5 +30,10 @@ def constrainable(name = nil, &block)
_constrainable[name]
end
+ # Delegator to Relation#constrain
+ def constrain(*args)
+ relation.constrain(*args)
+ end
+
end
end
@@ -5,14 +5,24 @@ module Bsm::Constrainable::Relation
#
# Post.constrain("created_at" => { "lt" => "2011-01-01" }})
#
- # You can also combine it with multiple scopes:
+ # # Use "custom" constraints
+ # Post.constrain(:custom, "created_at" => { "lt" => "2011-01-01" }})
#
+ # # Combine it with relations & scopes
# Post.archived.includes(:author).constrain(params[:where]).paginate :page => 1
#
def constrain(*args)
- params = args.extract_options!
- schema = @klass.constrainable(args.first)
- schema.merge self, params
+ scope = args.first.is_a?(Symbol) ? args.shift : nil
+ filters = args.last
+
+ case filters
+ when Bsm::Constrainable::FilterSet
+ filters.merge(self)
+ when Hash
+ klass.constrainable(scope).filter(filters).merge(self)
+ else
+ self
+ end
end
end
@@ -1,6 +1,5 @@
# Schema definition.
class Bsm::Constrainable::Schema < Hash
- include ::Bsm::Constrainable::Util
Field = ::Bsm::Constrainable::Field
def initialize(klass)
@@ -57,17 +56,18 @@ def match(*names)
end
alias_method :field, :match
- def respond_to?(sym)
- super || Field.registered?(sym)
+ # Creates a FilterSet object for given params. Filter-sets can be used to
+ # constrain relations as well as e.g. in forms. Example:
+ #
+ # filters = Post.constrainable.filter(params[:where])
+ # Post.archived.constrain(filters).limit(100)
+ #
+ def filter(params = nil)
+ Bsm::Constrainable::FilterSet.new self, params
end
- def merge(relation, params)
- each_part(params) do |name, part|
- self[name].each do |constrain|
- relation = constrain.merge(relation, part)
- end
- end
- relation
+ def respond_to?(sym)
+ super || Field.registered?(sym)
end
protected
@@ -81,13 +81,4 @@ def method_missing(sym, *args)
end
end
- private
-
- def each_part(params)
- params = normalized_hash(params)
- params.slice(*keys).each do |name, part|
- yield(name, part.symbolize_keys) if part.is_a?(Hash)
- end
- end
-
end
@@ -0,0 +1,79 @@
+require "spec_helper"
+
+describe Bsm::Constrainable::FilterSet do
+
+ let :schema do
+ Post._constrainable[:default].clone
+ end
+
+ let :relation do
+ Post.send(:relation)
+ end
+
+ subject do
+ filter_set 'author_id' => { 'in' => ['1', '2', '3'], 'lt' => '4' }, 'created' => { 'gt' => '2010-10-10' }, 'invalid' => "TRUE", 'empty' => {}
+ end
+
+ def filter_set(params = nil)
+ described_class.new schema, params
+ end
+
+ it { should be_a(Hash) }
+
+ it 'should store schema' do
+ subject.schema.should == schema
+ end
+
+ it 'should normalize params' do
+ subject.should == {"author_id"=>{:in=>["1", "2", "3"], :lt=>"4"}, "created"=>{:gt=>"2010-10-10"}}
+ end
+
+ it 'should merge params into relations' do
+ sql = subject.merge(relation).to_sql
+ sql.clean_sql.should == "SELECT posts.* FROM posts WHERE posts.author_id IN (1, 2, 3) AND (posts.created_at > '2010-10-10 00:00:00')"
+ end
+
+ it 'should have accessors to schema keys' do
+ subject.should respond_to(:author_id)
+ subject.author_id.should == { :in=>["1", "2", "3"], :lt=>"4" }
+ end
+
+ it 'should have form-processable accessors' do
+ subject.should respond_to(:"author_id[in]")
+ subject.send(:"author_id[in]").should == ["1", "2", "3"]
+
+ subject.should respond_to("created[between]")
+ subject.send("created[between]").should be_nil
+
+ subject.should_not respond_to("author[gt]")
+ end
+
+ describe "in forms" do
+
+ let :template do
+ ActionView::Base.new
+ end
+
+ let :filters do
+ filter_set 'author_id' => { 'in' => ['1', '2'] }, 'created' => { 'between' => ['2010-10-10', '2011-11-11'] }
+ end
+
+ def form(&block)
+ builder = ::ActionView::Helpers::FormBuilder.new(:where, filters, template, {}, block)
+ HTML::Document.new(block.call(builder))
+ end
+
+ it 'should be usable' do
+ doc = form do |f|
+ f.select :"author_id[in]", [1,2,3,4,5]
+ end
+ input = doc.find(:tag => "select")
+ input['name'].should == "where[author_id[in]]"
+ choices = input.find_all(:tag => 'option')
+ choices.map {|n| n['value'] }.should =~ ['1', '2', '3', '4', '5']
+ choices.select {|n| n['selected'] }.map {|n| n['value'] }.should =~ ['1', '2']
+ end
+
+ end
+end
+
@@ -2,7 +2,7 @@
describe Bsm::Constrainable::Model do
- let(:model) do
+ let :model do
Class.new(ActiveRecord::Base)
end
@@ -27,5 +27,11 @@
model.constrainable.should be_a(Bsm::Constrainable::Schema)
end
+ it 'should delegate constrain to relation' do
+ sql = Post.constrain(:author_id => {:in => 1}).to_sql
+ sql.clean_sql.should == "SELECT posts.* FROM posts WHERE posts.author_id IN (1)"
+ end
+
+
end
@@ -2,18 +2,32 @@
describe Bsm::Constrainable::Relation do
- let(:relation) do
- Post.scoped
+ let :relation do
+ Post.send(:relation)
end
it 'should be includable' do
relation.should respond_to(:constrain)
end
- it 'should allow to apply constraints' do
+ it 'should apply constraints as parameters' do
sql = relation.constrain(:author_id => {:in => [1, 2, 3]}, :created => { :gt => '2010-10-10' }).to_sql
sql.clean_sql.should == "SELECT posts.* FROM posts WHERE posts.author_id IN (1, 2, 3) AND (posts.created_at > '2010-10-10 00:00:00')"
end
+ it 'should apply constraints as filter-sets' do
+ filters = Post.constrainable.filter(:author_id => {:in => 1})
+ sql = relation.constrain(filters).to_sql
+ sql.clean_sql.should == "SELECT posts.* FROM posts WHERE posts.author_id IN (1)"
+ end
+
+ it 'should apply scopes' do
+ sql = relation.constrain(:default, :author_id => {:in => 1}).to_sql
+ sql.clean_sql.should == "SELECT posts.* FROM posts WHERE posts.author_id IN (1)"
+
+ sql = relation.constrain(:missing, :author_id => {:in => 1}).to_sql
+ sql.clean_sql.should == "SELECT posts.* FROM posts"
+ end
+
end
@@ -35,5 +35,11 @@
subject.should have(6).items
end
+ it 'should create filter-sets for given params' do
+ fs = subject.filter 'author_id' => { 'in' => '1' }, 'created' => { 'gt' => '2010-10-10' }
+ fs.should be_a(Bsm::Constrainable::FilterSet)
+ fs.should == { "author_id"=>{:in=>"1"}, "created"=>{:gt=>"2010-10-10"} }
+ end
+
end
View
@@ -9,6 +9,7 @@
require 'active_support'
require 'active_record'
require 'active_record/fixtures'
+require 'action_view'
require 'rspec'
require 'rspec/rails/adapters'
require 'rspec/rails/fixture_support'

0 comments on commit bf86c57

Please sign in to comment.