policy 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +2 -0
  3. data/.metrics +5 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +2 -0
  6. data/.travis.yml +18 -0
  7. data/.yardopts +3 -0
  8. data/Gemfile +7 -0
  9. data/Guardfile +15 -0
  10. data/LICENSE +21 -0
  11. data/README.md +223 -0
  12. data/Rakefile +17 -0
  13. data/config/metrics/STYLEGUIDE +231 -0
  14. data/config/metrics/cane.yml +5 -0
  15. data/config/metrics/churn.yml +6 -0
  16. data/config/metrics/flay.yml +2 -0
  17. data/config/metrics/metric_fu.yml +14 -0
  18. data/config/metrics/pippi.yml +3 -0
  19. data/config/metrics/reek.yml +1 -0
  20. data/config/metrics/roodi.yml +24 -0
  21. data/config/metrics/rubocop.yml +87 -0
  22. data/config/metrics/saikuro.yml +3 -0
  23. data/config/metrics/simplecov.yml +5 -0
  24. data/config/metrics/yardstick.yml +37 -0
  25. data/lib/policy/follower/followed_policies.rb +45 -0
  26. data/lib/policy/follower/followed_policy.rb +104 -0
  27. data/lib/policy/follower/names.rb +29 -0
  28. data/lib/policy/follower.rb +143 -0
  29. data/lib/policy/interface.rb +48 -0
  30. data/lib/policy/validations.rb +28 -0
  31. data/lib/policy/version.rb +9 -0
  32. data/lib/policy/violation_error.rb +52 -0
  33. data/lib/policy.rb +40 -0
  34. data/policy.gemspec +23 -0
  35. data/spec/features/follower_spec.rb +95 -0
  36. data/spec/spec_helper.rb +10 -0
  37. data/spec/tests/policy/follower/followed_policies_spec.rb +87 -0
  38. data/spec/tests/policy/follower/followed_policy_spec.rb +117 -0
  39. data/spec/tests/policy/follower/names_spec.rb +19 -0
  40. data/spec/tests/policy/follower_spec.rb +220 -0
  41. data/spec/tests/policy/interface_spec.rb +83 -0
  42. data/spec/tests/policy/validations_spec.rb +13 -0
  43. data/spec/tests/policy/violation_error_spec.rb +75 -0
  44. data/spec/tests/policy_spec.rb +35 -0
  45. metadata +142 -0
@@ -0,0 +1,87 @@
1
+ ---
2
+ # settings added by the 'hexx-suit' module
3
+ # output: "tmp/rubocop"
4
+ # format: "html"
5
+
6
+ AllCops:
7
+ Exclude:
8
+ - '**/db/schema.rb'
9
+
10
+ Lint/HandleExceptions:
11
+ Exclude:
12
+ - '**/*_spec.rb'
13
+
14
+ Lint/RescueException:
15
+ Exclude:
16
+ - '**/*_spec.rb'
17
+
18
+ Metrics/ClassLength:
19
+ Exclude:
20
+ - '**/generator*'
21
+
22
+ Style/AccessorMethodName:
23
+ Exclude:
24
+ - '**/*_spec.rb'
25
+
26
+ Style/AsciiComments:
27
+ Enabled: false
28
+
29
+ Style/ClassAndModuleChildren:
30
+ Exclude:
31
+ - '**/*_spec.rb'
32
+
33
+ Style/Documentation:
34
+ Exclude:
35
+ - '**/version.rb'
36
+ - '**/*_spec.rb'
37
+
38
+ Style/EmptyLinesAroundBlockBody:
39
+ Enabled: false
40
+
41
+ Style/EmptyLinesAroundClassBody:
42
+ Enabled: false
43
+
44
+ Style/EmptyLinesAroundMethodBody:
45
+ Enabled: false
46
+
47
+ Style/EmptyLinesAroundModuleBody:
48
+ Enabled: false
49
+
50
+ Style/EmptyLineBetweenDefs:
51
+ Enabled: false
52
+
53
+ Style/FileName:
54
+ Enabled: false
55
+
56
+ Style/RaiseArgs:
57
+ EnforcedStyle: compact
58
+
59
+ Style/RescueModifier:
60
+ Exclude:
61
+ - '**/*_spec.rb'
62
+
63
+ Style/SingleLineMethods:
64
+ Exclude:
65
+ - '**/*_spec.rb'
66
+
67
+ Style/SingleSpaceBeforeFirstArg:
68
+ Enabled: false
69
+
70
+ Style/SpecialGlobalVars:
71
+ Exclude:
72
+ - '**/Gemfile'
73
+ - '**/*.gemspec'
74
+
75
+ Style/StructInheritance:
76
+ Exclude:
77
+ - '**/*_spec.rb'
78
+
79
+ Style/StringLiterals:
80
+ EnforcedStyle: double_quotes
81
+
82
+ Style/StringLiteralsInInterpolation:
83
+ EnforcedStyle: double_quotes
84
+
85
+ Style/TrivialAccessors:
86
+ Exclude:
87
+ - '**/*_spec.rb'
@@ -0,0 +1,3 @@
1
+ ---
2
+ warn_cyclo: 4
3
+ error_cyclo: 6
@@ -0,0 +1,5 @@
1
+ ---
2
+ output: tmp/coverage
3
+ filters: # The list of paths to be excluded from coverage checkup
4
+ - "spec/"
5
+ groups: [] # The list of groups to be shown in the coverage report
@@ -0,0 +1,37 @@
1
+ ---
2
+ # Settings added by the 'hexx-suit' gem
3
+ output: "tmp/yardstick/output.log"
4
+ path: "lib/**/*.rb"
5
+ rules:
6
+ ApiTag::Presence:
7
+ enabled: true
8
+ exclude: []
9
+ ApiTag::Inclusion:
10
+ enabled: true
11
+ exclude: []
12
+ ApiTag::ProtectedMethod:
13
+ enabled: true
14
+ exclude: []
15
+ ApiTag::PrivateMethod:
16
+ enabled: false
17
+ exclude: []
18
+ ExampleTag:
19
+ enabled: true
20
+ exclude: []
21
+ ReturnTag:
22
+ enabled: true
23
+ exclude: []
24
+ Summary::Presence:
25
+ enabled: true
26
+ exclude: []
27
+ Summary::Length:
28
+ enabled: true
29
+ exclude: []
30
+ Summary::Delimiter:
31
+ enabled: true
32
+ exclude: []
33
+ Summary::SingleLine:
34
+ enabled: true
35
+ exclude: []
36
+ threshold: 100
37
+ verbose: false
@@ -0,0 +1,45 @@
1
+ # encoding: utf-8
2
+
3
+ module Policy
4
+
5
+ module Follower
6
+
7
+ # Describes the list of followed policies
8
+ #
9
+ # @api private
10
+ class FollowedPolicies < Hash
11
+
12
+ # Registers followed policy with given unique key
13
+ #
14
+ # @param [Policy::Follower::FollowedPolicy] policy
15
+ #
16
+ # @return [undefined]
17
+ def add(policy)
18
+ self[policy.name] = policy
19
+ end
20
+
21
+ # Applies to follower the policies, selected by names
22
+ #
23
+ # @param [Policy::Follower] follower
24
+ # @param [Array<#to_s>] names
25
+ #
26
+ # @raise [Policy::ViolationError]
27
+ # unless all policies are met
28
+ #
29
+ # @return [undefined]
30
+ def apply_to(follower, *names)
31
+ named_by(names).each { |policy| policy.apply_to(follower) }
32
+ end
33
+
34
+ private
35
+
36
+ def named_by(list)
37
+ names = Names.from list
38
+ names.any? ? names.map(&method(:[])).compact : values
39
+ end
40
+
41
+ end # class FollowedPolicies
42
+
43
+ end # module Follower
44
+
45
+ end # module Policy
@@ -0,0 +1,104 @@
1
+ # encoding: utf-8
2
+
3
+ module Policy
4
+
5
+ module Follower
6
+
7
+ # Stores the policy to be applied to all instances of the follower
8
+ #
9
+ # The policy object can be set either as a constant, or by name
10
+ # in given namespace. Namespace and policy name can be set separately.
11
+ #
12
+ # The separation is used at the {Policy::Follower#apply_policies}.
13
+ #
14
+ # @example The policy can be constant
15
+ # FollowedPolicy.new nil, Foo::Bar::Baz, :baz_policy, :baz
16
+ #
17
+ # @example The policy can be name, relative to the namespace
18
+ # FollowedPolicy.new Foo::Bar, :Baz, :baz_policy, :baz
19
+ # # Foo::Bar::Baz policy object will be used
20
+ #
21
+ # @api private
22
+ class FollowedPolicy
23
+
24
+ # @!scope class
25
+ # @!method new(namespace, policy, name, *attributes)
26
+ # Creates the immutable policy to be followed by given object
27
+ #
28
+ # @param [Module] namespace
29
+ # the namespace for the policy, given by name
30
+ # @param [Class, #to_s] policy
31
+ # the class for applicable policy
32
+ # @param [#to_sym, nil] name
33
+ # the name for the policy
34
+ # @param [Array<Symbol>] attributes
35
+ # the list of follower attributes to apply the policy to
36
+ #
37
+ # @return [Policy::Follower::FollowedPolicy]
38
+ # immutable object
39
+ def initialize(namespace, policy, name, *attributes)
40
+ @policy = find_policy(namespace, policy)
41
+ @name = (name || SecureRandom.uuid).to_sym
42
+ @attributes = check_attributes attributes
43
+ end
44
+
45
+ # @!attribute [r] name
46
+ # The name for the policy
47
+ #
48
+ # @return [Symbol]
49
+ attr_reader :name
50
+
51
+ # @!attribute [r] policy
52
+ # The policy object class to be followed
53
+ #
54
+ # @return [Class]
55
+ attr_reader :policy
56
+
57
+ # @!attribute [r] attributes
58
+ # The list of follower attributes to be send to the policy object
59
+ #
60
+ # @return [Array<Symbol>]
61
+ attr_reader :attributes
62
+
63
+ # Applies the policy to follower instance
64
+ #
65
+ # @param [Policy::Follower]
66
+ # follower
67
+ #
68
+ # @raise [Policy::ViolationError]
69
+ # it the follower doesn't meet the policy
70
+ #
71
+ # @return [undefined]
72
+ def apply_to(follower)
73
+ policy.apply(*attributes_of(follower))
74
+ end
75
+
76
+ private
77
+
78
+ def find_policy(namespace, policy)
79
+ return policy if policy.instance_of?(Class)
80
+ instance_eval [namespace, policy].join("::")
81
+ end
82
+
83
+ def attributes_of(follower)
84
+ attributes.map(&follower.method(:send))
85
+ end
86
+
87
+ def check_attributes(attributes)
88
+ number = policy.members.count
89
+ return attributes if attributes.count.equal?(number)
90
+ fail wrong_number(number, attributes)
91
+ end
92
+
93
+ def wrong_number(number, attributes)
94
+ ArgumentError.new [
95
+ "#{ policy } requires #{ number } attribute(s).",
96
+ "#{ attributes } cannot be assigned."
97
+ ].join(" ")
98
+ end
99
+
100
+ end # class FollowedPolicy
101
+
102
+ end # module Follower
103
+
104
+ end # module Policy
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ module Policy
4
+
5
+ module Follower
6
+
7
+ # Converter of items to array of unique symbols
8
+ #
9
+ # @api private
10
+ module Names
11
+
12
+ # Converts items to array of unique symbols
13
+ #
14
+ # @example
15
+ # Policy::Follower::Names.from "foo", [:foo, "bar"], "baz"
16
+ # # => [:foo, :bar, :baz]
17
+ #
18
+ # @param [Array<#to_sym>] items
19
+ #
20
+ # @return [Array<Symbol>]
21
+ def self.from(*items)
22
+ items.flatten.map(&:to_sym)
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,143 @@
1
+ # encoding: utf-8
2
+
3
+ module Policy
4
+
5
+ # Adds features for the object to follow external policies
6
+ module Follower
7
+
8
+ require_relative "follower/names"
9
+ require_relative "follower/followed_policy"
10
+ require_relative "follower/followed_policies"
11
+
12
+ # Methods to be added to the class the module is included to
13
+ #
14
+ # @private
15
+ module ClassMethods
16
+
17
+ # @!attribute [r] followed_policies
18
+ # The collection of policies to be followed by instances of the class
19
+ #
20
+ # @return [Policy::Follower::FollowedPolicies]
21
+ #
22
+ # @private
23
+ def followed_policies
24
+ @followed_policies ||= FollowedPolicies.new
25
+ end
26
+
27
+ # Adds a policy to the list of {#followed_policies}
28
+ #
29
+ # @param [Class] policy
30
+ # the policy object klass
31
+ # @param [Array<#to_sym>] attributes
32
+ # the list of attributes of the instance the policy should be applied to
33
+ #
34
+ # @option [#to_sym] :as
35
+ # the name for the policy to be used for selecting it
36
+ # in {#follow_policies!} and {#follow_policies?} methods
37
+ #
38
+ # @return [undefined]
39
+ def follow_policy(policy, *attributes, as: nil)
40
+ object = FollowedPolicy.new(__policies__, policy, as, *attributes)
41
+ followed_policies.add object
42
+ end
43
+
44
+ # Changes the namespace for applied policies
45
+ #
46
+ # @example For Policies::Finances::TransferConsistency
47
+ # use_policies Policies::Finances do
48
+ # apply_policy :TransferConstistency, :debet, :credit
49
+ # end
50
+ #
51
+ # @param [Module] namespace
52
+ #
53
+ # @yield the block in the current scope
54
+ #
55
+ # @return [undefined]
56
+ def use_policies(namespace, &block)
57
+ @__policies__ = namespace
58
+ instance_eval(&block)
59
+ ensure
60
+ @__policies__ = nil
61
+ end
62
+
63
+ private
64
+
65
+ def __policies__
66
+ @__policies__ ||= self
67
+ end
68
+
69
+ end
70
+
71
+ # Checks whether an instance meets selected policies
72
+ #
73
+ # Mutates the object by adding new #errors
74
+ #
75
+ # @param [Array<#to_sym>] names
76
+ # the ordered list of names to select policies by
77
+ # when not names selected all policies will be applied
78
+ #
79
+ # @raise [Policy::ViolationError]
80
+ # unless all selected policies has been met
81
+ #
82
+ # @return [undefined]
83
+ def follow_policies!(*names)
84
+ followed_policies.apply_to self, *names
85
+ rescue ViolationError => error
86
+ collect_errors_from(error)
87
+ raise
88
+ end
89
+
90
+ # Syntax shugar for the {#follow_policies!} with one argument
91
+ #
92
+ # @param [#to_sym] name
93
+ # the name of the policy to follow
94
+ #
95
+ # @raise (see #follow_policies!)
96
+ #
97
+ # @return [undefined]
98
+ def follow_policy!(name)
99
+ follow_policies! name
100
+ end
101
+
102
+ # Safely checks whether an instance meets selected policies
103
+ #
104
+ # Mutates the object by adding new #errors
105
+ #
106
+ # @param (see #follow_policies!)
107
+ #
108
+ # @return [Boolean]
109
+ def follow_policies?(*names)
110
+ follow_policies!(*names)
111
+ true
112
+ rescue ViolationError
113
+ false
114
+ end
115
+
116
+ # Syntax shugar for the {#follow_policies?} with one argument
117
+ #
118
+ # @param (see #follow_policy!)
119
+ #
120
+ # @return (see #follow_policies?)
121
+ def follow_policy?(name)
122
+ follow_policies? name
123
+ end
124
+
125
+ private
126
+
127
+ # @!parse extend Policy::Follower::ClassMethods
128
+ # @!parse include ActiveModel::Validations
129
+ def self.included(klass)
130
+ klass.extend(ClassMethods).__send__(:include, Validations)
131
+ end
132
+
133
+ def followed_policies
134
+ @followed_policies ||= self.class.followed_policies
135
+ end
136
+
137
+ def collect_errors_from(exception)
138
+ exception.messages.each { |text| errors.add :base, text }
139
+ end
140
+
141
+ end # module Follower
142
+
143
+ end # module Policy
@@ -0,0 +1,48 @@
1
+ # encoding: utf-8
2
+
3
+ module Policy
4
+
5
+ # Policy object interface
6
+ module Interface
7
+
8
+ # @private
9
+ def self.included(klass)
10
+ klass.extend(ClassMethods).__send__(:include, Validations)
11
+ end
12
+
13
+ # Container for the policy class methods
14
+ module ClassMethods
15
+
16
+ # Creates and validates the policy object
17
+ #
18
+ # @param (see Policy.new)
19
+ #
20
+ # @raise (see Policy::Interface#apply)
21
+ #
22
+ # @return [undefined]
23
+ def apply(*attributes)
24
+ new(*attributes).apply
25
+ end
26
+
27
+ end
28
+
29
+ # Returns the list of error messages
30
+ #
31
+ # @return [Array<String>]
32
+ def messages
33
+ errors.messages.values.flatten
34
+ end
35
+
36
+ # Validates the policy object
37
+ #
38
+ # @raise [ViolationError]
39
+ # if a policy is invalid
40
+ #
41
+ # @return [undefined]
42
+ def apply
43
+ fail ViolationError.new(self) unless valid?
44
+ end
45
+
46
+ end # class Interface
47
+
48
+ end # module Policy
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+ require "active_model"
3
+
4
+ module Policy
5
+
6
+ # Wrapper around the ActiveModel::Validations
7
+ #
8
+ # Provides shared interface for [Policy::Inteface] and [Policy::Follower].
9
+ #
10
+ # @todo Implement it later from scratch without excessive features
11
+ #
12
+ # @example
13
+ # MyClass.include, Policy::Validations
14
+ #
15
+ # @private
16
+ module Validations
17
+
18
+ # The implementation for validations
19
+ IMPLEMENTATION = ActiveModel::Validations
20
+
21
+ # @private
22
+ def self.included(klass)
23
+ klass.__send__(:include, IMPLEMENTATION)
24
+ end
25
+
26
+ end # module Validations
27
+
28
+ end # module Policy
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+
3
+ module Policy
4
+
5
+ # The semantic version of the module.
6
+ # @see http://semver.org/ Semantic versioning 2.0
7
+ VERSION = "1.0.1".freeze
8
+
9
+ end # module Hexx
@@ -0,0 +1,52 @@
1
+ # encoding: utf-8
2
+
3
+ module Policy
4
+
5
+ # An exception to be risen by {Policy::Interface#apply}
6
+ class ViolationError < RuntimeError
7
+ include Adamantium
8
+
9
+ # @!attribute [r] policy
10
+ # The violated policy object
11
+ #
12
+ # @return [Policy::Follower]
13
+ attr_reader :policy
14
+
15
+ # @!attribute [r] messages
16
+ # The list of messages from the broken policy
17
+ #
18
+ # @return [Array<String>]
19
+ attr_reader :messages
20
+
21
+ # @!scope class
22
+ # @!method new(policy)
23
+ # Constructs an exception
24
+ #
25
+ # @param [Policy::Follower] policy
26
+ # the violated policy object
27
+ #
28
+ # @return [Policy::ViolationError]
29
+ def initialize(policy)
30
+ @policy = policy.dup
31
+ @messages = @policy.messages
32
+ end
33
+
34
+ # The human-readable description for the exception
35
+ #
36
+ # @return [String]
37
+ def inspect
38
+ "#<#{ self }: #{ message }>"
39
+ end
40
+
41
+ # The human-readable exception message
42
+ #
43
+ # @return [String]
44
+ def message
45
+ "#{ policy } violated: #{ messages }"
46
+ end
47
+
48
+ memoize :policy, :messages
49
+
50
+ end # module Follower
51
+
52
+ end # module Policy
data/lib/policy.rb ADDED
@@ -0,0 +1,40 @@
1
+ # encoding: utf-8
2
+
3
+ require "adamantium"
4
+
5
+ # Policy Object builder
6
+ #
7
+ # @!parse include Policy::Interface
8
+ module Policy
9
+
10
+ require_relative "policy/version"
11
+ require_relative "policy/validations"
12
+ require_relative "policy/violation_error"
13
+ require_relative "policy/interface"
14
+ require_relative "policy/follower"
15
+
16
+ class << self
17
+
18
+ # Builds a base class for the policy object with some attributes
19
+ #
20
+ # @example
21
+ # class TransactionPolicy < Policy.new(:debet, :credit)
22
+ # end
23
+ #
24
+ # @param [Array<Symbol>] attributes
25
+ # names for the policy object attributes
26
+ #
27
+ # @return [Struct]
28
+ def new(*attributes)
29
+ Struct.new(*attributes) do
30
+ include Interface
31
+
32
+ def self.name
33
+ "Policy"
34
+ end
35
+ end
36
+ end
37
+
38
+ end # Policy singleton class
39
+
40
+ end # module Policy
data/policy.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require "policy/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "policy"
6
+ s.version = Policy::VERSION.dup
7
+ s.author = "Andrew Kozin"
8
+ s.email = "andrew.kozin@gmail.com"
9
+ s.homepage = "https://github.com/nepalez/policy"
10
+ s.summary = "Policy Objects for Ruby."
11
+ s.description = "A tiny library implementing the Policy Object pattern."
12
+ s.license = "MIT"
13
+
14
+ s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
15
+ s.test_files = Dir["spec/**/*.rb"]
16
+ s.extra_rdoc_files = Dir["README.md", "LICENSE", "config/metrics/STYLEGUIDE"]
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_runtime_dependency "activemodel", ">= 3.1"
20
+ s.add_runtime_dependency "adamantium", "~> 0.2"
21
+
22
+ s.add_development_dependency "hexx-rspec", "~> 0.1"
23
+ end