sequel-packer 0.1.0 → 0.2.0
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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +118 -10
- data/lib/sequel/packer/eager_hash.rb +173 -0
- data/lib/sequel/packer/version.rb +1 -1
- data/lib/sequel/packer.rb +36 -3
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 00bbe557f19cf23a18afe4e5d23173a3fb699977680fae2efa99670240f81847
|
4
|
+
data.tar.gz: 324688d708be91fa8e89eb99a65701f35dc8f3f5a799719588fa84adff877c95
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a626e68bd1053d818bd830a198bfe92b13bcbc0286a2ce985a996049ba7858a229aeabb00181e144fadfa8dbcc63ca90adef89619ddc32d86076ee53b718b424
|
7
|
+
data.tar.gz: 0344f7b561e830c58dd01c68da86728f57594a8073b920e4de70401e038e7a0cf92907f37726d84d9243268c12d3b256195df523d224224b69f53ee16f26136a
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,22 @@
|
|
1
1
|
# Sequel::Packer
|
2
2
|
|
3
|
-
|
3
|
+
`Sequel::Packer` is a Ruby JSON serialization library to be used with the Sequel
|
4
|
+
ORM offering the following features:
|
5
|
+
|
6
|
+
* *Declarative:* Define the shape of your serialized data with a simple,
|
7
|
+
straightforward DSL.
|
8
|
+
* *Flexible:* Certain contexts require different data. Packers provide an easy
|
9
|
+
way to opt-in to serializing certain data only when you need it. The library
|
10
|
+
also provides convenient escape hatches when you need to do something not
|
11
|
+
explicitly supported by the API.
|
12
|
+
* *Reusable:* The Packer library naturally composes well with itself. Nested
|
13
|
+
data can be serialized in the same way no matter what endpoint it's fetched
|
14
|
+
from.
|
15
|
+
* *Efficient:* When not using Sequel's [`TacticalEagerLoading`]
|
16
|
+
(https://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/TacticalEagerLoading.html)
|
17
|
+
plugin, the Packer library will intelligently determine which associations
|
18
|
+
and nested associations it needs to eager load in order to avoid any N+1 query
|
19
|
+
issues.
|
4
20
|
|
5
21
|
## Installation
|
6
22
|
|
@@ -156,8 +172,8 @@ class PostPacker < Sequel::Packer
|
|
156
172
|
...
|
157
173
|
|
158
174
|
# Eww!
|
159
|
-
-
|
160
|
-
+
|
175
|
+
- field :comments, CommentPacker
|
176
|
+
+ field :comments, CommentWithAuthorPacker
|
161
177
|
end
|
162
178
|
```
|
163
179
|
|
@@ -173,10 +189,9 @@ class CommentPacker < Sequel::Packer
|
|
173
189
|
field :id
|
174
190
|
field :content
|
175
191
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
end
|
192
|
+
+ trait :author do
|
193
|
+
+ field :author, UserPacker
|
194
|
+
+ end
|
180
195
|
end
|
181
196
|
```
|
182
197
|
|
@@ -214,8 +229,8 @@ class PostPacker < Sequel::Packer
|
|
214
229
|
field :title
|
215
230
|
field :content
|
216
231
|
|
217
|
-
-
|
218
|
-
+
|
232
|
+
- field :comments, CommentPacker
|
233
|
+
+ field :comments, CommentPacker, :author
|
219
234
|
end
|
220
235
|
```
|
221
236
|
|
@@ -290,7 +305,7 @@ class MyPacker < Sequel::Packer
|
|
290
305
|
end
|
291
306
|
```
|
292
307
|
|
293
|
-
### `self.field(association, packer_class, *traits)
|
308
|
+
### `self.field(association, packer_class, *traits)`
|
294
309
|
|
295
310
|
A Sequel association (defined in the model file using `one_to_many`, or
|
296
311
|
`many_to_one`, etc.), can be packed using another Packer class, possibly with
|
@@ -318,6 +333,99 @@ field do |model, hash|
|
|
318
333
|
end
|
319
334
|
```
|
320
335
|
|
336
|
+
### `self.trait(trait_name, &block)`
|
337
|
+
|
338
|
+
Define optional serialization behavior by defining additional fields within a
|
339
|
+
`trait` block. Traits can be opted into when initializing a packer by passing
|
340
|
+
the name of the trait as an argument:
|
341
|
+
|
342
|
+
```ruby
|
343
|
+
class MyPacker < Sequel::Packer
|
344
|
+
model MyObj
|
345
|
+
trait :my_trait do
|
346
|
+
field :my_optional_field
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
# packed objects don't have my_optional_field
|
351
|
+
MyPacker.new.pack(dataset)
|
352
|
+
# packed objects do have my_optional_field
|
353
|
+
MyPacker.new(:my_trait).pack(dataset)
|
354
|
+
```
|
355
|
+
|
356
|
+
Traits can also be used when packing associations by passing the name of the
|
357
|
+
traits after the packer class:
|
358
|
+
|
359
|
+
```ruby
|
360
|
+
class MyOtherPacker < Sequel::Packer
|
361
|
+
model MyObj
|
362
|
+
field :my_packers, MyPacker, :my_trait
|
363
|
+
end
|
364
|
+
```
|
365
|
+
|
366
|
+
### `self.eager(*associations)`
|
367
|
+
|
368
|
+
When packing an association, a Packer will automatically ensure that association
|
369
|
+
is eager loaded, but there may be cases when an association will be accessed
|
370
|
+
that the Packer doesn't know about. In these cases you can tell the Packer to
|
371
|
+
eager load that data by calling `eager(*associations)`, passing in arguments
|
372
|
+
the exact same way you would to [`Sequel::Dataset.eager`](
|
373
|
+
https://sequel.jeremyevans.net/rdoc/classes/Sequel/Model/Associations/DatasetMethods.html#method-i-eager).
|
374
|
+
|
375
|
+
One case where this may be useful is for a "count" field, that just lists the
|
376
|
+
number of associated objects, but doesn't actually return them:
|
377
|
+
|
378
|
+
```ruby
|
379
|
+
class UserPacker < Sequel::Packer
|
380
|
+
model User
|
381
|
+
|
382
|
+
field :id
|
383
|
+
|
384
|
+
eager(:posts)
|
385
|
+
field(:num_posts) do |user|
|
386
|
+
user.posts.counts
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
UserPacker.new.pack(User.dataset)
|
391
|
+
=> [
|
392
|
+
{id: 123, num_posts: 7},
|
393
|
+
{id: 456, num_posts: 3},
|
394
|
+
...
|
395
|
+
]
|
396
|
+
```
|
397
|
+
|
398
|
+
This helps prevent N+1 query problems when not using Sequel's
|
399
|
+
[`TacticalEagerLoading`]
|
400
|
+
(https://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/TacticalEagerLoading.html)
|
401
|
+
plugin.
|
402
|
+
|
403
|
+
Another use of `eager`, even when using `TacticalEagerLoading`, is to modify or
|
404
|
+
limit which records gets fetched from the database by using an eager proc. For
|
405
|
+
example, to only pack recent posts, published in the past month, we might do:
|
406
|
+
|
407
|
+
```
|
408
|
+
class UserPacker < Sequel::Packer
|
409
|
+
model User
|
410
|
+
|
411
|
+
field :id
|
412
|
+
|
413
|
+
trait :recent_posts do
|
414
|
+
eager posts: (proc {|ds| ds.where {created_at > Time.now - 1.month}})
|
415
|
+
field :posts, PostIdPacker
|
416
|
+
end
|
417
|
+
end
|
418
|
+
```
|
419
|
+
|
420
|
+
Keep in mind that this limits the association that gets used by ALL fields, so
|
421
|
+
if another field actually needs access to all the users posts, it might not make
|
422
|
+
sense to use `eager`.
|
423
|
+
|
424
|
+
Also, it's important to note that if `eager` is called multiple times, with
|
425
|
+
multiple procs, each proc will get applied to the dataset, likely resulting in
|
426
|
+
overly restrictive filtering.
|
427
|
+
|
428
|
+
|
321
429
|
### `initialize(*traits)`
|
322
430
|
|
323
431
|
When creating an instance of a Packer class, pass in any traits desired to
|
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
|
3
|
+
module Sequel
|
4
|
+
class Packer
|
5
|
+
module EagerHash
|
6
|
+
# An eager hash cannot have the form: {
|
7
|
+
# :assoc1 => :nested_assoc,
|
8
|
+
# <proc> => :assoc2,
|
9
|
+
# }
|
10
|
+
class MixedProcHashError < StandardError; end
|
11
|
+
# An eager hash cannot have multiple keys that are Procs.
|
12
|
+
class MultipleProcKeysError < StandardError; end
|
13
|
+
# If an eager hash contains a Proc as the key of a hash, that value at
|
14
|
+
# that key cannot be another hash with a Proc as a key.
|
15
|
+
class NestedEagerProcsError < StandardError; end
|
16
|
+
|
17
|
+
# Sequel's eager function can accept arguments in a number of different
|
18
|
+
# formats:
|
19
|
+
# .eager(:assoc)
|
20
|
+
# .eager([:assoc1, :assoc2])
|
21
|
+
# .eager(assoc: :nested_assoc)
|
22
|
+
# .eager(
|
23
|
+
# :assoc1,
|
24
|
+
# assoc2: {(proc {|ds| ...}) => [:nested_assoc1, :nested_assoc2]},
|
25
|
+
# )
|
26
|
+
#
|
27
|
+
# This method normalizes these arguments such that:
|
28
|
+
# - A Hash is returned
|
29
|
+
# - The keys of that hash are the names of associations
|
30
|
+
# - The values of that hash are either:
|
31
|
+
# - nil, representing no nested associations
|
32
|
+
# - a nested normalized hash, meeting these definitions
|
33
|
+
# - a "Proc hash", which is a hash with a single key, which is a proc,
|
34
|
+
# and whose value is either nil or a nested normalized hash.
|
35
|
+
#
|
36
|
+
# Notice that a normalized has the property that every value in the hash
|
37
|
+
# is either nil, or itself a normalized hash.
|
38
|
+
#
|
39
|
+
# Note that this method cannot return a "Proc hash" as the top level
|
40
|
+
# normalized hash; Proc hashes can only be nested under other
|
41
|
+
# associations.
|
42
|
+
def self.normalize_eager_args(*associations)
|
43
|
+
# Implementation largely borrowed from Sequel's
|
44
|
+
# eager_options_for_associations:
|
45
|
+
#
|
46
|
+
# https://github.com/jeremyevans/sequel/blob/5.32.0/lib/sequel/model/associations.rb#L3228-L3245
|
47
|
+
normalized_hash = {}
|
48
|
+
|
49
|
+
associations.flatten.each do |association|
|
50
|
+
case association
|
51
|
+
when Symbol
|
52
|
+
normalized_hash[association] = nil
|
53
|
+
when Hash
|
54
|
+
num_proc_keys = 0
|
55
|
+
num_symbol_keys = 0
|
56
|
+
|
57
|
+
association.each do |key, val|
|
58
|
+
case key
|
59
|
+
when Symbol
|
60
|
+
num_symbol_keys += 1
|
61
|
+
when Proc
|
62
|
+
num_proc_keys += 1
|
63
|
+
|
64
|
+
if val.is_a?(Proc) || is_proc_hash?(val)
|
65
|
+
raise(
|
66
|
+
NestedEagerProcsError,
|
67
|
+
"eager hash has nested Procs: #{associations.inspect}",
|
68
|
+
)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
if val.nil?
|
73
|
+
# Already normalized
|
74
|
+
normalized_hash[key] = nil
|
75
|
+
elsif val.is_a?(Proc)
|
76
|
+
# Convert Proc value to a Proc hash.
|
77
|
+
normalized_hash[key] = {val => nil}
|
78
|
+
else
|
79
|
+
# Otherwise recurse.
|
80
|
+
normalized_hash[key] = normalize_eager_args(val)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
if num_proc_keys > 1
|
85
|
+
raise(
|
86
|
+
MultipleProcKeysError,
|
87
|
+
"eager hash has multiple Proc keys: #{associations.inspect}",
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
if num_proc_keys > 0 && num_symbol_keys > 0
|
92
|
+
raise(
|
93
|
+
MixedProcHashError,
|
94
|
+
'eager hash has both symbol keys and Proc keys: ' +
|
95
|
+
associations.inspect,
|
96
|
+
)
|
97
|
+
end
|
98
|
+
else
|
99
|
+
raise(
|
100
|
+
Sequel::Error,
|
101
|
+
'Associations must be in the form of a symbol or hash',
|
102
|
+
)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
normalized_hash
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.is_proc_hash?(hash)
|
110
|
+
return false if !hash.is_a?(Hash)
|
111
|
+
return false if hash.size != 1
|
112
|
+
hash.keys[0].is_a?(Proc)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Merges two eager hashes together, without modifying either one.
|
116
|
+
def self.merge(hash1, hash2)
|
117
|
+
return deep_dup(hash2) if !hash1
|
118
|
+
merge!(deep_dup(hash1), hash2)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Merges two eager hashes together, modifying the first hash, while
|
122
|
+
# leaving the second unmodified. Since the first argument may be nil,
|
123
|
+
# callers should still use the return value, rather than the first
|
124
|
+
# argument.
|
125
|
+
def self.merge!(hash1, hash2)
|
126
|
+
return hash1 if !hash2
|
127
|
+
return deep_dup(hash2) if !hash1
|
128
|
+
|
129
|
+
hash2.each do |key, val2|
|
130
|
+
if !hash1.key?(key)
|
131
|
+
hash1[key] = deep_dup(val2)
|
132
|
+
next
|
133
|
+
end
|
134
|
+
|
135
|
+
val1 = hash1[key]
|
136
|
+
h1_is_proc_hash = is_proc_hash?(val1)
|
137
|
+
h2_is_proc_hash = is_proc_hash?(val2)
|
138
|
+
|
139
|
+
case [h1_is_proc_hash, h2_is_proc_hash]
|
140
|
+
when [false, false]
|
141
|
+
hash1[key] = merge!(val1, val2)
|
142
|
+
when [true, false]
|
143
|
+
# We want to actually merge the hash the proc points to.
|
144
|
+
eager_proc, nested_hash = val1.entries[0]
|
145
|
+
hash1[key] = {eager_proc => merge!(nested_hash, val2)}
|
146
|
+
when [false, true]
|
147
|
+
# Same as above, but flipped. Notice the order of the arguments to
|
148
|
+
# merge! to ensure hash2 is not modified!
|
149
|
+
eager_proc, nested_hash = val2.entries[0]
|
150
|
+
hash1[key] = {eager_proc => merge!(val1, nested_hash)}
|
151
|
+
when [true, true]
|
152
|
+
# Create a new proc that applies both procs, then merge their
|
153
|
+
# respective hashes.
|
154
|
+
proc1, nested_hash1 = val1.entries[0]
|
155
|
+
proc2, nested_hash2 = val2.entries[0]
|
156
|
+
|
157
|
+
new_proc = (proc {|ds| proc2.call(proc1.call(ds))})
|
158
|
+
|
159
|
+
hash1[key] = {new_proc => merge!(nested_hash1, nested_hash2)}
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
hash1
|
164
|
+
end
|
165
|
+
|
166
|
+
# Creates a deep copy of an eager hash.
|
167
|
+
def self.deep_dup(hash)
|
168
|
+
return nil if !hash
|
169
|
+
hash.transform_values {|val| deep_dup(val)}
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
data/lib/sequel/packer.rb
CHANGED
@@ -8,6 +8,7 @@ module Sequel
|
|
8
8
|
def self.inherited(subclass)
|
9
9
|
subclass.instance_variable_set(:@fields, [])
|
10
10
|
subclass.instance_variable_set(:@traits, {})
|
11
|
+
subclass.instance_variable_set(:@eager_hash, nil)
|
11
12
|
end
|
12
13
|
|
13
14
|
def self.model(klass)
|
@@ -194,9 +195,17 @@ module Sequel
|
|
194
195
|
@traits[name] = block
|
195
196
|
end
|
196
197
|
|
198
|
+
def self.eager(*associations)
|
199
|
+
@eager_hash = EagerHash.merge!(
|
200
|
+
@eager_hash,
|
201
|
+
EagerHash.normalize_eager_args(*associations),
|
202
|
+
)
|
203
|
+
end
|
204
|
+
|
197
205
|
def initialize(*traits)
|
198
206
|
@packers = nil
|
199
207
|
@fields = traits.any? ? packer_fields.dup : packer_fields
|
208
|
+
@eager_hash = traits.any? ? EagerHash.deep_dup(packer_eager_hash) : packer_eager_hash
|
200
209
|
|
201
210
|
traits.each do |trait|
|
202
211
|
trait_block = trait_blocks[trait]
|
@@ -209,14 +218,23 @@ module Sequel
|
|
209
218
|
|
210
219
|
@fields.each do |field_options|
|
211
220
|
if field_options[:type] == ASSOCIATION_FIELD
|
212
|
-
|
213
|
-
|
221
|
+
association = field_options[:name]
|
222
|
+
association_packer =
|
214
223
|
field_options[:packer].new(*field_options[:packer_traits])
|
224
|
+
|
225
|
+
@packers ||= {}
|
226
|
+
@packers[association] = association_packer
|
227
|
+
|
228
|
+
@eager_hash = EagerHash.merge!(
|
229
|
+
@eager_hash,
|
230
|
+
{association => association_packer.send(:eager_hash)},
|
231
|
+
)
|
215
232
|
end
|
216
233
|
end
|
217
234
|
end
|
218
235
|
|
219
236
|
def pack(dataset)
|
237
|
+
dataset = dataset.eager(@eager_hash) if @eager_hash
|
220
238
|
models = dataset.all
|
221
239
|
pack_models(models)
|
222
240
|
end
|
@@ -272,15 +290,30 @@ module Sequel
|
|
272
290
|
}
|
273
291
|
end
|
274
292
|
|
293
|
+
def eager_hash
|
294
|
+
@eager_hash
|
295
|
+
end
|
296
|
+
|
297
|
+
def eager(*associations)
|
298
|
+
@eager_hash = EagerHash.merge!(
|
299
|
+
@eager_hash,
|
300
|
+
EagerHash.normalize_eager_args(*associations),
|
301
|
+
)
|
302
|
+
end
|
303
|
+
|
275
304
|
def packer_fields
|
276
305
|
self.class.instance_variable_get(:@fields)
|
277
306
|
end
|
278
307
|
|
308
|
+
def packer_eager_hash
|
309
|
+
self.class.instance_variable_get(:@eager_hash)
|
310
|
+
end
|
311
|
+
|
279
312
|
def trait_blocks
|
280
313
|
self.class.instance_variable_get(:@traits)
|
281
314
|
end
|
282
315
|
end
|
283
316
|
end
|
284
317
|
|
318
|
+
require 'sequel/packer/eager_hash'
|
285
319
|
require "sequel/packer/version"
|
286
|
-
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sequel-packer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paul Julius Martinez
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-05-
|
11
|
+
date: 2020-05-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sequel
|
@@ -127,6 +127,7 @@ files:
|
|
127
127
|
- bin/console
|
128
128
|
- bin/setup
|
129
129
|
- lib/sequel/packer.rb
|
130
|
+
- lib/sequel/packer/eager_hash.rb
|
130
131
|
- lib/sequel/packer/version.rb
|
131
132
|
- sequel-packer.gemspec
|
132
133
|
homepage: https://github.com/PaulJuliusMartinez/sequel-packer
|