Create a gist now

Instantly share code, notes, and snippets.

@clyfe /cancan.rb
Last active Sep 29, 2015

What would you like to do?
Monkey patch for CanCan 1.6.7, replaces MetaWhere with Squeel and more
# Setup
# =====
#
# Put this gist in Rails.root/config/initializers/cancan.rb
# Add Squeel to Gemfile, see https://github.com/ernie/squeel
#
# gem "squeel", "~> 0.9.3"
#
# Load Squeel hash and symbol extensions in squeel config initializer
#
# Squeel.configure do |config|
# config.load_core_extensions :hash, :symbol
# end
#
# then you can write
#
# can :manage, User, :permissions.outer => {:type.matches => 'Manage%'}}
#
# This should offer all the old MetaWhere capabilities,
# and extra, also allows outer joins
#
# you might also be interested in https://gist.github.com/1012332
# if you use MetaWhere
# https://gist.github.com/1523940
class String
include Squeel::Nodes::PredicateOperators
end
module Squeel
module Visitors
class PredicateVisitor < Visitor
def visit_String(o, parent)
Arel::Nodes::SqlLiteral.new(o)
end
end
end
end
module CanCan
module ModelAdapters
class ActiveRecordAdapter < AbstractAdapter
def self.override_condition_matching?(subject, name, value)
name.kind_of?(Squeel::Nodes::Predicate) if defined? Squeel
end
def self.matches_condition?(subject, name, value)
subject_value = subject.send(name.expr)
method_name = name.method_name.to_s
if method_name.ends_with? "_any"
value.any? { |v| squeel_match? subject_value, method_name.sub("_any", ""), v }
elsif method_name.ends_with? "_all"
value.all? { |v| squeel_match? subject_value, method_name.sub("_all", ""), v }
else
squeel_match? subject_value, name.method_name, value
end
end
def self.squeel_match?(subject_value, method, value)
case method.to_sym
when :eq then subject_value == value
when :not_eq then subject_value != value
when :in then value.include?(subject_value)
when :not_in then !value.include?(subject_value)
when :lt then subject_value < value
when :lteq then subject_value <= value
when :gt then subject_value > value
when :gteq then subject_value >= value
when :matches then subject_value =~ Regexp.new("^" + Regexp.escape(value).gsub("%", ".*") + "$", true)
when :does_not_match then !squeel_match?(subject_value, :matches, value)
else raise NotImplemented, "The #{method} Squeel condition is not supported."
end
end
# mostly let Squeel do the job in building the query
def conditions
if @rules.size == 1 && @rules.first.base_behavior
# Return the conditions directly if there's just one definition
@rules.first.conditions.dup
else
@rules.reverse.inject(false_sql) do |accumulator, rule|
conditions = rule.conditions.dup
if conditions.blank?
rule.base_behavior ? (accumulator | true_sql) : (accumulator & false_sql)
else
rule.base_behavior ? (accumulator | conditions) : (accumulator & -conditions)
end
end
end
end
private
# override to fix overwrites
# do not write existing hashes using empty hashes
def merge_joins(base, add)
add.each do |name, nested|
if base[name].is_a?(Hash) && nested.present?
merge_joins(base[name], nested)
elsif !base[name].is_a?(Hash) || nested.present?
base[name] = nested
end
end
end
end
end
class Rule # allow Squeel
def matches_conditions_hash?(subject, conditions = @conditions)
return true if conditions.empty?
conditions.all? do |name, value|
if model_adapter(subject).override_condition_matching? subject, name, value
model_adapter(subject).matches_condition? subject, name, value
else
method_name = case name
when Symbol then name
when Squeel::Nodes::Join then name._name
when Squeel::Nodes::Predicate then name.expr
else raise name
end
attribute = subject.send(method_name)
if value.kind_of?(Hash)
case attribute
when Array, ActiveRecord::Associations::CollectionProxy
attribute.any? { |element| matches_conditions_hash? element, value }
else
!attribute.nil? && matches_conditions_hash?(attribute, value)
end
elsif value.kind_of?(Array) || value.kind_of?(Range)
value.include? attribute
else
attribute == value
end
end
end
end
end
end

gamov commented Feb 14, 2012

I'm interested in Cancan 2.0 compatibility too. I'll test and report.

Owner

clyfe commented Feb 14, 2012

The main difference with CanCan 2.0 would be made by Resource Attributes, meaning the queries should take them into account I belive. Otherwise things should be similar.

kirel commented Mar 22, 2012

For belongs_to :custom_name, class_name: ClassName I had to make some changes (notably get tableized_conditions back into the mix) - see https://gist.github.com/2157530

vddgil commented Sep 6, 2013

Hello clyfe, i'm using your Gist to integrate squeel DSL in cancan abilities and it's pretty great.
But i can't figure out how to make a more complex condition like this one:

(:key.not_in => ["invoice.created", "credit_note.created", "estimate.created"]) & (:parameters.does_not_match => ":private: true")

I keet getting a syntax error.

Can you help me,

Thanks in advance !

vddgil commented Sep 25, 2013

I found the solution, I tried a different syntax:

:key.not_in(["invoice.created", "credit_note.created", "estimate.created"]) & :parameters.does_not_match("%:private: true%")'
Owner

clyfe commented Oct 2, 2013

I think this might had worked as a hash based solution, since the default semantics of juxtaposition is "AND"

{
  :key.not_in => ["invoice.created", "credit_note.created", "estimate.created"],
  :parameters.does_not_match => ":private: true"
}

edestecd commented Dec 4, 2013

Forked and updated for cancan 1.6.10.
It was throwing exceptions since the patch:
fix namespace controllers not loading params (thanks andhapp) - issues #670, #664

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment