class ArSerializer::Field

Attributes

data_block[R]
except[R]
includes[R]
only[R]
order_column[R]
preloaders[R]

Public Class Methods

association_field(klass, name, only:, except:, type:, collection:) click to toggle source
# File lib/ar_serializer/field.rb, line 193
def self.association_field(klass, name, only:, except:, type:, collection:)
  if collection
    preloader = lambda do |models, _context, limit: nil, order: nil, **_option|
      preload_association klass, models, name, limit: limit, order: order
    end
    params_type = { limit?: :int, order?: [{ :* => %w[asc desc] }, 'asc', 'desc'] }
  else
    preloader = lambda do |models, _context, **_params|
      preload_association klass, models, name
    end
  end
  data_block = lambda do |preloaded, _context, **_params|
    preloaded ? preloaded[id] || [] : send(name)
  end
  new preloaders: [preloader], data_block: data_block, only: only, except: except, type: type, params_type: params_type
end
count_field(klass, association_name) click to toggle source
# File lib/ar_serializer/field.rb, line 82
def self.count_field(klass, association_name)
  preloader = lambda do |models|
    klass.joins(association_name).where(id: models.map(&:id)).group(:id).count
  end
  data_block = lambda do |preloaded, _context, **_params|
    preloaded[id] || 0
  end
  new preloaders: [preloader], data_block: data_block, type: :int
end
create(klass, name, type: nil, params_type: nil, count_of: nil, includes: nil, preload: nil, only: nil, except: nil, order_column: nil, &data_block) click to toggle source
# File lib/ar_serializer/field.rb, line 122
def self.create(klass, name, type: nil, params_type: nil, count_of: nil, includes: nil, preload: nil, only: nil, except: nil, order_column: nil, &data_block)
  if count_of
    if includes || preload || data_block || only || except
      raise ArgumentError, 'includes, preload block cannot be used with count_of'
    end
    return count_field klass, count_of
  end
  underscore_name = name.to_s.underscore
  association = klass.reflect_on_association underscore_name if klass.respond_to? :reflect_on_association
  if association
    if association.collection?
      type ||= -> { [association.klass] }
    elsif (association.belongs_to? && association.options[:optional] == true) || (association.has_one? && association.options[:required] != true)
      type ||= -> { [association.klass, nil] }
    else
      type ||= -> { association.klass }
    end
    return association_field klass, underscore_name, only: only, except: except, type: type, collection: association.collection? if !includes && !preload && !data_block && !params_type
  end
  type ||= lambda do
    if klass.respond_to? :column_for_attribute
      type_from_column_type klass, underscore_name
    elsif klass.respond_to? :attribute_types
      type_from_attribute_type(klass, underscore_name) || :any
    else
      :any
    end
  end
  custom_field klass, underscore_name, includes: includes, preload: preload, only: only, except: except, order_column: order_column, type: type, params_type: params_type, &data_block
end
custom_field(klass, name, includes:, preload:, only:, except:, order_column:, type:, params_type:, &data_block) click to toggle source
# File lib/ar_serializer/field.rb, line 153
def self.custom_field(klass, name, includes:, preload:, only:, except:, order_column:, type:, params_type:, &data_block)
  if preload
    preloaders = Array(preload).map do |preloader|
      next preloader if preloader.is_a? Proc
      unless klass._custom_preloaders.has_key?(preloader)
        raise ArgumentError, "preloader not found: #{preloader}"
      end
      klass._custom_preloaders[preloader]
    end
  else
    preloaders = []
    includes ||= name if klass.respond_to?(:reflect_on_association) && klass.reflect_on_association(name)
  end
  data_block ||= ->(preloaded, _context, **_params) { preloaded[id] } if preloaders.size == 1
  raise ArgumentError, 'data_block needed if multiple preloaders are present' if !preloaders.empty? && data_block.nil?
  new(
    includes: includes, preloaders: preloaders, only: only, except: except, order_column: order_column, type: type, params_type: params_type,
    data_block: data_block || ->(_context, **_params) { send name }
  )
end
new(includes: nil, preloaders: [], data_block:, only: nil, except: nil, order_column: nil, type: nil, params_type: nil) click to toggle source
# File lib/ar_serializer/field.rb, line 6
def initialize includes: nil, preloaders: [], data_block:, only: nil, except: nil, order_column: nil, type: nil, params_type: nil
  @includes = includes
  @preloaders = preloaders
  @only = only && [*only].map(&:to_s)
  @except = except && [*except].map(&:to_s)
  @data_block = data_block
  @order_column = order_column
  @type = type
  @params_type = params_type
end
parse_order(klass, order) click to toggle source
# File lib/ar_serializer/field.rb, line 174
def self.parse_order(klass, order)
  key, mode = begin
    case order
    when Hash
      raise ArSerializer::InvalidQuery, 'invalid order' unless order.size == 1
      order.first
    when Symbol, 'asc', 'desc'
      [klass.primary_key, order]
    when NilClass
      [klass.primary_key, :asc]
    end
  end
  info = klass._serializer_field_info(key)
  key = info&.order_column || key.to_s.underscore
  raise ArSerializer::InvalidQuery, "unpermitted order key: #{key}" unless klass.primary_key == key.to_s || (klass.has_attribute?(key) && info)
  raise ArSerializer::InvalidQuery, "invalid order mode: #{mode.inspect}" unless [:asc, :desc, 'asc', 'desc'].include? mode
  [key.to_sym, mode.to_sym]
end
preload_association(klass, models, name, limit: nil, order: nil) click to toggle source
# File lib/ar_serializer/field.rb, line 210
def self.preload_association(klass, models, name, limit: nil, order: nil)
  limit = limit&.to_i
  order_key, order_mode = parse_order klass.reflect_on_association(name).klass, order
  return TopNLoader.load_associations klass, models.map(&:id), name, limit: limit, order: { order_key => order_mode } if limit
  ActiveRecord::Associations::Preloader.new.preload models, name
  return if order.nil?
  models.map do |model|
    records_nonnils, records_nils = model.send(name).partition(&order_key)
    records = records_nils.sort_by(&:id) + records_nonnils.sort_by { |r| [r[order_key], r.id] }
    records.reverse! if order_mode == :desc
    [model.id, records]
  end.to_h
end
type_from_attribute_type(klass, name) click to toggle source
# File lib/ar_serializer/field.rb, line 98
def self.type_from_attribute_type(klass, name)
  attr_type = klass.attribute_types[name]
  if attr_type.is_a?(ActiveRecord::Enum::EnumType) && klass.respond_to?(name.pluralize)
    values = klass.send(name.pluralize).keys.compact
    values = values.map { |v| v.is_a?(Symbol) ? v.to_s : v }.uniq
    valid_classes = [TrueClass, FalseClass, String, Integer, Float]
    return if values.empty? || (values.map(&:class) - valid_classes).present?
    return values
  end
  {
    boolean: :boolean,
    integer: :int,
    float: :float,
    decimal: :float,
    string: :string,
    text: :string,
    json: :string,
    binary: :string,
    time: :string,
    date: :string,
    datetime: :string
  }[attr_type.type]
end
type_from_column_type(klass, name) click to toggle source
# File lib/ar_serializer/field.rb, line 92
def self.type_from_column_type(klass, name)
  type = type_from_attribute_type klass, name.to_s
  return :any if type.nil?
  klass.column_for_attribute(name).null ? [*type, nil] : type
end

Public Instance Methods

arguments() click to toggle source
# File lib/ar_serializer/field.rb, line 36
def arguments
  return @params_type if @params_type
  @preloaders.size
  @data_block.parameters
  parameters_list = [@data_block.parameters.drop(@preloaders.size + 1)]
  @preloaders.each do |preloader|
    parameters_list << preloader.parameters.drop(2)
  end
  arguments = {}
  any = false
  parameters_list.each do |parameters|
    ftype, fname = parameters.first
    if %i[opt req rest].include? ftype
      any = true unless fname.match?(/^_/)
      next
    end
    parameters.each do |type, name|
      case type
      when :keyreq
        arguments[name] ||= true
      when :key
        arguments[name] ||= false
      when :keyrest
        any = true unless name.match?(/^_/)
      when :opt, :req
        break
      end
    end
  end
  return :any if any && arguments.empty?
  arguments.map do |key, req|
    type = key.to_s.match?(/^(.+_)?id|Id$/) ? :int : :any
    name = key.to_s.underscore
    type = [type] if name.singularize.pluralize == name
    [req ? key : "#{key}?", type]
  end.to_h
end
type() click to toggle source
# File lib/ar_serializer/field.rb, line 17
def type
  type = @type.is_a?(Proc) ? @type.call : @type
  splat = lambda do |t|
    case t
    when Array
      if t.size == 1 || (t.size == 2 && t.compact.size == 1)
        t.map(&splat)
      else
        t.map { |v| v.is_a?(String) ? v : splat.call(v) }
      end
    when Hash
      t.transform_values(&splat)
    else
      t
    end
  end
  splat.call type
end
validate_attributes(attributes) click to toggle source
# File lib/ar_serializer/field.rb, line 74
def validate_attributes(attributes)
  return unless @only || @except
  keys = attributes.map(&:first).map(&:to_s) - ['*']
  return unless (@only && (keys - @only).present?) || (@except && (keys & @except).present?)
  invalid_keys = [*(@only && keys - @only), *(@except && keys & @except)].uniq
  raise ArSerializer::InvalidQuery, "unpermitted attribute: #{invalid_keys}"
end