flatter 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://secure.travis-ci.org/akuzko/flatter.png)](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
|
-
|