granite 0.7.0

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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/app/controllers/granite/controller.rb +44 -0
  4. data/lib/generators/USAGE +25 -0
  5. data/lib/generators/granite/install_controller_generator.rb +15 -0
  6. data/lib/generators/granite_generator.rb +32 -0
  7. data/lib/generators/templates/granite_action.rb.erb +22 -0
  8. data/lib/generators/templates/granite_action_spec.rb.erb +45 -0
  9. data/lib/generators/templates/granite_base_action.rb.erb +2 -0
  10. data/lib/generators/templates/granite_business_action.rb.erb +3 -0
  11. data/lib/granite.rb +24 -0
  12. data/lib/granite/action.rb +106 -0
  13. data/lib/granite/action/error.rb +14 -0
  14. data/lib/granite/action/performer.rb +23 -0
  15. data/lib/granite/action/performing.rb +132 -0
  16. data/lib/granite/action/policies.rb +92 -0
  17. data/lib/granite/action/policies/always_allow_strategy.rb +13 -0
  18. data/lib/granite/action/policies/any_strategy.rb +12 -0
  19. data/lib/granite/action/policies/required_performer_strategy.rb +14 -0
  20. data/lib/granite/action/preconditions.rb +107 -0
  21. data/lib/granite/action/preconditions/base_precondition.rb +25 -0
  22. data/lib/granite/action/preconditions/embedded_precondition.rb +42 -0
  23. data/lib/granite/action/projectors.rb +100 -0
  24. data/lib/granite/action/represents.rb +26 -0
  25. data/lib/granite/action/represents/attribute.rb +90 -0
  26. data/lib/granite/action/represents/reflection.rb +15 -0
  27. data/lib/granite/action/subject.rb +73 -0
  28. data/lib/granite/action/transaction.rb +40 -0
  29. data/lib/granite/action/translations.rb +39 -0
  30. data/lib/granite/action/types.rb +1 -0
  31. data/lib/granite/action/types/collection.rb +13 -0
  32. data/lib/granite/config.rb +23 -0
  33. data/lib/granite/context.rb +28 -0
  34. data/lib/granite/dispatcher.rb +64 -0
  35. data/lib/granite/error.rb +4 -0
  36. data/lib/granite/performer_proxy.rb +34 -0
  37. data/lib/granite/performer_proxy/proxy.rb +31 -0
  38. data/lib/granite/projector.rb +48 -0
  39. data/lib/granite/projector/controller_actions.rb +47 -0
  40. data/lib/granite/projector/error.rb +14 -0
  41. data/lib/granite/projector/helpers.rb +59 -0
  42. data/lib/granite/projector/translations.rb +52 -0
  43. data/lib/granite/projector/translations/helper.rb +22 -0
  44. data/lib/granite/projector/translations/view_helper.rb +12 -0
  45. data/lib/granite/rails.rb +11 -0
  46. data/lib/granite/routing.rb +4 -0
  47. data/lib/granite/routing/cache.rb +24 -0
  48. data/lib/granite/routing/caching.rb +18 -0
  49. data/lib/granite/routing/declarer.rb +25 -0
  50. data/lib/granite/routing/mapper.rb +15 -0
  51. data/lib/granite/routing/mapping.rb +23 -0
  52. data/lib/granite/routing/route.rb +29 -0
  53. data/lib/granite/rspec.rb +5 -0
  54. data/lib/granite/rspec/action_helpers.rb +8 -0
  55. data/lib/granite/rspec/have_projector.rb +24 -0
  56. data/lib/granite/rspec/projector_helpers.rb +54 -0
  57. data/lib/granite/rspec/raise_validation_error.rb +52 -0
  58. data/lib/granite/rspec/satisfy_preconditions.rb +96 -0
  59. metadata +338 -0
@@ -0,0 +1,92 @@
1
+ require 'granite/action/error'
2
+ require 'granite/action/policies/any_strategy'
3
+ require 'granite/action/policies/always_allow_strategy'
4
+ require 'granite/action/policies/required_performer_strategy'
5
+
6
+ module Granite
7
+ class Action
8
+ class NotAllowedError < Error
9
+ def initialize(action)
10
+ if action.performer.respond_to?(:id) && action.performer.id.present?
11
+ performer_id = "##{action.performer.id}"
12
+ end
13
+
14
+ super("#{action.class} action is not allowed " \
15
+ "for #{action.performer.class}#{performer_id}", action)
16
+ end
17
+ end
18
+
19
+ # Policies module used for abilities definition. Basically
20
+ # policies are defined as blocks which are executed in action
21
+ # instance context, so performer, object and all the attributes
22
+ # are available inside the block.
23
+ #
24
+ # By default action is allowed to be performed only by default performer.
25
+ #
26
+ module Policies
27
+ extend ActiveSupport::Concern
28
+
29
+ included do
30
+ class_attribute :_policies, :_policies_strategy, instance_writer: false
31
+ self._policies = []
32
+ self._policies_strategy = AnyStrategy
33
+ end
34
+
35
+ module ClassMethods
36
+ # The simplies policy. Takes block and executes it returning
37
+ # boolean result. Multiple policies are reduced with ||
38
+ #
39
+ # class Action < Granite::Action
40
+ # allow_if { performer.is_a?(Recruiter) }
41
+ # allow_if { performer.is_a?(AdvancedRecruiter) }
42
+ # end
43
+ #
44
+ # The first argument in block is a current action performer,
45
+ # so it is possible to use a short-cut performer methods:
46
+ #
47
+ # class Action < Granite::Action
48
+ # allow_if(&:staff?)
49
+ # end
50
+ #
51
+ def allow_if(&block)
52
+ self._policies += [block]
53
+ end
54
+
55
+ def allow_self
56
+ allow_if { performer == subject }
57
+ end
58
+ end
59
+
60
+ def try_perform!(*)
61
+ authorize!
62
+ super
63
+ end
64
+
65
+ def perform(*)
66
+ authorize!
67
+ super
68
+ end
69
+
70
+ def perform!(*)
71
+ authorize!
72
+ super
73
+ end
74
+
75
+ # Returns true if any of defined policies returns true
76
+ #
77
+ def allowed?
78
+ unless instance_variable_defined?(:@allowed)
79
+ @allowed = _policies_strategy.allowed?(self)
80
+ end
81
+ @allowed
82
+ end
83
+
84
+ # Raises Granite::Action::NotAllowedError if action is not allowed
85
+ #
86
+ def authorize!
87
+ fail Granite::Action::NotAllowedError, self unless allowed?
88
+ self
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,13 @@
1
+ module Granite
2
+ class Action
3
+ module Policies
4
+ # A Granite policies strategy which allows an action to be performed unconditionally.
5
+ # No defined policies are evaluated.
6
+ class AlwaysAllowStrategy
7
+ def self.allowed?(_action)
8
+ true
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module Granite
2
+ class Action
3
+ module Policies
4
+ # Granite BA policy which allows action to be performed if at least one defined policy evaluates to true
5
+ class AnyStrategy
6
+ def self.allowed?(action)
7
+ action._policies.any? { |policy| action.instance_exec(action.performer, &policy) }
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ module Granite
2
+ class Action
3
+ module Policies
4
+ # A Granite policies strategy which requires a performer to be present
5
+ #
6
+ # and at least one defined policy to be evaluated to true
7
+ class RequiredPerformerStrategy < AnyStrategy
8
+ def self.allowed?(action)
9
+ action.performer.present? && super
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,107 @@
1
+ require 'granite/action/preconditions/base_precondition'
2
+ require 'granite/action/preconditions/embedded_precondition'
3
+
4
+ module Granite
5
+ class Action
6
+ # Conditions module is used to define preconditions for actions.
7
+ # Each precondition is also defined as validation, so it always run
8
+ # before action execution. Precondition name is by default
9
+ # I18n key for +:base+ error, if precondition fails. Along with
10
+ # preconditions question methods with the same names are created.
11
+ #
12
+ module Preconditions
13
+ extend ActiveSupport::Concern
14
+
15
+ class PreconditionsCollection
16
+ def initialize(*preconditions)
17
+ @preconditions = preconditions.flatten
18
+ end
19
+
20
+ def +(other)
21
+ self.class.new(*@preconditions, other)
22
+ end
23
+
24
+ def execute!(context)
25
+ @preconditions.each { |precondition| precondition.execute!(context) }
26
+ end
27
+ end
28
+
29
+ included do
30
+ class_attribute :_preconditions, instance_writer: false
31
+ self._preconditions = PreconditionsCollection.new
32
+ end
33
+
34
+ module ClassMethods
35
+ # Define preconditions for current action.
36
+ #
37
+ # @param options [Hash] hash with
38
+ # @option message [String, Symbol] error message
39
+ # @option group [Symbol] procondition group(s)
40
+ # @param block [Block] which returns truthy value when precondition
41
+ # should pass.
42
+ def precondition(*args, &block)
43
+ options = args.extract_options!
44
+ if block
45
+ add_precondition(BasePrecondition, options, &block)
46
+ else
47
+ common_options = options.extract!(:if, :unless)
48
+ args.each do |type|
49
+ precondition common_options.merge(type => {})
50
+ end
51
+ options.each do |key, value|
52
+ value = Array.wrap(value)
53
+ precondition_options = value.extract_options!
54
+ add_precondition(klass(key), *value, precondition_options.merge!(common_options))
55
+ end
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def klass(key)
62
+ key = key.to_s.camelize
63
+ Granite.precondition_namespaces.reduce(nil) do |memo, ns|
64
+ memo || "#{ns.to_s.camelize}::#{key}Precondition".safe_constantize
65
+ end || fail(NameError, "No precondition class for #{key}Precondition")
66
+ end
67
+
68
+ def add_precondition(klass, *args, &block)
69
+ self._preconditions += klass.new(*args, &block)
70
+ end
71
+ end
72
+
73
+ attr_reader :failed_preconditions
74
+
75
+ def initialize(*)
76
+ @failed_preconditions = []
77
+ super
78
+ end
79
+
80
+ # Check if all preconditions are satisfied
81
+ #
82
+ # @return [Boolean] wheter all preconditions are satisfied
83
+ def satisfy_preconditions?
84
+ errors.clear
85
+ failed_preconditions.clear
86
+ run_preconditions!
87
+ end
88
+
89
+ # Adds passed error message and options to `errors` object
90
+ def decline_with(*args)
91
+ errors.add(:base, *args)
92
+ failed_preconditions << args.first
93
+ end
94
+
95
+ private
96
+
97
+ def run_preconditions!
98
+ _preconditions.execute! self
99
+ errors.empty?
100
+ end
101
+
102
+ def run_validations!
103
+ run_preconditions! && super
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,25 @@
1
+ module Granite
2
+ class Action
3
+ module Preconditions
4
+ class BasePrecondition
5
+ def initialize(*args, &block)
6
+ @options = args.extract_options!
7
+ @args = args
8
+ @block = block
9
+ end
10
+
11
+ def execute!(context)
12
+ return if @options[:if] && !context.instance_exec(&@options[:if])
13
+ return if @options[:unless] && context.instance_exec(&@options[:unless])
14
+ _execute(context)
15
+ end
16
+
17
+ private
18
+
19
+ def _execute(context)
20
+ context.instance_exec(&@block)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,42 @@
1
+ require 'granite/action/preconditions/base_precondition'
2
+
3
+ module Granite
4
+ class Action
5
+ module Preconditions
6
+ # Checks related business actions for precondition errors and adds them to current action.
7
+ #
8
+ # memoize def child_action
9
+ # ...
10
+ # end
11
+ # precondition embedded: :child_action
12
+ #
13
+ # memoize def child_action
14
+ # ...
15
+ # end
16
+ # memoize def child_actions
17
+ # ...
18
+ # end
19
+ # precondition embedded: [:child_action, :child_actions]
20
+ #
21
+ class EmbeddedPrecondition < BasePrecondition
22
+ private
23
+
24
+ def _execute(context)
25
+ associations = Array.wrap(@args.first)
26
+ associations.each do |name|
27
+ actions = Array.wrap(context.__send__(name))
28
+ actions.each do |action|
29
+ decline_action(context, action)
30
+ end
31
+ end
32
+ end
33
+
34
+ def decline_action(context, action)
35
+ return if action.satisfy_preconditions?
36
+ action.errors[:base].each { |error| context.errors.add(:base, error) }
37
+ action.failed_preconditions.each { |error| context.failed_preconditions << error }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,100 @@
1
+ module Granite
2
+ class Action
3
+ module Projectors
4
+ extend ActiveSupport::Concern
5
+
6
+ class ProjectorsCollection
7
+ def initialize(action_class)
8
+ @action_class = action_class
9
+ @storage = {}
10
+ @cache = {}
11
+ end
12
+
13
+ def fetch(name)
14
+ @cache[name.to_sym] ||= setup_projector(name)
15
+ end
16
+
17
+ def store(name, options, &block)
18
+ old_options, old_blocks = fetch_options_and_blocks(name)
19
+ @storage[name.to_sym] = [
20
+ old_options.merge(options || {}),
21
+ old_blocks + [block].compact
22
+ ]
23
+ end
24
+
25
+ def names
26
+ @storage.keys | (@action_class.superclass < Granite::Action ? @action_class.superclass._projectors.names : [])
27
+ end
28
+
29
+ private
30
+
31
+ def setup_projector(name)
32
+ options, blocks = fetch_options_and_blocks(name)
33
+
34
+ projector_name = "#{name}_projector".classify
35
+ controller_name = "#{name}_controller".classify
36
+
37
+ projector = Class.new(projector_superclass(name, projector_name, options))
38
+ projector.action_class = @action_class
39
+
40
+ redefine_const(projector_name, projector)
41
+ redefine_const(controller_name, projector.controller_class)
42
+
43
+ blocks.each do |block|
44
+ projector.class_eval(&block)
45
+ end
46
+
47
+ projector
48
+ end
49
+
50
+ def redefine_const(name, klass)
51
+ if @action_class.const_defined?(name, false)
52
+ @action_class.__send__(:remove_const, name)
53
+ # TODO: this remove is confusing, would be better to raise? - ask @pyromaniac
54
+ end
55
+ @action_class.const_set(name, klass)
56
+ end
57
+
58
+ def fetch_options_and_blocks(name)
59
+ name = name.to_sym
60
+ options, blocks = @storage[name.to_sym]
61
+ options ||= {}
62
+ blocks ||= []
63
+
64
+ [options, blocks]
65
+ end
66
+
67
+ def projector_superclass(name, projector_name, options)
68
+ superclass = options[:class_name].presence.try(:constantize)
69
+ superclass ||= @action_class.superclass._projectors.fetch(name) if @action_class.superclass < Granite::Action
70
+
71
+ superclass || projector_name.safe_constantize || Granite::Projector
72
+ end
73
+ end
74
+
75
+ module ClassMethods
76
+ def _projectors
77
+ @_projectors ||= ProjectorsCollection.new(self)
78
+ end
79
+
80
+ def projector_names
81
+ _projectors.names
82
+ end
83
+
84
+ def projector(name, options = {}, &block)
85
+ _projectors.store(name, options, &block)
86
+
87
+ class_eval <<-METHOD, __FILE__, __LINE__ + 1
88
+ def self.#{name}
89
+ _projectors.fetch(:#{name})
90
+ end
91
+
92
+ def #{name}
93
+ @#{name} ||= self.class._projectors.fetch(:#{name}).new(self)
94
+ end
95
+ METHOD
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,26 @@
1
+ require 'granite/action/represents/reflection'
2
+
3
+ module Granite
4
+ class Action
5
+ module Represents
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ private
10
+
11
+ def represents(*fields, &block)
12
+ options = fields.extract_options!.symbolize_keys
13
+
14
+ fields.each do |field|
15
+ add_attribute Granite::Action::Represents::Reflection, field, options, &block
16
+
17
+ before_validation do
18
+ attribute(field).sync if __send__ "#{field}_changed?"
19
+ true
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,90 @@
1
+ module Granite
2
+ class Action
3
+ module Represents
4
+ class Attribute < ActiveData::Model::Attributes::Attribute
5
+ delegate :writer, :reader, :reader_before_type_cast, to: :reflection
6
+
7
+ def initialize(*_args)
8
+ super
9
+
10
+ set_default_value
11
+ set_default_value_before_type_cast
12
+ end
13
+
14
+ def sync
15
+ reference.public_send(writer, read)
16
+ end
17
+
18
+ def typecast(value)
19
+ return value if value.class == type
20
+
21
+ typecaster.call(value, self) unless value.nil?
22
+ end
23
+
24
+ def type
25
+ return reflection.options[:type] if reflection.options[:type].present?
26
+ active_data_type || type_from_type_for_attribute || super
27
+ end
28
+
29
+ def typecaster
30
+ @typecaster ||= begin
31
+ type_class = type.instance_of?(Class) ? type : type.class
32
+ @typecaster = ActiveData.typecaster(type_class.ancestors.grep(Class))
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def reference
39
+ owner.__send__(reflection.reference)
40
+ end
41
+
42
+ def set_default_value
43
+ return unless reference.respond_to?(reader)
44
+
45
+ variable_cache(:value) do
46
+ normalize(enumerize(reference.public_send(reader)))
47
+ end
48
+ end
49
+
50
+ def set_default_value_before_type_cast
51
+ return unless reference.respond_to?(reader_before_type_cast)
52
+
53
+ variable_cache(:value_before_type_cast) do
54
+ defaultize(reference.public_send(reader_before_type_cast))
55
+ end
56
+ end
57
+
58
+ def active_data_type
59
+ return nil unless reference.is_a?(ActiveData::Model)
60
+
61
+ reference_attribute = reference.attribute(name)
62
+
63
+ return Granite::Action::Types::Collection.new(reference_attribute.type) if [
64
+ ActiveData::Model::Attributes::ReferenceMany,
65
+ ActiveData::Model::Attributes::Collection,
66
+ ActiveData::Model::Attributes::Dictionary
67
+ ].any? { |klass| reference_attribute.is_a? klass }
68
+
69
+ reference_attribute.type # TODO: create `type_for_attribute` method inside of ActiveData
70
+ end
71
+
72
+ def type_from_type_for_attribute
73
+ return nil unless reference.respond_to?(:type_for_attribute)
74
+
75
+ attribute_type = reference.type_for_attribute(name.to_s)
76
+
77
+ return Granite::Action::Types::Collection.new(convert_type_to_value_class(attribute_type.subtype)) if attribute_type.respond_to?(:subtype)
78
+
79
+ convert_type_to_value_class(attribute_type)
80
+ end
81
+
82
+ def convert_type_to_value_class(attribute_type)
83
+ return attribute_type.value_class if attribute_type.respond_to?(:value_class)
84
+
85
+ ActiveData::Model::Associations::PersistenceAdapters::ActiveRecord::TYPES[attribute_type.type&.to_sym]
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end