Class: Shamu::Services::Service

Inherits:
Object
  • Object
show all
Includes:
Scorpion::Object
Defined in:
lib/shamu/services/service.rb

Overview

...

Well Known Methos

  • list( list_scope ) - lists all of the entities matching the requested list scope. Often apply Entities::ListScope::Paging or other filters.

    def list( list_scope )
      list_scope = UserListScope.coerce! list_scope
      entity_list Models::User.by_list_scope( list_scope ) do |record|
        scorpion.fetch UserEntity, record: record
      end
    end
    
  • lookup( *ids ) - matches a given list of ids to their entities or a Entities::NullEntity for ids that can't be found. The lookup method is typically used to resolve related resources between services - similar to associations in an ActiveRecord model. Use #entity_lookup_list to transform a list of records or external resources to a lookup list of entities.

    def lookup( *ids )
      entity_lookup_list Models::User.where( id: ids ), ids, NullEntity.for( UserEntity ) do |record|
        scorpion.fetch UserEntity, record: record
      end
    end
    
  • find( id ) - finds a single entity with the given id, raising NotFoundError if the resource cannot be found. If the service also implements lookup then this can be implemented by simply aliasing find to #find_by_lookup.

    def find( id )
      find_by_lookup( id )
    end
    
  • report( report_scope ) - Compile a report including metrics, and master/detail data that make take longer to gather than a standard list request.

    def report( report_scope = nil )
      report_scope = UserReportScope.coerce! report_scope
      scorpion.fetch UserReport, report_scope, {}
    end
    

Instance Method Summary collapse

Instance Method Details

#cache_for(key: :id, entity: nil, coerce: :not_set) ⇒ Entities::IdentityCache

Get the Entities::IdentityCache for the given Entities::Entity class.

Parameters:

  • entity (Class) (defaults to: nil)

    the type of entity that will be cached. Only required if the service manages multiple entities.

  • key (Symbol, #call) (defaults to: :id)

    the attribute on the entity, or a custom block used to obtain the cache key from an entity.

  • coerce (Symbol, #call) (defaults to: :not_set)

    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.

Returns:



278
279
280
281
282
283
284
# File 'lib/shamu/services/service.rb', line 278

def cache_for( key: :id, entity: nil, coerce: :not_set )
  coerce = coerce_method( coerce, key )

  cache_key        = [ entity, key, coerce ]
  @entity_caches ||= {}
  @entity_caches[ cache_key ] ||= scorpion.fetch( Entities::IdentityCache, coerce )
end

#cached_lookup(ids, match: :id, coerce: :not_set, entity: nil) {|missing_ids| ... } ⇒ Object

Caches the results of looking up the given ids in an Entities::IdentityCache and only fetches the records that have not yet been cached.

Examples:


def lookup( *ids )
  cached_lookup( ids ) do |missing_ids|
    entity_lookup_list( Models::User.where( id: missing_ids ), missing_ids, UserEntity::Missing )
  end
end

Parameters:

  • ids (Array)

    to fetch.

  • entity (Class) (defaults to: nil)

    the type of entity that will be cached. Only required if the service manages multiple entities.

  • key (Symbol, #call)

    the attribute on the entity, or a custom block used to obtain the cache key from an entity.

  • coerce (Symbol, #call) (defaults to: :not_set)

    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.

Yields:

  • (missing_ids)

Yield Parameters:

  • missing_ids (Array)

    that have not been cached yet.

Yield Returns:



304
305
306
307
308
309
310
311
312
313
314
# File 'lib/shamu/services/service.rb', line 304

def cached_lookup( ids, match: :id, coerce: :not_set, entity: nil, &lookup )
  coerce      = coerce_method( coerce, match )
  ids         = ids.map( &coerce ) if coerce
  cache       = cache_for( key: match, coerce: coerce, entity: entity )
  missing_ids = cache.uncached_keys( ids )

  cache_entities( cache, match, missing_ids, &lookup ) if missing_ids.any?

  entities = ids.map { |id| cache.fetch( id ) || fail( Shamu::NotFoundError ) }
  Entities::List.new( entities )
end

#entity_list(records) {|record| ... } ⇒ Entities::List

Takes a raw enumerable list of records and transforms them to a proper Entities::List.

As simple as the method is, it also serves as a hook for mixins to add additional behavior when processing lists.

If a block is not provided, looks for a method build_entities( records ) that maps a set of records to their corresponding entities.

Parameters:

  • records (Enumerable)

    the raw list of records.

Yields:

  • (record)

Yield Parameters:

  • records (Enumerable<Object>)

    the raw values from the list to transform to an Entities::Entity.

Yield Returns:

Returns:



96
97
98
99
100
101
102
103
104
# File 'lib/shamu/services/service.rb', line 96

def entity_list( records, &transformer )
  return Entities::List.new [] unless records
  unless transformer
    fail "Either provide a block or add a private method `def build_entities( records )` to #{ self.class.name }." unless respond_to?( :build_entities, true ) # rubocop:disable Metrics/LineLength
    transformer ||= method( :build_entities )
  end

  build_entity_list build_records_transform( records, &transformer )
end

#entity_lookup_list(records, ids, null_class, match: :id, coerce: :not_set) {|record| ... } ⇒ Entities::List

Match a list of records with the ids used to look up those records. Uses a Entities::NullEntity if the id doesn't have a matching record.

Examples:

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 }, {}
  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 }, {}
  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 }, {}
  end
end

Parameters:

  • records (Enumerable)

    matching the requested ids.

  • ids (Array<Integer>)

    of records found.

  • null_class (Class)

    to use when an id doesn't have a matching record.

  • match (Symbol, #call(record)) (defaults to: :id)

    the attribute or a Proc used to extract the id used to compare records.

  • coerce (Symbol, #call) (defaults to: :not_set)

    a method that can be used to coerce id values to the same type (eg :to_i). If not set, automatically uses :to_model_id if match is an 'id' attribute.

Yields:

  • (record)

Yield Parameters:

  • records (Enumerable<Object>)

    the raw values from the list to transform to an Entities::Entity.

Yield Returns:

Returns:



158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/shamu/services/service.rb', line 158

def entity_lookup_list( records, ids, null_class, match: :id, coerce: :not_set, &transformer )
  matcher = entity_lookup_list_matcher( match )
  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 }, {} )
  end

  Entities::List.new( matched )
end

#error(attribute, message) ⇒ ErrorResult

Return an error #result from a service request.

Returns:

  • (ErrorResult)


337
338
339
340
341
# File 'lib/shamu/services/service.rb', line 337

def error( *args )
  Result.new.tap do |r|
    r.errors.add( *args )
  end
end

#find_by_lookup(id) ⇒ Entities::Entity

For services that expose a standard lookup method, find_by_lookup looks up a single entity and raises NotFoundError if the entity is nil or a Entities::NullEntity.

A find method can then be implemented in terms of the lookup method.

Examples:


class Example < Services::Service
  def lookup( *ids )
    # do something to find the entity
  end

  def find( id )
    find_by_lookup( id )
  end
end

Parameters:

  • id (Integer)

    of the entity.

Returns:

Raises:



209
210
211
212
213
# File 'lib/shamu/services/service.rb', line 209

def find_by_lookup( id )
  entity = lookup( id ).first
  raise Shamu::NotFoundError unless entity.present?
  entity
end

#lazy_association(id, service, &block) ⇒ LazyAssociation<Entity>

Perform a lazy #lookup_association and only load the entity if its actually dereferenced by the caller.

Parameters:

  • id (Object)

    of the associated Entities::Entity to find.

  • service (Service)

    used to locate the associated resource.

Returns:



261
262
263
264
265
# File 'lib/shamu/services/service.rb', line 261

def lazy_association( id, service, &block )
  LazyAssociation.new( id ) do
    lookup_association id, service, &block
  end
end

#lookup_association(id, service, &block) ⇒ Entity

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.

Examples:


def build_entity( record, records = nil )
  owner = lookup_association record.owner_id, users_service do
            records.pluck( :owner_id ) if records
          end

  scorpion.fetch UserEntity, { record: record, owner: owner }, {}
end

Parameters:

  • id (Object)

    of the associated Entities::Entity to find.

  • service (Service)

    used to locate the associated resource.

Returns:



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/shamu/services/service.rb', line 236

def lookup_association( id, service, &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

      cache.fetch( id )
    else
      association = service.lookup( id ).first
      cache.add( association.id, association )
    end
  end
end