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