granite 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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