ar_serializer 1.1.1 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7ba3188364c0f2ba77cd345116c161cb1586ba86f165ee8336266d3d69dc936
4
- data.tar.gz: de1a1f08e2eb1cee584bb9f34663fc51d460dbb6283aa4773681fb8ff585df9d
3
+ metadata.gz: ee32f3c64753466283134306f9c8bcae7435cd8cfb337591455de2455de5fc15
4
+ data.tar.gz: eab2dc2c198ef804b6d0f1070057131480dae5fcae0f394d2a4aee065b647e0b
5
5
  SHA512:
6
- metadata.gz: 987a6cad67b5db442cd5cbb0167fb754cf3cb841d0ea050bf148391ed6f4c61037e17e1448d50d89ebd6549541b4aa51566a233b42c578eded817f42bc77c156
7
- data.tar.gz: f6c971e38b6e96673ddddcac94ee30e37d12eeb05ef68a8c878fff3490ec273e91cb1146a8ba6a185cac5fb313c6a429d07dd89986475e73454db6f6f187ed7e
6
+ metadata.gz: 2d1aef11e66bca294d04217f7046d902063ba382e0ccbd4c11c0e843ad527929941f22b874753fefc24c550efbc28a3226939c61dc82fe81ed7e0352963f0e7e
7
+ data.tar.gz: 6f62a16b9bf8f971f9379c0860772e921844e8123506cbbb89e92f759d8f97f1b686ac803cfd1092bce8078be656d96ff2ceaa66377cc080685fdcf19b072a7d
@@ -0,0 +1,23 @@
1
+ name: Test
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ strategy:
6
+ fail-fast: false
7
+ matrix:
8
+ ruby: [ '3.1', '3.2', '3.3' ]
9
+ gemfiles:
10
+ - gemfiles/Gemfile-rails-6
11
+ - gemfiles/Gemfile-rails-7-0
12
+ - gemfiles/Gemfile-rails-7-1
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: ruby/setup-ruby@v1
17
+ with:
18
+ ruby-version: ${{ matrix.ruby }}
19
+ - run: |
20
+ sudo apt-get update
21
+ sudo apt-get install -y libsqlite3-dev
22
+ - run: bundle install --gemfile ${{ matrix.gemfiles }} --jobs 4 --retry 3
23
+ - run: bundle exec --gemfile ${{ matrix.gemfiles }} rake
data/.gitignore CHANGED
@@ -7,4 +7,6 @@
7
7
  /spec/reports/
8
8
  /tmp/
9
9
  *.sqlite3
10
+ *.sqlite3-*
10
11
  .ruby-version
12
+ Gemfile.lock
data/Gemfile CHANGED
@@ -4,3 +4,4 @@ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
5
  # Specify your gem's dependencies in ar_serializer.gemspec
6
6
  gemspec
7
+ gem "sqlite3", "~> 1.4"
data/README.md CHANGED
@@ -71,10 +71,10 @@ end
71
71
 
72
72
  # preloader
73
73
  class Foo < ActiveRecord::Base
74
- define_preloader :bar_count_loader do |models|
74
+ bar_count_loader = ->(models) do
75
75
  Bar.where(foo_id: models.map(&:id)).group(:foo_id).count
76
76
  end
77
- serializer_field :bar_count, preload: preloader_name_or_proc do |preloaded|
77
+ serializer_field :bar_count, preload: bar_count_loader do |preloaded|
78
78
  preloaded[id] || 0
79
79
  end
80
80
  # data_blockが `do |preloaded| preloaded[id] end` の場合は省略可能
@@ -85,7 +85,8 @@ class Post < ActiveRecord::Base
85
85
  has_many :comments
86
86
  serializer_field :comments
87
87
  end
88
- ArSerializer.serialize Post.all, { comments: [:id, params: { order: { id: :desc }, limit: 2 }] }
88
+ ArSerializer.serialize Post.all, { comments: [:id, params: { order_by: :createdAt, direction: :desc, first: 10 }] }
89
+ ArSerializer.serialize Post.all, { comments: [:id, params: { order_by: :updatedAt, last: 10 }] }
89
90
 
90
91
  # context and params
91
92
  class Post < ActiveRecord::Base
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
23
23
 
24
24
  spec.add_dependency 'activerecord'
25
25
  spec.add_dependency 'top_n_loader'
26
- %w[rake pry sqlite3 minitest simplecov].each do |gem_name|
26
+ %w[rake sqlite3 minitest simplecov].each do |gem_name|
27
27
  spec.add_development_dependency gem_name
28
28
  end
29
29
  end
data/bin/console CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'bundler/setup'
4
4
  require 'ar_serializer'
5
- require 'pry'
5
+ require 'irb'
6
6
  require_relative '../test/db'
7
7
 
8
- Pry.start
8
+ IRB.start
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in ar_serializer.gemspec
6
+ gemspec path: ".."
7
+
8
+ gem "sqlite3", "~> 1.4"
9
+ gem "activerecord", "~> 6.0"
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in ar_serializer.gemspec
6
+ gemspec path: ".."
7
+
8
+ gem "sqlite3", "~> 1.4"
9
+ gem "activerecord", "~> 7.0.0"
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in ar_serializer.gemspec
6
+ gemspec path: ".."
7
+
8
+ gem "sqlite3", "~> 1.4"
9
+ gem "activerecord", "~> 7.1.0"
@@ -1,19 +1,36 @@
1
1
  require 'ar_serializer/error'
2
2
  require 'top_n_loader'
3
+ require 'set'
3
4
 
4
5
  class ArSerializer::Field
5
- attr_reader :includes, :preloaders, :data_block, :only, :except, :order_column
6
- def initialize includes: nil, preloaders: [], data_block:, only: nil, except: nil, order_column: nil, type: nil, params_type: nil
6
+ attr_reader :includes, :preloaders, :data_block, :only, :except, :scoped_access, :order_column, :permission, :fallback
7
+ def initialize klass, name, includes: nil, preloaders: [], data_block:, only: nil, except: nil, private: false, scoped_access: nil, permission: nil, fallback: nil, order_column: nil, orderable: nil, type: nil, params_type: nil
8
+ @klass = klass
9
+ @name = name
7
10
  @includes = includes
8
11
  @preloaders = preloaders
9
12
  @only = only && [*only].map(&:to_s)
10
13
  @except = except && [*except].map(&:to_s)
14
+ @private = private
15
+ @scoped_access = scoped_access.nil? ? true : scoped_access
16
+ @permission = permission
17
+ @fallback = fallback
11
18
  @data_block = data_block
12
19
  @order_column = order_column
20
+ @orderable = orderable
13
21
  @type = type
14
22
  @params_type = params_type
15
23
  end
16
24
 
25
+ def orderable?
26
+ return @orderable unless @orderable.nil?
27
+ @orderable = !@permission && @klass.has_attribute?((@order_column || @name).to_s.underscore)
28
+ end
29
+
30
+ def private?
31
+ @private
32
+ end
33
+
17
34
  def type
18
35
  type = @type.is_a?(Proc) ? @type.call : @type
19
36
  splat = lambda do |t|
@@ -34,7 +51,7 @@ class ArSerializer::Field
34
51
  end
35
52
 
36
53
  def arguments
37
- return @params_type if @params_type
54
+ return @params_type.is_a?(Proc) ? @params_type.call : @params_type if @params_type
38
55
  @preloaders.size
39
56
  @data_block.parameters
40
57
  parameters_list = [@data_block.parameters.drop(@preloaders.size + 1)]
@@ -64,10 +81,19 @@ class ArSerializer::Field
64
81
  end
65
82
  return :any if any && arguments.empty?
66
83
  arguments.map do |key, req|
67
- type = key.to_s.match?(/^(.+_)?id|Id$/) ? :int : :any
68
- name = key.to_s.underscore
69
- type = [type] if name.singularize.pluralize == name
70
- [req ? key : "#{key}?", type]
84
+ camelcase = key.to_s.camelcase :lower
85
+ type = (
86
+ case key
87
+ when /^(.+_)?id$/
88
+ :int
89
+ when /^(.+_)?ids$/
90
+ [:int]
91
+ else
92
+ name = key.to_s.underscore
93
+ name.singularize.pluralize == name ? [:any] : :any
94
+ end
95
+ )
96
+ [req ? camelcase : "#{camelcase}?", type]
71
97
  end.to_h
72
98
  end
73
99
 
@@ -79,14 +105,14 @@ class ArSerializer::Field
79
105
  raise ArSerializer::InvalidQuery, "unpermitted attribute: #{invalid_keys}"
80
106
  end
81
107
 
82
- def self.count_field(klass, association_name)
108
+ def self.count_field(klass, name, association_name, permission:)
83
109
  preloader = lambda do |models|
84
110
  klass.joins(association_name).where(id: models.map(&:id)).group(:id).count
85
111
  end
86
112
  data_block = lambda do |preloaded, _context, **_params|
87
113
  preloaded[id] || 0
88
114
  end
89
- new preloaders: [preloader], data_block: data_block, type: :int
115
+ new klass, name, preloaders: [preloader], data_block: data_block, type: :int, orderable: false, permission: permission, fallback: 0
90
116
  end
91
117
 
92
118
  def self.type_from_column_type(klass, name)
@@ -98,7 +124,7 @@ class ArSerializer::Field
98
124
  def self.type_from_attribute_type(klass, name)
99
125
  attr_type = klass.attribute_types[name]
100
126
  if attr_type.is_a?(ActiveRecord::Enum::EnumType) && klass.respond_to?(name.pluralize)
101
- values = klass.send(name.pluralize).keys.compact
127
+ values = klass.__send__(name.pluralize).keys.compact
102
128
  values = values.map { |v| v.is_a?(Symbol) ? v.to_s : v }.uniq
103
129
  valid_classes = [TrueClass, FalseClass, String, Integer, Float]
104
130
  return if values.empty? || (values.map(&:class) - valid_classes).present?
@@ -111,7 +137,7 @@ class ArSerializer::Field
111
137
  decimal: :float,
112
138
  string: :string,
113
139
  text: :string,
114
- json: :string,
140
+ json: :unknown,
115
141
  binary: :string,
116
142
  time: :string,
117
143
  date: :string,
@@ -119,105 +145,145 @@ class ArSerializer::Field
119
145
  }[attr_type.type]
120
146
  end
121
147
 
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)
148
+ def self.create(klass, name, type: nil, params_type: nil, count_of: nil, includes: nil, preload: nil, only: nil, except: nil, private: nil, scoped_access: nil, permission: nil, fallback: nil, order_column: nil, orderable: nil, &data_block)
149
+ name = name.to_s
123
150
  if count_of
124
- if includes || preload || data_block || only || except
125
- raise ArgumentError, 'includes, preload block cannot be used with count_of'
151
+ if includes || preload || data_block || only || except || order_column || orderable || scoped_access != nil || fallback
152
+ raise ArgumentError, 'wrong options for count_of field'
126
153
  end
127
- return count_field klass, count_of
154
+ return count_field klass, name, count_of, permission: permission
128
155
  end
129
- underscore_name = name.to_s.underscore
130
- association = klass.reflect_on_association underscore_name if klass.respond_to? :reflect_on_association
156
+ association = klass.reflect_on_association name.underscore if klass.respond_to? :reflect_on_association
131
157
  if association
132
158
  if association.collection?
133
159
  type ||= -> { [association.klass] }
160
+ fallback ||= []
134
161
  elsif (association.belongs_to? && association.options[:optional] == true) || (association.has_one? && association.options[:required] != true)
135
162
  type ||= -> { [association.klass, nil] }
136
163
  else
137
164
  type ||= -> { association.klass }
138
165
  end
139
- return association_field klass, underscore_name, only: only, except: except, type: type, collection: association.collection? if !includes && !preload && !data_block && !params_type
166
+ return association_field klass, name, only: only, except: except, scoped_access: scoped_access, permission: permission, fallback: fallback, type: type, collection: association.collection? if !includes && !preload && !data_block && !params_type
140
167
  end
141
168
  type ||= lambda do
142
169
  if klass.respond_to? :column_for_attribute
143
- type_from_column_type klass, underscore_name
170
+ type_from_column_type klass, name.underscore
144
171
  elsif klass.respond_to? :attribute_types
145
- type_from_attribute_type(klass, underscore_name) || :any
172
+ type_from_attribute_type(klass, name.underscore) || :any
146
173
  else
147
174
  :any
148
175
  end
149
176
  end
150
- 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
177
+ custom_field klass, name, includes: includes, preload: preload, only: only, except: except, private: private, scoped_access: scoped_access, permission: permission, fallback: fallback, order_column: order_column, orderable: orderable, type: type, params_type: params_type, &data_block
151
178
  end
152
179
 
153
- def self.custom_field(klass, name, includes:, preload:, only:, except:, order_column:, type:, params_type:, &data_block)
180
+ def self.custom_field(klass, name, includes:, preload:, only:, except:, private:, scoped_access:, permission:, fallback:, order_column:, orderable:, type:, params_type:, &data_block)
181
+ underscore_name = name.underscore
154
182
  if preload
155
- preloaders = Array(preload).map do |preloader|
156
- next preloader if preloader.is_a? Proc
157
- unless klass._custom_preloaders.has_key?(preloader)
158
- raise ArgumentError, "preloader not found: #{preloader}"
159
- end
160
- klass._custom_preloaders[preloader]
161
- end
183
+ preloaders = [*preload]
162
184
  else
163
185
  preloaders = []
164
- includes ||= name if klass.respond_to?(:reflect_on_association) && klass.reflect_on_association(name)
186
+ includes ||= underscore_name if klass.respond_to?(:reflect_on_association) && klass.reflect_on_association(underscore_name)
187
+ end
188
+ if !data_block && preloaders.size == 1
189
+ data_block = ->(preloaded, _context, **_params) do
190
+ if preloaded.is_a? Set
191
+ preloaded.include? id
192
+ elsif fallback.nil?
193
+ preloaded[id] # returns preloaded.default unless preloaded.has_key?(id)
194
+ else
195
+ preloaded.has_key?(id) ? preloaded[id] : fallback
196
+ end
197
+ end
165
198
  end
166
- data_block ||= ->(preloaded, _context, **_params) { preloaded[id] } if preloaders.size == 1
167
199
  raise ArgumentError, 'data_block needed if multiple preloaders are present' if !preloaders.empty? && data_block.nil?
168
200
  new(
169
- includes: includes, preloaders: preloaders, only: only, except: except, order_column: order_column, type: type, params_type: params_type,
170
- data_block: data_block || ->(_context, **_params) { send name }
201
+ klass,
202
+ name,
203
+ includes: includes, preloaders: preloaders, only: only, except: except,
204
+ private: private, scoped_access: scoped_access, permission: permission, fallback: fallback,
205
+ order_column: order_column, orderable: orderable, type: type, params_type: params_type,
206
+ data_block: data_block || ->(_context, **_params) { __send__ underscore_name }
171
207
  )
172
208
  end
173
209
 
174
- def self.parse_order(klass, order)
175
- key, mode = begin
210
+ def self.parse_order(klass, order: nil, order_by: nil, direction: nil, only: nil, except: nil)
211
+ raise ArSerializer::InvalidQuery, 'invalid order' if order && (order_by || direction)
212
+ primary_key = klass.primary_key.to_sym
213
+ order_by = order_by&.to_s&.to_sym || primary_key
214
+ direction = direction&.to_s&.to_sym || :asc
215
+ if order # deprecated
176
216
  case order
177
217
  when Hash
178
218
  raise ArSerializer::InvalidQuery, 'invalid order' unless order.size == 1
179
- order.first
219
+ order_by, direction = order.first.map(&:to_sym)
180
220
  when Symbol, 'asc', 'desc'
181
- [klass.primary_key, order]
182
- when NilClass
183
- [klass.primary_key, :asc]
221
+ direction = order.to_sym
222
+ else
223
+ raise ArSerializer::InvalidQuery, 'invalid order'
184
224
  end
185
225
  end
186
- info = klass._serializer_field_info(key)
187
- key = info&.order_column || key.to_s.underscore
188
- raise ArSerializer::InvalidQuery, "unpermitted order key: #{key}" unless klass.primary_key == key.to_s || (klass.has_attribute?(key) && info)
189
- raise ArSerializer::InvalidQuery, "invalid order mode: #{mode.inspect}" unless [:asc, :desc, 'asc', 'desc'].include? mode
190
- [key.to_sym, mode.to_sym]
226
+ info = klass._serializer_field_info order_by
227
+ order_column = (info&.order_column || order_by).to_s.underscore.to_sym
228
+ raise ArSerializer::InvalidQuery, "invalid order direction: #{direction}" unless [:asc, :desc].include? direction
229
+ raise ArSerializer::InvalidQuery, "unpermitted order field: #{order_by}" unless order_by == primary_key || (info&.orderable? && (!only || only.include?(order_by)) && !except&.include?(order_by))
230
+ [order_column, direction]
191
231
  end
192
232
 
193
- def self.association_field(klass, name, only:, except:, type:, collection:)
233
+ def self.association_field(klass, name, only:, except:, scoped_access:, permission:, fallback:, type:, collection:)
234
+ underscore_name = name.underscore
235
+ only = [*only] if only
236
+ except = [*except] if except
194
237
  if collection
195
- preloader = lambda do |models, _context, limit: nil, order: nil, **_option|
196
- preload_association klass, models, name, limit: limit, order: order
238
+ preloader = lambda do |models, _context, limit: nil, order: nil, first: nil, last: nil, order_by: nil, direction: nil, **_option|
239
+ preload_association klass, models, underscore_name, limit: limit, order: order, first: first, last: last, order_by: order_by, direction: direction, only: only, except: except
240
+ end
241
+ params_type = -> {
242
+ orderable_keys = klass.reflect_on_association(underscore_name).klass._serializer_orderable_field_keys
243
+ orderable_keys &= only.map(&:to_s) if only
244
+ orderable_keys -= except.map(&:to_s) if except
245
+ orderable_keys |= ['id']
246
+ orderable_keys.sort!
247
+ modes = %w[asc desc]
248
+ {
249
+ first?: :int,
250
+ last?: :int,
251
+ orderBy?: orderable_keys.size == 1 ? orderable_keys.first : orderable_keys,
252
+ direction?: modes
253
+ }
254
+ }
255
+ data_block = lambda do |preloaded, _context, **_params|
256
+ preloaded ? preloaded[id || self] || [] : __send__(underscore_name)
197
257
  end
198
- params_type = { limit?: :int, order?: [{ :* => %w[asc desc] }, 'asc', 'desc'] }
199
258
  else
200
259
  preloader = lambda do |models, _context, **_params|
201
- preload_association klass, models, name
260
+ preload_association klass, models, underscore_name
261
+ end
262
+ data_block = lambda do |preloaded, _context, **_params|
263
+ preloaded ? preloaded[id || self] : __send__(underscore_name)
202
264
  end
203
265
  end
204
- data_block = lambda do |preloaded, _context, **_params|
205
- preloaded ? preloaded[id] || [] : send(name)
206
- end
207
- new preloaders: [preloader], data_block: data_block, only: only, except: except, type: type, params_type: params_type
266
+ new klass, name, preloaders: [preloader], data_block: data_block, only: only, except: except, scoped_access: scoped_access, permission: permission, fallback: fallback, type: type, params_type: params_type, orderable: false
208
267
  end
209
268
 
210
- def self.preload_association(klass, models, name, limit: nil, order: nil)
211
- limit = limit&.to_i
212
- order_key, order_mode = parse_order klass.reflect_on_association(name).klass, order
213
- return TopNLoader.load_associations klass, models.map(&:id), name, limit: limit, order: { order_key => order_mode } if limit
214
- ActiveRecord::Associations::Preloader.new.preload models, name
215
- return if order.nil?
269
+ def self.preload_association(klass, models, name, limit: nil, order: nil, first: nil, last: nil, order_by: nil, direction: nil, only: nil, except: nil)
270
+ raise ArSerializer::InvalidQuery, 'invalid count option' if (limit && (first || last)) || (first && last)
271
+ first = (first || limit)&.to_i
272
+ last = last&.to_i
273
+ order_column, order_direction = parse_order klass.reflect_on_association(name).klass, order: order, order_by: order_by, direction: direction, only: only, except: except
274
+ if first || last
275
+ order_option = { order_column => first ? order_direction : (order_direction == :asc ? :desc : :asc) }
276
+ result = TopNLoader.load_associations klass, models.map(&:id), name, limit: first || last, order: order_option
277
+ result = result.transform_values!(&:reverse!) if last
278
+ return result
279
+ end
280
+ ArSerializer.preload_associations models, name
281
+ return models.map { |m| [m.id || m, m.__send__(name)] }.to_h if !order && !order_by && !direction
216
282
  models.map do |model|
217
- records_nonnils, records_nils = model.send(name).partition(&order_key)
218
- records = records_nils.sort_by(&:id) + records_nonnils.sort_by { |r| [r[order_key], r.id] }
219
- records.reverse! if order_mode == :desc
220
- [model.id, records]
283
+ records_nonnils, records_nils = model.__send__(name).partition(&order_column)
284
+ records = records_nils.sort_by(&:id) + records_nonnils.sort_by { |r| [r[order_column], r.id] }
285
+ records.reverse! if order_direction == :desc
286
+ [model.id || model, records]
221
287
  end.to_h
222
288
  end
223
289
  end
@@ -103,7 +103,7 @@ module ArSerializer::GraphQL
103
103
  class InvalidType < StandardError; end
104
104
 
105
105
  def validate!
106
- valid_symbols = %i[number int float string boolean any]
106
+ valid_symbols = %i[number int float string boolean any unknown]
107
107
  invalids = []
108
108
  recursive_validate = lambda do |t|
109
109
  case t
@@ -153,8 +153,8 @@ module ArSerializer::GraphQL
153
153
  end
154
154
 
155
155
  def self.from(type, only = nil, except = nil)
156
- type = [type[0...-1].to_sym, nil] if type.is_a?(Symbol) && type.to_s.ends_with?('?')
157
- type = [type[0...-1], nil] if type.is_a?(String) && type.ends_with?('?')
156
+ type = [type[0...-1].to_sym, nil] if type.is_a?(Symbol) && type.to_s.end_with?('?')
157
+ type = [type[0...-1], nil] if type.is_a?(String) && type.end_with?('?')
158
158
  case type
159
159
  when Class
160
160
  SerializableTypeClass.new type, only, except
@@ -164,7 +164,7 @@ module ArSerializer::GraphQL
164
164
  if type.size == 1
165
165
  ListTypeClass.new type.first, only, except
166
166
  elsif type.size == 2 && type.last.nil?
167
- OptionalTypeClass.new type
167
+ OptionalTypeClass.new type, only, except
168
168
  else
169
169
  OrTypeClass.new type, only, except
170
170
  end
@@ -195,6 +195,8 @@ module ArSerializer::GraphQL
195
195
  :boolean
196
196
  when :other
197
197
  :other
198
+ when :unknown
199
+ :unknown
198
200
  else
199
201
  :any
200
202
  end
@@ -216,7 +218,7 @@ module ArSerializer::GraphQL
216
218
  ''
217
219
  when 'boolean'
218
220
  true
219
- when 'any'
221
+ when 'any', 'unknown'
220
222
  nil
221
223
  else
222
224
  type
@@ -227,7 +229,7 @@ module ArSerializer::GraphQL
227
229
  case type
228
230
  when :int, :float
229
231
  'number'
230
- when :string, :number, :boolean
232
+ when :string, :number, :boolean, :unknown
231
233
  type.to_s
232
234
  when Symbol
233
235
  'any'
@@ -266,7 +268,7 @@ module ArSerializer::GraphQL
266
268
  end
267
269
 
268
270
  def sample
269
- type.reject { |k| k.to_s.ends_with? '?' }.transform_values do |v|
271
+ type.reject { |k| k.to_s.end_with? '?' }.transform_values do |v|
270
272
  TypeClass.from(v).sample
271
273
  end
272
274
  end
@@ -1,66 +1,55 @@
1
1
  require 'ar_serializer/error'
2
2
 
3
- class ArSerializer::CompositeValue
4
- def initialize(pairs:, output:)
5
- @pairs = pairs
6
- @output = output
3
+ class ArSerializer::CustomSerializable
4
+ attr_reader :ar_custom_serializable_models
5
+ def initialize(models, &block)
6
+ @ar_custom_serializable_models = models
7
+ @block = block
7
8
  end
8
9
 
9
- def ar_serializer_build_sub_calls
10
- [@output, @pairs]
10
+ def ar_custom_serializable_data(result)
11
+ @block.call result
11
12
  end
12
13
  end
13
14
 
14
- module ArSerializer::ArrayLikeCompositeValue
15
- def ar_serializer_build_sub_calls
16
- output = []
17
- record_elements = []
18
- each do |record|
19
- data = {}
20
- output << data
21
- record_elements << [record, data]
22
- end
23
- [output, record_elements]
24
- end
15
+ module ArSerializer::ArrayLikeSerializable
25
16
  end
26
17
 
27
18
  module ArSerializer::Serializer
28
19
  def self.current_namespaces
29
- Thread.current[:ar_serializer_current_namespaces]
20
+ Thread.current[:ar_serializer_current_namespaces] || [nil]
30
21
  end
31
22
 
32
23
  def self.with_namespaces(namespaces)
33
24
  namespaces_was = Thread.current[:ar_serializer_current_namespaces]
34
- Thread.current[:ar_serializer_current_namespaces] = namespaces
25
+ Thread.current[:ar_serializer_current_namespaces] = Array(namespaces) | [nil]
35
26
  yield
36
27
  ensure
37
28
  Thread.current[:ar_serializer_current_namespaces] = namespaces_was
38
29
  end
39
30
 
40
- def self.serialize(model, query, context: nil, include_id: false, use: nil)
31
+ def self.serialize(model, query, context: nil, use: nil, permission: true)
32
+ return nil if model.nil?
41
33
  with_namespaces use do
42
34
  attributes = parse_args(query)[:attributes]
43
35
  if model.is_a?(ArSerializer::Serializable)
44
- output = {}
45
- _serialize [[model, output]], attributes, context, include_id
46
- output
36
+ result = _serialize [model], attributes, context, permission: permission
37
+ result[model]
47
38
  else
48
- sets = model.to_a.map do |record|
49
- [record, {}]
50
- end
51
- _serialize sets, attributes, context, include_id
52
- sets.map(&:last)
39
+ models = model.to_a
40
+ result = _serialize models, attributes, context, permission: permission
41
+ models.map { |m| result[m] }.compact
53
42
  end
54
43
  end
55
44
  end
56
45
 
57
- def self._serialize(mixed_value_outputs, attributes, context, include_id, only = nil, except = nil)
58
- mixed_value_outputs.group_by { |v, _o| v.class }.each do |klass, value_outputs|
46
+ def self._serialize(mixed_models, attributes, context, only: nil, except: nil, permission: true)
47
+ output_for_model = {}
48
+ mixed_models.group_by(&:class).each do |klass, models|
59
49
  next unless klass.respond_to? :_serializer_field_info
60
- models = value_outputs.map(&:first)
61
- value_outputs.each { |value, output| output[:id] = value.id } if include_id && klass.method_defined?(:id)
50
+ models.uniq!
62
51
  if attributes.any? { |k, _| k == :* }
63
- all_keys = klass._serializer_field_keys.map(&:to_sym) - [:defaults]
52
+ all_keys = klass._serializer_field_keys.map(&:to_sym)
64
53
  all_keys &= only.map(&:to_sym) if only
65
54
  all_keys -= except.map(&:to_sym) if except
66
55
  attributes = all_keys.map { |k| [k, {}] } + attributes.reject { |k, _| k == :* }
@@ -68,85 +57,147 @@ module ArSerializer::Serializer
68
57
  attributes.each do |name, sub_args|
69
58
  field_name = sub_args[:field_name] || name
70
59
  field = klass._serializer_field_info field_name
71
- raise ArSerializer::InvalidQuery, "No serializer field `#{field_name}`#{" namespaces: #{current_namespaces}" if current_namespaces} for #{klass}" unless field
72
- ActiveRecord::Associations::Preloader.new.preload models, field.includes if field.includes.present?
73
- end
74
-
75
- preloader_params = attributes.flat_map do |name, sub_args|
76
- field_name = sub_args[:field_name] || name
77
- klass._serializer_field_info(field_name).preloaders.map do |p|
78
- [p, sub_args[:params]]
60
+ if field.nil? || field.private?
61
+ message = "No serializer field `#{field_name}`#{" namespaces: #{current_namespaces.compact}" if current_namespaces.any?} for #{klass}"
62
+ raise ArSerializer::InvalidQuery, message
79
63
  end
64
+ ArSerializer.preload_associations models, field.includes if field.includes.present?
80
65
  end
81
- defaults = klass._serializer_field_info(:defaults)
82
- if defaults
83
- preloader_params += defaults.preloaders.map { |p| [p] }
84
- end
85
- preloader_values = preloader_params.compact.uniq.map do |key|
86
- preloader, params = key
66
+
67
+ preload = lambda do |preloader, params|
87
68
  has_keyword = preloader.parameters.any? { |type, _name| %i[key keyreq keyrest].include? type }
88
69
  arity = preloader.arity.abs
89
70
  arguments = [models]
90
71
  if has_keyword
91
72
  arguments << context unless arity == 2
92
- [key, preloader.call(*arguments, **(params || {}))]
73
+ preloader.call(*arguments, **(params || {}))
93
74
  else
94
75
  arguments << context unless arity == 1
95
- [key, preloader.call(*arguments)]
76
+ preloader.call(*arguments)
96
77
  end
97
- end.to_h
78
+ end
98
79
 
80
+ preloader_values = {}
81
+ if permission == true
82
+ permission_field = klass._serializer_field_info :permission
83
+ elsif permission
84
+ permission_field = klass._serializer_field_info permission
85
+ raise ArgumentError, "No permission field #{permission} for #{klass}" unless permission_field
86
+ end
87
+ if permission_field
88
+ preloadeds = permission_field.preloaders.map do |p|
89
+ preloader_values[[p, nil]] ||= preload.call p, nil
90
+ end
91
+ models = models.select do |model|
92
+ model.instance_exec(*preloadeds, context, {}, &permission_field.data_block)
93
+ end
94
+ end
95
+
96
+ defaults = klass._serializer_field_info :defaults
99
97
  if defaults
100
- preloadeds = defaults.preloaders.map { |p| preloader_values[[p]] } || []
101
- value_outputs.each do |value, output|
102
- data = value.instance_exec(*preloadeds, context, {}, &defaults.data_block)
103
- output.update data
98
+ defaults.preloaders.each do |p|
99
+ preloader_values[[p, nil]] ||= preload.call p, nil
100
+ end
101
+ end
102
+
103
+ attributes.each do |name, sub_args|
104
+ field_name = sub_args[:field_name] || name
105
+ klass._serializer_field_info(field_name).preloaders.each do |p|
106
+ params = sub_args[:params]
107
+ preloader_values[[p, params]] ||= preload.call p, params
104
108
  end
105
109
  end
106
110
 
111
+ models.each do |model|
112
+ output_for_model[model] = {}
113
+ end
114
+
107
115
  attributes.each do |name, sub_arg|
108
116
  params = sub_arg[:params]
109
- sub_calls = []
110
117
  column_name = sub_arg[:column_name] || name
111
118
  field_name = sub_arg[:field_name] || name
112
119
  info = klass._serializer_field_info field_name
113
120
  preloadeds = info.preloaders.map { |p| preloader_values[[p, params]] } || []
114
121
  data_block = info.data_block
115
- value_outputs.each do |value, output|
116
- child = value.instance_exec(*preloadeds, context, **(params || {}), &data_block)
117
- if child.is_a?(Array) && child.all? { |el| el.is_a? ArSerializer::Serializable }
118
- output[column_name] = child.map do |record|
119
- data = {}
120
- sub_calls << [record, data]
121
- data
122
- end
123
- elsif child.respond_to? :ar_serializer_build_sub_calls
124
- sub_output, record_elements = child.ar_serializer_build_sub_calls
125
- record_elements.each { |o| sub_calls << o }
126
- output[column_name] = sub_output
122
+ permission_block = info.permission
123
+ fallback = info.fallback
124
+ sub_results = {}
125
+ sub_models = []
126
+ models.each do |model|
127
+ next if permission_block && !model.instance_exec(context, **(params || {}), &permission_block)
128
+ child = model.instance_exec(*preloadeds, context, **(params || {}), &data_block)
129
+ if child.is_a?(ArSerializer::ArrayLikeSerializable) || (child.is_a?(Array) && child.any? { |el| el.is_a? ArSerializer::Serializable })
130
+ sub_results[model] = [:multiple, child]
131
+ sub_models << child.grep(ArSerializer::Serializable)
132
+ elsif child.respond_to?(:ar_custom_serializable_models) && child.respond_to?(:ar_custom_serializable_data)
133
+ sub_results[model] = [:custom, child]
134
+ sub_models << child.ar_custom_serializable_models
127
135
  elsif child.is_a? ArSerializer::Serializable
128
- data = {}
129
- sub_calls << [child, data]
130
- output[column_name] = data
136
+ sub_results[model] = [:single, child]
137
+ sub_models << child
138
+ else
139
+ sub_results[model] = [:data, child]
140
+ end
141
+ end
142
+
143
+ sub_models.flatten!
144
+ sub_models.uniq!
145
+ unless sub_models.empty?
146
+ sub_attributes = sub_arg[:attributes] || {}
147
+ info.validate_attributes sub_attributes
148
+ result = _serialize(
149
+ sub_models,
150
+ sub_attributes,
151
+ context,
152
+ only: info.only,
153
+ except: info.except,
154
+ permission: info.scoped_access
155
+ )
156
+ end
157
+
158
+ models.each do |model|
159
+ data = output_for_model[model]
160
+ type, res = sub_results[model]
161
+ case type
162
+ when :single
163
+ data[column_name] = result[res]
164
+ when :multiple
165
+ arr = data[column_name] = []
166
+ res.each do |r|
167
+ if r.is_a? ArSerializer::Serializable
168
+ arr << result[r] if result.key? r
169
+ else
170
+ arr << r
171
+ end
172
+ end
173
+ when :custom
174
+ data[column_name] = res.ar_custom_serializable_data result || {}
175
+ when :data
176
+ data[column_name] = res
131
177
  else
132
- output[column_name] = child
178
+ data[column_name] = fallback
133
179
  end
134
180
  end
135
- next if sub_calls.empty?
136
- sub_attributes = sub_arg[:attributes] || {}
137
- info.validate_attributes sub_attributes
138
- _serialize sub_calls, sub_attributes, context, include_id, info.only, info.except
181
+ end
182
+
183
+ if defaults
184
+ preloadeds = defaults.preloaders.map { |p| preloader_values[[p]] } || []
185
+ models.each do |model|
186
+ data = model.instance_exec(*preloadeds, context, {}, &defaults.data_block)
187
+ output_for_model[model].update data
188
+ end
139
189
  end
140
190
  end
191
+ output_for_model
141
192
  end
142
193
 
143
- def self.deep_with_indifferent_access params
194
+ def self.deep_underscore_keys params
144
195
  case params
145
196
  when Array
146
- params.map { |v| deep_with_indifferent_access v }
197
+ params.map { |v| deep_underscore_keys v }
147
198
  when Hash
148
- params.transform_keys(&:to_sym).transform_values! do |v|
149
- deep_with_indifferent_access v
199
+ params.transform_keys { |k| k.to_s.underscore.to_sym }.transform_values! do |v|
200
+ deep_underscore_keys v
150
201
  end
151
202
  else
152
203
  params
@@ -171,7 +222,7 @@ module ArSerializer::Serializer
171
222
  elsif !only_attributes && %i[attributes query].include?(sym_key)
172
223
  attributes.concat parse_args(value, only_attributes: true)
173
224
  elsif !only_attributes && sym_key == :params
174
- params = deep_with_indifferent_access value
225
+ params = deep_underscore_keys value
175
226
  else
176
227
  attributes << [sym_key, value == true ? {} : parse_args(value)]
177
228
  end
@@ -1,3 +1,3 @@
1
1
  module ArSerializer
2
- VERSION = '1.1.1'
2
+ VERSION = '1.2.1'
3
3
  end
data/lib/ar_serializer.rb CHANGED
@@ -7,6 +7,16 @@ module ArSerializer
7
7
  def self.serialize(model, query, **option)
8
8
  Serializer.serialize(model, query, **option)
9
9
  end
10
+
11
+ if ActiveRecord::VERSION::MAJOR >= 7
12
+ def self.preload_associations(models, associations)
13
+ ActiveRecord::Associations::Preloader.new(records: models, associations: associations).call
14
+ end
15
+ else
16
+ def self.preload_associations(models, associations)
17
+ ActiveRecord::Associations::Preloader.new.preload models, associations
18
+ end
19
+ end
10
20
  end
11
21
 
12
22
  module ArSerializer::Serializable
@@ -18,34 +28,32 @@ module ArSerializer::Serializable
18
28
  end
19
29
 
20
30
  def _serializer_field_info(name)
21
- namespaces = ArSerializer::Serializer.current_namespaces
22
- if namespaces
23
- Array(namespaces).each do |ns|
24
- field = _serializer_namespace(ns)[name.to_s]
25
- return field if field
26
- end
27
- end
28
- field = _serializer_namespace(nil)[name.to_s]
29
- if field
30
- field
31
- elsif superclass < ArSerializer::Serializable
32
- superclass._serializer_field_info name
31
+ ArSerializer::Serializer.current_namespaces.each do |ns|
32
+ field = _serializer_namespace(ns)[name.to_s]
33
+ return field if field
33
34
  end
35
+ superclass._serializer_field_info name if superclass < ArSerializer::Serializable
34
36
  end
35
37
 
36
- def _serializer_field_keys
37
- namespaces = ArSerializer::Serializer.current_namespaces
38
- keys = []
39
- if namespaces
40
- Array(namespaces).each do |ns|
41
- keys |= _serializer_namespace(ns).keys
38
+ def _serializer_field_keys(public_only = true)
39
+ keys = ArSerializer::Serializer.current_namespaces.map do |ns|
40
+ if public_only
41
+ fields = _serializer_namespace(ns)
42
+ fields.keys.reject { |key| fields[key].private? }
43
+ else
44
+ _serializer_namespace(ns).keys
42
45
  end
43
- end
44
- keys |= _serializer_namespace(nil).keys
45
- keys |= superclass._serializer_field_keys if superclass < ArSerializer::Serializable
46
+ end.inject(:|)
47
+ keys |= superclass._serializer_field_keys(public_only) if superclass < ArSerializer::Serializable
46
48
  keys
47
49
  end
48
50
 
51
+ def _serializer_orderable_field_keys
52
+ _serializer_field_keys.select do |name|
53
+ _serializer_field_info(name).orderable?
54
+ end
55
+ end
56
+
49
57
  def serializer_field(*names, namespace: nil, association: nil, **option, &data_block)
50
58
  namespaces = namespace.is_a?(Array) ? namespace : [namespace]
51
59
  namespaces.each do |ns|
@@ -56,22 +64,18 @@ module ArSerializer::Serializable
56
64
  end
57
65
  end
58
66
 
59
- def _custom_preloaders
60
- @_custom_preloaders ||= {}
61
- end
62
-
63
- def define_preloader(name, &block)
64
- _custom_preloaders[name] = block
67
+ def serializer_permission(**args, &data_block)
68
+ serializer_field(:permission, **args, private: true, &data_block)
65
69
  end
66
70
 
67
71
  def serializer_defaults(**args, &block)
68
- serializer_field :defaults, **args, &block
72
+ serializer_field(:defaults, **args, private: true, &block)
69
73
  end
70
74
  end
71
75
  end
72
76
 
73
77
  ActiveRecord::Base.include ArSerializer::Serializable
74
- ActiveRecord::Relation.include ArSerializer::ArrayLikeCompositeValue
78
+ ActiveRecord::Relation.include ArSerializer::ArrayLikeSerializable
75
79
 
76
80
  require 'ar_serializer/graphql'
77
81
  require 'ar_serializer/type_script'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ar_serializer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - tompng
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-01-27 00:00:00.000000000 Z
11
+ date: 2024-07-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -52,20 +52,6 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: pry
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: sqlite3
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -115,16 +101,19 @@ executables: []
115
101
  extensions: []
116
102
  extra_rdoc_files: []
117
103
  files:
104
+ - ".github/workflows/test.yml"
118
105
  - ".gitignore"
119
106
  - ".travis.yml"
120
107
  - Gemfile
121
- - Gemfile.lock
122
108
  - LICENSE.txt
123
109
  - README.md
124
110
  - Rakefile
125
111
  - ar_serializer.gemspec
126
112
  - bin/console
127
113
  - bin/setup
114
+ - gemfiles/Gemfile-rails-6
115
+ - gemfiles/Gemfile-rails-7-0
116
+ - gemfiles/Gemfile-rails-7-1
128
117
  - lib/ar_serializer.rb
129
118
  - lib/ar_serializer/error.rb
130
119
  - lib/ar_serializer/field.rb
@@ -138,7 +127,7 @@ homepage: https://github.com/tompng/ar_serializer
138
127
  licenses:
139
128
  - MIT
140
129
  metadata: {}
141
- post_install_message:
130
+ post_install_message:
142
131
  rdoc_options: []
143
132
  require_paths:
144
133
  - lib
@@ -153,8 +142,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
142
  - !ruby/object:Gem::Version
154
143
  version: '0'
155
144
  requirements: []
156
- rubygems_version: 3.1.2
157
- signing_key:
145
+ rubygems_version: 3.5.9
146
+ signing_key:
158
147
  specification_version: 4
159
148
  summary: ActiveRecord serializer, avoid N+1
160
149
  test_files: []
data/Gemfile.lock DELETED
@@ -1,59 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- ar_serializer (1.1.1)
5
- activerecord
6
- top_n_loader
7
-
8
- GEM
9
- remote: https://rubygems.org/
10
- specs:
11
- activemodel (6.0.2.1)
12
- activesupport (= 6.0.2.1)
13
- activerecord (6.0.2.1)
14
- activemodel (= 6.0.2.1)
15
- activesupport (= 6.0.2.1)
16
- activesupport (6.0.2.1)
17
- concurrent-ruby (~> 1.0, >= 1.0.2)
18
- i18n (>= 0.7, < 2)
19
- minitest (~> 5.1)
20
- tzinfo (~> 1.1)
21
- zeitwerk (~> 2.2)
22
- coderay (1.1.2)
23
- concurrent-ruby (1.1.5)
24
- docile (1.3.1)
25
- i18n (1.7.0)
26
- concurrent-ruby (~> 1.0)
27
- json (2.1.0)
28
- method_source (0.9.2)
29
- minitest (5.13.0)
30
- pry (0.12.2)
31
- coderay (~> 1.1.0)
32
- method_source (~> 0.9.0)
33
- rake (12.3.2)
34
- simplecov (0.16.1)
35
- docile (~> 1.1)
36
- json (>= 1.8, < 3)
37
- simplecov-html (~> 0.10.0)
38
- simplecov-html (0.10.2)
39
- sqlite3 (1.4.0)
40
- thread_safe (0.3.6)
41
- top_n_loader (1.0.1)
42
- activerecord
43
- tzinfo (1.2.6)
44
- thread_safe (~> 0.1)
45
- zeitwerk (2.2.2)
46
-
47
- PLATFORMS
48
- ruby
49
-
50
- DEPENDENCIES
51
- ar_serializer!
52
- minitest
53
- pry
54
- rake
55
- simplecov
56
- sqlite3
57
-
58
- BUNDLED WITH
59
- 2.1.2