Class: Grape::Entity

Inherits:
Object
  • Object
show all
Defined in:
lib/grape_entity/entity.rb,
lib/grape_entity/options.rb,
lib/grape_entity/exposure.rb,
lib/grape_entity/condition.rb,
lib/grape_entity/delegator.rb,
lib/grape_entity/deprecated.rb,
lib/grape_entity/exposure/base.rb,
lib/grape_entity/condition/base.rb,
lib/grape_entity/delegator/base.rb,
lib/grape_entity/delegator/hash_object.rb,
lib/grape_entity/delegator/plain_object.rb,
lib/grape_entity/exposure/block_exposure.rb,
lib/grape_entity/condition/hash_condition.rb,
lib/grape_entity/condition/block_condition.rb,
lib/grape_entity/exposure/nesting_exposure.rb,
lib/grape_entity/condition/symbol_condition.rb,
lib/grape_entity/delegator/openstruct_object.rb,
lib/grape_entity/exposure/delegator_exposure.rb,
lib/grape_entity/exposure/formatter_exposure.rb,
lib/grape_entity/exposure/represent_exposure.rb,
lib/grape_entity/exposure/formatter_block_exposure.rb,
lib/grape_entity/exposure/nesting_exposure/output_builder.rb,
lib/grape_entity/exposure/nesting_exposure/nested_exposures.rb

Overview

An Entity is a lightweight structure that allows you to easily represent data from your application in a consistent and abstracted way in your API. Entities can also provide documentation for the fields exposed.

Entities are not independent structures, rather, they create representations of other Ruby objects using a number of methods that are convenient for use in an API. Once you’ve defined an Entity, you can use it in your API like this:

Examples:

Entity Definition


module API
  module Entities
    class User < Grape::Entity
      expose :first_name, :last_name, :screen_name, :location
      expose :field, documentation: { type: "string", desc: "describe the field" }
      expose :latest_status, using: API::Status, as: :status, unless: { collection: true }
      expose :email, if: { type: :full }
      expose :new_attribute, if: { version: 'v2' }
      expose(:name) { |model, options| [model.first_name, model.last_name].join(' ') }
    end
  end
end

Usage in the API Layer


module API
  class Users < Grape::API
    version 'v2'

    desc 'User index', { params: API::Entities::User.documentation }
    get '/users' do
      @users = User.all
      type = current_user.admin? ? :full : :default
      present @users, with: API::Entities::User, type: type
    end
  end
end

Defined Under Namespace

Modules: Condition, DSL, Delegator, Exposure Classes: Deprecated, Options

Constant Summary collapse

OPTIONS =

All supported options.

%i[
  rewrite
  as
  if
  unless
  using
  with
  proc
  documentation
  format_with
  safe
  attr_path
  if_extras
  unless_extras
  merge
  expose_nil
  override
  default
].to_set.freeze

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(object, options = {}) ⇒ Entity

Returns a new instance of Entity.



488
489
490
491
492
# File 'lib/grape_entity/entity.rb', line 488

def initialize(object, options = {})
  @object = object
  @options = options.is_a?(Options) ? options : Options.new(options)
  @delegator = Delegator.new(object)
end

Class Attribute Details

.formattersHash

Returns all formatters that are registered for this and it’s ancestors

Returns:

  • (Hash)

    of formatters



109
110
111
# File 'lib/grape_entity/entity.rb', line 109

def formatters
  @formatters ||= {}
end

.root_exposureObject



101
102
103
# File 'lib/grape_entity/entity.rb', line 101

def root_exposure
  @root_exposure ||= Exposure.new(nil, nesting: true)
end

Instance Attribute Details

#delegatorObject (readonly)

Returns the value of attribute delegator.



44
45
46
# File 'lib/grape_entity/entity.rb', line 44

def delegator
  @delegator
end

#objectObject (readonly)

Returns the value of attribute object.



44
45
46
# File 'lib/grape_entity/entity.rb', line 44

def object
  @object
end

#optionsObject (readonly)

Returns the value of attribute options.



44
45
46
# File 'lib/grape_entity/entity.rb', line 44

def options
  @options
end

Class Method Details

.[](val) ⇒ Object

Satisfies the respond_to?(:[]) check in Grape::DryTypes (>= 3.2) so Entity subclasses can be used as param types.



133
134
135
# File 'lib/grape_entity/entity.rb', line 133

def [](val)
  val
end

.build_exposure_for_attribute(attribute, nesting_stack, options, block) ⇒ Object

rubocop:enable Layout/LineLength



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/grape_entity/entity.rb', line 222

def self.build_exposure_for_attribute(attribute, nesting_stack, options, block)
  exposure_list = nesting_stack.empty? ? root_exposures : nesting_stack.last.nested_exposures

  exposure = Exposure.new(attribute, options)

  exposure_list.delete_by(attribute) if exposure.override?

  exposure_list << exposure

  # Nested exposures are given in a block with no parameters.
  return unless exposure.nesting?

  nesting_stack << exposure
  block.call
  nesting_stack.pop
end

.can_unexpose?Boolean

Returns:

  • (Boolean)


261
262
263
# File 'lib/grape_entity/entity.rb', line 261

def self.can_unexpose?
  (@nesting_stack ||= []).empty?
end

.cannot_unexpose!Object



265
266
267
# File 'lib/grape_entity/entity.rb', line 265

def self.cannot_unexpose!
  raise "You cannot call 'unexpose` inside of nesting exposure!"
end

.delegation_optsObject



127
128
129
# File 'lib/grape_entity/entity.rb', line 127

def delegation_opts
  @delegation_opts ||= { hash_access: hash_access }
end

.documentationObject

Returns a hash, the keys are symbolized references to fields in the entity, the values are document keys in the entity’s documentation key. When calling #docmentation, any exposure without a documentation key will be ignored.



287
288
289
290
291
# File 'lib/grape_entity/entity.rb', line 287

def self.documentation
  @documentation ||= root_exposures.each_with_object({}) do |exposure, memo|
    memo[exposure.key] = exposure.documentation if exposure.documentation && !exposure.documentation.empty?
  end
end

.expose(*args, &block) ⇒ Object

This method is the primary means by which you will declare what attributes should be exposed by the entity.

Note the parameters passed in via the lambda syntax.

rubocop:disable Layout/LineLength

Examples:

as: a proc or lambda


object = OpenStruct(awesomeness: 'awesome_key', awesome: 'not-my-key', other: 'other-key' )

class MyEntity < Grape::Entity
  expose :awesome, as: proc { object.awesomeness }
  expose :awesomeness, as: ->(object, opts) { object.other }
end

=> { 'awesome_key': 'not-my-key', 'other-key': 'awesome_key' }

Parameters:

  • options (Hash)

    a customizable set of options



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/grape_entity/entity.rb', line 194

def self.expose(*args, &block)
  options = merge_options(args.last.is_a?(Hash) ? args.pop : {})

  if args.size > 1

    raise ArgumentError, 'You may not use the :as option on multi-attribute exposures.' if options[:as]
    raise ArgumentError, 'You may not use the :expose_nil on multi-attribute exposures.' if options.key?(:expose_nil)
    raise ArgumentError, 'You may not use block-setting on multi-attribute exposures.' if block_given?
  end

  if block_given?
    if options[:format_with].respond_to?(:call)
      raise ArgumentError, 'You may not use block-setting when also using format_with'
    end

    if block.parameters.any?
      options[:proc] = block
    else
      options[:nesting] = true
    end
  end

  @documentation = nil
  @nesting_stack ||= []
  args.each { |attribute| build_exposure_for_attribute(attribute, @nesting_stack, options, block) }
end

.find_exposure(attribute) ⇒ Object



245
246
247
# File 'lib/grape_entity/entity.rb', line 245

def self.find_exposure(attribute)
  root_exposures.find_by(attribute)
end

.format_with(name, &block) ⇒ Object

This allows you to declare a Proc in which exposures can be formatted with. It takes a block with a single argument which is passed as the value of the exposed attribute.

Examples:

Formatter declaration


module API
  module Entities
    class User < Grape::Entity
      format_with :timestamp do |date|
        date.strftime('%m/%d/%Y')
      end

      expose :birthday, :last_signed_in, format_with: :timestamp
    end
  end
end

Formatters are available to all decendants


Grape::Entity.format_with :timestamp do |date|
  date.strftime('%m/%d/%Y')
end

Parameters:

  • name (Symbol)

    the name of the formatter

  • block (Proc)

    the block that will interpret the exposed attribute

Raises:

  • (ArgumentError)


319
320
321
322
323
# File 'lib/grape_entity/entity.rb', line 319

def self.format_with(name, &block)
  raise ArgumentError, 'You must pass a block for formatters' unless block_given?

  formatters[name.to_sym] = block
end

.hash_accessObject



113
114
115
# File 'lib/grape_entity/entity.rb', line 113

def hash_access
  @hash_access ||= :to_sym
end

.hash_access=(value) ⇒ Object



117
118
119
120
121
122
123
124
125
# File 'lib/grape_entity/entity.rb', line 117

def hash_access=(value)
  @hash_access =
    case value
    when :to_s, :str, :string
      :to_s
    else
      :to_sym
    end
end

.inherited(subclass) ⇒ Object



140
141
142
143
144
145
# File 'lib/grape_entity/entity.rb', line 140

def self.inherited(subclass)
  subclass.root_exposure = root_exposure.dup
  subclass.formatters = formatters.dup

  super
end

.merge_options(options) ⇒ Object

Merges the given options with current block options.

Parameters:

  • options (Hash)

    Exposure options.



654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
# File 'lib/grape_entity/entity.rb', line 654

def self.merge_options(options)
  opts = {}

  merge_logic = proc do |key, existing_val, new_val|
    if %i[if unless].include?(key)
      if existing_val.is_a?(Hash) && new_val.is_a?(Hash)
        existing_val.merge(new_val)
      elsif new_val.is_a?(Hash)
        (opts[:"#{key}_extras"] ||= []) << existing_val
        new_val
      else
        (opts[:"#{key}_extras"] ||= []) << new_val
        existing_val
      end
    else
      new_val
    end
  end

  @block_options ||= []
  opts.merge @block_options.inject({}) { |final, step|
    final.merge(step, &merge_logic)
  }.merge(valid_options(options), &merge_logic)
end

.present_collection(present_collection = false, collection_name = :items) ⇒ Object

This allows you to present a collection of objects.

When false (default) every object in a collection to present will be wrapped separately
into an instance of your presenter.

Examples:

Entity Definition


module API
  module Entities
    class User < Grape::Entity
      expose :id
    end

    class Users < Grape::Entity
      present_collection true
      expose :items, as: 'users', using: API::Entities::User
      expose :version, documentation: { type: 'string',
                                        desc: 'actual api version',
                                        required: true }

      def version
        options[:version]
      end
    end
  end
end

Usage in the API Layer


module API
  class Users < Grape::API
    version 'v2'

    # this will render { "users" : [ { "id" : "1" }, { "id" : "2" } ], "version" : "v2" }
    get '/users' do
      @users = User.all
      present @users, with: API::Entities::Users
    end

    # this will render { "user" : { "id" : "1" } }
    get '/users/:id' do
      @user = User.find(params[:id])
      present @user, with: API::Entities::User
    end
  end
end

Parameters:

  • present_collection (true or false) (defaults to: false)

    when true all objects will be available as items in your presenter instead of wrapping each object in an instance of your presenter.

  • collection_name (Symbol) (defaults to: :items)

    the name of the collection accessor in your entity object. Default :items



420
421
422
423
# File 'lib/grape_entity/entity.rb', line 420

def self.present_collection(present_collection = false, collection_name = :items)
  @present_collection = present_collection
  @collection_name = collection_name
end

.represent(objects, options = {}) ⇒ Object

This convenience method allows you to instantiate one or more entities by passing either a singular or collection of objects. Each object will be initialized with the same options. If an array of objects is passed in, an array of entities will be returned. If a single object is passed in, a single entity will be returned.

Parameters:

  • objects (Object or Array)

    One or more objects to be represented.

  • options (Hash) (defaults to: {})

    Options that will be passed through to each entity representation.

Options Hash (options):

  • :root (String or false)

    override the default root name set for the entity. Pass nil or false to represent the object or objects with no root name even if one is defined for the entity.

  • :serializable (true or false)

    when true a serializable Hash will be returned

  • :only (Array)

    all the fields that should be returned

  • :except (Array)

    all the fields that should not be returned



442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
# File 'lib/grape_entity/entity.rb', line 442

def self.represent(objects, options = {})
  @present_collection ||= nil
  if objects.respond_to?(:to_ary) && !@present_collection
    root_element = root_element(:collection_root)
    inner = objects.to_ary.map { |object| new(object, options.reverse_merge(collection: true)).presented }
  else
    objects = { @collection_name => objects } if @present_collection
    root_element = root_element(:root)
    inner = new(objects, options).presented
  end

  root_element = options[:root] if options.key?(:root)

  root_element ? { root_element => inner } : inner
end

.root(plural, singular = nil) ⇒ Object

This allows you to set a root element name for your representation.

Examples:

Entity Definition


module API
  module Entities
    class User < Grape::Entity
      root 'users', 'user'
      expose :id
    end
  end
end

Usage in the API Layer


module API
  class Users < Grape::API
    version 'v2'

    # this will render { "users" : [ { "id" : "1" }, { "id" : "2" } ] }
    get '/users' do
      @users = User.all
      present @users, with: API::Entities::User
    end

    # this will render { "user" : { "id" : "1" } }
    get '/users/:id' do
      @user = User.find(params[:id])
      present @user, with: API::Entities::User
    end
  end
end

Parameters:

  • plural (String)

    the root key to use when representing a collection of objects. If missing or nil, no root key will be used when representing collections of objects.

  • singular (String) (defaults to: nil)

    the root key to use when representing a single object. If missing or nil, no root key will be used when representing an individual object.



364
365
366
367
# File 'lib/grape_entity/entity.rb', line 364

def self.root(plural, singular = nil)
  @collection_root = plural
  @root = singular
end

.root_element(root_type) ⇒ Object

This method returns the entity’s root or collection root node, or its parent’s

Parameters:

  • root_type:

    either :collection_root or just :root



460
461
462
463
464
465
466
467
# File 'lib/grape_entity/entity.rb', line 460

def self.root_element(root_type)
  instance_variable = "@#{root_type}"
  if instance_variable_defined?(instance_variable) && instance_variable_get(instance_variable)
    instance_variable_get(instance_variable)
  elsif superclass.respond_to? :root_element
    superclass.root_element(root_type)
  end
end

.root_exposuresArray

Returns exposures that have been declared for this Entity on the top level.

Returns:

  • (Array)

    of exposures



241
242
243
# File 'lib/grape_entity/entity.rb', line 241

def self.root_exposures
  root_exposure.nested_exposures
end

.unexpose(*attributes) ⇒ Object



249
250
251
252
253
# File 'lib/grape_entity/entity.rb', line 249

def self.unexpose(*attributes)
  cannot_unexpose! unless can_unexpose?
  @documentation = nil
  root_exposures.delete_by(*attributes)
end

.unexpose_allObject



255
256
257
258
259
# File 'lib/grape_entity/entity.rb', line 255

def self.unexpose_all
  cannot_unexpose! unless can_unexpose?
  @documentation = nil
  root_exposures.clear
end

.valid_options(options) ⇒ Object

Raises an error if the given options include unknown keys. Renames aliased options.

Parameters:

  • options (Hash)

    Exposure options.



683
684
685
686
687
688
689
690
# File 'lib/grape_entity/entity.rb', line 683

def self.valid_options(options)
  options.each_key do |key|
    raise ArgumentError, "#{key.inspect} is not a valid option." unless OPTIONS.include?(key)
  end

  options[:using] = options.delete(:with) if options.key?(:with)
  options
end

.with_options(options) ⇒ Object

Set options that will be applied to any exposures declared inside the block.

Examples:

Multi-exposure if


class MyEntity < Grape::Entity
  with_options if: { awesome: true } do
    expose :awesome, :sweet
  end
end


278
279
280
281
282
# File 'lib/grape_entity/entity.rb', line 278

def self.with_options(options)
  (@block_options ||= []).push(valid_options(options))
  yield
  @block_options.pop
end

Instance Method Details

#arity_requirement_for(method_name) ⇒ Object



555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'lib/grape_entity/entity.rb', line 555

def arity_requirement_for(method_name)
  origin_method = object.method(method_name)
  parameters = origin_method.parameters

  required_positional_arg_count = parameters.count { |type, _| type == :req }
  required_keyword_arg_count = parameters.count { |type, _| type == :keyreq }
  return nil if required_positional_arg_count.zero? && required_keyword_arg_count.zero?

  [required_positional_arg_count, required_keyword_arg_count, parameters.any? { |type, _| type == :rest }]
rescue NameError
  # Delegation wrappers and method_missing proxies may not expose a Method; let Ruby raise natively at call time.
  nil
end

#delegate_attribute(attribute) ⇒ Object



601
602
603
604
605
606
607
608
609
# File 'lib/grape_entity/entity.rb', line 601

def delegate_attribute(attribute)
  if is_defined_in_entity?(attribute)
    send(attribute)
  elsif delegator.accepts_options?
    delegator.delegate(attribute, **self.class.delegation_opts)
  else
    delegator.delegate(attribute)
  end
end

#documentationObject



502
503
504
# File 'lib/grape_entity/entity.rb', line 502

def documentation
  self.class.documentation
end

#ensure_block_arity!(block) ⇒ Object

Raises:

  • (ArgumentError)


536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
# File 'lib/grape_entity/entity.rb', line 536

def ensure_block_arity!(block)
  # Strict anchor to match MRI Proc#to_s format for symbol-to-proc: #<Proc:0x0...(&:method_name) (lambda)>
  match = block.to_s.match(/\A#<Proc:(?:0x)?\h+\(&:(?<name>.+)\) \(lambda\)>\z/)
  return unless match # Unrecognized format -> bail safe rather than misidentify

  origin_method_name = match[:name].to_sym
  required_positional_arg_count, required_keyword_arg_count, variadic_positional =
    arity_requirement_for(origin_method_name)
  return unless required_positional_arg_count

  required_arguments =
    required_arguments_summary(required_positional_arg_count, required_keyword_arg_count, variadic_positional)

  raise ArgumentError, <<~MSG
    Cannot use `&:#{origin_method_name}` because that method expects #{required_arguments}.
    Symbol-to-proc shorthand only works for methods that can be called with no arguments.
  MSG
end

#exec_with_attribute(attribute, &block) ⇒ Object



593
594
595
# File 'lib/grape_entity/entity.rb', line 593

def exec_with_attribute(attribute, &block)
  instance_exec(delegate_attribute(attribute), &block)
end

#exec_with_object(options, &block) ⇒ Object



525
526
527
528
529
530
531
532
533
534
# File 'lib/grape_entity/entity.rb', line 525

def exec_with_object(options, &block)
  if symbol_to_proc_wrapper?(block)
    ensure_block_arity!(block)
    instance_exec(object, &block)
  elsif block.arity == 1
    instance_exec(object, &block)
  else
    instance_exec(object, options, &block)
  end
end

#formattersObject



506
507
508
# File 'lib/grape_entity/entity.rb', line 506

def formatters
  self.class.formatters
end

#inspectObject

Prevent default serialization of :options or :delegator.



478
479
480
481
482
483
484
485
486
# File 'lib/grape_entity/entity.rb', line 478

def inspect
  object = serializable_hash
  if object.nil?
    "#<#{self.class.name}:#{object_id} nil>"
  else
    fields = object.map { |k, v| "#{k}=#{v}" }
    "#<#{self.class.name}:#{object_id} #{fields.join(' ')}>"
  end
end

#is_defined_in_entity?(attribute) ⇒ Boolean

Returns:

  • (Boolean)


611
612
613
614
615
616
# File 'lib/grape_entity/entity.rb', line 611

def is_defined_in_entity?(attribute)
  return false unless respond_to?(attribute, true)

  ancestors = self.class.ancestors
  ancestors.index(Grape::Entity) > ancestors.index(method(attribute).owner)
end

#presentedObject



469
470
471
472
473
474
475
# File 'lib/grape_entity/entity.rb', line 469

def presented
  if options[:serializable]
    serializable_hash
  else
    self
  end
end

#required_arguments_summary(required_positional_arg_count, required_keyword_arg_count, variadic_positional) ⇒ Object



569
570
571
572
573
574
575
576
577
578
579
580
581
582
# File 'lib/grape_entity/entity.rb', line 569

def required_arguments_summary(required_positional_arg_count, required_keyword_arg_count, variadic_positional)
  parts = []
  unless required_positional_arg_count.zero?
    suffix = required_positional_arg_count == 1 ? 'argument' : 'arguments'
    suffix += ' or more' if variadic_positional
    parts << "#{required_positional_arg_count} #{suffix}"
  end
  unless required_keyword_arg_count.zero?
    suffix = required_keyword_arg_count == 1 ? 'keyword argument' : 'keyword arguments'
    parts << "#{required_keyword_arg_count} #{suffix}"
  end

  parts.join(' and ')
end

#root_exposureObject



498
499
500
# File 'lib/grape_entity/entity.rb', line 498

def root_exposure
  self.class.root_exposure
end

#root_exposuresObject



494
495
496
# File 'lib/grape_entity/entity.rb', line 494

def root_exposures
  self.class.root_exposures
end

#serializable_hash(runtime_options = {}) ⇒ Object Also known as: as_json

The serializable hash is the Entity’s primary output. It is the transformed hash for the given data model and is used as the basis for serialization to JSON and other formats.

Parameters:

  • runtime_options (Hash) (defaults to: {})

    Any options you pass in here will be known to the entity representation, this is where you can trigger things from conditional options etc.



517
518
519
520
521
522
523
# File 'lib/grape_entity/entity.rb', line 517

def serializable_hash(runtime_options = {})
  return nil if object.nil?

  opts = options.merge(runtime_options || {})

  root_exposure.serializable_value(self, opts)
end

#symbol_to_proc_wrapper?(block) ⇒ Boolean

Returns:

  • (Boolean)


584
585
586
587
588
589
590
591
# File 'lib/grape_entity/entity.rb', line 584

def symbol_to_proc_wrapper?(block)
  params = block.parameters

  return false unless block.lambda? && block.source_location.nil?
  return false unless params.size >= 2

  params[0].first == :req && params[1].first == :rest
end

#to_json(options = {}) ⇒ Object



620
621
622
623
# File 'lib/grape_entity/entity.rb', line 620

def to_json(options = {})
  options = options.to_h if options&.respond_to?(:to_h)
  serializable_hash(options).to_json
end

#to_xml(options = {}) ⇒ Object



625
626
627
628
# File 'lib/grape_entity/entity.rb', line 625

def to_xml(options = {})
  options = options.to_h if options&.respond_to?(:to_h)
  serializable_hash(options).to_xml(options)
end

#value_for(key, options = Options.new) ⇒ Object



597
598
599
# File 'lib/grape_entity/entity.rb', line 597

def value_for(key, options = Options.new)
  root_exposure.valid_value_for(key, self, options)
end