indulgence 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.rdoc CHANGED
@@ -9,8 +9,8 @@ things:
9
9
  * Filtered a search of objects based on the those permissions
10
10
 
11
11
  It was apparent to me that if 'something' was one of the CRUD actions, it would
12
- cover all the use cases I could think of. So permissions were sub-divided into
13
- the 'abilities': create, read, update, and delete.
12
+ cover most of the use cases I could think of. So permissions were sub-divided
13
+ into the 'abilities': create, read, update, and delete.
14
14
 
15
15
  The other requirement was that the permission for an object could be defined
16
16
  succinctly within a single file.
@@ -45,12 +45,10 @@ placing it in app/permissions/thing_permission.rb
45
45
  == Users and Roles
46
46
 
47
47
  Indulgence assumes that permissions will be tested against an entity object
48
- (e.g. User), that has a role object associated with it, and that each role can
49
- be uniquely identified by a name method.
48
+ (e.g. User). The default behaviour assumes that the entity object has a :role
49
+ method that returns the role object, and that the role object has a :name method.
50
50
 
51
- The default behaviour assumes that the entity object has a :role method that
52
- returns the role object, and that the role object has a :name method. So
53
- typically, these objects could look like this:
51
+ So typically, these objects could look like this:
54
52
 
55
53
  class User < ActiveRecord::Base
56
54
  belongs_to :role
@@ -68,7 +66,7 @@ typically, these objects could look like this:
68
66
  :role_id => role.id
69
67
  )
70
68
 
71
- == indulge?
69
+ == Compare single item: indulge?
72
70
 
73
71
  Simple true/false permission can be determined using the :indulge? method:
74
72
 
@@ -79,7 +77,7 @@ Simple true/false permission can be determined using the :indulge? method:
79
77
  thing.indulge?(user, :update) == false
80
78
  thing.indulge?(user, :delete) == false
81
79
 
82
- == indulgence
80
+ == Filter many: indulgence
83
81
 
84
82
  The :indulgence method is used as a where filter:
85
83
 
@@ -128,8 +126,8 @@ _default_ rather than being defined in _emperor_, as it is already set to *all*.
128
126
 
129
127
  abilities is a hash of hashes. The lowest level, associates action names with
130
128
  ability objects. The top level associates role names to the lower level ability
131
- object hashes. The construction is perhaps clearer in the abilities above was
132
- written like this:
129
+ object hashes. In this simple case, construction is perhaps clearer if the
130
+ abilities method above was written like this:
133
131
 
134
132
  def abilities
135
133
  {
@@ -179,21 +177,27 @@ permissions. This can be done by adding this method to ThingPermission:
179
177
  def things_they_wrote
180
178
  define_ability(
181
179
  :name => :things_they_wrote,
182
- :truth => lambda {|thing| thing.author_id == entity.id},
183
- :where_clause => {:author_id => entity.id}
180
+ :compare_single => lambda {|thing, user| thing.author_id == user.id},
181
+ :filter_many => lambda {|things, user| things.where(:author_id => user.id)}
184
182
  )
185
183
  end
186
184
 
187
185
  This will create an Ability object with the following methods:
188
186
 
189
- [name] Allows abilities of the same kind to be matched
190
- [truth] Used by :indulge?
191
- [where_clause] Used by :indulgence
187
+ [name] Allows abilities of the same kind to be matched
188
+ [compare_single] Used by :indulge?
189
+ [filter_many] Used by :indulgence
192
190
 
193
- Note that Indulgence::Permission#entity returns the entity object passed to the
194
- instance on creation. In this example, that will be the User.
191
+ ==== Compare single
195
192
 
196
- ==== The where clause
193
+ _compare_single_ should be a lambda that is passed the object being tested and
194
+ the entity it is to be tested against. The lambda returns _true_ if permission
195
+ should be given. Otherwise _false_ should be returned.
196
+
197
+ Alternatively, instead of a lambda, any object can be used that returns _true_
198
+ or _false_ when it _call_ method is passed the object and the entity.
199
+
200
+ ==== Filter many
197
201
 
198
202
  In this example:
199
203
 
@@ -201,14 +205,6 @@ In this example:
201
205
 
202
206
  if the :read ability is matched to *things_they_wrote*
203
207
 
204
- ==== truth
205
-
206
- In *all* :truth is simply _true_, and in *none* :truth is _false_.
207
-
208
- For more complex abilities :truth should be a lambda that is passed the object
209
- being tested, and returns _true_ if permission should be given. Otherwise _false_
210
- should be returned.
211
-
212
208
  Once *things_they_wrote* has been defined, we can use it to define a new set
213
209
  of abilities:
214
210
 
@@ -297,8 +293,8 @@ The method names _indulgence_ and _indulge?_ may not suit your application. If
297
293
  you wish to use alternative names, they can be aliased like this:
298
294
 
299
295
  acts_as_indulgent(
300
- :truth_method => :permit?,
301
- :where_method => :permitted
296
+ :compare_single_method => :permit?,
297
+ :filter_many_method => :permitted
302
298
  )
303
299
 
304
300
  With this used to define indulgence in Thing, we can do this:
@@ -11,8 +11,8 @@ module ActiveRecord
11
11
  include Indulgence::Indulgent::InstanceMethods
12
12
  extend Indulgence::Indulgent::ClassMethods
13
13
 
14
- alias_method args[:truth_method], :indulge? if args[:truth_method]
15
- singleton_class.send(:alias_method, args[:where_method], :indulgence) if args[:where_method]
14
+ alias_method args[:compare_single_method], :indulge? if args[:compare_single_method]
15
+ singleton_class.send(:alias_method, args[:filter_many_method], :indulgence) if args[:filter_many_method]
16
16
  self.indulgent_permission_class = args[:using]
17
17
  end
18
18
 
@@ -1,19 +1,41 @@
1
1
 
2
2
  module Indulgence
3
3
  class Ability
4
- attr_reader :name, :truth, :where_clause
4
+ attr_reader :name, :compare_single, :filter_many
5
5
 
6
6
  def initialize(args = {})
7
7
  @name = args[:name]
8
- @truth = args[:truth]
9
- @where_clause = args[:where_clause]
8
+ @compare_single = args[:compare_single]
9
+ @filter_many = args[:filter_many]
10
+ valid?
10
11
  end
11
12
 
12
13
  def ==(another_ability)
13
- [:name, :truth, :where_clause].each do |method|
14
+ [:name, :compare_single, :filter_many].each do |method|
14
15
  return false if send(method) != another_ability.send(method)
15
16
  end
16
17
  return true
17
18
  end
19
+
20
+ def valid?
21
+ must_be_name
22
+ must_respond_to_call :compare_single
23
+ must_respond_to_call :filter_many
24
+ end
25
+
26
+ private
27
+ def must_be_name
28
+ unless name
29
+ raise AbilityConfigurationError, "A name is required for each ability"
30
+ end
31
+ end
32
+
33
+ def must_respond_to_call(method)
34
+ unless send(method).respond_to? :call
35
+ raise AbilityConfigurationError, "ability.#{method} must respond to call"
36
+ end
37
+ end
38
+
39
+
18
40
  end
19
41
  end
@@ -0,0 +1,5 @@
1
+ module Indulgence
2
+ class AbilityConfigurationError < StandardError; end
3
+ class NotFoundError < StandardError; end
4
+ class AbilityNotFound < StandardError; end
5
+ end
@@ -4,8 +4,9 @@ module Indulgence
4
4
  module ClassMethods
5
5
  def indulgence(entity, ability)
6
6
  permission = indulgent_permission_class.new(entity, ability)
7
- raise_not_found if permission.ability == Permission.none or permission.ability.blank?
8
- where(permission.where)
7
+ permission.filter_many(self)
8
+ rescue Indulgence::NotFoundError, Indulgence::AbilityNotFound
9
+ raise_not_found
9
10
  end
10
11
 
11
12
  private
@@ -18,7 +19,7 @@ module Indulgence
18
19
 
19
20
  def indulge?(entity, ability)
20
21
  permission = self.class.indulgent_permission_class.new(entity, ability)
21
- return permission.indulge? self
22
+ return permission.compare_single self
22
23
  end
23
24
 
24
25
  end
@@ -1,5 +1,3 @@
1
- require_relative 'ability'
2
-
3
1
  module Indulgence
4
2
  class Permission
5
3
  attr_reader :entity, :ability
@@ -8,6 +6,7 @@ module Indulgence
8
6
  self
9
7
  @entity = entity
10
8
  @ability = abilities_for_role[ability]
9
+ raise AbilityNotFound, "Unable to find an ability called #{ability}" unless @ability
11
10
  end
12
11
 
13
12
  def abilities
@@ -18,12 +17,14 @@ module Indulgence
18
17
  raise 'There must always be a default'
19
18
  end
20
19
 
21
- def where
22
- ability.where_clause
20
+ def filter_many(things)
21
+ check_method_can_be_called(:filter_many)
22
+ ability.filter_many.call things, entity
23
23
  end
24
24
 
25
- def indulge?(thing)
26
- ability.truth.respond_to?(:call) ? ability.truth.call(thing) : ability.truth
25
+ def compare_single(thing)
26
+ check_method_can_be_called(:compare_single)
27
+ ability.compare_single.call thing, entity
27
28
  end
28
29
 
29
30
  @@role_method = :role
@@ -47,21 +48,24 @@ module Indulgence
47
48
  end
48
49
 
49
50
  def self.none
50
- @none ||= define_ability(
51
+ Permission.define_ability(
51
52
  :name => :none,
52
- :truth => false
53
+ :compare_single => lambda {|thing, entity| false},
54
+ :filter_many => lambda {|things, entity| raise NotFoundError}
53
55
  )
54
56
  end
55
57
 
56
58
  def self.all
57
- @all ||= define_ability(
59
+ Permission.define_ability(
58
60
  :name => :all,
59
- :truth => true
61
+ :compare_single => lambda {|thing, entity| true},
62
+ :filter_many => lambda {|things, entity| things.where(nil)}
60
63
  )
61
64
  end
62
65
 
63
66
  def self.define_ability(args)
64
- Ability.new args
67
+ raise AbilityConfigurationError, "A name is required for each ability" unless args[:name]
68
+ ability_cache[args[:name].to_sym] ||= Ability.new args
65
69
  end
66
70
 
67
71
  def define_ability(args)
@@ -99,6 +103,14 @@ module Indulgence
99
103
 
100
104
  def none
101
105
  self.class.none
102
- end
106
+ end
107
+
108
+ def self.ability_cache
109
+ @ability_cache ||= {}
110
+ end
111
+
112
+ def check_method_can_be_called(name)
113
+ raise AbilityConfigurationError, "#{name} method must respond to call" unless ability.send(name).respond_to? :call
114
+ end
103
115
  end
104
116
  end
@@ -1,10 +1,28 @@
1
1
  module Indulgence
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
4
4
 
5
5
  # History
6
6
  # =======
7
7
  #
8
+ # 0.0.2 Rebuild with lessons learnt from first usage in host app
9
+ #
10
+ # Adds automatic caching of abilites. Required a reworking of ability
11
+ # lambdas, so that a particular entity id wasn't cached
12
+ #
13
+ # Renamed ability methods truth as compare_single, and where_clause as
14
+ # filter_many as more descriptive.
15
+ #
16
+ # Forced Ability methods #compare_single and #filter_many to use lambdas
17
+ # (or other object that responds to call). The reasons:
18
+ #
19
+ # 1. To remove the special way that the none ability was handled.
20
+ # 2. Previously, if Ability#filter_many was nil everything would be returned,
21
+ # which I think is counter-intuitive.
22
+ # 3. Also if Ability#filter_many was undefined, then everything would
23
+ # be returned, which is just poor design for a permission tool. Now
24
+ # an error is raised.
25
+ #
8
26
  # 0.0.1 Initial build
9
27
  # First alpa version
10
28
  #
data/lib/indulgence.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require_relative 'indulgence/exceptions'
2
+ require_relative 'indulgence/ability'
1
3
  require_relative 'indulgence/permission'
2
4
  require_relative 'indulgence/indulgent'
3
5
  require_relative 'active_record/acts/indulgent'
Binary file
data/test/lib/thing.rb CHANGED
@@ -1,11 +1,14 @@
1
-
1
+ require 'indulgence'
2
+ require 'role'
3
+ require 'user'
4
+ require 'thing_permission'
2
5
  class Thing < ActiveRecord::Base
3
6
 
4
7
  belongs_to :owner, :class_name => 'User'
5
8
 
6
9
  acts_as_indulgent(
7
- :truth_method => :permit?,
8
- :where_method => :permitted
10
+ :compare_single_method => :permit?,
11
+ :filter_many_method => :permitted
9
12
  )
10
13
 
11
14
  end
@@ -36,10 +36,10 @@ class ThingPermission < Indulgence::Permission
36
36
  end
37
37
 
38
38
  def things_they_own
39
- @things_they_own ||= define_ability(
39
+ define_ability(
40
40
  :name => :things_they_own,
41
- :truth => lambda {|thing| thing.owner_id == entity.id},
42
- :where_clause => {:owner_id => entity.id}
41
+ :compare_single => lambda {|thing, user| thing.owner_id == user.id},
42
+ :filter_many => lambda {|things, user| things.where(:owner_id => user.id)}
43
43
  )
44
44
  end
45
45
 
@@ -1,36 +1,45 @@
1
1
  require_relative '../../test_helper'
2
2
  require 'ability'
3
+ require 'exceptions'
3
4
 
4
5
  module Indulgence
5
6
  class AbilityTest < Test::Unit::TestCase
6
7
 
7
- def test_none
8
- none = Ability.new(
9
- :name => :none,
10
- :truth => false
11
- )
12
- assert_equal(:none, none.name)
13
- assert_equal(false, none.truth)
14
- assert_equal(nil, none.where_clause)
15
- end
16
-
17
- def test_all
18
- all = Ability.new(
19
- :name => :all,
20
- :truth => true
21
- )
22
- assert_equal(:all, all.name)
23
- assert_equal(true, all.truth)
24
- assert_equal(nil, all.where_clause)
8
+ def setup
9
+ @attributes = {
10
+ name: :foo,
11
+ compare_single: lambda {|thing, entity| true},
12
+ filter_many: lambda {|entity| nil}
13
+ }
25
14
  end
26
15
 
27
16
  def test_equality
28
- attributes = {:name => :same, :true => true}
29
- ability = Ability.new(attributes)
30
- other_ability = Ability.new(attributes)
17
+ ability = Ability.new(@attributes)
18
+ other_ability = Ability.new(@attributes)
31
19
  assert(ability == other_ability, "#{ability} == #{other_ability} should return true")
32
20
  assert_equal(ability, other_ability)
33
21
  end
34
22
 
23
+ def test_name_absence_raises_error
24
+ @attributes.delete(:name)
25
+ assert_initiation_raises_error
26
+ end
27
+
28
+ def test_indulge_must_respond_to_call
29
+ @attributes[:compare_single] = true
30
+ assert_initiation_raises_error
31
+ end
32
+
33
+ def test_indulgence_must_respond_to_call
34
+ @attributes[:filter_many] = true
35
+ assert_initiation_raises_error
36
+ end
37
+
38
+ def assert_initiation_raises_error
39
+ assert_raise AbilityConfigurationError do
40
+ Ability.new(@attributes)
41
+ end
42
+ end
43
+
35
44
  end
36
45
  end
@@ -1,5 +1,7 @@
1
1
  require_relative '../../test_helper'
2
2
  require 'user'
3
+ require 'permission'
4
+ require 'ability'
3
5
 
4
6
  module Indulgence
5
7
  class PermissionTest < Test::Unit::TestCase
@@ -10,6 +12,19 @@ module Indulgence
10
12
  end
11
13
  end
12
14
 
15
+ def test_define_ability_uses_cache_rather_than_duplicates
16
+ args = {
17
+ name: :test_ability,
18
+ compare_single: lambda {|thing, entity| true},
19
+ filter_many: lambda {|entity| nil}
20
+ }
21
+ abilities_at_start = ObjectSpace.each_object(Ability).count
22
+ Permission.define_ability args
23
+ Permission.define_ability args
24
+ Permission.define_ability args
25
+ assert_equal((abilities_at_start + 1), ObjectSpace.each_object(Ability).count)
26
+ assert_equal Ability, Permission.send(:ability_cache)[:test_ability].class
27
+ end
13
28
 
14
29
  end
15
30
  end
@@ -62,6 +62,12 @@ class ThingPermissionTest < Test::Unit::TestCase
62
62
  test_super_user_permissions
63
63
  end
64
64
 
65
+ def test_with_unspecified_ability
66
+ assert_raise Indulgence::AbilityNotFound do
67
+ ThingPermission.new(@user, :unspecified)
68
+ end
69
+ end
70
+
65
71
  def teardown
66
72
  User.delete_all
67
73
  Role.delete_all
@@ -28,10 +28,17 @@ class ThingTest < Test::Unit::TestCase
28
28
  make_second_thing
29
29
  @owner.update_attribute(:role_id, @god.id)
30
30
  assert_equal(true, @thing.indulge?(@owner, :read))
31
+ @thing.indulge?(@owner, :delete)
31
32
  assert_equal(true, @thing.indulge?(@owner, :delete))
32
33
  assert_equal(true, @other_thing.indulge?(@owner, :delete))
33
34
  end
34
35
 
36
+ def test_indulgence_by_god
37
+ make_second_thing
38
+ @owner.update_attribute(:role_id, @god.id)
39
+ assert_equal(Thing.all, Thing.indulgence(@owner, :delete))
40
+ end
41
+
35
42
  def test_indulge_by_demigod
36
43
  make_second_thing
37
44
  @owner.update_attribute(:role_id, @demigod.id)
@@ -57,6 +64,12 @@ class ThingTest < Test::Unit::TestCase
57
64
  end
58
65
  end
59
66
 
67
+ def test_indulgence_with_unspecified_ability
68
+ assert_raise ActiveRecord::RecordNotFound do
69
+ Thing.indulgence(@owner, :unspecified)
70
+ end
71
+ end
72
+
60
73
  def test_find
61
74
  make_second_thing
62
75
  @owner.update_attribute(:role_id, @demigod.id)
@@ -66,14 +79,14 @@ class ThingTest < Test::Unit::TestCase
66
79
  end
67
80
  end
68
81
 
69
- def test_truth_method
82
+ def test_aliased_compare_single_method
70
83
  make_second_thing
71
84
  assert_equal(true, @thing.permit?(@owner, :read))
72
85
  assert_equal(false, @thing.permit?(@owner, :delete))
73
86
  assert_equal(false, @other_thing.permit?(@owner, :delete))
74
87
  end
75
88
 
76
- def test_where_method
89
+ def test_aliased_filter_many_method
77
90
  make_second_thing
78
91
  @owner.update_attribute(:role_id, @demigod.id)
79
92
  assert_equal(Thing.order('id'), Thing.permitted(@owner, :read).order('id'))
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: indulgence
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-10 00:00:00.000000000 Z
12
+ date: 2013-04-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -52,6 +52,7 @@ extra_rdoc_files: []
52
52
  files:
53
53
  - lib/active_record/acts/indulgent.rb
54
54
  - lib/indulgence/version.rb
55
+ - lib/indulgence/exceptions.rb
55
56
  - lib/indulgence/indulgent.rb
56
57
  - lib/indulgence/ability.rb
57
58
  - lib/indulgence/permission.rb