ar_serializer 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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