module Shamu::Services::ActiveRecordCrud

Adds standard CRUD builders to an {ActiveRecordService} to reduce boilerplate for common methods.

@example

class UsersService < Shamu::Services::Service
  include Shamu::Services::ActievRecordCrud

  # Define the resource that the service will manage
  resource UserEntity, Models::User

  # Define finder methods #find, #list and #lookup using the given
  # default scope.
  define_finders Models::User.active

  # Define change methods
  define_create
  define_update

  # Common update/change behavior for #create and #update
  define_change do |request, model|
    model.last_updated_at = Time.now
  end

  # Standard destroy method
  define_destroy

  # Build the entity class from the given record.
  define_build_entities do |records|
    records.map do |record|
      parent = lookup_association( record.parent_id, self ) do
                 records.pluck( :parent_id )
               end

      scorpion.fetch UserEntity, { parent: parent }
    end
  end
end

Constants

DSL_METHODS

Known DSL methods defined by {ActiveRecordCrud}.

Private Instance Methods

authorize!( method, resource, additional_context = nil ) click to toggle source

@!visibility public

Hook to allow a security module to authorize actions taken by the standard CRUD methods. If authorization is not granted, then an exception should be raised. Default behavior is a no-op.

@param [Symbol] method on the service that was invoked. @param [Entities::Entity, Class, Symbol] resource the entity, class or

arbitrary symbol describing the resource that the service method
applies to.

@param [Object] additional_context that the security module might

consider when authorizing the transaction.

@return [resource] the resource given to authorize.

# File lib/shamu/services/active_record_crud.rb, line 76
def authorize!( method, resource, additional_context = nil )
  resource
end
authorize_relation( method, relation, additional_context = nil ) click to toggle source

@!visibility public

Hook to allow a security module to pre-filter ActiveRecord queries for the standard crud methods. Default behavior is a no-op.

@param [Symbol] method on the service that was invoked. @param [ActiveRecord::Relation] relation to filter @param [Object] additional_context that the security module might

consider when authorizing the transaction.

@return [relation] the filtered relation.

# File lib/shamu/services/active_record_crud.rb, line 91
def authorize_relation( method, relation, additional_context = nil )
  relation
end
define_build_entities( &block ) click to toggle source

Define a private `build_entities( records )` method that constructs an {Entities::Entity} for each of the given `records`.

If no block is given, creates a simple builder that simply constructs an instance of the {.entity_class} passing `record: record` to the initializer.

See {Service#lookup_association} for details on association caching.

@yield ( records ) @yieldparam [ActiveRecord::Relation] records to be mapped to

entities.

@yieldreturn [Array<Entities::Entity>] the projected entities. @return [void]

# File lib/shamu/services/active_record_crud.rb, line 367
def define_build_entities( &block )
  if block_given?
    define_method :build_entities, &block
  else
    define_method :build_entities do |records|
      records.map do |record|
        entity = scorpion.fetch( entity_class, record: record )
        authorize! :read, entity
      end
    end
  end

  private :build_entities
end
define_change( method, default_scope = model_class, &block ) click to toggle source

Define an change `method` on the service that takes the id of the resource to modify and a corresponding {Request} parameter.

@yield ( request, record, *args ) @yieldparam [Services::Request] request object. @yieldparam [ActiveRecord::Base] record. @yieldparam [Array] args any additional arguments injected by an overridden {#with_request} method. @return [Result] the result of the request. @return [void]

# File lib/shamu/services/active_record_crud.rb, line 191
def define_change( method, default_scope = model_class, &block )
  define_method method do |id, params = nil|
    klass = request_class( method )

    id, params = extract_params( id, params )

    with_partial_request params, klass do |request, *args|
      record = default_scope.find( id.to_model_id || request.id )
      entity = build_entity( record )

      backfill_attributes = entity.to_attributes( only: request.unassigned_attributes )
      request.assign_attributes backfill_attributes
      next unless request.valid?

      authorize! method, entity, request

      request.apply_to( record )
      if block_given?
        result = instance_exec record, request, *args, &block
        next result if result.is_a?( Services::Result )
        next unless request.valid?
      end

      next record unless record.save
      build_entity record
    end
  end
end
define_create( method = :create, &block ) click to toggle source

Define a `#create` method on the service that takes a single {Request} parameter.

@yield ( request, record, *args ) @yieldparam [Services::Request] request object. @yieldparam [ActiveRecord::Base] record. @yieldparam [Array] args any additional arguments injected by an overridden {#with_request} method. @return [void]

# File lib/shamu/services/active_record_crud.rb, line 162
def define_create( method = :create, &block )
  define_method method do |params = nil|
    with_request params, request_class( method ) do |request, *args|
      record = request.apply_to( model_class.new )

      if block_given?
        result = instance_exec record, request, *args, &block
        next result if result.is_a?( Services::Result )
        next unless request.valid?
      end

      authorize! method, build_entity( record ), request

      next record unless record.save
      build_entity record
    end
  end
end
define_crud() click to toggle source

Define all basic CRUD methods without any customization.

# File lib/shamu/services/active_record_crud.rb, line 146
def define_crud
  define_create
  define_update
  define_destroy
  define_finders
end
define_destroy( method = :destroy, default_scope = model_class, &block ) click to toggle source

Define a `destroy( id )` method that takes an {Entities::Entity} {Entities::Entity#id} and destroys the resource.

@yield ( request, record, *args ) @yieldparam [Services::Request] request object. @yieldparam [ActiveRecord::Base] record. @yieldparam [Array] args any additional arguments injected by an overridden {#with_request} method. @param [ActiveRecord::Relation] default_scope to use when finding

records.

@return [void]

# File lib/shamu/services/active_record_crud.rb, line 236
def define_destroy( method = :destroy, default_scope = model_class, &block )
  define_method method do |params|
    klass = request_class( method )

    params = { id: params } if params.respond_to?( :to_model_id )

    with_request params, klass do |request, *args|
      record = default_scope.find( request.id )
      authorize! method, build_entity( record ), request

      if block_given?
        instance_exec record, request, *args, &block
        next unless request.valid?
      end

      next record unless record.destroy
    end
  end
end
define_find( default_scope = model_class.all, &block ) click to toggle source

Define a `find( id )` method on the service that returns the entity with the given id if found or raises a {Shamu::NotFoundError} if the entity does not exist.

@param [ActiveRecord::Relation] default_scope to use when finding

records.

@yield (id) @yieldreturn (ActiveRecord::Base) the found record. @return [void]

# File lib/shamu/services/active_record_crud.rb, line 279
def define_find( default_scope = model_class.all, &block )
  if block_given?
    define_method :_find_block, &block
    define_method :find do |id|
      wrap_not_found do
        record = _find_block( id )
        authorize! :read, build_entity( record )
      end
    end
  else
    define_method :find do |id|
      authorize! :read, find_by_lookup( id )
    end
  end
end
define_finders( default_scope = model_class.all, only: nil, except: nil ) click to toggle source

Define the standard finder methods {.find}, {.lookup} and {.list}.

@param [ActiveRecord::Relation] default_scope to use when finding

records.

@return [void]

# File lib/shamu/services/active_record_crud.rb, line 261
def define_finders( default_scope = model_class.all, only: nil, except: nil )
  methods = Array( only || [ :find, :lookup, :list ] )
  methods -= Array( except ) if except

  methods.each do |method|
    send :"define_#{ method }", default_scope
  end
end
define_list( default_scope = model_class.all, &block ) click to toggle source

Define a `list( params = nil )` method that takes a {Entities::ListScope} and returns all the entities selected by that scope.

@param [ActiveRecord::Relation] default_scope to use when finding

records.

@yield (scope) @yieldparam [ListScope] scope to apply. @yieldreturn [ActiveRecord::Relation] records matching the given scope. @return [void]

# File lib/shamu/services/active_record_crud.rb, line 335
def define_list( default_scope = model_class.all, &block )
  define_method :list do |params = nil|
    list_scope = Entities::ListScope.for( entity_class ).coerce( params )
    authorize! :list, entity_class, list_scope

    records =
      if block_given?
        instance_exec( list_scope, &block )
      else
        scope_relation( default_scope, list_scope )
      end

    records = authorize_relation( :read, records, list_scope )

    entity_list records
  end
end
define_lookup( default_scope = model_class.all, &block ) click to toggle source

Define a `lookup( *ids )` method that takes a list of entity ids to find. Calls {#build_entities} to map all found records to entities, or constructs a {Entities::NullEntity} for ids that were not found.

@param [ActiveRecord::Relation] default_scope to use when finding

records.

@yield (uncached_ids) @yieldparam [Array<Object>] ids that need to be fetched from the

underlying resource.

@yieldreturn [ActiveRecord::Relation] records for ids found in the

underlying resource.

@return [void]

# File lib/shamu/services/active_record_crud.rb, line 307
def define_lookup( default_scope = model_class.all, &block )
  if block_given?
    define_method :_lookup_block, &block
  else
    define_method :_lookup_block do |ids|
      default_scope.where( id: ids )
    end
  end

  define_method :lookup do |*ids|
    cached_lookup( ids ) do |uncached_ids|
      records = _lookup_block( uncached_ids )
      records = authorize_relation :read, records
      entity_lookup_list records, uncached_ids, entity_class.null_entity
    end
  end
end
define_update( default_scope = model_class, &block ) click to toggle source

Defines an update method. See {#define_change} for details.

# File lib/shamu/services/active_record_crud.rb, line 221
def define_update( default_scope = model_class, &block )
  define_change :update, default_scope, &block
end
entity_class() click to toggle source
# File lib/shamu/services/active_record_crud.rb, line 59
def entity_class
  self.class.model_class
end
inferred_namespace() click to toggle source
# File lib/shamu/services/active_record_crud.rb, line 393
def inferred_namespace
  parts = ( name || "Resource" ).split( "::" )
  parts.pop
  return "" if parts.empty?
  parts.join( "::" ) << "::"
end
inferred_resource_name() click to toggle source
# File lib/shamu/services/active_record_crud.rb, line 388
def inferred_resource_name
  inferred = name || "Resource"
  inferred.split( "::" ).last.sub( /Service/, "" ).singularize
end
model_class() click to toggle source
# File lib/shamu/services/active_record_crud.rb, line 55
def model_class
  self.class.model_class
end
not_found!( id = :not_set ) click to toggle source
# File lib/shamu/services/active_record_crud.rb, line 95
def not_found!( id = :not_set )
  raise Shamu::NotFoundError, id: id, resource: entity_class
end
resource( entity_class, model_class, methods: nil, &block ) click to toggle source

Declare the entity and resource classes used by the service.

Creates instance and class level methods `entity_class` and `model_class`.

See {.build_entities} for build_entities block details.

@param [Class] entity_class the {Entities::Entity} class that will be

returned by finders and mutator methods.

@param [Class] model_class the {ActiveRecord::Base} model @param [Array<Symbol>] methods the {DSL_METHODS DSL methods} to

include (eg :create, :update, :find, etc.)

@yield ( records ) @yieldparam [ActiveRecord::Relation] records to be mapped to an

entity.

@yieldreturn [Entities::Entity] the entity projection for the given

record.

@return [void]

# File lib/shamu/services/active_record_crud.rb, line 119
def resource( entity_class, model_class, methods: nil, &block )
  private define_method( :entity_class )   { entity_class }
  define_singleton_method( :entity_class ) { entity_class }

  private define_method( :model_class )    { model_class }
  define_singleton_method( :model_class )  { model_class }

  ( Array( methods ) & DSL_METHODS ).each do |method|
    send :"define_#{ method }"
  end

  define_build_entities( &block )
end
resource_not_configured() click to toggle source
# File lib/shamu/services/active_record_crud.rb, line 384
def resource_not_configured
  raise IncompleteSetupError, "Resource has not been defined. Add `resource #{ inferred_namespace }#{ inferred_resource_name }Entity, #{ inferred_namespace }Models::#{ inferred_resource_name }` to #{ name }." # rubocop:disable Metrics/LineLength
end