sequel-packer 0.0.1 → 0.0.2
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 +9 -0
- data/README.md +219 -8
- data/lib/sequel/packer/version.rb +1 -1
- data/lib/sequel/packer.rb +199 -8
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 248126ba630e1d763f32e911c992b296cd32b5201503a67296bbb18fa4000679
|
4
|
+
data.tar.gz: abecaa06c7e4d64eef3e4b91be2311cd2921e966062fd03fc5a5d590604c6b92
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0231fbedf7d8d7ccd77093e00617bf15688ed57e52d169876780854023e3c6c758dd34d002d89952795291a8c1f5c7ef398f7f24543cf686be85e496059a5f58
|
7
|
+
data.tar.gz: da6163737a56d4a96205ff0c26ce9ece0b90b2763c1fe271b899d3b55261bc67cf36ea35d4e75cdac540c81fd587618fbb0272691bbd86fcca0375e712d31809
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
### 0.0.2 (2020-05-11)
|
2
|
+
|
3
|
+
* Added support for `Sequel::Packer.field(key, &block)` and
|
4
|
+
`Sequel::Packer.field(&block)`
|
5
|
+
* Added validation to `Sequel::Packer::field` to detect incorrect usage.
|
6
|
+
* Added support for nested packing of associations using
|
7
|
+
`Sequel::Packer.field(association, packer_class)`
|
8
|
+
* Update README with usage instructions and basic API reference.
|
9
|
+
|
1
10
|
### 0.0.1 (2020-05-10)
|
2
11
|
|
3
12
|
* Most basic functionality for serializing single fields
|
data/README.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# Sequel::Packer
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
3
|
+
A Ruby serialization library to be used with the Sequel ORM.
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
@@ -22,19 +20,232 @@ Or install it yourself as:
|
|
22
20
|
|
23
21
|
## Usage
|
24
22
|
|
25
|
-
|
23
|
+
Suppose we have the following basic database schema:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
DB.create_table(:users) do
|
27
|
+
primary_key :id
|
28
|
+
String :name
|
29
|
+
end
|
30
|
+
|
31
|
+
DB.create_table(:posts) do
|
32
|
+
primary_key :id
|
33
|
+
foreign_key :author_id, :users
|
34
|
+
String :title
|
35
|
+
String :content
|
36
|
+
end
|
37
|
+
|
38
|
+
DB.create_table(:comments) do
|
39
|
+
primary_key :id
|
40
|
+
foreign_key :author_id, :users
|
41
|
+
foreign_key :post_id, :posts
|
42
|
+
String :content
|
43
|
+
end
|
44
|
+
|
45
|
+
class User < Sequel::Model(:users); end
|
46
|
+
class Post < Sequel::Model(:posts); end
|
47
|
+
class Comment < Sequel::Model(:comments)
|
48
|
+
many_to_one :author, key: :author_id, class: :User
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
Suppose an endpoint wants to fetch all the ten most recent comments by a user.
|
53
|
+
After validating the user id, we end up with the Sequel dataset represting the
|
54
|
+
data we want to return:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
recent_comments = Comment
|
58
|
+
.where(author_id: user_id)
|
59
|
+
.order(:id.desc)
|
60
|
+
.limit(10)
|
61
|
+
```
|
62
|
+
|
63
|
+
We can define a Packer class to serialize just fields we want to, using a
|
64
|
+
custom DSL:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
class CommentPacker < Sequel::Packer
|
68
|
+
model Comment
|
69
|
+
|
70
|
+
field :id
|
71
|
+
field :content
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
This can then be used as follows:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
CommentPacker.new.pack(recent_comments)
|
79
|
+
=> [
|
80
|
+
{id: 536, "Great post, man!"},
|
81
|
+
{id: 436, "lol"},
|
82
|
+
{id: 413, "What a story..."},
|
83
|
+
]
|
84
|
+
```
|
85
|
+
|
86
|
+
In another context, suppose that we want to fetch the comments on a post along
|
87
|
+
with the authors of those comments.
|
88
|
+
|
89
|
+
In this case we could define two separate Packers, one for Users, and another
|
90
|
+
for Comments:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
class UserPacker < Sequel::Packer
|
94
|
+
model User
|
95
|
+
|
96
|
+
field :id
|
97
|
+
field :name
|
98
|
+
end
|
99
|
+
|
100
|
+
class CommentWithAuthorPacker < Sequel::Packer
|
101
|
+
model Comment
|
102
|
+
|
103
|
+
field :id
|
104
|
+
field :content
|
105
|
+
field :author, UserPacker
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
When we use this, we get:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
comments_on_post = Comment.where(post_id: post.id)
|
113
|
+
CommentWithAuthorPacker.new.pack(comments_on_post)
|
114
|
+
=> [
|
115
|
+
{
|
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"},
|
124
|
+
},
|
125
|
+
...
|
126
|
+
]
|
127
|
+
```
|
128
|
+
|
129
|
+
See the API Reference below for the exact API.
|
130
|
+
|
131
|
+
## API Reference
|
132
|
+
|
133
|
+
Custom packers are written by creating subclasses of `Sequel::Packer`. This
|
134
|
+
class defines a DSL for declaring how a Sequel Model will be converted into a
|
135
|
+
plain Ruby hash.
|
136
|
+
|
137
|
+
### `self.model(sequel_model_class)`
|
138
|
+
|
139
|
+
The beginning of each Packer class must begin with `model MySequelModel`, which
|
140
|
+
specifies which Sequel Model this Packer class will serialize. This is mostly
|
141
|
+
to catch certain errors at load time, rather than at run time.
|
142
|
+
|
143
|
+
### `self.field(column_name)` (or `self.field(method_name)`)
|
144
|
+
|
145
|
+
Defining the shape of the outputted data is done using the `field` method, which
|
146
|
+
exists in four different variants. This first variant is the simplest. It simply
|
147
|
+
fetches the value of the column from the model and stores it in the outputted
|
148
|
+
hash under a key of the same name. Essentially `field :my_column` eventually
|
149
|
+
results in `hash[:my_column] = model.my_column`.
|
150
|
+
|
151
|
+
Sequel Models define accessor methods for each column in the underlying table,
|
152
|
+
so technically underneath the hood Packer is actually calling the sending the
|
153
|
+
method `column_name` to the model: `hash[:my_column] = model.send(:my_column)`.
|
154
|
+
|
155
|
+
This means that the result of any method can be serialized using
|
156
|
+
`field :method_name`. For example, suppose a User model has a `first_name` and
|
157
|
+
`last_name` column, and a helper method `full_name`:
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
class User < Sequel::Model(:users)
|
161
|
+
def full_name
|
162
|
+
"#{first_name} #{last_name}"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
```
|
166
|
+
|
167
|
+
Then when `User.create(first_name: "Paul", last_name: "Martinez")` gets packed
|
168
|
+
with `field :full_name` specified, the outputted hash will contain
|
169
|
+
`full_name: "Paul Martinez"`.
|
170
|
+
|
171
|
+
### `self.field(key, &block)`
|
172
|
+
|
173
|
+
A block can be passed to `field` to perform arbitrary computation and store the
|
174
|
+
result under the specified `key`. The block will be passed the model as a single
|
175
|
+
argument. Use this to call methods on the model that may take additional
|
176
|
+
arguments, or to "rename" a column.
|
177
|
+
|
178
|
+
Examples:
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
class MyPacker < Sequel::Packer
|
182
|
+
model MyModel
|
183
|
+
|
184
|
+
field :friendly_public_name do |model|
|
185
|
+
model.unfriendly_internal_name
|
186
|
+
end
|
187
|
+
|
188
|
+
# Shorthand for above
|
189
|
+
field :friendly_public_name, &:unfriendly_internal_name
|
190
|
+
|
191
|
+
field :foo do |model|
|
192
|
+
model.bar(baz, quux)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
```
|
196
|
+
|
197
|
+
### `self.field(association, packer_class)
|
198
|
+
|
199
|
+
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:
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
field :association do |model|
|
205
|
+
packer_class.new.pack(model.association_dataset)
|
206
|
+
end
|
207
|
+
```
|
208
|
+
|
209
|
+
Though this version of course would result in many more queries to the database,
|
210
|
+
which are not required when using the shorthand form.
|
211
|
+
|
212
|
+
### `self.field(&block)`
|
213
|
+
|
214
|
+
Passing a block but no `key` to `field` allows for arbitrary manipulation of the
|
215
|
+
packed hash. The block will be passed the model and the partially packed hash.
|
216
|
+
One potential usage is for dynamic keys that cannot be determined at load time,
|
217
|
+
but otherwise it's meant as a general escape hatch.
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
field do |model, hash|
|
221
|
+
hash[model.compute_dynamic_key] = model.dynamic_value
|
222
|
+
end
|
223
|
+
```
|
224
|
+
|
225
|
+
### `pack(dataset)`
|
226
|
+
|
227
|
+
After creating a new instance of a Packer class, call `packer.pack(dataset)` to
|
228
|
+
materialize a dataset and convert it to an array of packed Ruby hashes.
|
26
229
|
|
27
230
|
## Development
|
28
231
|
|
29
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
232
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
233
|
+
`rake test` to run the tests. You can also run `bin/console` for an interactive
|
234
|
+
prompt that will allow you to experiment.
|
30
235
|
|
31
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To
|
236
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
237
|
+
release a new version, update the version number in `version.rb`, and then run
|
238
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
239
|
+
git commits and tags, and push the `.gem` file to
|
240
|
+
[rubygems.org](https://rubygems.org).
|
32
241
|
|
33
242
|
## Contributing
|
34
243
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at
|
244
|
+
Bug reports and pull requests are welcome on GitHub at
|
245
|
+
https://github.com/PaulJuliusMartinez/sequel-packer.
|
36
246
|
|
37
247
|
|
38
248
|
## License
|
39
249
|
|
40
|
-
The gem is available as open source under the terms of the
|
250
|
+
The gem is available as open source under the terms of the
|
251
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
data/lib/sequel/packer.rb
CHANGED
@@ -1,25 +1,216 @@
|
|
1
1
|
module Sequel
|
2
2
|
class Packer
|
3
|
+
# For invalid arguments provided to the field class method.
|
4
|
+
class FieldArgumentError < ArgumentError; end
|
5
|
+
# Must declare a model with `model MyModel` before calling field.
|
6
|
+
class ModelNotYetDeclaredError < StandardError; end
|
7
|
+
|
3
8
|
def self.inherited(subclass)
|
4
9
|
subclass.instance_variable_set(:@fields, [])
|
5
10
|
end
|
6
11
|
|
7
|
-
def self.
|
8
|
-
|
12
|
+
def self.model(klass)
|
13
|
+
if !(klass < Sequel::Model)
|
14
|
+
fail(
|
15
|
+
ArgumentError,
|
16
|
+
'model declaration must be a subclass of Sequel::Model',
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
fail ArgumentError, 'model already declared' if @model
|
21
|
+
|
22
|
+
@model = klass
|
9
23
|
end
|
10
24
|
|
11
|
-
|
12
|
-
|
25
|
+
# field(:foo)
|
26
|
+
METHOD_FIELD = :method_field
|
27
|
+
# field(:foo, &block)
|
28
|
+
BLOCK_FIELD = :block_field
|
29
|
+
# field(:association, packer_class)
|
30
|
+
ASSOCIATION_FIELD = :association_field
|
31
|
+
# field(&block)
|
32
|
+
ARBITRARY_MODIFICATION_FIELD = :arbitrary_modification_field
|
33
|
+
|
34
|
+
def self.field(field_name=nil, packer_class=nil, &block)
|
35
|
+
fail ModelNotYetDeclaredError if !@model
|
36
|
+
|
37
|
+
# This check applies to all invocations:
|
38
|
+
if field_name && !field_name.is_a?(Symbol) && !field_name.is_a?(String)
|
39
|
+
raise(
|
40
|
+
FieldArgumentError,
|
41
|
+
'Field name passed to Sequel::Packer::field must be a Symbol or ' +
|
42
|
+
'a String.',
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
if block
|
47
|
+
# If the user passed a block, we'll assume they either want:
|
48
|
+
# field :foo {|model| ...}
|
49
|
+
# or field {|model, hash| ...}
|
50
|
+
#
|
51
|
+
if packer_class
|
52
|
+
raise(
|
53
|
+
FieldArgumentError,
|
54
|
+
'When passing a block to Sequel::Packer::field, either pass the ' +
|
55
|
+
'name of field as a single argument (e.g., field(:foo) ' +
|
56
|
+
'{|model| ...}), or nothing at all to perform arbitrary ' +
|
57
|
+
'modifications of the final hash (e.g., field {|model, hash| ' +
|
58
|
+
'...}).',
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
arity = block.arity
|
63
|
+
|
64
|
+
# When using Symbol.to_proc (field(:foo, &:calculate_foo)), the block has arity -1.
|
65
|
+
if field_name && arity != 1 && arity != -1
|
66
|
+
raise(
|
67
|
+
FieldArgumentError,
|
68
|
+
"The block used to define :#{field_name} must accept exactly " +
|
69
|
+
'one argument.',
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
if !field_name && arity != 2
|
74
|
+
raise(
|
75
|
+
FieldArgumentError,
|
76
|
+
'When passing an arbitrary block to Sequel::Packer::field, the ' +
|
77
|
+
'block must accept exactly two arguments: the model and the ' +
|
78
|
+
'partially packed hash.',
|
79
|
+
)
|
80
|
+
end
|
81
|
+
else
|
82
|
+
# In this part of the if, block is not defined
|
83
|
+
|
84
|
+
if !field_name
|
85
|
+
# Note that this error isn't technically true, but usage of the
|
86
|
+
# field {|model, hash| ...} variant is likely pretty rare.
|
87
|
+
raise(
|
88
|
+
FieldArgumentError,
|
89
|
+
'Must pass a field name to Sequel::Packer::field.',
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
if packer_class
|
94
|
+
if !@model.associations.include?(field_name)
|
95
|
+
raise(
|
96
|
+
FieldArgumentError,
|
97
|
+
'Passing multiple arguments to Sequel::Packer::field ' +
|
98
|
+
'is used to serialize associations with designated ' +
|
99
|
+
"packers, but the association #{field_name} does not " +
|
100
|
+
"exist on #{@model}.",
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
if !(packer_class < Sequel::Packer)
|
105
|
+
raise(
|
106
|
+
FieldArgumentError,
|
107
|
+
'When declaring the serialization behavior for an ' +
|
108
|
+
'association, the second argument must be a Sequel::Packer ' +
|
109
|
+
"subclass. #{packer_class} is not a subclass of " +
|
110
|
+
'Sequel::Packer.',
|
111
|
+
)
|
112
|
+
end
|
113
|
+
|
114
|
+
association_model =
|
115
|
+
@model.association_reflections[field_name].associated_class
|
116
|
+
packer_class_model = packer_class.instance_variable_get(:@model)
|
117
|
+
|
118
|
+
if !(association_model <= packer_class_model)
|
119
|
+
raise(
|
120
|
+
FieldArgumentError,
|
121
|
+
"Model for association packer (#{packer_class_model}) " +
|
122
|
+
"doesn't match model for the #{field_name} association " +
|
123
|
+
"(#{association_model})",
|
124
|
+
)
|
125
|
+
end
|
126
|
+
else
|
127
|
+
if @model.associations.include?(field_name)
|
128
|
+
raise(
|
129
|
+
FieldArgumentError,
|
130
|
+
'When declaring a field for a model association, you must ' +
|
131
|
+
'also pass a Sequel::Packer class to use as the second ' +
|
132
|
+
'argument to field.',
|
133
|
+
)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
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
|
+
}
|
159
|
+
end
|
160
|
+
|
161
|
+
def initialize
|
162
|
+
@packers = nil
|
163
|
+
|
164
|
+
fields.each do |field_options|
|
165
|
+
if field_options[:type] == ASSOCIATION_FIELD
|
166
|
+
@packers ||= {}
|
167
|
+
@packers[field_options[:name]] = field_options[:packer].new
|
168
|
+
end
|
169
|
+
end
|
13
170
|
end
|
14
171
|
|
15
172
|
def pack(dataset)
|
16
|
-
dataset.
|
17
|
-
|
18
|
-
|
173
|
+
models = dataset.all
|
174
|
+
pack_models(models)
|
175
|
+
end
|
176
|
+
|
177
|
+
def pack_model(model)
|
178
|
+
h = {}
|
179
|
+
|
180
|
+
fields.each do |field_options|
|
181
|
+
field_name = field_options[:name]
|
182
|
+
|
183
|
+
case field_options[:type]
|
184
|
+
when METHOD_FIELD
|
19
185
|
h[field_name] = model.send(field_name)
|
186
|
+
when BLOCK_FIELD
|
187
|
+
h[field_name] = field_options[:block].call(model)
|
188
|
+
when ASSOCIATION_FIELD
|
189
|
+
associated_objects = model.send(field_name)
|
190
|
+
|
191
|
+
if !associated_objects
|
192
|
+
h[field_name] = nil
|
193
|
+
elsif associated_objects.is_a?(Array)
|
194
|
+
h[field_name] = @packers[field_name].pack_models(associated_objects)
|
195
|
+
else
|
196
|
+
@packers[field_name].pack_model(associated_objects)
|
197
|
+
end
|
198
|
+
when ARBITRARY_MODIFICATION_FIELD
|
199
|
+
field_options[:block].call(model, h)
|
20
200
|
end
|
21
|
-
h
|
22
201
|
end
|
202
|
+
|
203
|
+
h
|
204
|
+
end
|
205
|
+
|
206
|
+
def pack_models(models)
|
207
|
+
models.map {|m| pack_model(m)}
|
208
|
+
end
|
209
|
+
|
210
|
+
private
|
211
|
+
|
212
|
+
def fields
|
213
|
+
self.class.instance_variable_get(:@fields)
|
23
214
|
end
|
24
215
|
end
|
25
216
|
end
|