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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 171acd12e388c4b1016581a76445ff1ae6dc9051
4
+ data.tar.gz: be9fdc3c8d64c3caab3c4ee0f944cbe3892d965d
5
+ SHA512:
6
+ metadata.gz: 4773a166e6b738430fa0300894bb4889c603404a2cba4dfc2a703000de1f7cb1679742abaf9e9ec2872f4edf1b397524abc3b0bc89b7c4ca99a57e44c5c33bbd
7
+ data.tar.gz: 61a75b1ecd04c0a43c13e8b06714080fdc31bdeeda6f6b0d52fabb19d9723fbfd73bf10a5747f4306962c96c8a214910e5796c74d9fe2dc63d84cdee463c69f9
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2018 Toptal
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,44 @@
1
+ require 'granite/projector/translations/helper'
2
+ require 'action_controller'
3
+
4
+ module Granite
5
+ class Controller < Granite.base_controller_class
6
+ include Granite::Projector::Translations::Helper
7
+
8
+ singleton_class.__send__(:attr_accessor, :projector_class)
9
+ singleton_class.delegate :projector_path, :projector_name, :action_class, to: :projector_class
10
+ delegate :projector_path, :projector_name, :action_class, :projector_class, to: 'self.class'
11
+
12
+ abstract!
13
+
14
+ before_action :authorize_action!
15
+
16
+ def projector
17
+ @projector ||= begin
18
+ action_projector_class = action_class.public_send(projector_name)
19
+ if respond_to?(:projector_performer, true)
20
+ action_projector_class = action_projector_class.as(projector_performer)
21
+ end
22
+ action_projector_class.new(projector_params)
23
+ end
24
+ end
25
+ helper_method :projector
26
+
27
+ delegate :action, to: :projector
28
+ helper_method :action
29
+
30
+ def self.local_prefixes
31
+ [projector_path]
32
+ end
33
+
34
+ private
35
+
36
+ def projector_params
37
+ params
38
+ end
39
+
40
+ def authorize_action!
41
+ action.authorize!
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,25 @@
1
+ Description:
2
+ Generates a sample granite action.
3
+
4
+ Example:
5
+ `rails generate granite user/create`
6
+
7
+ Will create:
8
+ apq/actions/ba/user/business_action.rb
9
+ apq/actions/ba/user/create.rb
10
+ spec/apq/actions/ba/user/create_spec.rb
11
+
12
+ `rails generate granite user/create -C`
13
+ `rails generate granite user/create --collection`
14
+
15
+ Will create:
16
+ apq/actions/ba/user/create.rb
17
+ spec/apq/actions/ba/user/create_spec.rb
18
+
19
+ `rails generate granite user/create simple`
20
+
21
+ Will create:
22
+ apq/actions/ba/user/create/simple/
23
+ apq/actions/ba/user/business_action.rb
24
+ apq/actions/ba/user/create.rb
25
+ spec/apq/actions/ba/user/create_spec.rb
@@ -0,0 +1,15 @@
1
+ require 'rails/generators/base'
2
+
3
+ module Granite
4
+ module Generators
5
+ class InstallControllerGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('../../../..', __FILE__)
7
+
8
+ desc 'Creates a Granite::Controller for further customization'
9
+
10
+ def copy_controller
11
+ copy_file 'app/controllers/granite/controller.rb'
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ class GraniteGenerator < Rails::Generators::NamedBase
2
+ source_root File.expand_path('../templates', __FILE__)
3
+
4
+ argument :projector, type: :string, required: false
5
+ class_option :collection, type: :boolean, aliases: '-C', desc: 'Generate collection action'
6
+
7
+ def create_action
8
+ template 'granite_action.rb.erb', "apq/actions/ba/#{file_path}.rb"
9
+ template 'granite_business_action.rb.erb', "apq/actions/ba/#{class_path.join('/')}/business_action.rb" unless options.collection?
10
+ template 'granite_base_action.rb.erb', 'apq/actions/base_action.rb', skip: true
11
+ template 'granite_action_spec.rb.erb', "spec/apq/actions/ba/#{file_path}_spec.rb"
12
+ empty_directory "apq/actions/ba/#{file_path}/#{projector}" if projector
13
+ end
14
+
15
+ private
16
+
17
+ def base_class_name
18
+ if options.collection?
19
+ 'BaseAction'
20
+ else
21
+ "BA::#{class_path.join('/').camelize}::BusinessAction"
22
+ end
23
+ end
24
+
25
+ def subject_name
26
+ class_path.last
27
+ end
28
+
29
+ def subject_class_name
30
+ subject_name.classify
31
+ end
32
+ end
@@ -0,0 +1,22 @@
1
+ class BA::<%= class_name %> < <%= base_class_name %>
2
+ <% if projector -%>
3
+ projector :<%= projector %>
4
+
5
+ <% end -%>
6
+ allow_if { false }
7
+
8
+ precondition do
9
+ end
10
+ <% if options.collection? -%>
11
+
12
+ def subject
13
+ @subject ||= <%= subject_class_name %>.new
14
+ end
15
+ <% end -%>
16
+
17
+ private
18
+
19
+ def execute_perform!(*)
20
+ subject.save!
21
+ end
22
+ end
@@ -0,0 +1,45 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe BA::<%= class_name %> do
4
+ <% if options.collection? -%>
5
+ subject(:action) { described_class.as(performer).new(attributes) }
6
+
7
+ <% else -%>
8
+ subject(:action) { described_class.as(performer).new(<%= subject_name %>, attributes) }
9
+
10
+ let(:<%= subject_name %>) { <%= subject_class_name %>.new }
11
+ <% end -%>
12
+ let(:performer) { double }
13
+ let(:attributes) { {} }
14
+
15
+ describe 'policies' do
16
+ it { is_expected.to be_allowed }
17
+
18
+ context 'when user is not authorized' do
19
+ it { is_expected.not_to be_allowed }
20
+ end
21
+ end
22
+
23
+ describe 'preconditions' do
24
+ it { is_expected.to satisfy_preconditions }
25
+
26
+ context 'when preconditions fail' do
27
+ it { is_expected.not_to satisfy_preconditions }
28
+ end
29
+ end
30
+
31
+ describe 'validations' do
32
+ end
33
+
34
+ describe '#perform!' do
35
+ <% if options.collection? -%>
36
+ specify do
37
+ expect { perform! }.to change { <%= subject_class_name %>.count }.by(1)
38
+ end
39
+ <% else -%>
40
+ specify do
41
+ expect { perform!(<%= subject_name %>) }.to change { <%= subject_name %>.reload.attributes }.to(attributes)
42
+ end
43
+ <% end -%>
44
+ end
45
+ end
@@ -0,0 +1,2 @@
1
+ class BaseAction < Granite::Action
2
+ end
@@ -0,0 +1,3 @@
1
+ class BA::<%= class_path.join('/').camelize %>::BusinessAction < BaseAction
2
+ subject :<%= subject_name %>
3
+ end
@@ -0,0 +1,24 @@
1
+ require 'active_support/dependencies'
2
+ require 'action_controller'
3
+
4
+ require 'granite/config'
5
+ require 'granite/context'
6
+
7
+ module Granite
8
+ def self.config
9
+ Granite::Config.instance
10
+ end
11
+
12
+ def self.context
13
+ Granite::Context.instance
14
+ end
15
+
16
+ singleton_class.delegate(*Granite::Config.delegated, to: :config)
17
+ singleton_class.delegate(*Granite::Context.delegated, to: :context)
18
+ end
19
+
20
+ require 'granite/dispatcher'
21
+ require 'granite/action'
22
+ require 'granite/projector'
23
+ require 'granite/routing'
24
+ require 'granite/rails' if defined?(::Rails)
@@ -0,0 +1,106 @@
1
+ require 'active_data'
2
+ require 'active_record/errors'
3
+ require 'active_record/validations'
4
+ require 'active_support/callbacks'
5
+
6
+ require 'granite/action/types'
7
+ require 'granite/action/represents'
8
+ require 'granite/action/error'
9
+ require 'granite/action/performing'
10
+ require 'granite/action/performer'
11
+ require 'granite/action/preconditions'
12
+ require 'granite/action/policies'
13
+ require 'granite/action/projectors'
14
+ require 'granite/action/translations'
15
+ require 'granite/action/subject'
16
+
17
+ module Granite
18
+ class Action
19
+ class ValidationError < Error
20
+ delegate :errors, to: :action
21
+
22
+ def initialize(action)
23
+ errors = action.errors.full_messages.join(', ')
24
+ super(I18n.t(:"#{action.class.i18n_scope}.errors.messages.action_invalid", action: action.class, errors: errors, default: :'errors.messages.action_invalid'), action)
25
+ end
26
+ end
27
+
28
+ # We are using a lot of stacked additional logic for `assign_attributes`
29
+ # At least, represented and nested attributes modules in ActiveData
30
+ # are having such a method redefiniions. Both are prepended to the
31
+ # Granite action, so we have to prepend our patch as well in order
32
+ # to put it above all other, so it will handle the attributes first.
33
+ module AssignAttributes
34
+ def assign_attributes(attributes)
35
+ attributes = attributes.to_unsafe_hash if attributes.respond_to?(:to_unsafe_hash)
36
+ attributes = attributes.stringify_keys
37
+ if attributes.key?(model_name.param_key)
38
+ attributes = attributes.merge(attributes.delete(model_name.param_key))
39
+ end
40
+ super(attributes)
41
+ end
42
+ end
43
+
44
+ include ActiveSupport::Callbacks
45
+ include ActiveData::Model
46
+ include ActiveData::Model::Representation
47
+ include ActiveData::Model::Associations
48
+ include ActiveData::Model::Dirty
49
+ include ActiveModel::Validations::Callbacks
50
+ include Performing
51
+ include Subject
52
+ include Performer
53
+ include Preconditions
54
+ include Policies
55
+ include Projectors
56
+ include Translations
57
+ include Represents
58
+ prepend AssignAttributes
59
+
60
+ handle_exception ActiveRecord::RecordInvalid do |e|
61
+ errors.messages.deep_merge!(e.record.errors.messages) do |_, this, other|
62
+ (this + other).uniq
63
+ end
64
+ end
65
+
66
+ handle_exception ActiveData::ValidationError do |e|
67
+ errors.messages.deep_merge!(e.model.errors.messages) do |_, this, other|
68
+ (this + other).uniq
69
+ end
70
+ end
71
+
72
+ handle_exception Granite::Action::ValidationError do |e|
73
+ errors.messages.deep_merge!(e.action.errors.messages) do |_, this, other|
74
+ (this + other).uniq
75
+ end
76
+ end
77
+
78
+ def self.i18n_scope
79
+ :granite_action
80
+ end
81
+
82
+ # Almost the same as Dirty `#changed?` method, but
83
+ # doesn't check subject reference key
84
+ def attributes_changed?(except: [])
85
+ except = Array.wrap(except).push(self.class.reflect_on_association(:subject).reference_key)
86
+ changed_attributes.except(*except).present?
87
+ end
88
+
89
+ # Check if action is allowed to execute by current performer (see {Granite.performer})
90
+ # and satisfy all defined preconditions
91
+ #
92
+ # @return [Boolean] whether action is performable
93
+ def performable?
94
+ unless instance_variable_defined?(:@performable)
95
+ @performable = allowed? && satisfy_preconditions?
96
+ end
97
+ @performable
98
+ end
99
+
100
+ protected
101
+
102
+ def raise_validation_error(original_error = nil)
103
+ fail ValidationError, self, original_error&.backtrace
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,14 @@
1
+ require 'granite/error'
2
+
3
+ module Granite
4
+ class Action
5
+ class Error < Granite::Error
6
+ attr_reader :action
7
+
8
+ def initialize(message, action = nil)
9
+ @action = action
10
+ super(message)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ require 'granite/performer_proxy'
2
+
3
+ module Granite
4
+ class Action
5
+ # Performer module is responsible for setting performer for action.
6
+ #
7
+ module Performer
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ include PerformerProxy
12
+ attr_reader :performer
13
+ end
14
+
15
+ def initialize(*args)
16
+ @performer = self.class.proxy_performer
17
+ super
18
+ end
19
+
20
+ delegate :id, to: :performer, prefix: true, allow_nil: true
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,132 @@
1
+ require 'granite/action/transaction'
2
+ require 'granite/action/error'
3
+
4
+ module Granite
5
+ class Action
6
+ # Performing module used for defining perform procedure and error
7
+ # handling. Perform procedure is defined as block, which is
8
+ # executed in action instance context so all attributes are
9
+ # available there. Actions by default are performed in silent way
10
+ # (no validation exception raised), to raise exceptions, call bang
11
+ # method {Granite::Action::Performing#perform!}
12
+ #
13
+ # Defined exceptions handlers are also executed in action
14
+ # instance context, but additionally get raised exception as
15
+ # parameter.
16
+ #
17
+ module Performing
18
+ extend ActiveSupport::Concern
19
+
20
+ include Transaction
21
+
22
+ included do
23
+ class_attribute :_exception_handlers, instance_writer: false
24
+ self._exception_handlers = {}
25
+
26
+ protected :_exception_handlers
27
+
28
+ define_callbacks :execute_perform
29
+ end
30
+
31
+ module ClassMethods
32
+ # Register default handler for exceptions thrown inside execute_perform! method.
33
+ # @param klass Exception class, could be parent class too [Class]
34
+ # @param block [Block<Exception>] with default behavior for handling specified
35
+ # type exceptions. First block argument is raised exception instance.
36
+ #
37
+ # @return [Hash<Class, Proc>] Registered handlers
38
+ def handle_exception(klass, &block)
39
+ self._exception_handlers = _exception_handlers.merge(klass => block)
40
+ end
41
+
42
+ def perform(*)
43
+ fail 'Perform block declaration was removed! Please declare `private def execute_perform!(*)` method'
44
+ end
45
+ end
46
+
47
+ # Check preconditions and validations for action and associated objects, then
48
+ # in case of valid action run defined procedure. Procedure is wrapped with
49
+ # database transaction. Returns the result of execute_perform! method execution
50
+ # or true if method execution returned false or nil
51
+ #
52
+ # @param context [Symbol] can be optionally provided to define which
53
+ # validations to test against (the context is defined on validations
54
+ # using `:on`)
55
+ # @return [Object] result of execute_perform! method execution or false in case of errors
56
+ def perform(context: nil, **options)
57
+ transactional do
58
+ valid?(context) && perform_action(options)
59
+ end
60
+ end
61
+
62
+ # Check precondition and validations for action and associated objects, then
63
+ # raise exception in case of validation errors. In other case run defined procedure.
64
+ # Procedure is wraped with database transaction. After procedure execution check for
65
+ # errors, and raise exception if any. Returns the result of execute_perform! method execution
66
+ # or true if block execution returned false or nil
67
+ #
68
+ # @param context [Symbol] can be optionally provided to define which
69
+ # validations to test against (the context is defined on validations
70
+ # using `:on`)
71
+ # @return [Object] result of execute_perform! method execution
72
+ # @raise [Granite::Action::ValidationError] Action or associated objects are invalid
73
+ # @raise [NotImplementedError] execute_perform! method was not defined yet
74
+ def perform!(context: nil, **options)
75
+ transactional do
76
+ validate!(context)
77
+ perform_action!(**options)
78
+ end
79
+ end
80
+
81
+ # Performs action if preconditions are satisfied.
82
+ #
83
+ # @param context [Symbol] can be optionally provided to define which
84
+ # validations to test against (the context is defined on validations
85
+ # using `:on`)
86
+ # @return [Object] result of execute_perform! method execution
87
+ # @raise [Granite::Action::ValidationError] Action or associated objects are invalid
88
+ # @raise [NotImplementedError] execute_perform! method was not defined yet
89
+ def try_perform!(context: nil, **options)
90
+ return unless satisfy_preconditions?
91
+ transactional do
92
+ perform!(context: context, **options)
93
+ end
94
+ end
95
+
96
+ # Checks if action was successfully performed or not
97
+ #
98
+ # @return [Boolean] whether action was successfully performed or not
99
+ def performed?
100
+ @_action_performed.present?
101
+ end
102
+
103
+ private
104
+
105
+ def perform_action(raise_errors: false, **options)
106
+ apply_association_changes!
107
+ result = run_callbacks(:execute_perform) { execute_perform!(options) }
108
+ @_action_performed = true
109
+ result || true
110
+ rescue *_exception_handlers.keys => e
111
+ handle_exception(e)
112
+ raise_validation_error(e) if raise_errors
113
+ raise Rollback
114
+ end
115
+
116
+ def perform_action!(**options)
117
+ perform_action(raise_errors: true, **options)
118
+ end
119
+
120
+ def execute_perform!(**_options)
121
+ fail NotImplementedError, "BA perform body MUST be defined for #{self}"
122
+ end
123
+
124
+ def handle_exception(e)
125
+ klass = e.class.ancestors.detect do |ancestor|
126
+ ancestor <= Exception && _exception_handlers[ancestor]
127
+ end
128
+ instance_exec(e, &_exception_handlers[klass]) if klass
129
+ end
130
+ end
131
+ end
132
+ end