sequel-packer 0.0.2 → 0.1.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: 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