lib/shamu/services/service.rb in shamu-0.0.21 vs lib/shamu/services/service.rb in shamu-0.0.24
- old
+ new
@@ -51,21 +51,18 @@
# `list` request.
#
# ```
# def report( report_scope = nil )
# report_scope = UserReportScope.coerce! report_scope
- # scorpion.fetch UserReport, report_scope, {}
+ # scorpion.fetch UserReport, report_scope
# end
# ```
class Service
# Support dependency injection for related services.
include Scorpion::Object
- initialize do
- end
-
private
# Maps a single record to an entity. Requires a `build_entities` method
# that maps an enumerable set of records to entities.
#
@@ -131,28 +128,28 @@
#
# @example
# def lookup( *ids )
# records = Models::Favorite.all.where( id: ids )
# entity_lookup_list records, ids, NullEntity.for( FavoriteEntity ) do |record|
- # scorpion.fetch FavoriteEntity, { record: record }, {}
+ # scorpion.fetch FavoriteEntity, { record: record }
# end
# end
#
# def lookup_by_name( *names )
# records = Models::Favorite.all.where( :name.in( names ) )
#
# entity_lookup_list records, names, NullEntity.for( FavoriteEntity ), match: :name do |record|
- # scorpion.fetch FavoriteEntity, { record: record }, {}
+ # scorpion.fetch FavoriteEntity, { record: record }
# end
# end
#
# def lookup_by_lowercase( *names )
# records = Models::Favorite.all.where( :name.in( names.map( &:downcase ) ) )
# matcher = ->( record ) { record.name.downcase }
#
# entity_lookup_list records, names, NullEntity.for( FavoriteEntity ), match: matcher do |record|
- # scorpion.fetch FavoriteEntity, { record: record }, {}
+ # scorpion.fetch FavoriteEntity, { record: record }
# end
# end
#
#
def entity_lookup_list( records, ids, null_class, match: :id, coerce: :not_set, &transformer )
@@ -160,23 +157,29 @@
coerce = coerce_method( coerce, match )
ids = ids.map( &coerce ) if coerce
list = entity_list records, &transformer
matched = ids.map do |id|
- list.find { |e| matcher.call( e ) == id } || scorpion.fetch( null_class, { id: id }, {} )
+ list.find { |e| matcher.call( e ) == id } || scorpion.fetch( null_class, id: id )
end
Entities::List.new( matched )
end
+ ID_MATCHER = ->( record ) { record && record.id }
+
def entity_lookup_list_matcher( match )
if !match.is_a?( Symbol ) && match.respond_to?( :call )
match
elsif match == :id
- ->( record ) { record && record.id }
+ ID_MATCHER
else
- ->( record ) { record && record.send( match ) }
+ @@matcher_proc_cache ||= Hash.new do |hash, key| # rubocop:disable Style/ClassVars
+ hash[ key ] = ->( record ) { record && record.send( key ) }
+ end
+
+ @@matcher_proc_cache[ match ]
end
end
def coerce_method( coerce, match )
return coerce unless coerce == :not_set
@@ -206,39 +209,45 @@
# find_by_lookup( id )
# end
# end
def find_by_lookup( id )
entity = lookup( id ).first
- raise Shamu::NotFoundError unless entity.present?
+ not_found!( id ) unless entity.present?
entity
end
+ # @exception [Shamu::NotFoundError]
+ def not_found!( id = :not_set )
+ raise Shamu::NotFoundError, id: id
+ end
+
# @!visibility public
#
# Find an associated entity from a dependent service. Attempts to
# efficiently handle multiple requests to lookup associations by caching
# all the associated entities when {#lookup_association} is used
# repeatedly when building an entity.
#
# @param [Object] id of the associated {Entities::Entity} to find.
# @param [Service] service used to locate the associated resource.
+ # @param [IdentityCache] cache to store found associations.
# @return [Entity] the found entity or a {Entities::NullEntity} if the
# association doesn't exist.
#
# @example
#
- # def build_entity( record, records = nil )
- # owner = lookup_association record.owner_id, users_service do
+ # def build_entities( records )
+ # cache = cache_for( entity: users_service )
+ # owner = lookup_association record.owner_id, users_service, cache do
# records.pluck( :owner_id ) if records
# end
#
- # scorpion.fetch UserEntity, { record: record, owner: owner }, {}
+ # scorpion.fetch UserEntity, { record: record, owner: owner }
# end
- def lookup_association( id, service, &block )
+ def lookup_association( id, service, cache, &block )
return unless id
- cache = cache_for( entity: service )
cache.fetch( id ) || begin
if block_given? && ( ids = yield )
service.lookup( *ids ).map do |entity|
cache.add( entity.id, entity )
end
@@ -251,34 +260,49 @@
end
end
# @!visibility public
#
- # Perform a lazy {#lookup_association} and only load the entity if its
- # actually dereferenced by the caller.
+ # Build a proxy object that delays yielding to the block until a method
+ # on the association is invoked.
#
- # @param (see #lookup_association)
+ # @example
+ # user = lazy_association 10, Users::UserEntity do
+ # expensive_lookup_user.find( 10 )
+ # end
+ #
+ # user.id # => 10 expensive lookup not performed
+ # user.name # => "Trump" expensive lookup executed, cached, then
+ # # method invoked on real object
+ #
+ # @param [Integer] id of the resource.
+ # @param [Class] entity_class of the resource.
# @return [LazyAssociation<Entity>]
- def lazy_association( id, service, &block )
- LazyAssociation.new( id ) do
- lookup_association id, service, &block
- end
+ def lazy_association( id, entity_class, &block )
+ return nil if id.nil?
+
+ LazyAssociation.class_for( entity_class ).new( id, &block )
end
# @!visibility public
#
# Get the {Entities::IdentityCache} for the given {Entities::Entity} class.
+ # @param [Service#entity_class] dependency_service the dependent
+ # {Service} to cache results from. Must respond to `#entity_class` that
+ # returns the {Entities::Entity} class to cache.
# @param [Class] entity the type of entity that will be cached. Only
# required if the service manages multiple entities.
# @param [Symbol,#call] key the attribute on the entity, or a custom
# block used to obtain the cache key from an entity.
# @param [Symbol,#call] coerce a method that can be used to coerce key values
# to the same type (eg :to_i). If not set, automatically uses :to_i
# if key is an 'id' attribute.
# @return [Entities::IdentityCache]
- def cache_for( key: :id, entity: nil, coerce: :not_set )
+ def cache_for( dependency_service = nil, key: :id, entity: nil, coerce: :not_set )
coerce = coerce_method( coerce, key )
+ entity ||= dependency_service
+ entity = entity.entity_class if entity.respond_to?( :entity_class )
cache_key = [ entity, key, coerce ]
@entity_caches ||= {}
@entity_caches[ cache_key ] ||= scorpion.fetch( Entities::IdentityCache, coerce )
end
@@ -326,19 +350,20 @@
end
end
end
end
- # @!visibility public
+ # @!visbility public
#
- # Return an error {#result} from a service request.
- # @overload error( attribute, message )
- # @param (see ErrorResult#initialize)
- # @return [ErrorResult]
- def error( *args )
- Result.new.tap do |r|
- r.errors.add( *args )
- end
+ # After a mutation method call to make sure the cache for the entity
+ # is updated to reflect the new entity state.
+ #
+ # @param [Entity] entity in the new modified state.
+ def recache_entity( entity, match: :id )
+ matcher = entity_lookup_list_matcher( match )
+ cache = cache_for( key: match )
+
+ cache.add( matcher.call( entity ), entity )
end
end
end
end