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