granite 0.13.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17fb5bd6029503d159cdb3e5dd88d0f58961a30e81e1752327bc261733c62df9
4
- data.tar.gz: cf15fd7b77203667a28c57ede076ee17a1df1014eca1cf97f15cc6efc2a3755a
3
+ metadata.gz: d0524a0055b0ea48aeab0691a1063cf332455b87094e2a78dbb6b66cc24b168e
4
+ data.tar.gz: e07a9c6a636eed28916860e213beea548aa92943da442b7d39019b346079e3fc
5
5
  SHA512:
6
- metadata.gz: 3540905bc5f0198b002bfd0be1c7a2dca41b594c6a21538c4cfa0763d85fe770fafb45227fb6f28b73e206b63dfe302a3cbe04af97cfe1e1f055f9adaa48f4dc
7
- data.tar.gz: f8837fc5c591f9c4169bcf60f6c6c9de8a3158e66c52d961b871f8bfe753f63ee353d7e560fa7fd3cb8667656fb30da01acf2065377ea50d6162f74ce8757202
6
+ metadata.gz: 217ac1d410c18e838f648af770c610b24ad4a28fc70e37514060509b5ac0a9303907912da5a2e7151162df2d842c78cdf3dd4c03e64ae464427bea9707923410
7
+ data.tar.gz: 2824f499e042ce4df4565d450367ccf3c33b711586c496c752fde675c4a1bd4d2a5810b92c84916603436981ef61c100583d89865e7f5c758c5033fbc3d97e20
@@ -14,11 +14,12 @@ module Granite
14
14
  before_action :authorize_action!
15
15
 
16
16
  def projector
17
- @projector ||= begin
18
- action_projector_class = action_class.public_send(projector_name)
19
- action_projector_class = action_projector_class.as(projector_performer) if respond_to?(:projector_performer, true)
20
- action_projector_class.new(projector_params)
21
- end
17
+ @projector ||=
18
+ begin
19
+ projector_class = action_class.public_send(projector_name)
20
+ projector_class = projector_class.with(projector_context) if respond_to?(:projector_context, true)
21
+ projector_class.new(projector_params)
22
+ end
22
23
  end
23
24
  helper_method :projector
24
25
 
@@ -0,0 +1,23 @@
1
+ module Granite
2
+ class Action
3
+ module Instrumentation
4
+ def perform!(*, **)
5
+ instrument_perform(:perform!) { super }
6
+ end
7
+
8
+ def perform(*, **)
9
+ instrument_perform(:perform) { super }
10
+ end
11
+
12
+ def try_perform!(*, **)
13
+ instrument_perform(:try_perform!) { super }
14
+ end
15
+
16
+ private
17
+
18
+ def instrument_perform(using, &block)
19
+ ActiveSupport::Notifications.instrument('granite.perform_action', action: self, using: using, &block)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,4 +1,4 @@
1
- require 'granite/performer_proxy'
1
+ require 'granite/context_proxy'
2
2
 
3
3
  module Granite
4
4
  class Action
@@ -8,15 +8,16 @@ module Granite
8
8
  extend ActiveSupport::Concern
9
9
 
10
10
  included do
11
- include PerformerProxy
12
- attr_reader :performer
11
+ include ContextProxy
12
+ attr_reader :ctx
13
13
  end
14
14
 
15
15
  def initialize(*args)
16
- @performer = self.class.proxy_performer
16
+ @ctx = self.class.proxy_context
17
17
  super
18
18
  end
19
19
 
20
+ delegate :performer, to: :ctx, allow_nil: true
20
21
  delegate :id, to: :performer, prefix: true, allow_nil: true
21
22
  end
22
23
  end
@@ -5,6 +5,7 @@ require 'active_support/callbacks'
5
5
 
6
6
  require 'granite/action/types'
7
7
  require 'granite/action/error'
8
+ require 'granite/action/instrumentation'
8
9
  require 'granite/action/performing'
9
10
  require 'granite/action/performer'
10
11
  require 'granite/action/precondition'
@@ -48,6 +49,7 @@ module Granite
48
49
  include Policies
49
50
  include Projectors
50
51
  prepend AssignAttributes
52
+ prepend Instrumentation
51
53
 
52
54
  handle_exception ActiveRecord::RecordInvalid do |e|
53
55
  merge_errors(e.record.errors)
@@ -0,0 +1,20 @@
1
+ module Granite
2
+ module ContextProxy
3
+ # Contains all the arbitrary data that is passed to BA with `with`
4
+ class Data
5
+ attr_reader :performer
6
+
7
+ def self.wrap(data)
8
+ if data.is_a?(self)
9
+ data
10
+ else
11
+ new(**data || {})
12
+ end
13
+ end
14
+
15
+ def initialize(performer: nil)
16
+ @performer = performer
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,23 +1,22 @@
1
1
  module Granite
2
- module PerformerProxy
3
- # Proxy helps to wrap the following method call with
4
- # performer-enabled context.
2
+ module ContextProxy
3
+ # Proxy which wraps the following method calls with BA context.
5
4
  #
6
5
  class Proxy
7
6
  extend Granite::Ruby3Compatibility
8
7
 
9
- def initialize(klass, performer)
8
+ def initialize(klass, context)
10
9
  @klass = klass
11
- @performer = performer
10
+ @context = context
12
11
  end
13
12
 
14
13
  def inspect
15
- "<#{@klass}PerformerProxy #{@performer}>"
14
+ "<#{@klass}ContextProxy #{@context}>"
16
15
  end
17
16
 
18
17
  ruby2_keywords def method_missing(method, *args, &block)
19
18
  if @klass.respond_to?(method)
20
- @klass.with_proxy_performer(@performer) do
19
+ @klass.with_context(@context) do
21
20
  @klass.public_send(method, *args, &block)
22
21
  end
23
22
  else
@@ -0,0 +1,34 @@
1
+ require 'granite/context_proxy/data'
2
+ require 'granite/context_proxy/proxy'
3
+
4
+ module Granite
5
+ # This concern contains class methods used for actions and projectors
6
+ #
7
+ module ContextProxy
8
+ extend ActiveSupport::Concern
9
+
10
+ module ClassMethods
11
+ PROXY_CONTEXT_KEY = :granite_proxy_context
12
+
13
+ def with(data)
14
+ Proxy.new(self, Data.wrap(data))
15
+ end
16
+
17
+ def as(performer)
18
+ with(performer: performer)
19
+ end
20
+
21
+ def with_context(context)
22
+ old_context = proxy_context
23
+ Thread.current[PROXY_CONTEXT_KEY] = context
24
+ yield
25
+ ensure
26
+ Thread.current[PROXY_CONTEXT_KEY] = old_context
27
+ end
28
+
29
+ def proxy_context
30
+ Thread.current[PROXY_CONTEXT_KEY]
31
+ end
32
+ end
33
+ end
34
+ end
@@ -2,11 +2,11 @@ require 'granite/projector/controller_actions'
2
2
  require 'granite/projector/error'
3
3
  require 'granite/projector/helpers'
4
4
  require 'granite/projector/translations'
5
- require 'granite/performer_proxy'
5
+ require 'granite/context_proxy'
6
6
 
7
7
  module Granite
8
8
  class Projector
9
- include PerformerProxy
9
+ include ContextProxy
10
10
  include ControllerActions
11
11
  include Helpers
12
12
  include Translations
@@ -42,7 +42,7 @@ module Granite
42
42
  private
43
43
 
44
44
  def build_action(*args)
45
- action_class.as(self.class.proxy_performer).new(*args)
45
+ action_class.with(self.class.proxy_context).new(*args)
46
46
  end
47
47
  end
48
48
  end
@@ -0,0 +1,89 @@
1
+ RSpec::Matchers.define :perform_action do |klass|
2
+ chain :using do |using|
3
+ @using = using
4
+ end
5
+
6
+ chain :as do |performer|
7
+ @performer = performer
8
+ end
9
+
10
+ chain :with do |attributes|
11
+ @attributes = attributes
12
+ end
13
+
14
+ match do |block|
15
+ @klass = klass
16
+ @using ||= :perform!
17
+
18
+ @payloads = []
19
+ subscriber = ActiveSupport::Notifications.subscribe('granite.perform_action') do |_, _, _, _, payload|
20
+ @payloads << payload
21
+ end
22
+
23
+ block.call
24
+
25
+ ActiveSupport::Notifications.unsubscribe(subscriber)
26
+
27
+ @payloads.detect { |payload| action_matches?(payload[:action]) && payload[:using] == @using }
28
+ end
29
+
30
+ failure_message do
31
+ output = "expected to call #{performed_entity}"
32
+ add_performer_message(output, @performer) if defined?(@performer)
33
+ add_attributes_message(output, @attributes) if defined?(@attributes)
34
+
35
+ similar_payloads = @payloads.select { |payload| class_matches?(payload[:action]) && payload[:using] == @using }
36
+ if similar_payloads.present?
37
+ output << "\nreceived calls to #{performed_entity}:"
38
+ similar_payloads.each { |payload| add_message_from_payload(output, payload) }
39
+ end
40
+
41
+ output
42
+ end
43
+
44
+ failure_message_when_negated do
45
+ "expected not to call #{performed_entity}"
46
+ end
47
+
48
+ supports_block_expectations
49
+
50
+ private
51
+
52
+ def add_message_from_payload(output, payload)
53
+ action = payload[:action]
54
+ add_performer_message(output, action.performer) if defined?(@performer)
55
+ add_attributes_message(output, actual_attributes(action)) if defined?(@attributes)
56
+ end
57
+
58
+ def add_performer_message(output, performer)
59
+ output << "\n AS #{performer.inspect}"
60
+ end
61
+
62
+ def add_attributes_message(output, attributes)
63
+ output << "\n WITH #{attributes.inspect}"
64
+ end
65
+
66
+ def performed_entity
67
+ "#{@klass}##{@using}"
68
+ end
69
+
70
+ def actual_attributes(action)
71
+ @attributes.keys.to_h { |attr| [attr, action.public_send(attr)] }
72
+ end
73
+
74
+ def action_matches?(action)
75
+ class_matches?(action) && performer_matches?(action) && attributes_match?(action)
76
+ end
77
+
78
+ def class_matches?(action)
79
+ action.is_a?(@klass)
80
+ end
81
+
82
+ def performer_matches?(action)
83
+ !defined?(@performer) || action.performer == @performer
84
+ end
85
+
86
+ def attributes_match?(action)
87
+ !defined?(@attributes) || match(@attributes).matches?(actual_attributes(action))
88
+ end
89
+ end
data/lib/granite/rspec.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'granite/rspec/action_helpers'
2
2
  require 'granite/rspec/have_projector'
3
+ require 'granite/rspec/perform_action'
3
4
  require 'granite/rspec/projector_helpers'
4
5
  require 'granite/rspec/raise_validation_error'
5
6
  require 'granite/rspec/satisfy_preconditions'
@@ -1,3 +1,3 @@
1
1
  module Granite
2
- VERSION = '0.13.0'.freeze
2
+ VERSION = '0.14.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: granite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Toptal Engineering
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-14 00:00:00.000000000 Z
11
+ date: 2022-08-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -323,6 +323,7 @@ files:
323
323
  - lib/granite/action.rb
324
324
  - lib/granite/action/error.rb
325
325
  - lib/granite/action/exceptions_handling.rb
326
+ - lib/granite/action/instrumentation.rb
326
327
  - lib/granite/action/performer.rb
327
328
  - lib/granite/action/performing.rb
328
329
  - lib/granite/action/policies.rb
@@ -346,10 +347,11 @@ files:
346
347
  - lib/granite/base.rb
347
348
  - lib/granite/config.rb
348
349
  - lib/granite/context.rb
350
+ - lib/granite/context_proxy.rb
351
+ - lib/granite/context_proxy/data.rb
352
+ - lib/granite/context_proxy/proxy.rb
349
353
  - lib/granite/dispatcher.rb
350
354
  - lib/granite/error.rb
351
- - lib/granite/performer_proxy.rb
352
- - lib/granite/performer_proxy/proxy.rb
353
355
  - lib/granite/projector.rb
354
356
  - lib/granite/projector/controller_actions.rb
355
357
  - lib/granite/projector/error.rb
@@ -369,6 +371,7 @@ files:
369
371
  - lib/granite/rspec.rb
370
372
  - lib/granite/rspec/action_helpers.rb
371
373
  - lib/granite/rspec/have_projector.rb
374
+ - lib/granite/rspec/perform_action.rb
372
375
  - lib/granite/rspec/projector_helpers.rb
373
376
  - lib/granite/rspec/raise_validation_error.rb
374
377
  - lib/granite/rspec/satisfy_preconditions.rb
@@ -399,7 +402,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
399
402
  - !ruby/object:Gem::Version
400
403
  version: '0'
401
404
  requirements: []
402
- rubygems_version: 3.3.17
405
+ rubygems_version: 3.3.9
403
406
  signing_key:
404
407
  specification_version: 4
405
408
  summary: Another business actions architecture for Rails apps
@@ -1,34 +0,0 @@
1
- require 'granite/performer_proxy/proxy'
2
-
3
- module Granite
4
- # This concern contains class methods used for actions and projectors
5
- #
6
- module PerformerProxy
7
- extend ActiveSupport::Concern
8
-
9
- module ClassMethods
10
- def as(performer)
11
- Proxy.new(self, performer)
12
- end
13
-
14
- def with_proxy_performer(performer)
15
- key = proxy_performer_key
16
- old_performer = Thread.current[key]
17
- Thread.current[key] = performer
18
- yield
19
- ensure
20
- Thread.current[key] = old_performer
21
- end
22
-
23
- def proxy_performer
24
- Thread.current[proxy_performer_key]
25
- end
26
-
27
- private
28
-
29
- def proxy_performer_key
30
- :"granite_proxy_performer_#{hash}"
31
- end
32
- end
33
- end
34
- end