flatter 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/README.md +679 -5
- data/flatter.gemspec +1 -0
- data/lib/flatter/mapper/attribute_methods.rb +72 -5
- data/lib/flatter/mapper/collection.rb +193 -0
- data/lib/flatter/mapper/factory.rb +24 -4
- data/lib/flatter/mapper/mapping.rb +2 -4
- data/lib/flatter/mapper/mounting.rb +37 -13
- data/lib/flatter/mapper/options.rb +1 -1
- data/lib/flatter/mapper/persistence.rb +11 -2
- data/lib/flatter/mapper/target.rb +53 -7
- data/lib/flatter/mapper/traits.rb +12 -23
- data/lib/flatter/mapper/write_with_indifferent_access.rb +8 -0
- data/lib/flatter/mapper.rb +6 -2
- data/lib/flatter/version.rb +1 -1
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 543de04a22f3559983f3dd01ee360a7f78117df6
|
4
|
+
data.tar.gz: 5e6e173e3c3b60d0109ddb9a898b61fcd756254f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d818795d50e0d5a138e430ad5b1e5c3b4983e93bba7f5a34561592d92a35ca17ac57e3d340f7674f3662602e6a290fcadf7a78482f6b9fb66bdcc288d02655a
|
7
|
+
data.tar.gz: 052590d63bd471a285e3268497189bf14185d9bd24b1a69058a363b550f4370c590239abe7277d29f91247a20b180146ab34bf3264be689897ad1b7bfca71687
|
data/README.md
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
# Flatter
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
[](http://travis-ci.org/akuzko/flatter)
|
4
|
+
|
5
|
+
This gem supersedes [FlatMap](https://github.com/TMXCredit/flat_map) gem. With
|
6
|
+
only it's core concepts in mind it has been written from complete scratch to
|
7
|
+
provide more pure, clean, extensible code and reliable functionality.
|
5
8
|
|
6
9
|
## Installation
|
7
10
|
|
@@ -21,11 +24,683 @@ Or install it yourself as:
|
|
21
24
|
|
22
25
|
## Usage
|
23
26
|
|
24
|
-
|
27
|
+
If you happen to use `FlatMap` gem , check out
|
28
|
+
[Flatter and FlatMap: What's Changed](https://github.com/akuzko/flatter/wiki/Flatter-and-FlatMap:-What's-Changed) wiki page.
|
29
|
+
|
30
|
+
Flatter's main working units are instances of `Mapper` class. **Mappers** are essentially
|
31
|
+
wrappers around your related ActiveModel-like objects, map their attributes to mapper's
|
32
|
+
accessors via **mappings**, can be **mounted** by other mappers, and can define flexible
|
33
|
+
behavior via **traits**. Let's cover this topics one by one.
|
34
|
+
|
35
|
+
### Mappings
|
36
|
+
|
37
|
+
Mappings represent a mapper's property, which maps it to some attribute of the
|
38
|
+
target object. Since eventually mappers are used in combination with each other, it is
|
39
|
+
better to map model's attribute with a unique "full name" to avoid collisions, for example:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
# models:
|
43
|
+
class Person
|
44
|
+
include ActiveModel::Model
|
45
|
+
|
46
|
+
attr_accessor :first_name, :last_name
|
47
|
+
end
|
48
|
+
|
49
|
+
class Group
|
50
|
+
include ActiveModel::Model
|
51
|
+
|
52
|
+
attr_accessor :name
|
53
|
+
end
|
54
|
+
|
55
|
+
class Department
|
56
|
+
include ActiveModel::Model
|
57
|
+
|
58
|
+
attr_accessor :name
|
59
|
+
end
|
60
|
+
|
61
|
+
# mappers:
|
62
|
+
class PersonMapper < Flatter::Mapper
|
63
|
+
map :first_name, :last_name
|
64
|
+
# it's ok, since :first_name and :last_name attributes are
|
65
|
+
# not likely to be used somewhere else
|
66
|
+
end
|
67
|
+
|
68
|
+
class GroupMapper < Flatter::Mapper
|
69
|
+
map group_name: :name
|
70
|
+
# maps mapper's :group_name attribute to target's :name attribute
|
71
|
+
end
|
72
|
+
|
73
|
+
class DepartmentMapper < Flatter::Mapper
|
74
|
+
map department_name: :name
|
75
|
+
# maps mapper's :department_name attribute to target's :name attribute
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
#### Mapping Options
|
80
|
+
|
81
|
+
- `:reader` Allows to add a custom logic for reading target's attribute.
|
82
|
+
When value is `String` or `Symbol`, calls a method defined by a **mapper** class.
|
83
|
+
If that method accepts an argument, mapping name will be passed to it.
|
84
|
+
When value is `Proc`, it is executed in context of mapper object, yielding
|
85
|
+
mapping name if block has arity of 1. For other arbitrary objects will simply
|
86
|
+
return that object.
|
87
|
+
|
88
|
+
- `:writer` Allows to control a way how value is assigned (written).
|
89
|
+
When value is `String` or `Symbol`, calls a method defined by a **mapper** class,
|
90
|
+
passing a value to it. If that method accepts second argument, mapping name will be
|
91
|
+
additionally passed to it.
|
92
|
+
When value is `Proc`, it is executed in context of mapper object, yielding
|
93
|
+
value and optionally mapping name if block has arity of 2. For other values will
|
94
|
+
raise error.
|
95
|
+
|
96
|
+
### Mountings
|
97
|
+
|
98
|
+
Stand-alone mappers provide not very much benefit. However, mappers have a powerful
|
99
|
+
ability to be mounted on top of each other. When mapper mounts another one, it
|
100
|
+
gains access to all of it's mappings, and they become accessible in a plain way.
|
101
|
+
|
102
|
+
For example, having `Person`, `Department` and `Group` classes defined above with
|
103
|
+
additional sample relationship we might have:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
# models:
|
107
|
+
class Person
|
108
|
+
def department
|
109
|
+
@department ||= Department.new(name: 'Default')
|
110
|
+
end
|
111
|
+
|
112
|
+
def group
|
113
|
+
@group ||= Group.new(name: 'General')
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# mappers:
|
118
|
+
class PersonMapper < Flatter::Mapper
|
119
|
+
map :first_name, :last_name
|
120
|
+
|
121
|
+
mount :department
|
122
|
+
mount :group
|
123
|
+
end
|
124
|
+
|
125
|
+
person = Person.new(first_name: 'John', last_name: 'Smith')
|
126
|
+
mapper = PersonMapper.new(person)
|
127
|
+
|
128
|
+
mapper.read # =>
|
129
|
+
# { 'first_name' => 'John',
|
130
|
+
# 'last_name' => 'Smith',
|
131
|
+
# 'department_name' => 'Default',
|
132
|
+
# 'group_name' => 'General' }
|
133
|
+
|
134
|
+
mapper.group_name = 'Managers'
|
135
|
+
person.group.name # => "Managers"
|
136
|
+
```
|
137
|
+
|
138
|
+
#### Mounting Options
|
139
|
+
|
140
|
+
- `:mapper_class_name` Name of the mapper class (`String`) if it cannot be
|
141
|
+
determined from the mounting name itself. By default it is camelized name
|
142
|
+
followed by 'Mapper', for example, for `:group` mounting, default mapper
|
143
|
+
class name is `'GroupMapper'`.
|
144
|
+
|
145
|
+
- `:mapper_class` Used mostly internally, but allows to specify mapper class
|
146
|
+
itself. Has more priority than `:mapper_class_name` option.
|
147
|
+
|
148
|
+
- `:target` Allows to manually set mounted mapper's target. By default target is
|
149
|
+
obtained from mounting mapper's target by sending it mounting name. In example
|
150
|
+
above target for `:group` mapping was obtained by sending `:group` method to
|
151
|
+
`person` object, which was the target of root mapper.
|
152
|
+
When value is `String` or `Symbol`, it is considered as a method name of the
|
153
|
+
**mapper**, which is called with no arguments.
|
154
|
+
When value is `Proc`, it is called yielding mapper's target to it.
|
155
|
+
For other objects, objects themselves are used as targets.
|
156
|
+
|
157
|
+
- `:traits` Allows to specify a list of traits to be applied for mounted mappers.
|
158
|
+
See **Traits** section bellow.
|
159
|
+
|
160
|
+
### Callbacks
|
161
|
+
|
162
|
+
Mappers include `ActiveModel::Validation` module and thus support `ActiveSupport`'s
|
163
|
+
callbacks. Additionally, `:save` callbacks have been defined for `Flatter::Mapper`,
|
164
|
+
so you can do something like `set_callback :save, :after, :send_invitation`.
|
165
|
+
|
166
|
+
### Traits
|
167
|
+
|
168
|
+
Traits are another powerful mapper ability. Traits allow to encapsulate named sets
|
169
|
+
of additional definitions, and optionally use them on mapper initialization or
|
170
|
+
when mounting mapper in other one. Everything that can be defined within the mapper
|
171
|
+
can be defined withing the trait. For example (suppose we have some additional
|
172
|
+
`:with_counts` trait defined on `DepartmentMapper` alongside with model relationships):
|
173
|
+
|
174
|
+
```ruby
|
175
|
+
class PersonMapper < Flatter::Mapper
|
176
|
+
map :first_name, :last_name
|
177
|
+
|
178
|
+
trait :full_info do
|
179
|
+
map :middle_name, dob: :date_of_birth
|
180
|
+
|
181
|
+
mount :group
|
182
|
+
end
|
183
|
+
|
184
|
+
trait :with_department do
|
185
|
+
mount :department, traits: :with_counts
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
mapper = PersonMapper.new(person)
|
190
|
+
full_mapper = PersonMapper.new(person, :full_info, :with_department)
|
191
|
+
|
192
|
+
mapper.read # =>
|
193
|
+
# { 'first_name' => 'John',
|
194
|
+
# 'last_name' => 'Smith' }
|
195
|
+
|
196
|
+
full_mapper.read # =>
|
197
|
+
# { 'first_name' => 'John',
|
198
|
+
# 'last_name' => 'Smith',
|
199
|
+
# 'middle_name' => nil,
|
200
|
+
# 'dob' => Wed, 18 Feb 1981,
|
201
|
+
# 'group_name' => 'General'
|
202
|
+
# 'department_name' => 'Default',
|
203
|
+
# 'department_people_count' => 31 }
|
204
|
+
```
|
205
|
+
|
206
|
+
#### Traits and callbacks
|
207
|
+
|
208
|
+
Since traits are internally mappers (which allows you to define everything mapper
|
209
|
+
can), you can also define callbacks on traits, allowing you to dynamically opt-in,
|
210
|
+
opt-out and reuse functionality. Keep in mind that `ActiveModel`'s validation
|
211
|
+
routines are also just a set of callbacks, meaning that you can define sets of
|
212
|
+
validation in traits, mix them together in any way. For example:
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
class PersonMapper < Flatter::Mapper
|
216
|
+
map :first_name, :last_name
|
217
|
+
|
218
|
+
trait :registration do
|
219
|
+
map personal_email: :email
|
220
|
+
|
221
|
+
validates_presence_of :first_name, :last_name
|
222
|
+
validates :personal_email, :presence: true, email: true
|
223
|
+
|
224
|
+
set_callback :save, :after, :send_greeting
|
225
|
+
|
226
|
+
def send_greeting
|
227
|
+
PersonMailer.greeting(target).deliver_now
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
```
|
232
|
+
|
233
|
+
#### Traits and shared methods
|
234
|
+
|
235
|
+
Despite the fact traits are separate objects, you can call methods defined in
|
236
|
+
one trait from another trait, as well as methods defined in root mapper itself
|
237
|
+
(such as attribute methods). That allows you to treat traits as parts of the
|
238
|
+
root mapper.
|
239
|
+
|
240
|
+
#### Inline extension traits
|
241
|
+
|
242
|
+
When initializing a mapper, or defining a mounting, you can pass a block with
|
243
|
+
additional definitions. This block will be treated as an anonymous extension trait.
|
244
|
+
For example, let's suppose that `email` from example above is actually a part
|
245
|
+
of another `User` model that has it's own `UserMapper` with defined `:email` mapping.
|
246
|
+
Then we might have something like:
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
class PersonMapper < Flatter::Mapper
|
250
|
+
map :first_name, :last_name
|
251
|
+
|
252
|
+
trait :registration do
|
253
|
+
validates_presence_of :first_name, :last_name
|
254
|
+
|
255
|
+
mount :user do
|
256
|
+
validates :email, :presence: true, email: true
|
257
|
+
set_callback :save, :after, :send_greeting
|
258
|
+
|
259
|
+
def send_greeting
|
260
|
+
UserMailer.greeting(target).deliver_now
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
```
|
267
|
+
|
268
|
+
### Processing Order
|
269
|
+
|
270
|
+
`Flatter` mappers have a well-defined processing order of mountings (including
|
271
|
+
traits), best shown by example. Suppose we have something like this:
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
class AMapper < Flatter::Mapper
|
275
|
+
trait :trait_a1 do
|
276
|
+
mount :b, traits: :trait_b do
|
277
|
+
# extension callbacks definitions
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
trait :trait_a2 do
|
282
|
+
mount :c
|
283
|
+
end
|
284
|
+
|
285
|
+
mount :d
|
286
|
+
end
|
287
|
+
```
|
288
|
+
|
289
|
+
Mappers are processed (validated and saved) from top to bottom. Let's have initialized
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
mapper = AMapper.new(a, :trait_a2, :trait_a1)
|
293
|
+
```
|
294
|
+
|
295
|
+
Please note traits order, it is very important: `:trait_a2` goes first, so it's
|
296
|
+
callbacks and mountings will go first too. So if we call `mapper.save`, we will have
|
297
|
+
following execution order (suppose, we have defined callbacks for all traits and mappers):
|
298
|
+
|
299
|
+
```
|
300
|
+
trait_a2.before_save
|
301
|
+
trait_a1.before_save
|
302
|
+
A.before_save
|
303
|
+
A.save
|
304
|
+
A.after_save
|
305
|
+
trait_a1.after_save
|
306
|
+
trait_a2.after_save
|
307
|
+
C.before_save
|
308
|
+
C.save
|
309
|
+
C.after_save
|
310
|
+
trait_b.before_save
|
311
|
+
B_extension.before_save
|
312
|
+
B.before_save
|
313
|
+
B.save
|
314
|
+
B.after_save
|
315
|
+
B_extension.after_save
|
316
|
+
trait_b.after_save
|
317
|
+
D.before_save
|
318
|
+
D.save
|
319
|
+
D.after_save
|
320
|
+
```
|
321
|
+
|
322
|
+
### Attribute methods
|
323
|
+
|
324
|
+
All mappers can access mapped values via attribute methods that match mapping names.
|
325
|
+
That allows you to easily use mappers for building forms or developing other
|
326
|
+
functionality.
|
327
|
+
|
328
|
+
You also have reader methods that match mounting names. They will return
|
329
|
+
value read for a specific mounting (including it's own nested mountings).
|
330
|
+
For example:
|
331
|
+
|
332
|
+
```ruby
|
333
|
+
class UserMapper < Flatter::Mapper
|
334
|
+
map :email
|
335
|
+
|
336
|
+
mount :person do
|
337
|
+
map :first_name, :last_name
|
338
|
+
|
339
|
+
mount :phone do
|
340
|
+
map phone_number: :number
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
mapper = UserMapper.new(User.new)
|
346
|
+
mapper.email = "user@email.com"
|
347
|
+
mapper.first_name = "John"
|
348
|
+
mapper.phone_number = "111-222-3333"
|
349
|
+
|
350
|
+
mapper.read # =>
|
351
|
+
# { "email" => "user@email.com",
|
352
|
+
# "first_name" => "John",
|
353
|
+
# "last_name" => nil,
|
354
|
+
# "phone_number" => "111-222-3333" }
|
355
|
+
|
356
|
+
mapper.person # =>
|
357
|
+
# { "first_name" => "John",
|
358
|
+
# "last_name" => nil,
|
359
|
+
# "phone_number" => "111-222-3333" }
|
360
|
+
|
361
|
+
mapper.phone # =>
|
362
|
+
# { "phone_number" => "111-222-3333" }
|
363
|
+
```
|
364
|
+
|
365
|
+
Please also read "Attribute methods" subsection for Collections bellow
|
366
|
+
for details on what methods do you get when mapping collections.
|
367
|
+
|
368
|
+
### Collections
|
369
|
+
|
370
|
+
Starting from version `0.2.0`, Flatter mappers also support handling of collections.
|
371
|
+
|
372
|
+
#### Declaration
|
373
|
+
|
374
|
+
To declare a mapper that will handle a collection of items, simply mount it
|
375
|
+
with a pluralized name:
|
376
|
+
|
377
|
+
```ruby
|
378
|
+
class PersonMapper < Flatter::Mapper
|
379
|
+
mount :phones
|
380
|
+
end
|
381
|
+
```
|
382
|
+
|
383
|
+
If you need to mount a mapper with already pluralized name to handle single
|
384
|
+
item in common fashion, mount it with `collection: false` option:
|
385
|
+
|
386
|
+
```ruby
|
387
|
+
class SeamstressMapper < Flatter::Mapper
|
388
|
+
mount :scissors, collection: false
|
389
|
+
end
|
390
|
+
```
|
391
|
+
|
392
|
+
If you need your root mapper to handle a collection of items, initialize it
|
393
|
+
with `collection: true` option:
|
394
|
+
|
395
|
+
```ruby
|
396
|
+
mapper = PhoneMapper.new(user.phones, collection: true)
|
397
|
+
```
|
398
|
+
|
399
|
+
#### Key
|
400
|
+
|
401
|
+
Mapper that will be used for mapping collection should define `key` mapping.
|
402
|
+
`Flatter` offers `key` class-level method to do it easier. You can call it
|
403
|
+
on mapper definition:
|
404
|
+
|
405
|
+
```ruby
|
406
|
+
class PhoneMapper
|
407
|
+
key :id
|
408
|
+
end
|
409
|
+
```
|
410
|
+
|
411
|
+
or when mounting mapper for collection handling:
|
412
|
+
|
413
|
+
```ruby
|
414
|
+
class PersonMapper
|
415
|
+
mount :phones do
|
416
|
+
key -> { target.number }
|
417
|
+
end
|
418
|
+
end
|
419
|
+
```
|
420
|
+
|
421
|
+
All non-nil `key` mappings have to have unique value (within collection they
|
422
|
+
belong to). Otherwise `NonUniqKeysError` will be raised on reading. All items
|
423
|
+
that have `nil` as a key value are considered to be "new items". All such
|
424
|
+
items are removed from collection on writing.
|
425
|
+
|
426
|
+
#### Reading
|
427
|
+
|
428
|
+
As well as can be expected, collection mappers provide an array of hashes
|
429
|
+
derived from reading from all items in the collection. Each hash in this array
|
430
|
+
will have `"key"` key for item identification. It should be used for writing
|
431
|
+
(see bellow). For example:
|
432
|
+
|
433
|
+
```ruby
|
434
|
+
class CompanyMapper < Flatter::Mapper
|
435
|
+
map company_name: :name
|
436
|
+
|
437
|
+
mount :departments do
|
438
|
+
key :id
|
439
|
+
|
440
|
+
mount :location
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
class DepartmentMapper < Flatter::Mapper
|
445
|
+
map department_name: :name
|
446
|
+
end
|
447
|
+
|
448
|
+
class LocationMapper < Flatter::Mapper
|
449
|
+
map location_name: :name
|
450
|
+
end
|
451
|
+
|
452
|
+
# ...
|
453
|
+
|
454
|
+
mapper = CompanyMapper.new(company)
|
455
|
+
|
456
|
+
mapper.read # =>
|
457
|
+
# { "company_name" => "Web Developers, Inc.",
|
458
|
+
# "departments" => [{
|
459
|
+
# "key" => 1,
|
460
|
+
# "department_name" => "R & D",
|
461
|
+
# "location_name" => "Good Office"
|
462
|
+
# }, {
|
463
|
+
# "key" => 2,
|
464
|
+
# "department_name" => "QA",
|
465
|
+
# "location_name" => "QA Office"
|
466
|
+
# }]
|
467
|
+
# }
|
468
|
+
|
469
|
+
```
|
470
|
+
|
471
|
+
#### Writing
|
472
|
+
|
473
|
+
To update collection items, you should pass an array of hashes to it's mapper.
|
474
|
+
Value of the `:key` key of each hash is important and defines how each set of
|
475
|
+
params will be used.
|
476
|
+
|
477
|
+
- If `key` is present in the original collection, `params` hash will be used
|
478
|
+
to update mapped item via `write` method
|
479
|
+
|
480
|
+
- If `key` is `nil`, params are treated as attributes for the new record, so
|
481
|
+
new instance of mapped target class is created and updated via `write` method.
|
482
|
+
|
483
|
+
- In original collection, *all* items with keys that are not listed in given
|
484
|
+
array of hash params considered to be marked for destruction and corresponding
|
485
|
+
items will be removed from mapped collection. The same concerns for *all*
|
486
|
+
current items in collection, which have `key` mapped to `nil`.
|
487
|
+
|
488
|
+
Example:
|
489
|
+
|
490
|
+
```ruby
|
491
|
+
company.departments.map(&:id) # => [1, 2]
|
492
|
+
company.departments.map(&:name) # => ["R & D", "QA"]
|
493
|
+
|
494
|
+
company_mapper.write(departments: [
|
495
|
+
{key: 1, department_name: "D & R"},
|
496
|
+
{department_name: "Testers"}
|
497
|
+
])
|
498
|
+
|
499
|
+
company.departments.map(&:id) # => [1, nil]
|
500
|
+
company.departments.map(&:name) # => ["D & R", "Testers"]
|
501
|
+
```
|
502
|
+
|
503
|
+
#### Attribute Methods
|
504
|
+
|
505
|
+
When you use mappers to map collection of items, attribute method behavior
|
506
|
+
is slightly different. For example, when you have
|
507
|
+
|
508
|
+
```ruby
|
509
|
+
class PersonMapper < Flatter::Mapper
|
510
|
+
map :first_name, :last_name
|
511
|
+
mount :phone
|
512
|
+
end
|
513
|
+
|
514
|
+
class DepartmentMapper < Flatter::Mapper
|
515
|
+
mount :people do
|
516
|
+
key :id
|
517
|
+
end
|
518
|
+
end
|
519
|
+
```
|
520
|
+
|
521
|
+
`mapper.first_name` no longer able to return specific value, since it's
|
522
|
+
not clear which first name should it be. Thus, when mapper is mounted as
|
523
|
+
a collection item, instead of singular value accessors you gain pluralized
|
524
|
+
reader methods:
|
525
|
+
|
526
|
+
```ruby
|
527
|
+
# all first_names of all people of the mapped department:
|
528
|
+
mapper.first_names # => ["John", "Derek"]
|
529
|
+
```
|
530
|
+
|
531
|
+
The same concerns for all nested (singular or collection) mappings and mountings
|
532
|
+
under collection mapper:
|
533
|
+
|
534
|
+
```ruby
|
535
|
+
# all phone number of all people of the mapped department
|
536
|
+
mapper.phone_numbers # => ["111-222-3333", "222-111-33333"]
|
537
|
+
|
538
|
+
# all the people
|
539
|
+
mapper.people # =>
|
540
|
+
# [{"first_name" => "John", "last_name" => "Smith", "key" => 1, "phone_number" => "111-222-3333"},
|
541
|
+
# {"first_name" => "Derek", "last_name" => "Parker", "key" => 2, "phone_number" => "222-111-3333"}]
|
542
|
+
|
543
|
+
# all phones (note the :phone mapper mounted on :people, opposed to it's :phone_number mapping)
|
544
|
+
mapper.phones # =>
|
545
|
+
# [{"phone_number" => "111-222-3333"}, {"phone_number" => "222-111-33333"}]
|
546
|
+
```
|
547
|
+
|
548
|
+
Please note that attempt to use writer method to update collection of mappings,
|
549
|
+
such as `first_names=` will raise runtime `"Cannot directly write to a collection"`
|
550
|
+
error. To update collection items and their data you have to use `write`/`apply`
|
551
|
+
methods to utilize `key`-dependent logic to properly update your collection items
|
552
|
+
alongside with all nested mappings/mountings they might have.
|
553
|
+
|
554
|
+
#### Errors
|
555
|
+
|
556
|
+
Since all errors after validation process are consolidated into a plain hash
|
557
|
+
of errors, there is a need to distinct errors of one collection items from
|
558
|
+
another ones. To achieve this, Flatter adds special prefix to error key, which is
|
559
|
+
formed from collection name and item **index** (not id or key). For example:
|
560
|
+
|
561
|
+
```ruby
|
562
|
+
class Person
|
563
|
+
include ActiveModel::Model
|
564
|
+
|
565
|
+
attr_accessor :name, :age
|
566
|
+
end
|
567
|
+
|
568
|
+
class Department
|
569
|
+
include ActiveModel::Model
|
570
|
+
|
571
|
+
attr_accessor :name
|
572
|
+
|
573
|
+
def people
|
574
|
+
@people ||= []
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
class PersonMapper < Flatter::Mapper
|
579
|
+
map :age, person_name: :name
|
580
|
+
|
581
|
+
validates :age, numericality: {only_integer: true, greater_than_or_equal_to: 1}
|
582
|
+
end
|
583
|
+
|
584
|
+
class DepartmentMapper < Flatter::Mapper
|
585
|
+
map department_name: :name
|
586
|
+
|
587
|
+
mount :people
|
588
|
+
end
|
589
|
+
|
590
|
+
department = Department.new
|
591
|
+
mapper = DepartmentMapper.new(department)
|
592
|
+
mapper.apply(people: [
|
593
|
+
{ person_name: "John", age: "22.5" },
|
594
|
+
{ person_name: "Dave", age: "18" },
|
595
|
+
{ person_name: "Kile", age: "0" }
|
596
|
+
]) # => false
|
597
|
+
|
598
|
+
mapper.errors.messages # =>
|
599
|
+
# { :"people.0.age" => ["must be an integer"],
|
600
|
+
# :"people.2.age" => ["must be greater than or equal to 1"] }
|
601
|
+
```
|
602
|
+
|
603
|
+
### Extensions
|
604
|
+
|
605
|
+
Aside from core functionality and behavior defined in this gem, there is also
|
606
|
+
[flatter-extensions](https://github.com/akuzko/flatter-extensions) gem that
|
607
|
+
provides extensions to help you use mappers more efficiently. At this point there
|
608
|
+
are following extensions:
|
609
|
+
|
610
|
+
- `:multiparam` Allows you to define multiparam mappings by adding `:multiparam`
|
611
|
+
option to mapping. Works pretty much like `Rails` multiparam attribute assignment.
|
612
|
+
- `:skipping` Allows to skip mappers (mountings) from the processing chain by
|
613
|
+
calling `skip!` method on a particular mapper. When used in before validation
|
614
|
+
callbacks, for example, allows you to ignore some extra processing.
|
615
|
+
- `:order` Allows you to manually control processing order of mappers and their
|
616
|
+
mountings. Provides `:index` option for mountings, which can be either a Number,
|
617
|
+
which means order for both validation and saving routines, or a hash like
|
618
|
+
`index: {validate: -1, save: 2}`. By default all mappers have index of `0` and
|
619
|
+
processed from top to bottom.
|
620
|
+
- `:active_record` Very useful extension that allows you to effectively use mappers
|
621
|
+
when working with ActiveRecord objects with defined relationships and associations
|
622
|
+
that form a structured graph that you want to work with as a plain data structure.
|
623
|
+
|
624
|
+
### Public API
|
625
|
+
|
626
|
+
Some methods of the public API that should help you building your mappers:
|
627
|
+
|
628
|
+
#### Mapper methods
|
629
|
+
|
630
|
+
- `name` - return a mapper name.
|
631
|
+
|
632
|
+
- `target` - returns mapper target - an object mapper extracts values from
|
633
|
+
and assigns values to using defined mappings.
|
634
|
+
|
635
|
+
- `mappings` - returns a plain hash of all the mappings (including ones related
|
636
|
+
to mounted mappers) in a form of `{name <String> => mapping object <Mapping>}`.
|
637
|
+
Note that for empty collections there will be no mentions of item mappings at all.
|
638
|
+
If collection has only one item, it's mappings will be listed as the rest.
|
639
|
+
If there are multiple same-named mappings, they will be listed in array.
|
640
|
+
|
641
|
+
- `mapping_names` - returns a list of all **available** mappings. This differs
|
642
|
+
from `mappings.keys`, since `mapping_names` represents a list of all mappings
|
643
|
+
that may be used by mapper. Essentially, this is the list of mapper's
|
644
|
+
attribute accessor methods.
|
645
|
+
|
646
|
+
- `mapping(name)` - returns a mapping with a `name` name. The same as `mappings[name.to_s]`
|
647
|
+
|
648
|
+
- `mountings` - returns a plain hash of all mounted mappers (including all used traits)
|
649
|
+
in a form of `{name <String> => mapper object <Mapper>}`. Just like in case with
|
650
|
+
mappings, mountings with same name will be listed in array.
|
651
|
+
|
652
|
+
- `mounting_names` - returns a list of all **available** mountings. This represents
|
653
|
+
a list of reader methods that will return a sub-hash of specific mounting or
|
654
|
+
an array of such hashes for collections.
|
655
|
+
|
656
|
+
- `mounting(name)` - finds a mounting by name. Best used for addressing singular
|
657
|
+
mountings within a mapper, but also has other internal usages under the hood
|
658
|
+
(see sources of `Flatter::Mapper::AttributeMethods` module).
|
659
|
+
|
660
|
+
- `read` - returns a hash of all values obtained by all mappings in a form of
|
661
|
+
`{name <String> => value <Object>}`.
|
662
|
+
|
663
|
+
- `write(params)` - for each defined mapping, including mappings from mounted
|
664
|
+
mappers and traits, passes value from params that corresponds to mapping name
|
665
|
+
to that mapping's `write` method.
|
666
|
+
|
667
|
+
- `valid?` - runs validation routines and returns `true` if there are no errors.
|
668
|
+
|
669
|
+
- `errors` - returns mapper's `Errors` object.
|
670
|
+
|
671
|
+
- `save` - runs save routines. If target object responds to `save` method, will
|
672
|
+
call it and return it's value. Returns true otherwise. If multiple mappers
|
673
|
+
are mounted, returns `true` only if all mounted mappers returned `true` on saving
|
674
|
+
their targets.
|
675
|
+
|
676
|
+
- `apply(params)` - writes `params`, runs validation and runs save routines if
|
677
|
+
validation passed.
|
678
|
+
|
679
|
+
- `collection?` - returns `true` if mapper is a collection mapper.
|
680
|
+
|
681
|
+
- `trait?` - returns `true` if mapper is a trait mapper.
|
682
|
+
|
683
|
+
#### Mapping methods
|
684
|
+
|
685
|
+
- `name` - returns mapping name.
|
686
|
+
|
687
|
+
- `target_attribute` - returns an attribute name which mapping maps to.
|
688
|
+
|
689
|
+
- `read` - reads value from target according to setup.
|
690
|
+
|
691
|
+
- `read!` - tries to directly read value from target based on mapping's `target_attribute`
|
692
|
+
property. Ignores `:reader` option.
|
693
|
+
|
694
|
+
- `write(value)` - assigns a `value` to target according to setup.
|
695
|
+
|
696
|
+
- `write!(value)` - tries to directly assign a value to target based on mapping's
|
697
|
+
`target_attribute` property. Ignores `:writer` option.
|
25
698
|
|
26
699
|
## Development
|
27
700
|
|
28
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
701
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
702
|
+
`rake spec` to run the tests. You can also run `bin/console` for an interactive
|
703
|
+
prompt that will allow you to experiment.
|
29
704
|
|
30
705
|
## Contributing
|
31
706
|
|
@@ -35,4 +710,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/akuzko
|
|
35
710
|
## License
|
36
711
|
|
37
712
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
38
|
-
|