sequel-packer 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|