ar_serializer 1.1.1 → 1.2.0

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: 87a5c3a6d707be1abec6c92335dbb5888b120a53b6e429eb1849b2aca42a1f1d
4
+ data.tar.gz: bdd382384bd5452fc909b26cb25fbefd252117fb552b0fc42bc4eddad016a13d
5
5
  SHA512:
6
- metadata.gz: 987a6cad67b5db442cd5cbb0167fb754cf3cb841d0ea050bf148391ed6f4c61037e17e1448d50d89ebd6549541b4aa51566a233b42c578eded817f42bc77c156
7
- data.tar.gz: f6c971e38b6e96673ddddcac94ee30e37d12eeb05ef68a8c878fff3490ec273e91cb1146a8ba6a185cac5fb313c6a429d07dd89986475e73454db6f6f187ed7e
6
+ metadata.gz: 86a9f5895dd2c9f5a9d3a1e51781d5286c4cfbb02687d2d6a9ec502ed85c0971a0343a08218ee0be3233a8f93e0be993e1ce7ab68b5fe75548a76e068247b3a9
7
+ data.tar.gz: 829e8871640abf2a4863d77df8ce703d9e85b38cf46496bd8573f6913572c5ebe18fc1a6c799e1b68124835abc1398841e810413b30cbddb5fe174b0301c85b1
@@ -0,0 +1,22 @@
1
+ name: Test
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ strategy:
6
+ fail-fast: false
7
+ matrix:
8
+ ruby: [ '2.7', '3.0', '3.1' ]
9
+ gemfiles:
10
+ - gemfiles/Gemfile-rails-6
11
+ - gemfiles/Gemfile-rails-7
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v2
15
+ - uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: ${{ matrix.ruby }}
18
+ - run: |
19
+ sudo apt-get update
20
+ sudo apt-get install -y libsqlite3-dev
21
+ - run: bundle install --gemfile ${{ matrix.gemfiles }} --jobs 4 --retry 3
22
+ - run: bundle exec --gemfile ${{ matrix.gemfiles }} rake
data/Gemfile.lock CHANGED
@@ -1,48 +1,45 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ar_serializer (1.1.1)
4
+ ar_serializer (1.2.0)
5
5
  activerecord
6
6
  top_n_loader
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
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)
11
+ activemodel (7.0.2.3)
12
+ activesupport (= 7.0.2.3)
13
+ activerecord (7.0.2.3)
14
+ activemodel (= 7.0.2.3)
15
+ activesupport (= 7.0.2.3)
16
+ activesupport (7.0.2.3)
17
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)
18
+ i18n (>= 1.6, < 2)
19
+ minitest (>= 5.1)
20
+ tzinfo (~> 2.0)
21
+ coderay (1.1.3)
22
+ concurrent-ruby (1.1.10)
23
+ docile (1.4.0)
24
+ i18n (1.10.0)
26
25
  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)
26
+ method_source (1.0.0)
27
+ minitest (5.15.0)
28
+ pry (0.14.1)
29
+ coderay (~> 1.1)
30
+ method_source (~> 1.0)
31
+ rake (13.0.6)
32
+ simplecov (0.21.2)
35
33
  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)
34
+ simplecov-html (~> 0.11)
35
+ simplecov_json_formatter (~> 0.1)
36
+ simplecov-html (0.12.3)
37
+ simplecov_json_formatter (0.1.4)
38
+ sqlite3 (1.4.2)
39
+ top_n_loader (1.0.2)
42
40
  activerecord
43
- tzinfo (1.2.6)
44
- thread_safe (~> 0.1)
45
- zeitwerk (2.2.2)
41
+ tzinfo (2.0.4)
42
+ concurrent-ruby (~> 1.0)
46
43
 
47
44
  PLATFORMS
48
45
  ruby
@@ -56,4 +53,4 @@ DEPENDENCIES
56
53
  sqlite3
57
54
 
58
55
  BUNDLED WITH
59
- 2.1.2
56
+ 2.3.3
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
@@ -0,0 +1,8 @@
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 "activerecord", "~> 6.0.0"
@@ -0,0 +1,8 @@
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 "activerecord", "~> 7.0.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
@@ -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.0'
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.0
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: 2022-04-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -115,6 +115,7 @@ executables: []
115
115
  extensions: []
116
116
  extra_rdoc_files: []
117
117
  files:
118
+ - ".github/workflows/test.yml"
118
119
  - ".gitignore"
119
120
  - ".travis.yml"
120
121
  - Gemfile
@@ -125,6 +126,8 @@ files:
125
126
  - ar_serializer.gemspec
126
127
  - bin/console
127
128
  - bin/setup
129
+ - gemfiles/Gemfile-rails-6
130
+ - gemfiles/Gemfile-rails-7
128
131
  - lib/ar_serializer.rb
129
132
  - lib/ar_serializer/error.rb
130
133
  - lib/ar_serializer/field.rb
@@ -138,7 +141,7 @@ homepage: https://github.com/tompng/ar_serializer
138
141
  licenses:
139
142
  - MIT
140
143
  metadata: {}
141
- post_install_message:
144
+ post_install_message:
142
145
  rdoc_options: []
143
146
  require_paths:
144
147
  - lib
@@ -153,8 +156,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
156
  - !ruby/object:Gem::Version
154
157
  version: '0'
155
158
  requirements: []
156
- rubygems_version: 3.1.2
157
- signing_key:
159
+ rubygems_version: 3.3.3
160
+ signing_key:
158
161
  specification_version: 4
159
162
  summary: ActiveRecord serializer, avoid N+1
160
163
  test_files: []