Skip to content

Latest commit

 

History

History
210 lines (139 loc) · 7.64 KB

model_adapter.md

File metadata and controls

210 lines (139 loc) · 7.64 KB

Model Adapter

CanCanCan includes a model adapter system that allows developers to add their own adapters for handling behaviour depending on the model used.

CanCanCan provides maintained adapters for the following model types:

  • ActiveRecord (native in cancancan gem)
    • ActiveRecord 4
    • ActiveRecord 5
  • Mongoid

Creating a Model Adapter

Due to its flexible and extendable system of adapters, it is easy to implement a custom adapter if the currently provided adapters do not suffice.

To facilitate an easy implementation of a new adapter CanCanCan provides you with an Abstract Adapter you can extend and build upon. This design allows for dynamic adapter handling and a decoupled handling of information.

The Abstract Adapter

The abstract adapter has multiple methods that one has to overwrite in order to match the behaviour that is expected. It is used by the system to delegate the handling of fetching entries base on defined rules and conditions.

for_class

The for_class? method is a static method on the abstract adapter that has to be overwritten in your adapter.

This method is used to determine whether a model should be passed to the adapter or not.

If your for_class? implementation returns true, the adapter will be provided with the model to build and match the rules defined.

Otherwise the adapter will be skipped and the other subclasses of the abstract adapter will be checked.

database_records

Used to implement the loading of entries from the database, by a developer-defined handling of the given rules for a model.

Dependencies

Because cancancan wants to provide an easy method of writing and testing your own adapters it uses appraisals to test the code against different versions of dependencies.

Appraisals

Thus you can add your own entry for your gems and dependencies.

An example could look like:

cancancan/Appraisals

appraise 'cancancan_custom_adapter' do
  gem 'activerecord', '~> 5.0.2', require: 'active_record'

  gemfile.platforms :jruby do
    gem 'jdbc-postgres'
  end

  gemfile.platforms :ruby, :mswin, :mingw do
    gem 'pg', '~> 0.21'
  end
end

You would have to replace the dependencies with ones that fit your custom adapter.

After creating your dependency definition, run

bundle exec appraisal install

to install dependencies for your adapter.

The Specs

To illustrate what a test for an adapter could look like, we will use Mongoid as an example.

In good TDD fashion we create a spec / test for the new adapter to later confirm our implementation.

RSpec.describe CanCan::ModelAdapters::MongoidAdapter do

  it 'is for only Mongoid classes' do
    expect(CanCan::ModelAdapters::MongoidAdapter).not_to be_for_class(Object)
    expect(CanCan::ModelAdapters::MongoidAdapter).to be_for_class(MongoidProject)
  end

  it 'finds record' do
      project = MongoidProject.create
      expect(CanCan::ModelAdapters::MongoidAdapter.find(MongoidProject, project.id)).to eq(project)
  end

  it "should return the correct records based on the defined ability" do
        @ability.can :read, MongoidProject, :title => "Sir"
        sir   = MongoidProject.create(:title => 'Sir')
        lord  = MongoidProject.create(:title => 'Lord')
        MongoidProject.accessible_by(@ability, :read).entries.should == [sir]
  end

end

In this case MongoidProject is a descendant of MongoidDocument. The implementation of this class will not be shown as it only acts as an example.

Running tests

You can run tests for the project by running

bundle exec appraisal rake

or you can run tests only for your adapter with

bundle exec appraisal adapter_name rake

File specific tests can be run with:

bundle exec appraisal adapter_name rspec spec/cancan/model_adapters/adapter_name.rb

Because we haven't implemented any functionality yet, the tests will fail.

The Implementation

First add a line to lib/cancan.rb to include the adapter if a condition is met. In this case we check if Mongoid is present.

require 'cancan/model_adapters/mongoid_adapter' if defined? Mongoid

And after that, create a new adapter in model_adapters:

module CanCan
  module ModelAdapters
    class MongoidAdapter < AbstractAdapter
      def self.for_class?(model_class)
        model_class <= Mongoid::Document
      end

      def database_records
        if @rules.size == 0
          @model_class.where(:_id => {'$exists' => false, '$type' => 7}) # return no records in Mongoid
        else
          @rules.inject(@model_class.all) do |records, rule|
            if rule.base_behavior
              records.or(rule.conditions)
            else
              records.excludes(rule.conditions)
            end
          end
        end
      end
    end
  end
end

module Mongoid::Document::ClassMethods
  include CanCan::ModelAdditions::ClassMethods
end

As mentioned before, there are methods that have to be overwritten in order to pass as a valid adapter.

In this case we overwrite the for_class? method to validate that the given model is a descendant of MongoidDocument. The adapter will only be used if for_class? evalues to true.

And in database_records we define the way data is loaded from the storage device. This message is used in accessible_by. In this example we fetch all entries for a model that match a given rule.

If no rules for an object are defined, a query will be run that returns no results.

If rules are present, we apply each of the rule conditions to them. The rule.base_behavior defines whether the rule should be additive or subtractive. It will result in false for :cannot and true for :can.

Some model types add additional features to the conditions hash. With Mongoid, for example, you can do something like :age.gt => 13. Because the abstract adapter has no knowledge of this, we have to overwrite the provided methods in the new adapter.

def self.override_conditions_hash_matching?(subject, conditions)
  conditions.any? { |k,v| !k.kind_of?(Symbol) }
end

def self.matches_conditions_hash?(subject, conditions)
  subject.matches? subject.class.where(conditions).selector
end

Additional Examples

Eventhough CanCanCan tries to make the implementation of custom adapters easy and flexible, it can be hard task.

Thus you'd probably be best served with inspecting the actual implementation of the activerecord adapter to get a better overview how a battle tested adapter is structured and implemented.

Implementation

Tests / Specs

Mongoid, the adapter used in this entry as an example, can be found at: