policy 1.0.1

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.
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