├── .document ├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE ├── README.rdoc ├── Rakefile ├── VERSION ├── lib ├── role_model.rb └── role_model │ ├── class_methods.rb │ ├── implementation.rb │ └── roles.rb ├── role_model.gemspec └── spec ├── custom_matchers.rb ├── custom_matchers_spec.rb ├── role_model_spec.rb ├── roles_spec.rb └── spec_helper.rb /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## BUNDLE 17 | .bundle 18 | 19 | ## PROJECT::GENERAL 20 | coverage 21 | rdoc 22 | pkg 23 | 24 | ## PROJECT::SPECIFIC 25 | .rvmrc 26 | Gemfile.lock 27 | vendor 28 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | - 2.0.0 4 | - 2.1.1 5 | - jruby 6 | notifications: 7 | recipients: 8 | - martin.rehfeld@glnetworks.de 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Martin Rehfeld 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = RoleModel 2 | 3 | RoleModel is the framework agnostic, efficient and declarative way to do 4 | (user) roles. Assigned roles will be efficiently stored as a bitmask 5 | directly into your model within a configurable attribute. 6 | 7 | It works like this: 8 | 9 | # given a User class with a roles_mask attribute 10 | require 'rubygems' 11 | require 'role_model' 12 | 13 | class User 14 | attr_accessor :roles_mask # just for demo purposes 15 | # in real life this would usually be a persistent attribute, 16 | # e.g. if your User model is persisted in a SQL-DB add an integer 17 | # column named roles_mask to your users table -- just remove/replace 18 | # above attr_accessor line with whatever is needed for your 19 | # persistence solution 20 | 21 | include RoleModel 22 | 23 | # if you want to use a different integer attribute to store the 24 | # roles in, set it with roles_attribute :my_roles_attribute, 25 | # :roles_mask is the default name 26 | roles_attribute :roles_mask 27 | 28 | # declare the valid roles -- do not change the order if you add more 29 | # roles later, always append them at the end! 30 | # 31 | # Set dynamic: false to skip creating the dynamic methods for the roles. 32 | roles :admin, :manager, :author, prefix: "is_" 33 | end 34 | 35 | # 36 | # Test drive (check the RDoc or source for additional finesse) 37 | # 38 | 39 | >> u = User.new 40 | => # 41 | 42 | # role assignment 43 | >> u.roles = [:admin] # ['admin'] works as well 44 | => [:admin] 45 | 46 | # adding roles 47 | >> u.roles << :manager 48 | >> u.roles.add(:manager) 49 | >> u.manager = true # if dynamic is enabled (by default) or... 50 | >> u.is_manager = true # If :prefix => "is_" 51 | => [:admin, :manager] 52 | 53 | # deleting roles 54 | >> u.roles.delete(:manager) 55 | >> u.manager = false # if dynamic is enabled (by default) or... 56 | >> u.is_manager = false # If :prefix => "is_" 57 | => [:admin] 58 | 59 | # querying roles... 60 | 61 | # get all valid roles that have been declared 62 | >> User.valid_roles 63 | => [:admin, :manager, :author] 64 | 65 | # ... retrieve all assigned roles 66 | >> u.roles # also: u.role_symbols for DeclarativeAuthorization compatibility 67 | => [:admin, :manager] 68 | 69 | # ... check for individual roles 70 | >> u.has_role? :author # has_role? is also aliased to is? 71 | => false 72 | 73 | # ... check for individual roles with dynamic methods (set dynamic: false to disable) 74 | >> u.author? # Or... 75 | >> u.is_author? # If :prefix => "is_" 76 | => false 77 | 78 | # ... check for multiple roles 79 | >> u.has_any_role? :author, :manager # has_any_role? is also aliased to is_any_of? 80 | => true 81 | 82 | >> u.has_all_roles? :author, :manager # has_all_roles? is also aliased to is? 83 | => false 84 | 85 | # see the internal bitmask representation (3 = 0b0011) 86 | >> u.roles_mask 87 | => 3 88 | 89 | # see the role mask for certain role(s) 90 | >> User.mask_for :admin, :author 91 | => 5 92 | 93 | Once you have included RoleModel, your model is perfectly fit to be used 94 | together with a role-based authorization solution, such as 95 | http://github.com/ryanb/cancan or 96 | http://github.com/stffn/declarative_authorization . 97 | 98 | == Installation 99 | 100 | gem install role_model 101 | 102 | == Reasoning 103 | 104 | Whenever I introduce a role-based authorization scheme into a project, the 105 | code gets coupled somehow to the available roles. So it usually does not make 106 | any sense to have a separate Role model stored within the database. This Role 107 | model will contain a predefined set of roles anyhow -- changing that set will 108 | need to be reflected within the authorization code. Putting the available 109 | roles right into the model that actually uses them, makes things much easier 110 | and efficient. 111 | 112 | == Note on Patches/Pull Requests 113 | 114 | * Fork the project. 115 | * Make your feature addition or bug fix. 116 | * Add tests for it. This is important so I don't break it in a 117 | future version unintentionally. 118 | * Commit, do not mess with Rakefile, version, or history. 119 | (if you want to have your own version, that is fine but bump version in a 120 | commit by itself I can ignore when I pull) 121 | * Send me a pull request. Bonus points for topic branches. 122 | 123 | == Credits 124 | 125 | RoleModel is an implementation of the Role Based Authorization scheme 126 | proposed by Ryan Bates 127 | (http://wiki.github.com/ryanb/cancan/role-based-authorization). 128 | 129 | == Copyright 130 | 131 | Copyright (c) 2010 Martin Rehfeld. See LICENSE for details. 132 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | begin 4 | Bundler.setup(:default, :development) 5 | rescue Bundler::BundlerError => e 6 | $stderr.puts e.message 7 | $stderr.puts "Run `bundle install` to install missing gems" 8 | exit e.status_code 9 | end 10 | require 'rake' 11 | 12 | require 'rspec/core' 13 | require 'rspec/core/rake_task' 14 | RSpec::Core::RakeTask.new(:spec) do |spec| 15 | spec.pattern = FileList['spec/**/*_spec.rb'] 16 | end 17 | 18 | RSpec::Core::RakeTask.new(:rcov) do |spec| 19 | spec.pattern = 'spec/**/*_spec.rb' 20 | spec.rcov = true 21 | end 22 | 23 | task :default => :spec 24 | 25 | require 'rdoc/task' 26 | RDoc::Task.new do |rdoc| 27 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 28 | 29 | rdoc.rdoc_dir = 'rdoc' 30 | rdoc.title = "role_model #{version}" 31 | rdoc.rdoc_files.include('README*') 32 | rdoc.rdoc_files.include('lib/**/*.rb') 33 | end 34 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.8.2 2 | -------------------------------------------------------------------------------- /lib/role_model.rb: -------------------------------------------------------------------------------- 1 | require 'role_model/implementation' 2 | require 'role_model/class_methods' 3 | require 'role_model/roles' 4 | 5 | module RoleModel 6 | 7 | INHERITABLE_CLASS_ATTRIBUTES = [:roles_attribute_name, :valid_roles] 8 | 9 | include Implementation 10 | 11 | def self.included(base) # :nodoc: 12 | base.extend ClassMethods 13 | base.class_eval do 14 | class << self 15 | attr_accessor(*::RoleModel::INHERITABLE_CLASS_ATTRIBUTES) 16 | end 17 | roles_attribute :roles_mask # set default bitmask attribute 18 | self.valid_roles = [] 19 | end 20 | end 21 | 22 | end 23 | -------------------------------------------------------------------------------- /lib/role_model/class_methods.rb: -------------------------------------------------------------------------------- 1 | module RoleModel 2 | module ClassMethods 3 | def inherited(subclass) # :nodoc: 4 | ::RoleModel::INHERITABLE_CLASS_ATTRIBUTES.each do |attribute| 5 | instance_var = "@#{attribute}" 6 | subclass.instance_variable_set(instance_var, instance_variable_get(instance_var)) 7 | end 8 | super 9 | end 10 | 11 | # set the bitmask attribute role assignments will be stored in 12 | def roles_attribute(name) 13 | self.roles_attribute = name 14 | end 15 | 16 | # alternative method signature: set the bitmask attribute role assignments will be stored in 17 | def roles_attribute=(name) 18 | self.roles_attribute_name = name.to_sym 19 | end 20 | 21 | def mask_for(*roles) 22 | sanitized_roles = roles.map { |role| Array(role) }.flatten.map(&:to_sym) 23 | 24 | (valid_roles & sanitized_roles).inject(0) { |sum, role| sum + 2**valid_roles.index(role) } 25 | end 26 | 27 | def roles_from_mask(mask) 28 | valid_roles.reject { |role| (mask.to_i & 2**valid_roles.index(role)).zero? } 29 | end 30 | 31 | protected 32 | 33 | # :call-seq: 34 | # roles(:role_1, ..., :role_n) 35 | # roles('role_1', ..., 'role_n') 36 | # roles([:role_1, ..., :role_n]) 37 | # roles(['role_1', ..., 'role_n']) 38 | # 39 | # declare valid roles 40 | def roles(*roles) 41 | opts = roles.last.is_a?(Hash) ? roles.pop : {} 42 | self.valid_roles = roles.flatten.map(&:to_sym) 43 | unless (opts[:dynamic] == false) 44 | self.define_dynamic_queries(self.valid_roles, opts[:prefix]) 45 | end 46 | end 47 | 48 | # Defines dynamic queries for :role. Providing a prefix is optional. 49 | # #<:role>? 50 | # #<:role>=(true|false) 51 | # 52 | # Defines new methods which call #is?(:role) and roles.(add|remove) 53 | def define_dynamic_queries(roles, prefix=nil) 54 | dynamic_module = Module.new do 55 | roles.each do |role| 56 | define_method("#{prefix}#{role}?") { is? role } 57 | define_method("#{prefix}#{role}=") { |x| self.roles.send(x ? :add : :delete, role) } 58 | end 59 | end 60 | include dynamic_module 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/role_model/implementation.rb: -------------------------------------------------------------------------------- 1 | module RoleModel 2 | module Implementation 3 | 4 | # assign roles 5 | def roles=(*roles) 6 | self.send("#{self.class.roles_attribute_name}=", self.class.mask_for(*roles)) 7 | end 8 | 9 | # query assigned roles 10 | def roles 11 | Roles.new(self, self.class.valid_roles.reject { |r| ((self.send(self.class.roles_attribute_name) || 0) & 2**self.class.valid_roles.index(r)).zero? }) 12 | end 13 | 14 | # query assigned roles returning an Array for the 15 | # declarative_authorization gem 16 | def role_symbols 17 | roles.to_a 18 | end 19 | 20 | # :call-seq: 21 | # has_all_roles?(:role) 22 | # has_all_roles?('role') 23 | # has_all_roles?(:role_1, ..., :role_n) 24 | # has_all_roles?('role_1', ..., 'role_n') 25 | # has_all_roles?([:role_1, ..., :role_n]) 26 | # has_all_roles?(['role_1', ..., 'role_n']) 27 | # 28 | # check if ALL of the given roles have been assigned 29 | # this method is aliased as #is? and #has_roles? 30 | def has_all_roles?(*roles) 31 | roles.flatten.map(&:to_sym).all? { |r| self.roles.include?(r) } 32 | end 33 | alias_method :is?, :has_all_roles? 34 | alias_method :has_roles?, :has_all_roles? 35 | 36 | # :call-seq: 37 | # has_any_role?(:role) 38 | # has_any_role?('role') 39 | # has_any_role?(:role_1, ..., :role_n) 40 | # has_any_role?('role_1', ..., 'role_n') 41 | # has_any_role?([:role_1, ..., :role_n]) 42 | # has_any_role?(['role_1', ..., 'role_n']) 43 | # 44 | # check if any (at least ONE) of the given roles have been assigned 45 | # this method is aliased as #is_any_of? and #has_role? 46 | def has_any_role?(*roles) 47 | roles.flatten.map(&:to_sym).any? { |r| self.roles.include?(r) } 48 | end 49 | alias_method :is_any_of?, :has_any_role? 50 | alias_method :has_role?, :has_any_role? 51 | 52 | # :call-seq: 53 | # has_only_roles?(:role) 54 | # has_only_roles?('role') 55 | # has_only_roles?(:role_1, ..., :role_n) 56 | # has_only_roles?('role_1', ..., 'role_n') 57 | # has_only_roles?([:role_1, ..., :role_n]) 58 | # has_only_roles?(['role_1', ..., 'role_n']) 59 | # 60 | # check if ONLY of the given roles have been assigned 61 | # this method is aliased as #is_exactly? 62 | def has_only_roles?(*roles) 63 | self.send("#{self.class.roles_attribute_name}") == self.class.mask_for(*roles) 64 | end 65 | alias_method :is_exactly?, :has_only_roles? 66 | 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/role_model/roles.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module RoleModel 4 | class Roles < ::Set # :nodoc: 5 | attr_reader :model_instance 6 | 7 | def initialize(sender, roles) 8 | super(roles) 9 | @model_instance = sender 10 | end 11 | 12 | def add(role) 13 | roles = super 14 | model_instance.roles = roles if model_instance 15 | self 16 | end 17 | 18 | alias_method :<<, :add 19 | 20 | def delete(role) 21 | if role.is_a? Array 22 | model_instance.roles = subtract(role) 23 | else 24 | model_instance.roles = super(role.to_sym) 25 | end 26 | self 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /role_model.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "role_model" 3 | s.version = "0.8.2" 4 | 5 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 6 | s.require_paths = ["lib"] 7 | s.authors = ["Martin Rehfeld"] 8 | s.date = "2015-02-12" 9 | s.description = "Ever needed to assign roles to a model, say a User, and build conditional behaviour on top of that? Enter RoleModel -- roles have never been easier! Just declare your roles and you are done. Assigned roles will be stored as a bitmask." 10 | s.email = "martin.rehfeld@glnetworks.de" 11 | s.extra_rdoc_files = [ 12 | "LICENSE", 13 | "README.rdoc" 14 | ] 15 | s.files = [ 16 | ".document", 17 | ".rspec", 18 | ".travis.yml", 19 | "Gemfile", 20 | "LICENSE", 21 | "README.rdoc", 22 | "Rakefile", 23 | "VERSION", 24 | "lib/role_model.rb", 25 | "lib/role_model/class_methods.rb", 26 | "lib/role_model/implementation.rb", 27 | "lib/role_model/roles.rb", 28 | "role_model.gemspec", 29 | "spec/custom_matchers.rb", 30 | "spec/custom_matchers_spec.rb", 31 | "spec/role_model_spec.rb", 32 | "spec/roles_spec.rb", 33 | "spec/spec_helper.rb" 34 | ] 35 | s.homepage = "http://github.com/martinrehfeld/role_model" 36 | s.licenses = ["MIT"] 37 | s.rubygems_version = "2.2.2" 38 | s.summary = "Declare, assign and query roles with ease" 39 | 40 | s.add_development_dependency(%q, ["< 11.0.1"]) 41 | s.add_development_dependency(%q, ["2.99.0"]) 42 | s.add_development_dependency(%q, [">= 2.4.2"]) 43 | end 44 | 45 | -------------------------------------------------------------------------------- /spec/custom_matchers.rb: -------------------------------------------------------------------------------- 1 | module CustomMatchers 2 | class ArrayIncludingMatcher 3 | def initialize(*expected) 4 | @expected = Array[*expected] 5 | end 6 | 7 | def ==(actual) 8 | return false if actual.size < @expected.size 9 | @expected.each do | value | 10 | return false unless actual.any? { |actual_value| value == actual_value } 11 | end 12 | true 13 | rescue NoMethodError => ex 14 | return false 15 | end 16 | 17 | def description 18 | "array_including(#{@expected.inspect.sub(/^\[/,"").sub(/\]$/,"")})" 19 | end 20 | end 21 | 22 | def array_including(*args) 23 | ArrayIncludingMatcher.new(*args) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/custom_matchers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module CustomMatchers 4 | describe ArrayIncludingMatcher do 5 | 6 | it "should describe itself properly" do 7 | ArrayIncludingMatcher.new(:a, :b).description.should == "array_including(:a, :b)" 8 | end 9 | 10 | describe "passing" do 11 | it "should match the same array" do 12 | array_including(:a).should == [:a] 13 | end 14 | 15 | it "should match a array with extra stuff" do 16 | array_including(:a).should == [:a, :b] 17 | end 18 | 19 | it "should match a array regardless of element position" do 20 | array_including(:a, :b).should == [:b, :a] 21 | end 22 | 23 | describe "when matching against other matchers" do 24 | it "should match a symbol against anything()" do 25 | array_including(anything, :b).should == [:a, :b] 26 | end 27 | 28 | it "should match an int against anything()" do 29 | array_including(anything, :b).should == [1, :b] 30 | end 31 | 32 | it "should match a string against anything()" do 33 | array_including(anything, :b).should == ["1", :b] 34 | end 35 | 36 | it "should match an arbitrary object against anything()" do 37 | array_including(anything, :b).should == [Class.new.new, :b] 38 | end 39 | end 40 | end 41 | 42 | describe "failing" do 43 | it "should not match a non-array" do 44 | array_including(:a).should_not == :a 45 | end 46 | 47 | it "should not match a array with a missing element" do 48 | array_including(:a).should_not == [:b] 49 | end 50 | 51 | it "should not match an empty array with a given key" do 52 | array_including(:a).should_not == [] 53 | end 54 | 55 | describe "when matching against other matchers" do 56 | it "should not match without additional elements" do 57 | array_including(anything, 1).should_not == [1] 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/role_model_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RoleModel do 4 | 5 | let(:model_class) { Class.new } 6 | let(:role_options) { {} } 7 | 8 | before(:each) do 9 | role_options = self.role_options # must be defined outside scope. 10 | model_class.class_eval do 11 | attr_accessor :roles_mask 12 | attr_accessor :custom_roles_mask 13 | include RoleModel 14 | roles :foo, :bar, :third, role_options 15 | end 16 | end 17 | 18 | [:roles_attribute, :roles_attribute=].each do |roles_attribute_setter_method| 19 | describe ".#{roles_attribute_setter_method}" do 20 | before(:each) do 21 | model_class.instance_eval do 22 | send(roles_attribute_setter_method, :custom_roles_mask) 23 | roles :sample 24 | end 25 | end 26 | subject { model_class.new } 27 | 28 | it "should change the bitmask attribute used to store the assigned roles" do 29 | subject.roles = [:sample] 30 | subject.roles_mask.should be_nil 31 | subject.custom_roles_mask.should == 1 32 | end 33 | end 34 | end 35 | 36 | describe ".roles" do 37 | subject { model_class.new } 38 | it "should define the valid roles" do 39 | subject.roles = %w(foo bar baz) 40 | subject.roles.should include(:foo, :bar) 41 | subject.roles.should_not include(:baz) 42 | end 43 | 44 | it "should accept an argument list with roles" do 45 | 46 | end 47 | 48 | it "should define the bitvalue of each role by position" do 49 | subject.roles = :foo 50 | subject.roles_mask.should == 1 51 | subject.roles = :bar 52 | subject.roles_mask.should == 2 53 | end 54 | end 55 | 56 | describe '.roles_from_mask(mask)' do 57 | subject { model_class.roles_from_mask(mask) } 58 | 59 | before do 60 | model_class.instance_eval do 61 | include RoleModel 62 | roles :foo, :bar, :third 63 | end 64 | end 65 | 66 | context 'when roles_mask is 0' do 67 | let(:mask) { 0 } 68 | it 'returns empty set' do 69 | subject.should eq [] 70 | end 71 | end 72 | 73 | context 'when valid roles_mask' do 74 | let(:mask) { 3 } 75 | it 'returns the list of roles associated with a roles_mask' do 76 | subject.should eq [:foo, :bar] 77 | end 78 | end 79 | end 80 | 81 | [:roles, :role_symbols].each do |role_query_method| 82 | describe "##{role_query_method}" do 83 | let(:model) { model_class.new } 84 | subject { model.send(role_query_method) } 85 | 86 | it "should return the assigned roles as symbols" do 87 | model.roles = [:foo, :bar] 88 | subject.should include(:foo, :bar) 89 | expect(subject.size).to eq(2) 90 | end 91 | 92 | it "should be empty when no roles have been assigned" do 93 | subject.should be_empty 94 | end 95 | 96 | if role_query_method == :role_symbols 97 | # DeclarativeAuthorization specifically wants an Array 98 | it { should be_an(Array) } 99 | end 100 | end 101 | end 102 | 103 | describe "#roles=" do 104 | subject { model_class.new } 105 | 106 | it "should accept an array of symbols" do 107 | subject.roles = [:foo, :bar] 108 | subject.roles.should include(:foo, :bar) 109 | expect(subject.roles.size).to eq(2) 110 | subject.roles = [:bar] 111 | subject.roles.should include(:bar) 112 | expect(subject.roles.size).to eq(1) 113 | end 114 | 115 | it "should accept an array of strings" do 116 | subject.roles = %w(foo bar) 117 | subject.roles.should include(:foo, :bar) 118 | expect(subject.roles.size).to eq(2) 119 | subject.roles = ['bar'] 120 | subject.roles.should include(:bar) 121 | expect(subject.roles.size).to eq(1) 122 | end 123 | 124 | it "should accept a single symbol" do 125 | subject.roles = :foo 126 | subject.roles.should include(:foo) 127 | expect(subject.roles.size).to eq(1) 128 | end 129 | 130 | it "should accept a single string" do 131 | subject.roles = 'foo' 132 | subject.roles.should include(:foo) 133 | expect(subject.roles.size).to eq(1) 134 | end 135 | 136 | it "should accept multiple arguments as symbols" do 137 | subject.send(:roles=, :foo, :bar) 138 | subject.roles.should include(:foo, :bar) 139 | expect(subject.roles.size).to eq(2) 140 | end 141 | 142 | it "should accept multiple arguments as strings" do 143 | subject.send(:roles=, 'foo', 'bar') 144 | subject.roles.should include(:foo, :bar) 145 | expect(subject.roles.size).to eq(2) 146 | end 147 | 148 | it "should silently ignore undefined roles" do 149 | subject.roles = :baz 150 | subject.roles.should be_empty 151 | end 152 | 153 | context "with previouly assigned roles" do 154 | before(:each) do 155 | subject.roles = :foo 156 | subject.roles.should include(:foo) 157 | expect(subject.roles.size).to eq(1) 158 | end 159 | 160 | it "should set set assigned roles regardless overwriting previouly assigned roles" do 161 | subject.roles = :bar 162 | subject.roles.should include(:bar) 163 | expect(subject.roles.size).to eq(1) 164 | end 165 | 166 | it "should allow reassigning the #roles value aka Roles object" do 167 | subject.roles << :bar 168 | subject.roles = subject.roles 169 | subject.roles.should include(:foo, :bar) 170 | expect(subject.roles.size).to eq(2) 171 | end 172 | end 173 | end 174 | 175 | describe "#<<" do 176 | subject { model_class.new } 177 | 178 | context "with roles :foo and :bar already assigned" do 179 | before(:each) do 180 | subject.roles = [:foo, :bar] 181 | end 182 | 183 | it "should add a role given as a symbol" do 184 | subject.roles << :third 185 | subject.roles.should include(:foo, :bar, :third) 186 | expect(subject.roles.size).to eq(3) 187 | end 188 | 189 | it "should add a role given as a string" do 190 | subject.roles << 'third' 191 | subject.roles.should include(:foo, :bar, :third) 192 | expect(subject.roles.size).to eq(3) 193 | end 194 | 195 | it "should not show a role twice in the return value" do 196 | subject.roles << :foo 197 | expect(subject.roles.size).to eq(2) 198 | end 199 | end 200 | 201 | context "without any previouly assigned roles" do 202 | it "should add a single symbol" do 203 | subject.roles << :foo 204 | subject.roles.should include(:foo) 205 | expect(subject.roles.size).to eq(1) 206 | end 207 | 208 | it "should add a single string" do 209 | subject.roles << 'foo' 210 | subject.roles.should include(:foo) 211 | expect(subject.roles.size).to eq(1) 212 | end 213 | 214 | it "should allow chaining of <<" do 215 | subject.roles << :foo << :bar 216 | subject.roles.should include(:foo, :bar) 217 | expect(subject.roles.size).to eq(2) 218 | end 219 | 220 | it "should silently ignore undefined roles" do 221 | subject.roles << :baz 222 | subject.roles.should be_empty 223 | end 224 | end 225 | end 226 | 227 | describe "#delete" do 228 | subject { model_class.new } 229 | 230 | context "with roles :foo and :bar already assigned" do 231 | before(:each) do 232 | subject.roles = [:foo, :bar] 233 | end 234 | 235 | it "should delete a existing role given as a symbol" do 236 | subject.roles.delete(:foo) 237 | subject.roles.should_not include(:foo) 238 | expect(subject.roles.size).to eq 1 239 | end 240 | 241 | it "should delete a existing role given as a string" do 242 | subject.roles.delete('foo') 243 | subject.roles.should_not include(:foo) 244 | expect(subject.roles.size).to eq 1 245 | end 246 | 247 | it "should delete existing roles given as an array of symbols" do 248 | subject.roles.delete [:foo, :bar] 249 | expect(subject.roles).not_to include([:foo, :bar]) 250 | expect(subject.roles.size).to eq 0 251 | end 252 | 253 | it "should delete existing roles given as an array of strings" do 254 | subject.roles.delete %w(foo bar) 255 | expect(subject.roles).not_to include([:foo, :bar]) 256 | expect(subject.roles.size).to eq(0) 257 | end 258 | 259 | it "should not change anything if a non existing role is given" do 260 | subject.roles.delete(:third) 261 | subject.roles.should include(:foo, :bar) 262 | expect(subject.roles.size).to eq(2) 263 | end 264 | end 265 | 266 | context "without roles assigned" do 267 | it "should have 0 roles if a role is given as a symbol" do 268 | subject.roles.delete(:foo) 269 | expect(subject.roles.size).to eq(0) 270 | end 271 | 272 | it "should have 0 roles if a role is given as a string" do 273 | subject.roles.delete('foo') 274 | expect(subject.roles.size).to eq(0) 275 | end 276 | end 277 | end 278 | 279 | describe "#-=" do 280 | subject { model_class.new } 281 | 282 | context "with roles :foo and :bar already assigned" do 283 | before(:each) do 284 | subject.roles = [:foo, :bar] 285 | end 286 | 287 | it "should delete a existing role given as a symbol" do 288 | subject.roles -= [:foo] 289 | subject.roles.should_not include(:foo) 290 | expect(subject.roles.size).to eq(1) 291 | end 292 | 293 | it "should delete a existing role given as a string" do 294 | subject.roles -= ['foo'] 295 | subject.roles.should_not include(:foo) 296 | expect(subject.roles.size).to eq(1) 297 | end 298 | 299 | it "should not change anything if a non existing role is given" do 300 | subject.roles -= [:third] 301 | subject.roles.should include(:foo, :bar) 302 | expect(subject.roles.size).to eq(2) 303 | end 304 | end 305 | 306 | context "without roles assigned" do 307 | it "should have 0 roles if a role is given as a symbol" do 308 | subject.roles -= [:foo] 309 | expect(subject.roles.size).to eq(0) 310 | end 311 | 312 | it "should have 0 roles if a role is given as a string" do 313 | subject.roles -= ['foo'] 314 | expect(subject.roles.size).to eq(0) 315 | end 316 | end 317 | end 318 | 319 | context "query for an individual role" do 320 | [:has_any_role?, :is_any_of?, :has_role?, :has_all_roles?, :is?, :has_roles?].each do |check_role_assignment_method| 321 | describe "##{check_role_assignment_method}" do 322 | subject { model_class.new } 323 | 324 | it "should return true when the given role was assigned" do 325 | subject.roles = :foo 326 | subject.send(check_role_assignment_method, :foo).should be true 327 | end 328 | 329 | it "should return false when the given role was not assigned" do 330 | subject.roles = :bar 331 | subject.send(check_role_assignment_method, :foo).should be false 332 | end 333 | 334 | it "should return false when no role was assigned" do 335 | subject.send(check_role_assignment_method, :foo).should be false 336 | subject.send(check_role_assignment_method, :bar).should be false 337 | end 338 | 339 | it "should return false when asked for an undefined role" do 340 | subject.send(check_role_assignment_method, :baz).should be false 341 | end 342 | end 343 | end 344 | end 345 | 346 | describe "dynamically query for an individual role" do 347 | subject { model_class.new } 348 | 349 | it "should return true when the given role was assigned" do 350 | subject.foo = true 351 | subject.foo?.should be true 352 | end 353 | 354 | it "should return false when the given role was not assigned" do 355 | subject.foo = false 356 | subject.foo?.should be false 357 | end 358 | 359 | it "should return false when no role was assigned" do 360 | subject.bar?.should be false 361 | end 362 | 363 | it "should throw NoMethodError when asked for an undefined role" do 364 | lambda { subject.baz? }.should raise_error(NoMethodError) 365 | end 366 | 367 | it "should not define dynamic finders when opting out" do 368 | non_dynamic_klass = Class.new do 369 | attr_accessor :roles_mask 370 | attr_accessor :custom_roles_mask 371 | include RoleModel 372 | roles :foo, :bar, :third, :dynamic => false 373 | end 374 | 375 | model = non_dynamic_klass.new 376 | lambda { model.is_foo? }.should raise_error(NoMethodError) 377 | lambda { model.bar? }.should raise_error(NoMethodError) 378 | end 379 | 380 | it "should be able to override the default dynamic query methods and call super" do 381 | klass = Class.new do 382 | def bar? 383 | return false 384 | end 385 | 386 | attr_accessor :roles_mask 387 | attr_accessor :custom_roles_mask 388 | include RoleModel 389 | roles :foo, :bar, :baz 390 | 391 | def baz? 392 | return false 393 | end 394 | 395 | def foo? 396 | ret = super 397 | !ret 398 | end 399 | end 400 | 401 | model = klass.new 402 | model.roles = [:foo, :bar, :baz] 403 | 404 | model.foo?.should be false 405 | 406 | model.bar?.should be false 407 | model.roles.should include(:bar) 408 | 409 | model.baz?.should be false 410 | model.roles.should include(:baz) 411 | end 412 | 413 | context 'when prefix set to "is_"' do 414 | let(:role_options) { {prefix: 'is_'} } 415 | 416 | it "should return true when the given role was assigned" do 417 | subject.is_foo = true 418 | subject.is_foo?.should be true 419 | end 420 | 421 | it "should return false when the given role was not assigned" do 422 | subject.is_foo = false 423 | subject.is_foo?.should be false 424 | end 425 | 426 | it "should return false when no role was assigned" do 427 | subject.is_foo?.should be false 428 | end 429 | 430 | it "should throw NoMethodError when asked for an undefined role" do 431 | lambda { subject.baz? }.should raise_error(NoMethodError) 432 | end 433 | 434 | end 435 | 436 | end 437 | 438 | context "query for multiple roles" do 439 | [:has_any_role?, :is_any_of?, :has_role?].each do |check_role_assignment_method| 440 | describe "##{check_role_assignment_method}" do 441 | subject { model_class.new } 442 | 443 | it "should return true when any of the given roles was assigned" do 444 | subject.roles = [:foo, :baz] 445 | subject.send(check_role_assignment_method, :foo, :bar).should be true 446 | end 447 | 448 | it "should return false when none of the given roles were assigned" do 449 | subject.roles = [:foo, :bar] 450 | subject.send(check_role_assignment_method, :baz, :quux).should be false 451 | end 452 | end 453 | end 454 | 455 | [:has_all_roles?, :is?, :has_roles?].each do |check_role_assignment_method| 456 | describe "##{check_role_assignment_method}" do 457 | subject { model_class.new } 458 | 459 | it "returns true when the assigned roles include the given roles" do 460 | subject.roles = [:foo, :bar, :third] 461 | subject.send(check_role_assignment_method, :foo, :bar).should be true 462 | end 463 | 464 | it "should return true when all of the given roles were assigned" do 465 | subject.roles = [:foo, :bar] 466 | subject.send(check_role_assignment_method, :foo, :bar).should be true 467 | end 468 | 469 | it "should return false when only some of the given roles were assigned" do 470 | subject.roles = [:foo, :bar] 471 | subject.send(check_role_assignment_method, :bar, :baz).should be false 472 | end 473 | 474 | it "should return false when none of the given roles were assigned" do 475 | subject.roles = [:foo, :bar] 476 | subject.send(check_role_assignment_method, :baz, :quux).should be false 477 | end 478 | end 479 | end 480 | end 481 | 482 | context "query for exact roles" do 483 | [:has_only_roles?, :is_exactly?].each do |check_role_assignment_method| 484 | describe "##{check_role_assignment_method}" do 485 | subject { model_class.new } 486 | 487 | it "returns false when the given roles are a subset of those assigned" do 488 | subject.roles = [:foo, :bar, :third] 489 | subject.send(check_role_assignment_method, :foo, :bar).should be false 490 | end 491 | 492 | it "returns true when the given roles exactly match those assigned" do 493 | subject.roles = [:foo, :bar] 494 | subject.send(check_role_assignment_method, :foo, :bar).should be true 495 | end 496 | 497 | it "should return false when only some of the given roles were assigned" do 498 | subject.roles = [:foo, :bar] 499 | subject.send(check_role_assignment_method, :bar, :baz).should be false 500 | end 501 | end 502 | end 503 | end 504 | 505 | context "query for roles when none defined in model" do 506 | [:has_any_role?, :is_any_of?, :has_role?, :has_all_roles?, :is?, :has_roles?].each do |check_role_assignment_method| 507 | describe "##{check_role_assignment_method}" do 508 | 509 | let(:model_class_without_roles) { Class.new } 510 | 511 | before(:each) do 512 | model_class_without_roles.instance_eval do 513 | attr_accessor :roles_mask 514 | attr_accessor :custom_roles_mask 515 | include RoleModel 516 | end 517 | end 518 | 519 | 520 | subject { model_class_without_roles.new } 521 | 522 | it "should return false when a role was assigned" do 523 | subject.roles = :foo 524 | subject.send(check_role_assignment_method, :foo).should be false 525 | end 526 | 527 | it "should return false when no role was assigned" do 528 | subject.send(check_role_assignment_method, :foo).should be false 529 | end 530 | 531 | end 532 | end 533 | end 534 | 535 | context "ClassMethods" do 536 | 537 | subject { model_class } 538 | 539 | describe ".roles" do 540 | it "should not allow public access to set roles" do 541 | lambda do 542 | subject.roles :foo, :quux 543 | end.should raise_exception(NoMethodError, /protected method.*roles.*called/) 544 | end 545 | end 546 | 547 | end 548 | 549 | context "inheritance" do 550 | let(:superclass_instance) { model_class.new } 551 | let(:inherited_model_class) { Class.new(model_class) } 552 | subject { inherited_model_class.new } 553 | 554 | it "should not allow public access to set roles" do 555 | lambda do 556 | inherited_model_class.roles :foo, :quux 557 | end.should raise_exception(NoMethodError, /protected method.*roles.*called/) 558 | end 559 | 560 | it "should not alter the superclass behaviour" do 561 | inherited_model_class.instance_eval do 562 | roles :quux, :quuux 563 | end 564 | superclass_instance.roles = [:foo, :bar, :quux, :quuux] 565 | superclass_instance.roles.should include(:foo, :bar) 566 | expect(superclass_instance.roles.size).to eq(2) 567 | end 568 | 569 | it "should inherit the valid roles" do 570 | subject.roles = [:foo, :bar, :quux, :quuux] 571 | subject.roles.should include(:foo, :bar) 572 | expect(subject.roles.size).to eq(2) 573 | end 574 | 575 | it "should not inherit the assigned roles" do 576 | subject.roles.should be_empty 577 | end 578 | 579 | it "should allow overriding the valid roles for the inherited class" do 580 | inherited_model_class.instance_eval do 581 | roles :quux, :quuux 582 | end 583 | subject.roles = [:foo, :bar, :quux, :quuux] 584 | subject.roles.should include(:quux, :quuux) 585 | expect(subject.roles.size).to eq(2) 586 | subject.roles_mask.should == 3 587 | end 588 | 589 | it "should allow overriding the attribute to store the roles for the inherited class" do 590 | inherited_model_class.instance_eval do 591 | roles_attribute :custom_roles_mask 592 | end 593 | subject.roles = [:foo, :bar, :quux, :quuux] 594 | subject.roles.should include(:foo, :bar) 595 | expect(subject.roles.size).to eq(2) 596 | subject.custom_roles_mask.should == 3 597 | end 598 | end 599 | 600 | context "independent usage" do 601 | let(:model_instance) { model_class.new } 602 | let(:other_model_class) { Class.new } 603 | before(:each) do 604 | other_model_class.instance_eval do 605 | attr_accessor :roles_mask 606 | include RoleModel 607 | roles :quux, :quuux 608 | end 609 | end 610 | subject { other_model_class.new } 611 | 612 | it "should not alter the behaviour of other classes" do 613 | model_instance.roles = [:foo, :bar, :quux, :quuux] 614 | model_instance.roles.should include(:foo) 615 | model_instance.roles.should include(:bar) 616 | expect(model_instance.roles.size).to be(2) 617 | end 618 | 619 | it "should allow using different roles in other models" do 620 | subject.roles = [:foo, :bar, :quux, :quuux] 621 | subject.roles.should include(:quux) 622 | subject.roles.should include(:quuux) 623 | expect(subject.roles.size).to eq(2) 624 | end 625 | end 626 | 627 | describe ".mask_for" do 628 | subject { model_class.new } 629 | 630 | it "should return the role mask of a role" do 631 | subject.class.mask_for(:foo).should == 1 632 | subject.class.mask_for(:bar).should == 2 633 | subject.class.mask_for(:third).should == 4 634 | end 635 | 636 | it "should return the role mask of an array of roles" do 637 | subject.class.mask_for(:foo, :bar).should == 3 638 | subject.class.mask_for(:foo, :third).should == 5 639 | subject.class.mask_for(:foo, :bar, :third).should == 7 640 | end 641 | 642 | it "should return the role mask of a string array of roles" do 643 | subject.class.mask_for("foo").should == 1 644 | subject.class.mask_for("foo", "bar").should == 3 645 | subject.class.mask_for("foo", "third").should == 5 646 | end 647 | 648 | it "should return the role mask of the existing roles" do 649 | subject.class.mask_for(:foo, :quux).should == 1 650 | end 651 | 652 | it "should return 0 when a role that does not exist is passed" do 653 | subject.class.mask_for(:quux).should == 0 654 | end 655 | end 656 | 657 | end 658 | -------------------------------------------------------------------------------- /spec/roles_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RoleModel::Roles do 4 | 5 | let(:model_instance) { Class.new.new } 6 | let(:array) { [:foo, :bar] } 7 | subject { RoleModel::Roles.new(model_instance, array) } 8 | before(:each) do 9 | model_instance.stub(:roles=) 10 | end 11 | 12 | specify { subject.model_instance.should equal(model_instance) } 13 | 14 | it { should include(:foo, :bar) } 15 | it { should respond_to(:each) } 16 | 17 | describe "#<<" do 18 | it "should add the given element to the model_instance.roles by re-assigning all roles" do 19 | model_instance.should_receive(:roles=).with(array_including(:foo, :bar, :baz)) 20 | subject << :baz 21 | end 22 | end 23 | 24 | describe "#add" do 25 | it "should add the given element to the model_instance.roles by re-assigning all roles" do 26 | model_instance.should_receive(:roles=).with(array_including(:foo, :bar, :baz)) 27 | subject.add(:baz) 28 | end 29 | end 30 | 31 | describe "#merge" do 32 | it "should add the given enum to the model_instance.roles by re-assigning all roles" do 33 | model_instance.should_receive(:roles=).with(array_including(:foo, :bar, :baz, :quux)) 34 | subject.merge([:baz, :quux]) 35 | end 36 | end 37 | 38 | describe "#delete" do 39 | it "should delete the given element to the model_instance.roles by re-assigning all roles" do 40 | model_instance.should_receive(:roles=).with(subject) 41 | subject.delete :foo 42 | subject.should_not include(:foo) 43 | end 44 | end 45 | 46 | describe "#subtract" do 47 | it "should remove the given enum to the model_instance.roles by re-assigning all roles" do 48 | model_instance.should_receive(:roles=).with(subject) 49 | subject.subtract([:foo, :bar]) 50 | expect(subject.size).to eq(0) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 3 | require 'rspec' 4 | require File.dirname(__FILE__) + '/custom_matchers' 5 | require 'role_model' 6 | 7 | RSpec.configure do |config| 8 | config.include(CustomMatchers) 9 | config.mock_with :rspec 10 | end 11 | 12 | # Requires supporting ruby files with custom matchers and macros, etc, 13 | # in spec/support/ and its subdirectories. 14 | Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each {|f| require f} 15 | --------------------------------------------------------------------------------