Class: ArSerializer::Field

Inherits:
Object
  • Object
show all
Defined in:
lib/ar_serializer/field.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(includes: nil, preloaders: [], data_block:, only: nil, except: nil, order_column: nil, type: nil, params_type: nil) ⇒ Field

Returns a new instance of Field.



6
7
8
9
10
11
12
13
14
15
# 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

Instance Attribute Details

#data_blockObject (readonly)

Returns the value of attribute data_block.



5
6
7
# File 'lib/ar_serializer/field.rb', line 5

def data_block
  @data_block
end

#exceptObject (readonly)

Returns the value of attribute except.



5
6
7
# File 'lib/ar_serializer/field.rb', line 5

def except
  @except
end

#includesObject (readonly)

Returns the value of attribute includes.



5
6
7
# File 'lib/ar_serializer/field.rb', line 5

def includes
  @includes
end

#onlyObject (readonly)

Returns the value of attribute only.



5
6
7
# File 'lib/ar_serializer/field.rb', line 5

def only
  @only
end

#order_columnObject (readonly)

Returns the value of attribute order_column.



5
6
7
# File 'lib/ar_serializer/field.rb', line 5

def order_column
  @order_column
end

#preloadersObject (readonly)

Returns the value of attribute preloaders.



5
6
7
# File 'lib/ar_serializer/field.rb', line 5

def preloaders
  @preloaders
end

Class Method Details

.association_field(klass, name, only:, except:, type:, collection:) ⇒ Object



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# 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) ⇒ Object



82
83
84
85
86
87
88
89
90
# 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) ⇒ Object



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# 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) ⇒ Object

Raises:

  • (ArgumentError)


153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# 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

.parse_order(klass, order) ⇒ Object



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# 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) ⇒ Object



210
211
212
213
214
215
216
217
218
219
220
221
222
# 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) ⇒ Object



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# 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) ⇒ Object



92
93
94
95
96
# 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

Instance Method Details

#argumentsObject



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# 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

#typeObject



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 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) ⇒ Object



74
75
76
77
78
79
80
# 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