shamu 0.0.21 → 0.0.24
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 +4 -4
- data/.rubocop.yml +1 -1
- data/.ruby-version +1 -1
- data/Gemfile +3 -1
- data/Gemfile.lock +70 -69
- data/app/README +1 -0
- data/config/environment.rb +1 -0
- data/lib/shamu/attributes.rb +26 -0
- data/lib/shamu/attributes/equality.rb +10 -1
- data/lib/shamu/entities/entity.rb +11 -0
- data/lib/shamu/entities/list_scope.rb +3 -0
- data/lib/shamu/entities/list_scope/sorting.rb +6 -1
- data/lib/shamu/entities/list_scope/window_paging.rb +1 -1
- data/lib/shamu/entities/null_entity.rb +18 -12
- data/lib/shamu/entities/opaque_id.rb +9 -8
- data/lib/shamu/error.rb +24 -3
- data/lib/shamu/events/active_record/migration.rb +0 -2
- data/lib/shamu/events/active_record/service.rb +3 -1
- data/lib/shamu/events/events_service.rb +2 -2
- data/lib/shamu/events/in_memory/async_service.rb +4 -2
- data/lib/shamu/events/in_memory/service.rb +3 -1
- data/lib/shamu/features/features_service.rb +3 -1
- data/lib/shamu/features/support.rb +1 -1
- data/lib/shamu/json_api/rails/controller.rb +1 -1
- data/lib/shamu/locale/en.yml +14 -2
- data/lib/shamu/security/active_record_policy.rb +2 -1
- data/lib/shamu/security/error.rb +1 -1
- data/lib/shamu/security/policy.rb +14 -4
- data/lib/shamu/security/principal.rb +22 -2
- data/lib/shamu/security/roles.rb +4 -3
- data/lib/shamu/services.rb +3 -1
- data/lib/shamu/services/active_record.rb +2 -1
- data/lib/shamu/services/active_record_crud.rb +43 -19
- data/lib/shamu/services/lazy_association.rb +50 -18
- data/lib/shamu/services/observable_support.rb +73 -0
- data/lib/shamu/services/observed_request.rb +76 -0
- data/lib/shamu/services/request.rb +12 -0
- data/lib/shamu/services/request_support.rb +24 -2
- data/lib/shamu/services/result.rb +62 -1
- data/lib/shamu/services/service.rb +58 -33
- data/lib/shamu/sessions/cookie_store.rb +3 -1
- data/lib/shamu/sessions/session_store.rb +2 -2
- data/lib/shamu/version.rb +1 -1
- data/shamu.gemspec +1 -1
- data/spec/lib/shamu/entities/entity_lookup_service_spec.rb +1 -1
- data/spec/lib/shamu/entities/list_scope/sorting_spec.rb +1 -1
- data/spec/lib/shamu/entities/null_entity_spec.rb +6 -1
- data/spec/lib/shamu/entities/opaque_id_spec.rb +13 -6
- data/spec/lib/shamu/entities/static_repository_spec.rb +2 -2
- data/spec/lib/shamu/rails/entity_spec.rb +1 -1
- data/spec/lib/shamu/rails/features_spec.rb +1 -1
- data/spec/lib/shamu/security/principal_spec.rb +25 -0
- data/spec/lib/shamu/services/active_record_crud_spec.rb +39 -4
- data/spec/lib/shamu/services/lazy_association_spec.rb +19 -6
- data/spec/lib/shamu/services/observable_support_spec.rb +55 -0
- data/spec/lib/shamu/services/observed_request_spec.rb +46 -0
- data/spec/lib/shamu/services/request_support_spec.rb +54 -3
- data/spec/lib/shamu/services/service_spec.rb +16 -27
- metadata +15 -5
@@ -2,30 +2,62 @@ module Shamu
|
|
2
2
|
module Services
|
3
3
|
|
4
4
|
# Lazily look up an associated resource
|
5
|
-
|
5
|
+
module LazyAssociation
|
6
|
+
EXCLUDE_PATTERN = /\A(block_given\?|id|send|public_send|iterator|object_id|to_model_id|binding|class|kind_of\?|is_a\?|instance_of\?|respond_to\?|p.+_methods|__.+__)\z/ # rubocop:disable Metrics/LineLength
|
7
|
+
MUTEX = Mutex.new
|
6
8
|
|
7
|
-
#
|
8
|
-
|
9
|
-
#
|
9
|
+
def self.class_for( klass ) # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
|
10
|
+
return klass.const_get( :Lazy_ ) if klass.const_defined?( :Lazy_ )
|
10
11
|
|
11
|
-
|
12
|
-
# @return [Object] the primary key id of the association. Not delegated so
|
13
|
-
# it is safe to use and will not trigger an unnecessary fetch.
|
14
|
-
attr_reader :id
|
12
|
+
MUTEX.synchronize do
|
15
13
|
|
16
|
-
|
17
|
-
|
14
|
+
# Check again in case another thread defined it while waiting for the
|
15
|
+
# mutex
|
16
|
+
return klass.const_get( :Lazy_ ) if klass.const_defined?( :Lazy_ )
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
18
|
+
lazy_class = Class.new( klass ) do
|
19
|
+
# Remove all existing public methods so that they can be delegated
|
20
|
+
# with #method_missing.
|
21
|
+
klass.public_instance_methods.each do |method|
|
22
|
+
next if EXCLUDE_PATTERN =~ method
|
23
|
+
undef_method method
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize( id, &block )
|
27
|
+
@id = id
|
28
|
+
@block = block
|
29
|
+
end
|
30
|
+
|
31
|
+
# @!attribute
|
32
|
+
# @return [Object] the primary key id of the association. Not delegated so
|
33
|
+
# it is safe to use and will not trigger an unnecessary fetch.
|
34
|
+
attr_reader :id
|
35
|
+
|
36
|
+
def __getobj__
|
37
|
+
return @association if defined? @association
|
23
38
|
|
24
|
-
|
25
|
-
|
39
|
+
@association = @block.call( @id ) if @block
|
40
|
+
end
|
26
41
|
|
27
|
-
|
42
|
+
def method_missing( method, *args, &block )
|
43
|
+
if respond_to_missing?( method )
|
44
|
+
__getobj__.public_send( method, *args, &block )
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def respond_to_missing?( method, include_private = false )
|
51
|
+
__getobj__.respond_to?( method, include_private ) || super
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
klass.const_set( :Lazy_, lazy_class )
|
57
|
+
lazy_class
|
58
|
+
end
|
28
59
|
end
|
60
|
+
|
29
61
|
end
|
30
62
|
end
|
31
|
-
end
|
63
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Services
|
3
|
+
|
4
|
+
# Adds the ability for other services to observe requests on the service.
|
5
|
+
#
|
6
|
+
# In contrast to {Events} that are async and independent, observers are
|
7
|
+
# called immediately when a request is performed and may influence the
|
8
|
+
# behavior of the request.
|
9
|
+
#
|
10
|
+
# See {#ObserverSupport} for details on observing other services.
|
11
|
+
module ObservableSupport
|
12
|
+
|
13
|
+
# Ask to be notified of important actions as they are executed on the
|
14
|
+
# service.
|
15
|
+
#
|
16
|
+
# @yield (observed_request)
|
17
|
+
# @yieldparam [ObservedRequest] action
|
18
|
+
def observe( &block )
|
19
|
+
@observers ||= []
|
20
|
+
@observers << block
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# @!visibility public
|
26
|
+
#
|
27
|
+
# Invoke a block notifying observers before the action is performed
|
28
|
+
# allowing them to modify inputs or request the action be canceled.
|
29
|
+
#
|
30
|
+
# @param [Request] request the service request
|
31
|
+
# @return [Result]
|
32
|
+
# @yield [Request]
|
33
|
+
def with_observers( request, &block )
|
34
|
+
observed = ObservedRequest.new( request: request )
|
35
|
+
notify_observers( observed )
|
36
|
+
|
37
|
+
returned =
|
38
|
+
if observed.cancel_requested?
|
39
|
+
request.error :base, :canceled
|
40
|
+
else
|
41
|
+
yield( request )
|
42
|
+
end
|
43
|
+
|
44
|
+
result = Result.coerce( returned, request: request )
|
45
|
+
|
46
|
+
observed.complete( result, false )
|
47
|
+
end
|
48
|
+
|
49
|
+
# @!visibility public
|
50
|
+
#
|
51
|
+
# Notify all registered observers about the pending request.
|
52
|
+
# @param [ObservedAction] observed_action
|
53
|
+
def notify_observers( observed_action )
|
54
|
+
return unless defined? @observers
|
55
|
+
|
56
|
+
@observers.each do |observer|
|
57
|
+
observer.call( observed_action )
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Override {Shamu::Services::RequestSupport#with_partial_request} to make all requests observable.
|
62
|
+
# {#audit_request audit the request}.
|
63
|
+
def with_partial_request( *args, &block )
|
64
|
+
super( *args ) do |request|
|
65
|
+
with_observers request do |wrapped_request|
|
66
|
+
yield wrapped_request
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Shamu
|
2
|
+
module Services
|
3
|
+
|
4
|
+
# Describes request that will be/has been performed by a service and the
|
5
|
+
# associated data properties.
|
6
|
+
class ObservedRequest
|
7
|
+
include Shamu::Attributes
|
8
|
+
|
9
|
+
# ============================================================================
|
10
|
+
# @!group Attributes
|
11
|
+
#
|
12
|
+
|
13
|
+
# @!attribute
|
14
|
+
# @return [Request] the original request submitted to the service. The
|
15
|
+
# request may be modified by the observers.
|
16
|
+
attribute :request
|
17
|
+
|
18
|
+
# @return [Result] the result of a dependency that asked for the request
|
19
|
+
# to be canceled.
|
20
|
+
attr_reader :cancel_reason
|
21
|
+
|
22
|
+
#
|
23
|
+
# @!endgroup Attributes
|
24
|
+
|
25
|
+
# Ask that the service cancel the request.
|
26
|
+
#
|
27
|
+
# @return [Result] a nested result that should be reported for why the
|
28
|
+
# request was canceled.
|
29
|
+
def request_cancel( result = Result.new )
|
30
|
+
@cancel_reason = result
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [Boolean] true if an observer has asked the request to be
|
34
|
+
# canceled.
|
35
|
+
def cancel_requested?
|
36
|
+
!!cancel_reason
|
37
|
+
end
|
38
|
+
|
39
|
+
# Execute block if the action was canceled by another observer.
|
40
|
+
# @yield(result)
|
41
|
+
# @yieldresult [Result]
|
42
|
+
def on_canceled( &block )
|
43
|
+
@on_cancel_blocks ||= []
|
44
|
+
@on_cancel_blocks << block
|
45
|
+
end
|
46
|
+
|
47
|
+
# Mark the action as complete and run any {#on_success} or #{on_fail}
|
48
|
+
# callbacks.
|
49
|
+
#
|
50
|
+
# @param [Result] result the result of the action. If valid success callbacks are invoked.
|
51
|
+
# @param [Boolean] canceled true if the action was canceled and not
|
52
|
+
# processed.
|
53
|
+
#
|
54
|
+
# @return [Result] the result of all the observed callbacks.
|
55
|
+
def complete( result, canceled )
|
56
|
+
invoke_callbacks( result, @on_cancel_blocks ) if canceled
|
57
|
+
|
58
|
+
result
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def invoke_callbacks( result, callbacks )
|
64
|
+
return unless callbacks.present?
|
65
|
+
|
66
|
+
callbacks.each do |callback|
|
67
|
+
nested = callback.call( result )
|
68
|
+
result.join( nested ) if nested
|
69
|
+
end
|
70
|
+
|
71
|
+
result
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -85,6 +85,18 @@ module Shamu
|
|
85
85
|
@on_complete_blocks && @on_complete_blocks.each( &:call )
|
86
86
|
end
|
87
87
|
|
88
|
+
# Adds an error to {#errors} and returns self. Used when performing an
|
89
|
+
# early return in a service method
|
90
|
+
#
|
91
|
+
# @example
|
92
|
+
# next request.error( :title, "should be clever" ) unless title_is_clever?
|
93
|
+
#
|
94
|
+
# @return [self]
|
95
|
+
def error( *args )
|
96
|
+
errors.add( *args )
|
97
|
+
self
|
98
|
+
end
|
99
|
+
|
88
100
|
class << self
|
89
101
|
# Coerces a hash or params object to a proper {Request} object.
|
90
102
|
# @param [Object] params to be coerced.
|
@@ -58,7 +58,7 @@ module Shamu
|
|
58
58
|
# order = Models::Order.find( request.id )
|
59
59
|
#
|
60
60
|
# # Custom validation
|
61
|
-
# next error( :base, "can't do that" ) if order.state == 'processed'
|
61
|
+
# next request.error( :base, "can't do that" ) if order.state == 'processed'
|
62
62
|
#
|
63
63
|
# request.apply_to( order )
|
64
64
|
#
|
@@ -66,7 +66,7 @@ module Shamu
|
|
66
66
|
# next order unless order.save
|
67
67
|
#
|
68
68
|
# # All good, return an entity for the order
|
69
|
-
# scorpion.fetch OrderEntity, { order: order }
|
69
|
+
# scorpion.fetch OrderEntity, { order: order }
|
70
70
|
# end
|
71
71
|
# end
|
72
72
|
def with_request( params, request_class, &block )
|
@@ -92,10 +92,32 @@ module Shamu
|
|
92
92
|
|
93
93
|
result = Result.coerce( sources, request: request )
|
94
94
|
request.complete( result.valid? )
|
95
|
+
recache_entity( result.entity ) if result.valid? && result.entity
|
95
96
|
|
96
97
|
result
|
97
98
|
end
|
98
99
|
|
100
|
+
# Support convenient calling convention on update/delete style methods
|
101
|
+
# that might pass an id and params or a single params hash with
|
102
|
+
# associated id.
|
103
|
+
#
|
104
|
+
# @example
|
105
|
+
# users.update user, name: "Changed" # Backend service
|
106
|
+
# users.update id: 1, name: "Changed" # HTTP request params
|
107
|
+
#
|
108
|
+
def extract_params( id, params )
|
109
|
+
if !params && !id.respond_to?( :to_model_id )
|
110
|
+
params, id = id, id[ :id ] || id[ "id" ]
|
111
|
+
end
|
112
|
+
|
113
|
+
if params
|
114
|
+
params = params.symbolize_keys if params.respond_to?( :symbolize_keys )
|
115
|
+
params[ :id ] ||= id
|
116
|
+
end
|
117
|
+
|
118
|
+
[ id, params ]
|
119
|
+
end
|
120
|
+
|
99
121
|
# Static methods added to {RequestSupport}
|
100
122
|
class_methods do
|
101
123
|
|
@@ -38,6 +38,12 @@ module Shamu
|
|
38
38
|
value
|
39
39
|
end
|
40
40
|
|
41
|
+
# @return [Array<Result>] results from calling dependent assemblies that
|
42
|
+
# may have caused the request to fail.
|
43
|
+
def nested_results
|
44
|
+
@nested_results ||= []
|
45
|
+
end
|
46
|
+
|
41
47
|
#
|
42
48
|
# @!endgroup Attributes
|
43
49
|
|
@@ -48,7 +54,7 @@ module Shamu
|
|
48
54
|
# the first {Request} object found in the `values`.
|
49
55
|
# @param [Entities::Entity] entity submitted to the service. If :not_set,
|
50
56
|
# uses the first {Entity} object found in the `values`.
|
51
|
-
def initialize( *values, request: :not_set, entity: :not_set ) # rubocop:disable Metrics/LineLength, Metrics/
|
57
|
+
def initialize( *values, request: :not_set, entity: :not_set ) # rubocop:disable Metrics/LineLength, Metrics/PerceivedComplexity
|
52
58
|
@values = values
|
53
59
|
@value = values.first
|
54
60
|
|
@@ -97,6 +103,12 @@ module Shamu
|
|
97
103
|
self
|
98
104
|
end
|
99
105
|
|
106
|
+
# Joins a dependency's result to the result of the request.
|
107
|
+
def join( result )
|
108
|
+
nested_results << result
|
109
|
+
append_error_source result
|
110
|
+
end
|
111
|
+
|
100
112
|
# @return [Result] the value coerced to a {Result}.
|
101
113
|
def self.coerce( value, **args )
|
102
114
|
if value.is_a?( Result )
|
@@ -106,6 +118,55 @@ module Shamu
|
|
106
118
|
end
|
107
119
|
end
|
108
120
|
|
121
|
+
# @return [String] debug friendly string
|
122
|
+
def inspect # rubocop:disable Metrics/AbcSize
|
123
|
+
result = "#<#{ self.class } valid: #{ valid? }"
|
124
|
+
result << ", errors: #{ errors.inspect }" if errors.any?
|
125
|
+
result << ", entity: #{ entity.inspect }" if entity
|
126
|
+
result << ", value: #{ value.inspect }" if value && value != entity
|
127
|
+
result << ", values: #{ values.inspect }" if values.length > 1
|
128
|
+
result << ">"
|
129
|
+
result
|
130
|
+
end
|
131
|
+
|
132
|
+
# @return [String] even friendlier debug string.
|
133
|
+
def pretty_print( pp ) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
134
|
+
pp.object_address_group( self ) do
|
135
|
+
pp.breakable " "
|
136
|
+
pp.text "valid: "
|
137
|
+
pp.pp valid?
|
138
|
+
|
139
|
+
if errors.any?
|
140
|
+
pp.comma_breakable
|
141
|
+
pp.text "errors:"
|
142
|
+
pp.breakable " "
|
143
|
+
pp.pp errors
|
144
|
+
end
|
145
|
+
|
146
|
+
if entity
|
147
|
+
pp.comma_breakable
|
148
|
+
pp.text "entity:"
|
149
|
+
pp.breakable " "
|
150
|
+
pp.pp entity
|
151
|
+
end
|
152
|
+
|
153
|
+
if !value.nil? && value != entity
|
154
|
+
pp.comma_breakable
|
155
|
+
pp.text "value:"
|
156
|
+
pp.breakable " "
|
157
|
+
pp.pp value
|
158
|
+
end
|
159
|
+
|
160
|
+
if values.length > 1
|
161
|
+
pp.comma_breakable
|
162
|
+
pp.text "values:"
|
163
|
+
pp.breakable " "
|
164
|
+
pp.pp values - [ value ]
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
|
109
170
|
private
|
110
171
|
|
111
172
|
def append_error_source( source )
|
@@ -53,7 +53,7 @@ module Shamu
|
|
53
53
|
# ```
|
54
54
|
# def report( report_scope = nil )
|
55
55
|
# report_scope = UserReportScope.coerce! report_scope
|
56
|
-
# scorpion.fetch UserReport, report_scope
|
56
|
+
# scorpion.fetch UserReport, report_scope
|
57
57
|
# end
|
58
58
|
# ```
|
59
59
|
class Service
|
@@ -61,9 +61,6 @@ module Shamu
|
|
61
61
|
# Support dependency injection for related services.
|
62
62
|
include Scorpion::Object
|
63
63
|
|
64
|
-
initialize do
|
65
|
-
end
|
66
|
-
|
67
64
|
private
|
68
65
|
|
69
66
|
# Maps a single record to an entity. Requires a `build_entities` method
|
@@ -133,7 +130,7 @@ module Shamu
|
|
133
130
|
# def lookup( *ids )
|
134
131
|
# records = Models::Favorite.all.where( id: ids )
|
135
132
|
# entity_lookup_list records, ids, NullEntity.for( FavoriteEntity ) do |record|
|
136
|
-
# scorpion.fetch FavoriteEntity, { record: record }
|
133
|
+
# scorpion.fetch FavoriteEntity, { record: record }
|
137
134
|
# end
|
138
135
|
# end
|
139
136
|
#
|
@@ -141,7 +138,7 @@ module Shamu
|
|
141
138
|
# records = Models::Favorite.all.where( :name.in( names ) )
|
142
139
|
#
|
143
140
|
# entity_lookup_list records, names, NullEntity.for( FavoriteEntity ), match: :name do |record|
|
144
|
-
# scorpion.fetch FavoriteEntity, { record: record }
|
141
|
+
# scorpion.fetch FavoriteEntity, { record: record }
|
145
142
|
# end
|
146
143
|
# end
|
147
144
|
#
|
@@ -150,7 +147,7 @@ module Shamu
|
|
150
147
|
# matcher = ->( record ) { record.name.downcase }
|
151
148
|
#
|
152
149
|
# entity_lookup_list records, names, NullEntity.for( FavoriteEntity ), match: matcher do |record|
|
153
|
-
# scorpion.fetch FavoriteEntity, { record: record }
|
150
|
+
# scorpion.fetch FavoriteEntity, { record: record }
|
154
151
|
# end
|
155
152
|
# end
|
156
153
|
#
|
@@ -162,19 +159,25 @@ module Shamu
|
|
162
159
|
|
163
160
|
list = entity_list records, &transformer
|
164
161
|
matched = ids.map do |id|
|
165
|
-
list.find { |e| matcher.call( e ) == id } || scorpion.fetch( null_class,
|
162
|
+
list.find { |e| matcher.call( e ) == id } || scorpion.fetch( null_class, id: id )
|
166
163
|
end
|
167
164
|
|
168
165
|
Entities::List.new( matched )
|
169
166
|
end
|
170
167
|
|
168
|
+
ID_MATCHER = ->( record ) { record && record.id }
|
169
|
+
|
171
170
|
def entity_lookup_list_matcher( match )
|
172
171
|
if !match.is_a?( Symbol ) && match.respond_to?( :call )
|
173
172
|
match
|
174
173
|
elsif match == :id
|
175
|
-
|
174
|
+
ID_MATCHER
|
176
175
|
else
|
177
|
-
|
176
|
+
@@matcher_proc_cache ||= Hash.new do |hash, key| # rubocop:disable Style/ClassVars
|
177
|
+
hash[ key ] = ->( record ) { record && record.send( key ) }
|
178
|
+
end
|
179
|
+
|
180
|
+
@@matcher_proc_cache[ match ]
|
178
181
|
end
|
179
182
|
end
|
180
183
|
|
@@ -208,10 +211,15 @@ module Shamu
|
|
208
211
|
# end
|
209
212
|
def find_by_lookup( id )
|
210
213
|
entity = lookup( id ).first
|
211
|
-
|
214
|
+
not_found!( id ) unless entity.present?
|
212
215
|
entity
|
213
216
|
end
|
214
217
|
|
218
|
+
# @exception [Shamu::NotFoundError]
|
219
|
+
def not_found!( id = :not_set )
|
220
|
+
raise Shamu::NotFoundError, id: id
|
221
|
+
end
|
222
|
+
|
215
223
|
# @!visibility public
|
216
224
|
#
|
217
225
|
# Find an associated entity from a dependent service. Attempts to
|
@@ -221,22 +229,23 @@ module Shamu
|
|
221
229
|
#
|
222
230
|
# @param [Object] id of the associated {Entities::Entity} to find.
|
223
231
|
# @param [Service] service used to locate the associated resource.
|
232
|
+
# @param [IdentityCache] cache to store found associations.
|
224
233
|
# @return [Entity] the found entity or a {Entities::NullEntity} if the
|
225
234
|
# association doesn't exist.
|
226
235
|
#
|
227
236
|
# @example
|
228
237
|
#
|
229
|
-
# def
|
230
|
-
#
|
238
|
+
# def build_entities( records )
|
239
|
+
# cache = cache_for( entity: users_service )
|
240
|
+
# owner = lookup_association record.owner_id, users_service, cache do
|
231
241
|
# records.pluck( :owner_id ) if records
|
232
242
|
# end
|
233
243
|
#
|
234
|
-
# scorpion.fetch UserEntity, { record: record, owner: owner }
|
244
|
+
# scorpion.fetch UserEntity, { record: record, owner: owner }
|
235
245
|
# end
|
236
|
-
def lookup_association( id, service, &block )
|
246
|
+
def lookup_association( id, service, cache, &block )
|
237
247
|
return unless id
|
238
248
|
|
239
|
-
cache = cache_for( entity: service )
|
240
249
|
cache.fetch( id ) || begin
|
241
250
|
if block_given? && ( ids = yield )
|
242
251
|
service.lookup( *ids ).map do |entity|
|
@@ -253,20 +262,33 @@ module Shamu
|
|
253
262
|
|
254
263
|
# @!visibility public
|
255
264
|
#
|
256
|
-
#
|
257
|
-
#
|
265
|
+
# Build a proxy object that delays yielding to the block until a method
|
266
|
+
# on the association is invoked.
|
267
|
+
#
|
268
|
+
# @example
|
269
|
+
# user = lazy_association 10, Users::UserEntity do
|
270
|
+
# expensive_lookup_user.find( 10 )
|
271
|
+
# end
|
272
|
+
#
|
273
|
+
# user.id # => 10 expensive lookup not performed
|
274
|
+
# user.name # => "Trump" expensive lookup executed, cached, then
|
275
|
+
# # method invoked on real object
|
258
276
|
#
|
259
|
-
# @param
|
277
|
+
# @param [Integer] id of the resource.
|
278
|
+
# @param [Class] entity_class of the resource.
|
260
279
|
# @return [LazyAssociation<Entity>]
|
261
|
-
def lazy_association( id,
|
262
|
-
|
263
|
-
|
264
|
-
|
280
|
+
def lazy_association( id, entity_class, &block )
|
281
|
+
return nil if id.nil?
|
282
|
+
|
283
|
+
LazyAssociation.class_for( entity_class ).new( id, &block )
|
265
284
|
end
|
266
285
|
|
267
286
|
# @!visibility public
|
268
287
|
#
|
269
288
|
# Get the {Entities::IdentityCache} for the given {Entities::Entity} class.
|
289
|
+
# @param [Service#entity_class] dependency_service the dependent
|
290
|
+
# {Service} to cache results from. Must respond to `#entity_class` that
|
291
|
+
# returns the {Entities::Entity} class to cache.
|
270
292
|
# @param [Class] entity the type of entity that will be cached. Only
|
271
293
|
# required if the service manages multiple entities.
|
272
294
|
# @param [Symbol,#call] key the attribute on the entity, or a custom
|
@@ -275,8 +297,10 @@ module Shamu
|
|
275
297
|
# to the same type (eg :to_i). If not set, automatically uses :to_i
|
276
298
|
# if key is an 'id' attribute.
|
277
299
|
# @return [Entities::IdentityCache]
|
278
|
-
def cache_for( key: :id, entity: nil, coerce: :not_set )
|
300
|
+
def cache_for( dependency_service = nil, key: :id, entity: nil, coerce: :not_set )
|
279
301
|
coerce = coerce_method( coerce, key )
|
302
|
+
entity ||= dependency_service
|
303
|
+
entity = entity.entity_class if entity.respond_to?( :entity_class )
|
280
304
|
|
281
305
|
cache_key = [ entity, key, coerce ]
|
282
306
|
@entity_caches ||= {}
|
@@ -328,16 +352,17 @@ module Shamu
|
|
328
352
|
end
|
329
353
|
end
|
330
354
|
|
331
|
-
# @!
|
355
|
+
# @!visbility public
|
332
356
|
#
|
333
|
-
#
|
334
|
-
#
|
335
|
-
#
|
336
|
-
# @
|
337
|
-
def
|
338
|
-
|
339
|
-
|
340
|
-
|
357
|
+
# After a mutation method call to make sure the cache for the entity
|
358
|
+
# is updated to reflect the new entity state.
|
359
|
+
#
|
360
|
+
# @param [Entity] entity in the new modified state.
|
361
|
+
def recache_entity( entity, match: :id )
|
362
|
+
matcher = entity_lookup_list_matcher( match )
|
363
|
+
cache = cache_for( key: match )
|
364
|
+
|
365
|
+
cache.add( matcher.call( entity ), entity )
|
341
366
|
end
|
342
367
|
|
343
368
|
end
|