sequel-packer 0.0.2 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 248126ba630e1d763f32e911c992b296cd32b5201503a67296bbb18fa4000679
4
- data.tar.gz: abecaa06c7e4d64eef3e4b91be2311cd2921e966062fd03fc5a5d590604c6b92
3
+ metadata.gz: 1420dc6158b5ec7447d40e2818513a0b73c948fe1f2c9dced0732f1ad10e97db
4
+ data.tar.gz: e18293bafbe14d0af6c81ddde1e64e026498c61492bc26338bbc9fdbbd08507b
5
5
  SHA512:
6
- metadata.gz: 0231fbedf7d8d7ccd77093e00617bf15688ed57e52d169876780854023e3c6c758dd34d002d89952795291a8c1f5c7ef398f7f24543cf686be85e496059a5f58
7
- data.tar.gz: da6163737a56d4a96205ff0c26ce9ece0b90b2763c1fe271b899d3b55261bc67cf36ea35d4e75cdac540c81fd587618fbb0272691bbd86fcca0375e712d31809
6
+ metadata.gz: 43da46c32b0f2b068dc7c0c29022ea6ffebd6bf7becaae55185db95626779ed968c82f06d8edb424b6c33430edd58c53e44785ef0b1b9bdb240429b24b70aaa8
7
+ data.tar.gz: 40d48122b9ce084f0c6b585b805481f2ef057ce2158a505782e2d2b0d2f58db4d1c4fd32a22de4f7178cbe82fdf6013694020558e305a1bd4845086648675731
data/CHANGELOG.md CHANGED
@@ -1,9 +1,13 @@
1
+ ### 0.1.0 (2020-05-11)
2
+
3
+ * Add traits.
4
+
1
5
  ### 0.0.2 (2020-05-11)
2
6
 
3
- * Added support for `Sequel::Packer.field(key, &block)` and
7
+ * Add support for `Sequel::Packer.field(key, &block)` and
4
8
  `Sequel::Packer.field(&block)`
5
- * Added validation to `Sequel::Packer::field` to detect incorrect usage.
6
- * Added support for nested packing of associations using
9
+ * Add validation to `Sequel::Packer::field` to detect incorrect usage.
10
+ * Add support for nested packing of associations using
7
11
  `Sequel::Packer.field(association, packer_class)`
8
12
  * Update README with usage instructions and basic API reference.
9
13
 
data/README.md CHANGED
@@ -49,6 +49,8 @@ class Comment < Sequel::Model(:comments)
49
49
  end
50
50
  ```
51
51
 
52
+ ### Basic Fields
53
+
52
54
  Suppose an endpoint wants to fetch all the ten most recent comments by a user.
53
55
  After validating the user id, we end up with the Sequel dataset represting the
54
56
  data we want to return:
@@ -77,17 +79,54 @@ This can then be used as follows:
77
79
  ```ruby
78
80
  CommentPacker.new.pack(recent_comments)
79
81
  => [
80
- {id: 536, "Great post, man!"},
81
- {id: 436, "lol"},
82
- {id: 413, "What a story..."},
82
+ {id: 536, content: "Great post, man!"},
83
+ {id: 436, content: "lol"},
84
+ {id: 413, content: "What a story..."},
83
85
  ]
84
86
  ```
85
87
 
86
- In another context, suppose that we want to fetch the comments on a post along
87
- with the authors of those comments.
88
+ ### Packing associations by nesting Packers
88
89
 
89
- In this case we could define two separate Packers, one for Users, and another
90
- for Comments:
90
+ Now, suppose that we want to fetch a post and all of its comments. We can do
91
+ this by defining another packer for Post that uses the CommentPacker:
92
+
93
+ ```ruby
94
+ class PostPacker < Sequel::Packer
95
+ model Post
96
+
97
+ field :id
98
+ field :title
99
+ field :content
100
+ field :comments, CommentPacker
101
+ end
102
+ ```
103
+
104
+ Since `post.comments` is an array of Sequel::Models and not a primitive value,
105
+ we must tell the Packer how to serialize them using another packer. This is
106
+ what the second argument in `field :comments, CommentPacker` is doing.
107
+
108
+ We can then use this as follows:
109
+
110
+ ```ruby
111
+ PostPacker.new.pack(Post.order(:id.desc).limit(1))
112
+ => [
113
+ {
114
+ id: 682,
115
+ title: "Announcing sequel-packer",
116
+ content: "I've written a new gem...",
117
+ comments: [
118
+ {id: 536, content: "Great post, man!"},
119
+ {id: 541, content: "Incredible, this solves my EXACT problem!"},
120
+ ...
121
+ ],
122
+ }
123
+ ]
124
+ ```
125
+
126
+ ### Traits
127
+
128
+ But suppose we want to be able to show who authored each comment on the post. We
129
+ first have to define a packer for users:
91
130
 
92
131
  ```ruby
93
132
  class UserPacker < Sequel::Packer
@@ -96,37 +135,94 @@ class UserPacker < Sequel::Packer
96
135
  field :id
97
136
  field :name
98
137
  end
138
+ ```
99
139
 
140
+ We could now define a new packer, `CommentWithAuthorPacker`, and use that in the
141
+ PostPacker instead, but then we'd have to redeclare all the other fields we want
142
+ on a packed Comment:
143
+
144
+ ```ruby
100
145
  class CommentWithAuthorPacker < Sequel::Packer
101
146
  model Comment
102
147
 
148
+ field :author, UserPacker
149
+
150
+ # Also defined in CommentPacker!
103
151
  field :id
104
152
  field :content
105
- field :author, UserPacker
153
+ end
154
+
155
+ class PostPacker < Sequel::Packer
156
+ ...
157
+
158
+ # Eww!
159
+ - field :comments, CommentPacker
160
+ + field :comments, CommentWithAuthorPacker
106
161
  end
107
162
  ```
108
163
 
109
- When we use this, we get:
164
+ Declaring these fields in two places could cause them to get out of sync
165
+ as more fields are added. Instead, we will use a _trait_. A _trait_ is a
166
+ way to define a set of fields that we only want to pack sometimes. Instead
167
+ of defining a totally new packer, we can extend the CommentPacker as follows:
110
168
 
111
169
  ```ruby
112
- comments_on_post = Comment.where(post_id: post.id)
113
- CommentWithAuthorPacker.new.pack(comments_on_post)
170
+ class CommentPacker < Sequel::Packer
171
+ model Comment
172
+
173
+ field :id
174
+ field :content
175
+
176
+ # Added!
177
+ trait :author do
178
+ field :author, UserPacker
179
+ end
180
+ end
181
+ ```
182
+
183
+ To use a trait, simply pass it in when creating the packer instance:
184
+
185
+ ```ruby
186
+ # Without the trait
187
+ CommentPacker.new.pack(Comment.dataset)
188
+ => [
189
+ {id: 536, content: "Great post, man!"},
190
+ ...
191
+ ]
192
+
193
+ # With the trait
194
+ CommentPacker.new(:author).pack(Comment.dataset)
114
195
  => [
115
196
  {
116
- id: 752,
117
- content: "Great gem",
118
- author: {id: 382, name: "Jeremy Evans"},
119
- },
120
- {
121
- id: 162,
122
- content: "Never seen anything like it",
123
- author: {id: 382, name: "Donald Knuth"},
197
+ id: 536,
198
+ content: "Great post, man!",
199
+ author: {id: 1, name: "Paul Martinez"},
124
200
  },
125
201
  ...
126
202
  ]
127
203
  ```
128
204
 
129
- See the API Reference below for the exact API.
205
+ To use a trait when packing an association in another packer, simply include
206
+ the name of the trait as additional argument to `field`. Thus, to modify our
207
+ PostPacker to pack comments with their authors we make the following change:
208
+
209
+ ```ruby
210
+ class PostPacker < Sequel::Packer
211
+ model Post
212
+
213
+ field :id
214
+ field :title
215
+ field :content
216
+
217
+ - field :comments, CommentPacker
218
+ + field :comments, CommentPacker, :author
219
+ end
220
+ ```
221
+
222
+ While the basic Packer DSL is convenient, traits are the things that make
223
+ Packers so powerful. Each packer should define a small set of fields that every
224
+ endpoint needs, but then traits can be used to pack additional data only when
225
+ it's needed.
130
226
 
131
227
  ## API Reference
132
228
 
@@ -194,15 +290,15 @@ class MyPacker < Sequel::Packer
194
290
  end
195
291
  ```
196
292
 
197
- ### `self.field(association, packer_class)
293
+ ### `self.field(association, packer_class, *traits)
198
294
 
199
295
  A Sequel association (defined in the model file using `one_to_many`, or
200
- `many_to_one`, etc.), can be packed using another Packer class. A similar
201
- output could be generated by doing:
296
+ `many_to_one`, etc.), can be packed using another Packer class, possibly with
297
+ multiple traits specified. A similar output could be generated by doing:
202
298
 
203
299
  ```ruby
204
300
  field :association do |model|
205
- packer_class.new.pack(model.association_dataset)
301
+ packer_class.new(*traits).pack(model.association_dataset)
206
302
  end
207
303
  ```
208
304
 
@@ -222,6 +318,11 @@ field do |model, hash|
222
318
  end
223
319
  ```
224
320
 
321
+ ### `initialize(*traits)`
322
+
323
+ When creating an instance of a Packer class, pass in any traits desired to
324
+ specify what additional data should be packed, if any.
325
+
225
326
  ### `pack(dataset)`
226
327
 
227
328
  After creating a new instance of a Packer class, call `packer.pack(dataset)` to
@@ -1,5 +1,5 @@
1
1
  module Sequel
2
2
  class Packer
3
- VERSION = "0.0.2"
3
+ VERSION = "0.1.0"
4
4
  end
5
5
  end
data/lib/sequel/packer.rb CHANGED
@@ -7,6 +7,7 @@ module Sequel
7
7
 
8
8
  def self.inherited(subclass)
9
9
  subclass.instance_variable_set(:@fields, [])
10
+ subclass.instance_variable_set(:@traits, {})
10
11
  end
11
12
 
12
13
  def self.model(klass)
@@ -31,7 +32,45 @@ module Sequel
31
32
  # field(&block)
32
33
  ARBITRARY_MODIFICATION_FIELD = :arbitrary_modification_field
33
34
 
34
- def self.field(field_name=nil, packer_class=nil, &block)
35
+ def self.field(field_name=nil, packer_class=nil, *traits, &block)
36
+ validate_field_args(field_name, packer_class, *traits, &block)
37
+ field_type = determine_field_type(field_name, packer_class, &block)
38
+
39
+ @fields << {
40
+ type: field_type,
41
+ name: field_name,
42
+ packer: packer_class,
43
+ packer_traits: traits,
44
+ block: block,
45
+ }
46
+ end
47
+
48
+ private_class_method def self.determine_field_type(
49
+ field_name=nil,
50
+ packer_class=nil,
51
+ &block
52
+ )
53
+ if block
54
+ if field_name
55
+ BLOCK_FIELD
56
+ else
57
+ ARBITRARY_MODIFICATION_FIELD
58
+ end
59
+ else
60
+ if packer_class
61
+ ASSOCIATION_FIELD
62
+ else
63
+ METHOD_FIELD
64
+ end
65
+ end
66
+ end
67
+
68
+ private_class_method def self.validate_field_args(
69
+ field_name=nil,
70
+ packer_class=nil,
71
+ *traits,
72
+ &block
73
+ )
35
74
  fail ModelNotYetDeclaredError if !@model
36
75
 
37
76
  # This check applies to all invocations:
@@ -48,7 +87,7 @@ module Sequel
48
87
  # field :foo {|model| ...}
49
88
  # or field {|model, hash| ...}
50
89
  #
51
- if packer_class
90
+ if packer_class || traits.any?
52
91
  raise(
53
92
  FieldArgumentError,
54
93
  'When passing a block to Sequel::Packer::field, either pass the ' +
@@ -90,7 +129,7 @@ module Sequel
90
129
  )
91
130
  end
92
131
 
93
- if packer_class
132
+ if packer_class || traits.any?
94
133
  if !@model.associations.include?(field_name)
95
134
  raise(
96
135
  FieldArgumentError,
@@ -123,6 +162,17 @@ module Sequel
123
162
  "(#{association_model})",
124
163
  )
125
164
  end
165
+
166
+ packer_class_traits = packer_class.instance_variable_get(:@traits)
167
+ traits.each do |trait|
168
+ if !packer_class_traits.key?(trait)
169
+ raise(
170
+ FieldArgumentError,
171
+ "Trait :#{trait} isn't defined for #{packer_class} used to " +
172
+ "pack #{field_name} association.",
173
+ )
174
+ end
175
+ end
126
176
  else
127
177
  if @model.associations.include?(field_name)
128
178
  raise(
@@ -134,37 +184,34 @@ module Sequel
134
184
  end
135
185
  end
136
186
  end
187
+ end
137
188
 
138
- field_type =
139
- if block
140
- if field_name
141
- BLOCK_FIELD
142
- else
143
- ARBITRARY_MODIFICATION_FIELD
144
- end
145
- else
146
- if packer_class
147
- ASSOCIATION_FIELD
148
- else
149
- METHOD_FIELD
150
- end
151
- end
152
-
153
- @fields << {
154
- type: field_type,
155
- name: field_name,
156
- packer: packer_class,
157
- block: block,
158
- }
189
+ def self.trait(name, &block)
190
+ raise ArgumentError, "Trait :#{name} already defined" if @traits.key?(name)
191
+ if !block_given?
192
+ raise ArgumentError, 'Must give a block when defining a trait'
193
+ end
194
+ @traits[name] = block
159
195
  end
160
196
 
161
- def initialize
197
+ def initialize(*traits)
162
198
  @packers = nil
199
+ @fields = traits.any? ? packer_fields.dup : packer_fields
200
+
201
+ traits.each do |trait|
202
+ trait_block = trait_blocks[trait]
203
+ if !trait_block
204
+ raise ArgumentError, "Unknown trait for #{self.class}: :#{trait}"
205
+ end
206
+
207
+ self.instance_exec(&trait_block)
208
+ end
163
209
 
164
- fields.each do |field_options|
210
+ @fields.each do |field_options|
165
211
  if field_options[:type] == ASSOCIATION_FIELD
166
212
  @packers ||= {}
167
- @packers[field_options[:name]] = field_options[:packer].new
213
+ @packers[field_options[:name]] =
214
+ field_options[:packer].new(*field_options[:packer_traits])
168
215
  end
169
216
  end
170
217
  end
@@ -177,7 +224,7 @@ module Sequel
177
224
  def pack_model(model)
178
225
  h = {}
179
226
 
180
- fields.each do |field_options|
227
+ @fields.each do |field_options|
181
228
  field_name = field_options[:name]
182
229
 
183
230
  case field_options[:type]
@@ -193,7 +240,7 @@ module Sequel
193
240
  elsif associated_objects.is_a?(Array)
194
241
  h[field_name] = @packers[field_name].pack_models(associated_objects)
195
242
  else
196
- @packers[field_name].pack_model(associated_objects)
243
+ h[field_name] = @packers[field_name].pack_model(associated_objects)
197
244
  end
198
245
  when ARBITRARY_MODIFICATION_FIELD
199
246
  field_options[:block].call(model, h)
@@ -209,10 +256,31 @@ module Sequel
209
256
 
210
257
  private
211
258
 
212
- def fields
259
+ def field(field_name=nil, packer_class=nil, *traits, &block)
260
+ klass = self.class
261
+ klass.send(
262
+ :validate_field_args, field_name, packer_class, *traits, &block)
263
+ field_type =
264
+ klass.send(:determine_field_type, field_name, packer_class, &block)
265
+
266
+ @fields << {
267
+ type: field_type,
268
+ name: field_name,
269
+ packer: packer_class,
270
+ packer_traits: traits,
271
+ block: block,
272
+ }
273
+ end
274
+
275
+ def packer_fields
213
276
  self.class.instance_variable_get(:@fields)
214
277
  end
278
+
279
+ def trait_blocks
280
+ self.class.instance_variable_get(:@traits)
281
+ end
215
282
  end
216
283
  end
217
284
 
218
285
  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.0.2
4
+ version: 0.1.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 00:00:00.000000000 Z
11
+ date: 2020-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel