granite 0.13.0 → 0.14.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.
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