Compare commits
784 Commits
github-rev
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
098746782c | ||
|
|
db043e35df | ||
|
|
8d88f85a6b | ||
|
|
6dae4d4617 | ||
|
|
751c6bb089 | ||
|
|
813bb27a07 | ||
|
|
bb2025276a | ||
|
|
99b6217a42 | ||
|
|
50d8d250f4 | ||
|
|
fd46656f86 | ||
|
|
ebe9ef9986 | ||
|
|
87a7bd851c | ||
|
|
d6b0128cab | ||
|
|
408bde2425 | ||
|
|
3098f8e7bb | ||
|
|
674de0c342 | ||
|
|
b1606ead5f | ||
|
|
50d10a6853 | ||
|
|
c713a23bf5 | ||
|
|
a08ba893d6 | ||
|
|
3930492012 | ||
|
|
9d34a114e0 | ||
|
|
eec57547b6 | ||
|
|
465826427e | ||
|
|
9f27293364 | ||
|
|
8dbb8b547c | ||
|
|
399713dbfd | ||
|
|
8ae87acabc | ||
|
|
a16d18f635 | ||
|
|
7e640910ef | ||
|
|
cdb82df5dd | ||
|
|
9f860bafa8 | ||
|
|
691277c14f | ||
|
|
09a1f31c32 | ||
|
|
51e4f70224 | ||
|
|
0f18db37d0 | ||
|
|
c32280d4dc | ||
|
|
eaa7cc3536 | ||
|
|
6a4141363f | ||
|
|
e0eb39df7b | ||
|
|
c624c6ec73 | ||
|
|
8a1d9f8c48 | ||
|
|
7ac1d0e571 | ||
|
|
e781ead0d4 | ||
|
|
54d82b7417 | ||
|
|
20ac7b96d3 | ||
|
|
afa50afc30 | ||
|
|
ea61b4ee8c | ||
|
|
dd655697a7 | ||
|
|
99dba73b0d | ||
|
|
05a2c99849 | ||
|
|
2707b0bc01 | ||
|
|
0f811cbd47 | ||
|
|
234b69846b | ||
|
|
857c82691d | ||
|
|
66af2bf345 | ||
|
|
956bfaf190 | ||
|
|
7690ba7944 | ||
|
|
c456fd5e8c | ||
|
|
5a9cc70ee0 | ||
|
|
958dbd8c9c | ||
|
|
0cb962c52b | ||
|
|
c48ae3572e | ||
|
|
a2b4a96776 | ||
|
|
b11cd11dd1 | ||
|
|
b83f2c60a1 | ||
|
|
e5d59be0dc | ||
|
|
633b653b80 | ||
|
|
6a9bd2687b | ||
|
|
dcca4baf8f | ||
|
|
09001acec7 | ||
|
|
4f00e0f97a | ||
|
|
4ca6073945 | ||
|
|
af8dbe89dc | ||
|
|
1b37d354b8 | ||
|
|
0747420c3e | ||
|
|
7a9c012fdd | ||
|
|
daab4ea561 | ||
|
|
52c30e08da | ||
|
|
973bc03066 | ||
|
|
459bfb863d | ||
|
|
26f0e7e384 | ||
|
|
d41e8d3c8c | ||
|
|
93c0a5c3e4 | ||
|
|
ec98991888 | ||
|
|
f03df68add | ||
|
|
704673bbe0 | ||
|
|
c9ce8e70f1 | ||
|
|
4a909589bc | ||
|
|
8f937de2fc | ||
|
|
6cc31af211 | ||
|
|
bcabb0e024 | ||
|
|
9338df7a65 | ||
|
|
e508420c9d | ||
|
|
67c0a49c43 | ||
|
|
b7cf3e9cdd | ||
|
|
88f4d8e399 | ||
|
|
dca0524825 | ||
|
|
c247fb8bd1 | ||
|
|
853e569b7a | ||
|
|
d236caa864 | ||
|
|
1c8bc90ae6 | ||
|
|
c55800e6a2 | ||
|
|
b7974f64b3 | ||
|
|
dc859b6906 | ||
|
|
76a83fa781 | ||
|
|
e21bafdbb2 | ||
|
|
5665290afc | ||
|
|
02b40b1ec8 | ||
|
|
526b6a24db | ||
|
|
e176a9d44e | ||
|
|
61023465ae | ||
|
|
097177c8cd | ||
|
|
79ae468c2c | ||
|
|
9077997b3c | ||
|
|
6765b64cee | ||
|
|
1da815e969 | ||
|
|
7f1a69b194 | ||
|
|
8848226647 | ||
|
|
676f5f556f | ||
|
|
66b2f61cc2 | ||
|
|
989479c9b5 | ||
|
|
56226e27c5 | ||
|
|
5d864afc4c | ||
|
|
a24ed455f9 | ||
|
|
34eae472dc | ||
|
|
4fc19e04bb | ||
|
|
bcad84d700 | ||
|
|
86e5cb30fd | ||
|
|
39ee0f7f0e | ||
|
|
f9f91eaef7 | ||
|
|
92bafa11f8 | ||
|
|
8f233cfa9b | ||
|
|
91e49881e6 | ||
|
|
ddf43709de | ||
|
|
050121a64b | ||
|
|
c50019b93e | ||
|
|
ed1a76b493 | ||
|
|
1da84b5731 | ||
|
|
620f824c03 | ||
|
|
8a5080ddb7 | ||
|
|
42648b5577 | ||
|
|
7688e1db93 | ||
|
|
8d8ce021f5 | ||
|
|
cb245e05e6 | ||
|
|
36df3c5157 | ||
|
|
2c03c34f33 | ||
|
|
c76c30aa7f | ||
|
|
801aaf834f | ||
|
|
1b239f3317 | ||
|
|
e3b059b718 | ||
|
|
b10f2d5b9d | ||
|
|
d4d1a7f993 | ||
|
|
58c66eec7d | ||
|
|
cc5f79d9b9 | ||
|
|
5ac1eb38a2 | ||
|
|
67bfc9770d | ||
|
|
19ffa8851b | ||
|
|
f43c4f5848 | ||
|
|
eeb9a4bfb1 | ||
|
|
a7722b950e | ||
|
|
0430c5b580 | ||
|
|
2dc5573b80 | ||
|
|
74edd2e56b | ||
|
|
e1a81094aa | ||
|
|
a4a26feb8f | ||
|
|
7076652fb2 | ||
|
|
7f0176b6e4 | ||
|
|
d9dc95002c | ||
|
|
30ab200964 | ||
|
|
26fd9ea250 | ||
|
|
f797bd4837 | ||
|
|
2b4c8687fc | ||
|
|
aac4b5072b | ||
|
|
ad7fb691b7 | ||
|
|
9a02e73677 | ||
|
|
bd4eb45572 | ||
|
|
f27593af3b | ||
|
|
1b79c4b889 | ||
|
|
735aa29db7 | ||
|
|
40abed6e58 | ||
|
|
e4d78656d9 | ||
|
|
be96163343 | ||
|
|
6f14d652cf | ||
|
|
ccf0f27778 | ||
|
|
81ef8e38b4 | ||
|
|
7900285dda | ||
|
|
8af6da6ee4 | ||
|
|
c49a0e1b44 | ||
|
|
94c25a786c | ||
|
|
6492e04af1 | ||
|
|
72ae460a88 | ||
|
|
9a7d85aee1 | ||
|
|
3a15600b40 | ||
|
|
bb22a8b659 | ||
|
|
25a8554f4c | ||
|
|
08ba09c0fb | ||
|
|
5dc5dfaa70 | ||
|
|
75453c78b6 | ||
|
|
f5851c60ba | ||
|
|
16559af89f | ||
|
|
d4bd02f067 | ||
|
|
e2df6cf7fb | ||
|
|
7f66bc531f | ||
|
|
4832294b48 | ||
|
|
9ec57c724f | ||
|
|
ce8384d3a6 | ||
|
|
37ec1d98fd | ||
|
|
6e03319932 | ||
|
|
b05a8d3057 | ||
|
|
8b48fcc788 | ||
|
|
3a3b17728a | ||
|
|
0fd44a3716 | ||
|
|
456a2b9a92 | ||
|
|
a5018746d9 | ||
|
|
1547358dba | ||
|
|
efc39e48a8 | ||
|
|
e5c94535fd | ||
|
|
2ea559bcb7 | ||
|
|
988dcc1e58 | ||
|
|
7e489a447c | ||
|
|
4c9767df80 | ||
|
|
20cefb05cc | ||
|
|
1513d3f1ef | ||
|
|
3ed52a57bb | ||
|
|
f6483e51ec | ||
|
|
c93d7d5e59 | ||
|
|
4cffd20ccb | ||
|
|
f8f9f80d90 | ||
|
|
b8c3e2095a | ||
|
|
5d789de7c9 | ||
|
|
5273cf675f | ||
|
|
4cabb8a142 | ||
|
|
3a8e052d77 | ||
|
|
1c36aa52bc | ||
|
|
fe3a65987b | ||
|
|
301472f453 | ||
|
|
ab1b23bde1 | ||
|
|
541623ca9c | ||
|
|
e524388f19 | ||
|
|
5321e825d4 | ||
|
|
234ae6c94f | ||
|
|
cf9f2e6ca7 | ||
|
|
9add845e75 | ||
|
|
357c2c2794 | ||
|
|
e4e3a11ca4 | ||
|
|
651a8beaa0 | ||
|
|
1644a8fdc4 | ||
|
|
6b87fb85e6 | ||
|
|
00285dfb58 | ||
|
|
bce55c32b9 | ||
|
|
94aae0f2ea | ||
|
|
176e15ab3a | ||
|
|
b1d3eac764 | ||
|
|
425edb3525 | ||
|
|
e679d9f95c | ||
|
|
b117ff9fc8 | ||
|
|
da585dc703 | ||
|
|
9e85e99a15 | ||
|
|
c9a6414631 | ||
|
|
984dfef8fa | ||
|
|
8e80404b3d | ||
|
|
598d5caf96 | ||
|
|
ee0c95cf2b | ||
|
|
aa045a0568 | ||
|
|
ee881f9931 | ||
|
|
216c6113f4 | ||
|
|
69ac21101a | ||
|
|
f3f62de1cb | ||
|
|
1ca75a9d6c | ||
|
|
807c00e965 | ||
|
|
6052a7dde9 | ||
|
|
68d537ab2c | ||
|
|
2a21b686c1 | ||
|
|
78a0ebfaeb | ||
|
|
2b7ac7b016 | ||
|
|
1fc58a488e | ||
|
|
aa047f71cf | ||
|
|
8fc086fdc5 | ||
|
|
c58a89a0b7 | ||
|
|
209e19dcd6 | ||
|
|
c271353120 | ||
|
|
5692bbdc00 | ||
|
|
d0834c5c67 | ||
|
|
b4bf1242e2 | ||
|
|
6f2e85c7ae | ||
|
|
e28b12af4e | ||
|
|
97be74e576 | ||
|
|
c6a4f0b06c | ||
|
|
9f5f0aad4d | ||
|
|
3c891ac87a | ||
|
|
175ec19e43 | ||
|
|
652ce9be1f | ||
|
|
020d3cc91b | ||
|
|
06d10eaf22 | ||
|
|
6400511dd5 | ||
|
|
272fde5dda | ||
|
|
d0bcc34333 | ||
|
|
423be80a94 | ||
|
|
b0b8d5325b | ||
|
|
d882faff57 | ||
|
|
a44c73b1a6 | ||
|
|
d4ecaf36cd | ||
|
|
3d3ac1de55 | ||
|
|
d496494327 | ||
|
|
658951199d | ||
|
|
159cd2179c | ||
|
|
f6644252cc | ||
|
|
cf82e6aa3d | ||
|
|
6eb3e51920 | ||
|
|
769069d46c | ||
|
|
95c4477021 | ||
|
|
f0c4dcc3db | ||
|
|
24bf9f9762 | ||
|
|
92fda1cc29 | ||
|
|
293bb13d11 | ||
|
|
e4b48500ae | ||
|
|
fc23337607 | ||
|
|
1a40c363f7 | ||
|
|
bab985d49e | ||
|
|
a2e22ea9f7 | ||
|
|
5965656abe | ||
|
|
22678eac1f | ||
|
|
d52d0f118a | ||
|
|
4e2cd7e413 | ||
|
|
33b6d894da | ||
|
|
cc242d325e | ||
|
|
27f782aeac | ||
|
|
6b3dea3815 | ||
|
|
295c68f69a | ||
|
|
1e6f44a91d | ||
|
|
c79cb0951e | ||
|
|
3a7242c7ba | ||
|
|
12ed03063b | ||
|
|
f70e670c0f | ||
|
|
944948ee8b | ||
|
|
8e36f6d666 | ||
|
|
4f7901b82d | ||
|
|
2e291ff081 | ||
|
|
6a411e839b | ||
|
|
b164a8773d | ||
|
|
ff6abe587a | ||
|
|
5732b1efdf | ||
|
|
1acfaae19a | ||
|
|
9c25cc3d4f | ||
|
|
3eacee9e5e | ||
|
|
6405134059 | ||
|
|
2d9f7b3c58 | ||
|
|
1e609e29e4 | ||
|
|
746c088dba | ||
|
|
4eb44fa1ab | ||
|
|
f94b7ecb6e | ||
|
|
9f25806d5f | ||
|
|
4d3391a0cd | ||
|
|
1c1cb42a47 | ||
|
|
2985e35db2 | ||
|
|
4950c1f8a5 | ||
|
|
7f0515b285 | ||
|
|
fc1009a01f | ||
|
|
5cfd2fec5f | ||
|
|
19981517ea | ||
|
|
db43557fd0 | ||
|
|
c2955052c4 | ||
|
|
ae14eb762a | ||
|
|
4de1334bac | ||
|
|
5c74d82970 | ||
|
|
587457f384 | ||
|
|
f82f903d8e | ||
|
|
fb5b08e4ee | ||
|
|
c4b4d9a81b | ||
|
|
6eff1512ff | ||
|
|
a782b0fcff | ||
|
|
47bf9162f8 | ||
|
|
066ce61fe5 | ||
|
|
093589b698 | ||
|
|
22eb742fe3 | ||
|
|
1bec4fc2d3 | ||
|
|
f3dc78ec2d | ||
|
|
fa5be2cee6 | ||
|
|
2571a72727 | ||
|
|
a2418b851c | ||
|
|
44525fa53f | ||
|
|
193a6cbc2b | ||
|
|
5f88beaee1 | ||
|
|
fb7b9011a7 | ||
|
|
ec1f44a0ff | ||
|
|
73c8ace698 | ||
|
|
ed29ca476d | ||
|
|
b7f7065902 | ||
|
|
4c784807a9 | ||
|
|
5e0d955e6f | ||
|
|
ae322bf125 | ||
|
|
0fa5ccdc44 | ||
|
|
ed15668ced | ||
|
|
99b48e9cb9 | ||
|
|
4fb6d2a908 | ||
|
|
f4069f8e06 | ||
|
|
d34a138a5f | ||
|
|
ed477e2f79 | ||
|
|
c7ffb811cb | ||
|
|
af1ea58570 | ||
|
|
bf0a5e7236 | ||
|
|
3fd838e18c | ||
|
|
d641f119bb | ||
|
|
d0cc019ab1 | ||
|
|
1e2e25bc89 | ||
|
|
d5739b1ed0 | ||
|
|
1a4507e9b1 | ||
|
|
61d026b490 | ||
|
|
9beadc85a6 | ||
|
|
36a79809ff | ||
|
|
b677ad6196 | ||
|
|
4971735f93 | ||
|
|
0597f7934f | ||
|
|
199b34e3da | ||
|
|
873fc8603e | ||
|
|
efdf5e783a | ||
|
|
3f9d37ebea | ||
|
|
0c52681f36 | ||
|
|
29e23b8489 | ||
|
|
bfdc96a35d | ||
|
|
c8298bae3c | ||
|
|
9399be03d8 | ||
|
|
67fc62e6d7 | ||
|
|
ecf3d76f9c | ||
|
|
2478828a82 | ||
|
|
0370a83fcf | ||
|
|
3e1438d773 | ||
|
|
0644728d40 | ||
|
|
4aea0d4835 | ||
|
|
481b7a611f | ||
|
|
44e5615885 | ||
|
|
d9440640bc | ||
|
|
e546dc09d9 | ||
|
|
fffba6f84d | ||
|
|
1d68b9bd7b | ||
|
|
9295ab638a | ||
|
|
e43e3b464e | ||
|
|
ab6b277deb | ||
|
|
86b502d053 | ||
|
|
49e2bcb5b8 | ||
|
|
f5b1739b99 | ||
|
|
6d205da711 | ||
|
|
939dfbf062 | ||
|
|
793b5fa570 | ||
|
|
571d511a47 | ||
|
|
652a2a57f1 | ||
|
|
de2f2bbc23 | ||
|
|
70cd8d032a | ||
|
|
79697df4b5 | ||
|
|
24c6e13481 | ||
|
|
c70543576b | ||
|
|
659fac6db7 | ||
|
|
d9d8f97394 | ||
|
|
5537d67580 | ||
|
|
ae161c5246 | ||
|
|
6e148c59fd | ||
|
|
55c264734b | ||
|
|
c4812ac2b3 | ||
|
|
b7c3544347 | ||
|
|
f69daa4330 | ||
|
|
8eed7558b6 | ||
|
|
ac6feeb36e | ||
|
|
7f3e346b20 | ||
|
|
eac31d82fb | ||
|
|
248f3a881d | ||
|
|
c3203f3f02 | ||
|
|
dcb2f9de60 | ||
|
|
df03aa9d4b | ||
|
|
cf3bb6e267 | ||
|
|
bacacf534b | ||
|
|
a4c25f854a | ||
|
|
d135e197fb | ||
|
|
837bdd5ee6 | ||
|
|
2807e4e4d7 | ||
|
|
1a6be4ee76 | ||
|
|
bc2a670ef5 | ||
|
|
1862668365 | ||
|
|
24a593d027 | ||
|
|
4b7cd96751 | ||
|
|
06746f3649 | ||
|
|
4986d7dec7 | ||
|
|
61024f8376 | ||
|
|
61b3f667b7 | ||
|
|
027e8c1c4c | ||
|
|
13a39ca0ba | ||
|
|
e21c9c327c | ||
|
|
b3fc341bd0 | ||
|
|
e51c31f765 | ||
|
|
eb2178ab43 | ||
|
|
df762eefe1 | ||
|
|
34626329ba | ||
|
|
0e7dde3d7a | ||
|
|
338a6607df | ||
|
|
b7d96f174f | ||
|
|
bcda70bfe0 | ||
|
|
3b860fd086 | ||
|
|
43910cd458 | ||
|
|
6c793549c9 | ||
|
|
005fddfeab | ||
|
|
5e1398101f | ||
|
|
7753325d73 | ||
|
|
f5d02a7b4f | ||
|
|
78a885af73 | ||
|
|
cba71b4695 | ||
|
|
0937988d92 | ||
|
|
08dd953e22 | ||
|
|
b4ddc0d779 | ||
|
|
b523dbd04a | ||
|
|
537a64ba87 | ||
|
|
22712f9863 | ||
|
|
8fd0a3ef02 | ||
|
|
1cd58fb6fa | ||
|
|
fff1f75b68 | ||
|
|
439be82a87 | ||
|
|
7a474371f2 | ||
|
|
b5a8f7281f | ||
|
|
9f0736cc00 | ||
|
|
e963954227 | ||
|
|
45229532cc | ||
|
|
62b4804a48 | ||
|
|
f9f70db9e3 | ||
|
|
16706aeba4 | ||
|
|
d2ef95c24a | ||
|
|
6bc1d00d2c | ||
|
|
7f817e6d7f | ||
|
|
c60379122c | ||
|
|
e1cbe5334a | ||
|
|
a7a769333a | ||
|
|
c8f6f64a6d | ||
|
|
2d1b8dd7bc | ||
|
|
1197413b7c | ||
|
|
3acf6449d8 | ||
|
|
71e9154730 | ||
|
|
7c0f041ac2 | ||
|
|
f55733e05c | ||
|
|
46833e7bcc | ||
|
|
fcc87d16b4 | ||
|
|
aa16e313c1 | ||
|
|
4ae988aa7e | ||
|
|
c07f533d8d | ||
|
|
9708508807 | ||
|
|
6b8fad56a0 | ||
|
|
c4c0069646 | ||
|
|
3aeda47b9e | ||
|
|
146163d327 | ||
|
|
a103772f1e | ||
|
|
8aef509d18 | ||
|
|
9c93241600 | ||
|
|
f4633f60ba | ||
|
|
05aa99685c | ||
|
|
4f87c6b783 | ||
|
|
5280349bb7 | ||
|
|
630431078c | ||
|
|
b11a90f1a3 | ||
|
|
34a0b741fd | ||
|
|
a4171df623 | ||
|
|
9ee7015afc | ||
|
|
f6951ecd1f | ||
|
|
1376dcedd7 | ||
|
|
d746ee32cc | ||
|
|
2586cfc384 | ||
|
|
22fce6512e | ||
|
|
502175f690 | ||
|
|
fa4620138a | ||
|
|
1eaafc5fa1 | ||
|
|
c08877395c | ||
|
|
72c5455537 | ||
|
|
6da6e309c8 | ||
|
|
d2d4a51d53 | ||
|
|
243ff42dc5 | ||
|
|
f7c83bef13 | ||
|
|
bfc8ab3155 | ||
|
|
2c268ab2a2 | ||
|
|
21ae50296d | ||
|
|
19cf0c2560 | ||
|
|
2e7860f6c5 | ||
|
|
870fc7c475 | ||
|
|
4fec757b93 | ||
|
|
aa6194c4d5 | ||
|
|
7b5c965ac2 | ||
|
|
b403d42caf | ||
|
|
b3c5c46e3b | ||
|
|
2f5f86f11d | ||
|
|
71c9191a41 | ||
|
|
a0ebbe0c4a | ||
|
|
858a162a18 | ||
|
|
ac469e7fb2 | ||
|
|
c3370df403 | ||
|
|
67279cb29d | ||
|
|
99b3c72c19 | ||
|
|
952db2ab7e | ||
|
|
b3298754e8 | ||
|
|
e0ae9384b5 | ||
|
|
7dd64d3891 | ||
|
|
d78160af02 | ||
|
|
43103501a1 | ||
|
|
8fd199d897 | ||
|
|
2598eb86c1 | ||
|
|
66b138582f | ||
|
|
246cb8b309 | ||
|
|
67dfe13d39 | ||
|
|
2cdc967f85 | ||
|
|
c558644076 | ||
|
|
7539afe392 | ||
|
|
45ef45f00a | ||
|
|
471b181b22 | ||
|
|
78b750a8cc | ||
|
|
16c3e87901 | ||
|
|
ad7743d446 | ||
|
|
91643209f1 | ||
|
|
ea63804d9c | ||
|
|
ecd90e21da | ||
|
|
0e635b5830 | ||
|
|
83aef840fd | ||
|
|
16734e1fb7 | ||
|
|
432ea57297 | ||
|
|
8df8f6b8fe | ||
|
|
6f2f896401 | ||
|
|
3a063d53f2 | ||
|
|
221474aca4 | ||
|
|
0763e6cfd6 | ||
|
|
cb77448cf4 | ||
|
|
d7f69bc9c4 | ||
|
|
503a2ca40b | ||
|
|
3054d9f9df | ||
|
|
a19b6f9b75 | ||
|
|
297631c4df | ||
|
|
d0638371d3 | ||
|
|
4e6485461d | ||
|
|
8695996ad4 | ||
|
|
6ea33b9cd0 | ||
|
|
d422ec86a4 | ||
|
|
49c833f034 | ||
|
|
8f02c9401e | ||
|
|
259970d098 | ||
|
|
ff4b646114 | ||
|
|
e69396ca1a | ||
|
|
4da5c614d1 | ||
|
|
7eecee4f26 | ||
|
|
c82064b9a1 | ||
|
|
663512c6e5 | ||
|
|
6cbdbebc44 | ||
|
|
6f919235e9 | ||
|
|
20e14e4d73 | ||
|
|
96d2ab95b3 | ||
|
|
9852a8b920 | ||
|
|
2cebc434fd | ||
|
|
4913f6d817 | ||
|
|
f522bd0105 | ||
|
|
7a45248206 | ||
|
|
aea453c81c | ||
|
|
9d9d9b536b | ||
|
|
cd24d09722 | ||
|
|
48f46d7dc8 | ||
|
|
74444a6aa6 | ||
|
|
f3d15d800b | ||
|
|
c6ee880f37 | ||
|
|
4a3ed6d193 | ||
|
|
0ce245c251 | ||
|
|
cf93b9b387 | ||
|
|
4ea3f80368 | ||
|
|
6f046f7092 | ||
|
|
7c8eefc9dd | ||
|
|
981e5b3f2d | ||
|
|
d58c7adc90 | ||
|
|
796b8eae8f | ||
|
|
2550434089 | ||
|
|
5191930bf2 | ||
|
|
a98449147c | ||
|
|
5e915245d8 | ||
|
|
06f8b6e3b3 | ||
|
|
480b501d4d | ||
|
|
8c90017634 | ||
|
|
9578541d62 | ||
|
|
75ecc76925 | ||
|
|
9218025916 | ||
|
|
45705984f7 | ||
|
|
4f69bcc943 | ||
|
|
762c382cc6 | ||
|
|
979cfc54fb | ||
|
|
a33a4b422d | ||
|
|
e45915eac8 | ||
|
|
135a67fe10 | ||
|
|
335015a5d0 | ||
|
|
90ce2e097f | ||
|
|
48a518c4ef | ||
|
|
43d0207bef | ||
|
|
d7a1accdb2 | ||
|
|
10e0de526c | ||
|
|
59969fd280 | ||
|
|
9e7cbce6d3 | ||
|
|
737b44e1cf | ||
|
|
fc35c6aeaf | ||
|
|
a4ed59fbbd | ||
|
|
6020f8f7db | ||
|
|
e106acbf1a | ||
|
|
c800628e5e | ||
|
|
9db0959aad | ||
|
|
b70e9b8706 | ||
|
|
ccb6bcb2dc | ||
|
|
acf6d3e3b9 | ||
|
|
11fcaee6e9 | ||
|
|
a2784a003e | ||
|
|
2e8674b2db | ||
|
|
0d9f898d88 | ||
|
|
fe1a19708a | ||
|
|
ec793ea38f | ||
|
|
136012cc11 | ||
|
|
ac0b801ad9 | ||
|
|
6bf88174f8 | ||
|
|
688bad8949 | ||
|
|
bd0c716eac | ||
|
|
d38c81f4ea | ||
|
|
73f2edc5f5 | ||
|
|
2123a7efb9 | ||
|
|
09a7a40615 | ||
|
|
1c83d3ce03 | ||
|
|
0093075488 | ||
|
|
39c13ef42f | ||
|
|
87046ddaf0 | ||
|
|
9d5aea54e9 | ||
|
|
11428f538d | ||
|
|
2e493d3e7b | ||
|
|
92022bec3f | ||
|
|
b64f46f70b | ||
|
|
570e4ea968 | ||
|
|
bf486e6ab3 | ||
|
|
d2094446df | ||
|
|
2eb7a4e5c2 | ||
|
|
387d0c0a13 | ||
|
|
a1b49d9511 | ||
|
|
ca63bb7d7a | ||
|
|
717c9c6c07 | ||
|
|
0e08e8497b | ||
|
|
9b94578446 | ||
|
|
6157b36ff3 | ||
|
|
c64937089c | ||
|
|
01a2843402 | ||
|
|
3ba1f84346 | ||
|
|
b7e790a9f3 | ||
|
|
c74ff1fb45 | ||
|
|
3a5cb4a9f5 | ||
|
|
8a2bccde86 | ||
|
|
31962394cb | ||
|
|
ed74feb699 | ||
|
|
d540792a86 | ||
|
|
f36efb163f | ||
|
|
32a02c06e0 | ||
|
|
de53d47967 | ||
|
|
58c11dc256 | ||
|
|
8233151c06 | ||
|
|
d68ecf4de9 | ||
|
|
1dd5b4a924 | ||
|
|
fc1e034e57 | ||
|
|
df905c7cea | ||
|
|
e348e835ef | ||
|
|
adb531041c | ||
|
|
2473d1fee1 | ||
|
|
2dc9f88d44 | ||
|
|
9452da44e9 | ||
|
|
054b880214 | ||
|
|
9e3df793b9 | ||
|
|
102e80ab01 | ||
|
|
f5b60081a8 | ||
|
|
0af08cb8a6 | ||
|
|
a85deb1810 | ||
|
|
f53a5f78fa | ||
|
|
9f31a375ec | ||
|
|
80b4ee6ffe | ||
|
|
1d8258dc81 | ||
|
|
83d16f0a4f | ||
|
|
8534df2407 | ||
|
|
67f7ca1201 | ||
|
|
f45d1d4a74 | ||
|
|
bde0cd0d51 | ||
|
|
ad7eb9ae23 | ||
|
|
beaf904d7b | ||
|
|
068a26d1ae | ||
|
|
a07eed5eca | ||
|
|
791f7da098 | ||
|
|
1b5f331b9d | ||
|
|
613c84a8a0 | ||
|
|
f872fb6a26 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -13,5 +13,9 @@ build/*
|
||||
xcuserdata
|
||||
profile
|
||||
*.moved-aside
|
||||
# AppCode etc.
|
||||
.idea/
|
||||
# Desktop Servies
|
||||
.DS_Store
|
||||
|
||||
Carthage/Build
|
||||
|
||||
20
.gitmodules
vendored
20
.gitmodules
vendored
@ -1,9 +1,13 @@
|
||||
[submodule "MantleTests/expecta"]
|
||||
path = MantleTests/expecta
|
||||
url = git://github.com/github/expecta.git
|
||||
[submodule "MantleTests/specta"]
|
||||
path = MantleTests/specta
|
||||
url = git://github.com/github/specta.git
|
||||
[submodule "Configuration"]
|
||||
path = Configuration
|
||||
[submodule "Carthage.checkout/Nimble"]
|
||||
path = Carthage/Checkouts/Nimble
|
||||
url = https://github.com/Quick/Nimble.git
|
||||
[submodule "Carthage.checkout/Quick"]
|
||||
path = Carthage/Checkouts/Quick
|
||||
url = https://github.com/Quick/Quick.git
|
||||
[submodule "Carthage.checkout/xcconfigs"]
|
||||
path = Carthage/Checkouts/xcconfigs
|
||||
url = https://github.com/jspahrsummers/xcconfigs.git
|
||||
[submodule "Carthage/Checkouts/Nimble"]
|
||||
url = https://github.com/Quick/Nimble.git
|
||||
[submodule "Carthage/Checkouts/Quick"]
|
||||
url = https://github.com/Quick/Quick.git
|
||||
|
||||
67
.travis.yml
67
.travis.yml
@ -1,5 +1,70 @@
|
||||
language: objective-c
|
||||
script: script/cibuild
|
||||
xcode_workspace: Mantle.xcworkspace
|
||||
git:
|
||||
submodules: false
|
||||
before_install:
|
||||
- git submodule update --init --recursive
|
||||
script:
|
||||
- set -o pipefail
|
||||
- xcodebuild $XCODE_ACTION
|
||||
-workspace "$TRAVIS_XCODE_WORKSPACE"
|
||||
-scheme "$TRAVIS_XCODE_SCHEME"
|
||||
-sdk "$XCODE_SDK"
|
||||
-destination "$XCODE_DESTINATION"
|
||||
RUN_CLANG_STATIC_ANALYZER=NO
|
||||
| xcpretty
|
||||
matrix:
|
||||
include:
|
||||
# Xcode 8
|
||||
- xcode_scheme: "Mantle Mac"
|
||||
osx_image: xcode8.3
|
||||
env:
|
||||
- XCODE_ACTION=test
|
||||
- XCODE_SDK=macosx
|
||||
- XCODE_DESTINATION="arch=x86_64"
|
||||
- xcode_scheme: "Mantle iOS"
|
||||
osx_image: xcode8.3
|
||||
env:
|
||||
- XCODE_ACTION=test
|
||||
- XCODE_SDK=iphonesimulator
|
||||
- XCODE_DESTINATION="name=iPhone 7"
|
||||
- xcode_scheme: "Mantle-tvOS"
|
||||
osx_image: xcode8.3
|
||||
env:
|
||||
- XCODE_ACTION=test
|
||||
- XCODE_SDK=appletvsimulator
|
||||
- XCODE_DESTINATION="name=Apple TV 1080p"
|
||||
- xcode_scheme: "Mantle-watchOS"
|
||||
osx_image: xcode8.3
|
||||
env:
|
||||
- XCODE_ACTION=build
|
||||
- XCODE_SDK=watchsimulator
|
||||
- XCODE_DESTINATION="name=Apple Watch - 38mm"
|
||||
# Xcode 9
|
||||
- xcode_scheme: "Mantle Mac"
|
||||
osx_image: xcode9
|
||||
env:
|
||||
- XCODE_ACTION=test
|
||||
- XCODE_SDK=macosx
|
||||
- XCODE_DESTINATION="arch=x86_64"
|
||||
- xcode_scheme: "Mantle iOS"
|
||||
osx_image: xcode9
|
||||
env:
|
||||
- XCODE_ACTION=test
|
||||
- XCODE_SDK=iphonesimulator
|
||||
- XCODE_DESTINATION="name=iPhone 7"
|
||||
- xcode_scheme: "Mantle-tvOS"
|
||||
osx_image: xcode9
|
||||
env:
|
||||
- XCODE_ACTION=test
|
||||
- XCODE_SDK=appletvsimulator
|
||||
- XCODE_DESTINATION="name=Apple TV 1080p"
|
||||
- xcode_scheme: "Mantle-watchOS"
|
||||
osx_image: xcode9
|
||||
env:
|
||||
- XCODE_ACTION=build
|
||||
- XCODE_SDK=watchsimulator
|
||||
- XCODE_DESTINATION="name=Apple Watch - 38mm"
|
||||
notifications:
|
||||
email: false
|
||||
campfire:
|
||||
|
||||
188
CHANGELOG.md
Normal file
188
CHANGELOG.md
Normal file
@ -0,0 +1,188 @@
|
||||
# 2.0
|
||||
|
||||
This release of Mantle contains major breaking changes that we were unable to
|
||||
make after freezing the 1.0 API.
|
||||
|
||||
The changes in 2.0 focus on simplifying concepts and increasing flexibility in
|
||||
the framework.
|
||||
|
||||
For a complete list of the changes made in Mantle 2.0, see [the
|
||||
milestone](https://github.com/Mantle/Mantle/issues?q=milestone%3A2.0+is%3Aclosed).
|
||||
|
||||
**[Breaking changes](#breaking-changes)**
|
||||
|
||||
1. [Explicit JSON key paths](#explicit-json-key-paths)
|
||||
1. [Predefined transformers now part of JSON adapter](#predefined-transformers-now-part-of-json-adapter)
|
||||
1. [Core Data adapter now separate](#core-data-adapter-now-separate)
|
||||
1. [Managed object transformers reversed](#managed-object-transformers-reversed)
|
||||
1. [OS X 10.9 and iOS 8](#os-x-109-and-ios-8)
|
||||
1. [JSON key paths can only traverse objects](#json-key-paths-can-only-traverse-objects)
|
||||
|
||||
**[Additions and improvements](#additions-and-improvements)**
|
||||
|
||||
1. [MTLModel protocol](#mtlmodel-protocol)
|
||||
1. [Error handling for value transformers](#error-handling-for-value-transformers)
|
||||
1. [Storage behaviors for properties](#storage-behaviors-for-properties)
|
||||
1. [Type checking during JSON parsing](#type-checking-during-json-parsing)
|
||||
1. [Mapping multiple JSON fields to a single property](#mapping-multiple-json-fields-to-a-single-property)
|
||||
|
||||
## Breaking changes
|
||||
|
||||
### Explicit JSON key paths
|
||||
|
||||
`+JSONKeyPathsByPropertyKey` will [no
|
||||
longer](https://github.com/Mantle/Mantle/pull/170) infer your property mappings
|
||||
automatically.
|
||||
|
||||
Instead, you must explicitly specify every property that should
|
||||
be mapped, and any properties omitted will not be considered for JSON
|
||||
serialization or deserialization.
|
||||
|
||||
For convenience, you can use `+[NSDictionary mtl_identityPropertyMapWithModel:]`
|
||||
to automatically create a one-to-one mapping that matches the previous default
|
||||
behavior.
|
||||
|
||||
**To update:**
|
||||
|
||||
* Explicitly declare any property mappings in `+JSONKeyPathsByPropertyKey`
|
||||
that were previously implicit.
|
||||
* Optionally use `+[NSDictionary mtl_identityPropertyMapWithModel:]` for an
|
||||
initial property map.
|
||||
|
||||
### Predefined transformers now part of JSON adapter
|
||||
|
||||
The `+mtl_JSONDictionaryTransformerWithModelClass:` and
|
||||
`+mtl_JSONArrayWithModelClass:` methods [have
|
||||
moved](https://github.com/Mantle/Mantle/pull/474) to `MTLJSONAdapter`.
|
||||
|
||||
This allows custom JSON adapter subclasses to substitute their own transformers
|
||||
with additional logic, and moves the transformers closer to their actual point
|
||||
of use.
|
||||
|
||||
**To update:**
|
||||
|
||||
* Replace occurrences of `+[NSValueTransformer
|
||||
mtl_JSONDictionaryTransformerWithModelClass:]` with `+[MTLJSONAdapter
|
||||
dictionaryTransformerWithModelClass:]`
|
||||
* Replace occurrences of `+[NSValueTransformer
|
||||
mtl_JSONArrayTransformerWithModelClass:]` with `+[MTLJSONAdapter
|
||||
arrayTransformerWithModelClass:]`
|
||||
|
||||
### Core Data adapter now separate
|
||||
|
||||
The `MTLManagedObjectAdapter` class, used for converting to and from Core Data
|
||||
objects, has been moved to [its own
|
||||
framework](https://github.com/Mantle/MTLManagedObjectAdapter). This better
|
||||
indicates its “semi-official” status, as it gets less attention than the core
|
||||
Mantle features.
|
||||
|
||||
**To update:**
|
||||
|
||||
* Import the
|
||||
[MTLManagedObjectAdapter](https://github.com/Mantle/MTLManagedObjectAdapter)
|
||||
framework into your project.
|
||||
|
||||
### Managed object transformers reversed
|
||||
|
||||
In addition to being [a separate framework](#core-data-adapter-now-separate),
|
||||
the behavior of `MTLManagedObjectAdapter` has changed as well—specifically, the
|
||||
direction of managed object attribute transformers has been flipped.
|
||||
|
||||
Managed object transformers now convert _from_ managed object attributes _to_
|
||||
model properties in the forward direction. In the reverse direction, they
|
||||
convert from properties to managed object attributes.
|
||||
|
||||
**To update:**
|
||||
|
||||
* Swap the forward and reverse transformation logic of any custom managed
|
||||
object transformers, or use `-mtl_invertedTransformer` to do it
|
||||
automatically.
|
||||
|
||||
### OS X 10.9 and iOS 8
|
||||
|
||||
Mantle now requires OS X 10.9+ or iOS 8+, for the use of Swift and dynamic
|
||||
frameworks.
|
||||
|
||||
**To update:**
|
||||
|
||||
* Increase your project’s deployment target to at least OS X 10.9 or iOS 8.
|
||||
|
||||
### JSON key paths can only traverse objects
|
||||
|
||||
Every element of a JSON key path specified in `+JSONKeyPathsByPropertyKey` [must
|
||||
now refer to an object](https://github.com/Mantle/Mantle/pull/275) (dictionary).
|
||||
|
||||
It was [previously possible](https://github.com/Mantle/Mantle/issues/257) to use
|
||||
an array as a key path element, but this was unintended behavior, and is now
|
||||
explicitly disallowed.
|
||||
|
||||
**To update:**
|
||||
|
||||
* If you were using an array as an element in a key path, change the key path
|
||||
to end at the array, and [update your JSON transformer](https://github.com/Mantle/Mantle/issues/257#issuecomment-36846503)
|
||||
to handle the nested elements instead.
|
||||
|
||||
## Additions and improvements
|
||||
|
||||
### MTLModel protocol
|
||||
|
||||
The [new `<MTLModel>` protocol](https://github.com/Mantle/Mantle/pull/219) represents the basic behaviors expected from any
|
||||
model object, and can be used instead of the `MTLModel` class when inheritance
|
||||
is impossible, or to create more generic APIs.
|
||||
|
||||
For example, `<MTLModel>` conformance can be added to the objects from other
|
||||
persistence frameworks in order to use those objects in conjunction with
|
||||
Mantle’s adapters.
|
||||
|
||||
Accordingly, `MTLJSONAdapter` has been updated to only depend on `<MTLModel>`
|
||||
conformance, and no longer requires a `MTLModel` subclass in order to serialize
|
||||
or deserialize from JSON.
|
||||
|
||||
### Error handling for value transformers
|
||||
|
||||
The [new `<MTLTransformerErrorHandling>`
|
||||
protocol](https://github.com/Mantle/Mantle/pull/153) can be used to add error
|
||||
reporting behaviors to any `NSValueTransformer`.
|
||||
|
||||
`MTLValueTransformer` has been updated to take advantage of the new interface,
|
||||
with the following new methods that provide error information:
|
||||
|
||||
* `+transformerUsingForwardBlock:`
|
||||
* `+transformerUsingReversibleBlock:`
|
||||
* `+transformerUsingForwardBlock:reverseBlock:`
|
||||
|
||||
Similarly, the predefined transformers that Mantle provides now provide error
|
||||
information upon failure as well.
|
||||
|
||||
### Storage behaviors for properties
|
||||
|
||||
The [new `+storageBehaviorForPropertyWithKey:`
|
||||
method](https://github.com/Mantle/Mantle/pull/210) can be used to redefine the
|
||||
default behavior of methods like `-dictionaryValue`, `-isEqual:`,
|
||||
`-description`, and `-copy` all at once.
|
||||
|
||||
Properties which have been omitted from `+propertyKeys` by default will continue
|
||||
to be omitted under the new API, with a default behavior of
|
||||
`MTLPropertyStorageNone`.
|
||||
|
||||
### Type checking during JSON parsing
|
||||
|
||||
`MTLJSONAdapter` now [implicitly
|
||||
validates](https://github.com/Mantle/Mantle/pull/251) the type of values
|
||||
assigned to your `<MTLModel>` objects during JSON parsing.
|
||||
|
||||
This can be prevent errors like an `NSString` being assigned to a `BOOL`
|
||||
property.
|
||||
|
||||
This is only a simple safety check, though, and cannot catch every kind of
|
||||
error! Continue to verify that your types align ahead of time.
|
||||
|
||||
### Mapping multiple JSON fields to a single property
|
||||
|
||||
`MTLJSONAdapter` can now map multiple fields to a single property, and
|
||||
vice-versa. Specify an array of keypaths for the property when implementing
|
||||
`+JSONKeyPathsByPropertyKey` rather than an `NSString`.
|
||||
|
||||
The default behaviour is to set the property to a dictionary of values for the
|
||||
specified keypaths. If you specify a value transformer for the given property
|
||||
key, this transformer will receive an `NSDictionary` of values.
|
||||
3
Cartfile.private
Normal file
3
Cartfile.private
Normal file
@ -0,0 +1,3 @@
|
||||
github "jspahrsummers/xcconfigs" "1ef9763"
|
||||
github "Quick/Quick" ~> 1.1.0
|
||||
github "Quick/Nimble" ~> 7.0.1
|
||||
3
Cartfile.resolved
Normal file
3
Cartfile.resolved
Normal file
@ -0,0 +1,3 @@
|
||||
github "Quick/Nimble" "v7.0.1"
|
||||
github "Quick/Quick" "v1.1.0"
|
||||
github "jspahrsummers/xcconfigs" "1ef97639ffbe041da0b1392b2114fa19b922a7a1"
|
||||
1
Carthage/Checkouts/Nimble
vendored
Submodule
1
Carthage/Checkouts/Nimble
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 39b67002306fda9de4c9fd1290a6295f97edd09e
|
||||
1
Carthage/Checkouts/Quick
vendored
Submodule
1
Carthage/Checkouts/Quick
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit e4fa1e85c0305ba4e0866f25812d3fa398f3a048
|
||||
1
Carthage/Checkouts/xcconfigs
vendored
Submodule
1
Carthage/Checkouts/xcconfigs
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 1ef97639ffbe041da0b1392b2114fa19b922a7a1
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit 23f7c17fbd74a091694632907d3a3a4e65125d58
|
||||
@ -1,4 +1,4 @@
|
||||
**Copyright (c) 2012 - 2013, GitHub, Inc.**
|
||||
**Copyright (c) GitHub, Inc.**
|
||||
**All rights reserved.**
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0500"
|
||||
version = "1.3">
|
||||
LastUpgradeVersion = "0800"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
parallelizeBuildables = "NO"
|
||||
buildImplicitDependencies = "NO">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@ -16,62 +16,121 @@
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D042FC3B15F72B23004E8054"
|
||||
BuildableName = "Mantle.framework"
|
||||
BlueprintName = "Mantle Mac"
|
||||
BlueprintName = "Mantle-Mac"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "YES">
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1F925EAC195C0D6300ED456B"
|
||||
BuildableName = "Nimble.framework"
|
||||
BlueprintName = "Nimble-macOS"
|
||||
ReferencedContainer = "container:Carthage/Checkouts/Nimble/Nimble.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DAEB6B8D1943873100289F44"
|
||||
BuildableName = "Quick.framework"
|
||||
BlueprintName = "Quick-macOS"
|
||||
ReferencedContainer = "container:Carthage/Checkouts/Quick/Quick.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D042FC5315F72B23004E8054"
|
||||
BuildableName = "Mantle Mac Tests.octest"
|
||||
BlueprintName = "Mantle Mac Tests"
|
||||
BuildableName = "Mantle-MacTests.xctest"
|
||||
BlueprintName = "Mantle-MacTests"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
buildConfiguration = "Debug">
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D042FC5315F72B23004E8054"
|
||||
BuildableName = "Mantle Mac Tests.octest"
|
||||
BlueprintName = "Mantle Mac Tests"
|
||||
BuildableName = "Mantle-MacTests.xctest"
|
||||
BlueprintName = "Mantle-MacTests"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D042FC3B15F72B23004E8054"
|
||||
BuildableName = "Mantle.framework"
|
||||
BlueprintName = "Mantle-Mac"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Debug"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugXPCServices = "NO"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D042FC3B15F72B23004E8054"
|
||||
BuildableName = "Mantle.framework"
|
||||
BlueprintName = "Mantle-Mac"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Profile"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D042FC3B15F72B23004E8054"
|
||||
BuildableName = "Mantle.framework"
|
||||
BlueprintName = "Mantle-Mac"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0500"
|
||||
LastUpgradeVersion = "0800"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
parallelizeBuildables = "NO"
|
||||
buildImplicitDependencies = "NO">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@ -14,64 +14,122 @@
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D042FC7815F72BC7004E8054"
|
||||
BuildableName = "libMantle.a"
|
||||
BlueprintName = "Mantle iOS"
|
||||
BlueprintIdentifier = "D0E9C35619F6DB38000D427D"
|
||||
BuildableName = "Mantle.framework"
|
||||
BlueprintName = "Mantle-iOS"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "YES">
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D042FC8615F72BC7004E8054"
|
||||
BuildableName = "Mantle iOS Tests.octest"
|
||||
BlueprintName = "Mantle iOS Tests"
|
||||
BlueprintIdentifier = "1F1A74281940169200FFFC47"
|
||||
BuildableName = "Nimble.framework"
|
||||
BlueprintName = "Nimble-iOS"
|
||||
ReferencedContainer = "container:Carthage/Checkouts/Nimble/Nimble.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5A5D117B19473F2100F6D13D"
|
||||
BuildableName = "Quick.framework"
|
||||
BlueprintName = "Quick-iOS"
|
||||
ReferencedContainer = "container:Carthage/Checkouts/Quick/Quick.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0E9C36019F6DB38000D427D"
|
||||
BuildableName = "Mantle-iOSTests.xctest"
|
||||
BlueprintName = "Mantle-iOSTests"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
buildConfiguration = "Debug">
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D042FC8615F72BC7004E8054"
|
||||
BuildableName = "Mantle iOS Tests.octest"
|
||||
BlueprintName = "Mantle iOS Tests"
|
||||
BlueprintIdentifier = "D0E9C36019F6DB38000D427D"
|
||||
BuildableName = "Mantle-iOSTests.xctest"
|
||||
BlueprintName = "Mantle-iOSTests"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0E9C35619F6DB38000D427D"
|
||||
BuildableName = "Mantle.framework"
|
||||
BlueprintName = "Mantle-iOS"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Debug"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0E9C35619F6DB38000D427D"
|
||||
BuildableName = "Mantle.framework"
|
||||
BlueprintName = "Mantle-iOS"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Profile"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0E9C35619F6DB38000D427D"
|
||||
BuildableName = "Mantle.framework"
|
||||
BlueprintName = "Mantle-iOS"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
|
||||
141
Mantle.xcodeproj/xcshareddata/xcschemes/Mantle-tvOS.xcscheme
Normal file
141
Mantle.xcodeproj/xcshareddata/xcschemes/Mantle-tvOS.xcscheme
Normal file
@ -0,0 +1,141 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0800"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "NO"
|
||||
buildImplicitDependencies = "NO">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CDEEABA71D33FC5100240A4B"
|
||||
BuildableName = "Mantle.framework"
|
||||
BlueprintName = "Mantle-tvOS"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1F5DF1541BDCA0CE00C3A531"
|
||||
BuildableName = "Nimble.framework"
|
||||
BlueprintName = "Nimble-tvOS"
|
||||
ReferencedContainer = "container:Carthage/Checkouts/Nimble/Nimble.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "1F118CD41BDCA4AB005013A2"
|
||||
BuildableName = "Quick.framework"
|
||||
BlueprintName = "Quick-tvOS"
|
||||
ReferencedContainer = "container:Carthage/Checkouts/Quick/Quick.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CDEEABD31D33FC7900240A4B"
|
||||
BuildableName = "Mantle-tvOSTests.xctest"
|
||||
BlueprintName = "Mantle-tvOSTests"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CDEEABD31D33FC7900240A4B"
|
||||
BuildableName = "Mantle-tvOSTests.xctest"
|
||||
BlueprintName = "Mantle-tvOSTests"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CDEEABA71D33FC5100240A4B"
|
||||
BuildableName = "Mantle.framework"
|
||||
BlueprintName = "Mantle-tvOS"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CDEEABA71D33FC5100240A4B"
|
||||
BuildableName = "Mantle.framework"
|
||||
BlueprintName = "Mantle-tvOS"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CDEEABA71D33FC5100240A4B"
|
||||
BuildableName = "Mantle.framework"
|
||||
BlueprintName = "Mantle-tvOS"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0800"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "NO"
|
||||
buildImplicitDependencies = "NO">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CD7C6D881D33ACCC002EC294"
|
||||
BuildableName = "Mantle.framework"
|
||||
BlueprintName = "Mantle-watchOS"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CD7C6D881D33ACCC002EC294"
|
||||
BuildableName = "Mantle.framework"
|
||||
BlueprintName = "Mantle-watchOS"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CD7C6D881D33ACCC002EC294"
|
||||
BuildableName = "Mantle.framework"
|
||||
BlueprintName = "Mantle-watchOS"
|
||||
ReferencedContainer = "container:Mantle.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
13
Mantle.xcworkspace/contents.xcworkspacedata
generated
Normal file
13
Mantle.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Mantle.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Carthage/Checkouts/Quick/Quick.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Carthage/Checkouts/Nimble/Nimble.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@ -3,17 +3,15 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.github.${PRODUCT_NAME:rfc1034identifier}</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
@ -21,9 +19,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2012 GitHub. All rights reserved.</string>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
@ -8,120 +8,280 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class MTLModel;
|
||||
@protocol MTLModel;
|
||||
@protocol MTLTransformerErrorHandling;
|
||||
|
||||
// A MTLModel object that supports being parsed from and serialized to JSON.
|
||||
@protocol MTLJSONSerializing
|
||||
/// A MTLModel object that supports being parsed from and serialized to JSON.
|
||||
@protocol MTLJSONSerializing <MTLModel>
|
||||
@required
|
||||
|
||||
// Specifies how to map property keys to different key paths in JSON.
|
||||
//
|
||||
// Subclasses overriding this method should combine their values with those of
|
||||
// `super`.
|
||||
//
|
||||
// Any property keys not present in the dictionary are assumed to match the JSON
|
||||
// key that should be used. Any keys associated with NSNull will not participate
|
||||
// in JSON serialization.
|
||||
//
|
||||
// Returns a dictionary mapping property keys to JSON key paths (as strings) or
|
||||
// NSNull values.
|
||||
/// Specifies how to map property keys to different key paths in JSON.
|
||||
///
|
||||
/// Subclasses overriding this method should combine their values with those of
|
||||
/// `super`.
|
||||
///
|
||||
/// Values in the dictionary can either be key paths in the JSON representation
|
||||
/// of the receiver or an array of such key paths. If an array is used, the
|
||||
/// deserialized value will be a dictionary containing all of the keys in the
|
||||
/// array.
|
||||
///
|
||||
/// Any keys omitted will not participate in JSON serialization.
|
||||
///
|
||||
/// Examples
|
||||
///
|
||||
/// + (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
/// return @{
|
||||
/// @"name": @"POI.name",
|
||||
/// @"point": @[ @"latitude", @"longitude" ],
|
||||
/// @"starred": @"starred"
|
||||
/// };
|
||||
/// }
|
||||
///
|
||||
/// This will map the `starred` property to `JSONDictionary[@"starred"]`, `name`
|
||||
/// to `JSONDictionary[@"POI"][@"name"]` and `point` to a dictionary equivalent
|
||||
/// to:
|
||||
///
|
||||
/// @{
|
||||
/// @"latitude": JSONDictionary[@"latitude"],
|
||||
/// @"longitude": JSONDictionary[@"longitude"]
|
||||
/// }
|
||||
///
|
||||
/// Returns a dictionary mapping property keys to one or multiple JSON key paths
|
||||
/// (as strings or arrays of strings).
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey;
|
||||
|
||||
@optional
|
||||
|
||||
// Specifies how to convert a JSON value to the given property key. If
|
||||
// reversible, the transformer will also be used to convert the property value
|
||||
// back to JSON.
|
||||
//
|
||||
// If the receiver implements a `+<key>JSONTransformer` method, MTLJSONAdapter
|
||||
// will use the result of that method instead.
|
||||
//
|
||||
// Returns a value transformer, or nil if no transformation should be performed.
|
||||
/// Specifies how to convert a JSON value to the given property key. If
|
||||
/// reversible, the transformer will also be used to convert the property value
|
||||
/// back to JSON.
|
||||
///
|
||||
/// If the receiver implements a `+<key>JSONTransformer` method, MTLJSONAdapter
|
||||
/// will use the result of that method instead.
|
||||
///
|
||||
/// Returns a value transformer, or nil if no transformation should be performed.
|
||||
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key;
|
||||
|
||||
// Overridden to parse the receiver as a different class, based on information
|
||||
// in the provided dictionary.
|
||||
//
|
||||
// This is mostly useful for class clusters, where the abstract base class would
|
||||
// be passed into -[MTLJSONAdapter initWithJSONDictionary:modelClass:], but
|
||||
// a subclass should be instantiated instead.
|
||||
//
|
||||
// JSONDictionary - The JSON dictionary that will be parsed.
|
||||
//
|
||||
// Returns the class that should be parsed (which may be the receiver), or nil
|
||||
// to abort parsing (e.g., if the data is invalid).
|
||||
/// Overridden to parse the receiver as a different class, based on information
|
||||
/// in the provided dictionary.
|
||||
///
|
||||
/// This is mostly useful for class clusters, where the abstract base class would
|
||||
/// be passed into -[MTLJSONAdapter initWithJSONDictionary:modelClass:], but
|
||||
/// a subclass should be instantiated instead.
|
||||
///
|
||||
/// JSONDictionary - The JSON dictionary that will be parsed.
|
||||
///
|
||||
/// Returns the class that should be parsed (which may be the receiver), or nil
|
||||
/// to abort parsing (e.g., if the data is invalid).
|
||||
+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary;
|
||||
|
||||
@end
|
||||
|
||||
// The domain for errors originating from MTLJSONAdapter.
|
||||
/// The domain for errors originating from MTLJSONAdapter.
|
||||
extern NSString * const MTLJSONAdapterErrorDomain;
|
||||
|
||||
// +classForParsingJSONDictionary: returned nil for the given dictionary.
|
||||
/// +classForParsingJSONDictionary: returned nil for the given dictionary.
|
||||
extern const NSInteger MTLJSONAdapterErrorNoClassFound;
|
||||
|
||||
// Converts a MTLModel object to and from a JSON dictionary.
|
||||
/// The provided JSONDictionary is not valid.
|
||||
extern const NSInteger MTLJSONAdapterErrorInvalidJSONDictionary;
|
||||
|
||||
/// The model's implementation of +JSONKeyPathsByPropertyKey included a key which
|
||||
/// does not actually exist in +propertyKeys.
|
||||
extern const NSInteger MTLJSONAdapterErrorInvalidJSONMapping;
|
||||
|
||||
/// An exception was thrown and caught.
|
||||
extern const NSInteger MTLJSONAdapterErrorExceptionThrown;
|
||||
|
||||
/// Associated with the NSException that was caught.
|
||||
extern NSString * const MTLJSONAdapterThrownExceptionErrorKey;
|
||||
|
||||
/// Converts a MTLModel object to and from a JSON dictionary.
|
||||
@interface MTLJSONAdapter : NSObject
|
||||
|
||||
// The model object that the receiver was initialized with, or that the receiver
|
||||
// parsed from a JSON dictionary.
|
||||
@property (nonatomic, strong, readonly) MTLModel<MTLJSONSerializing> *model;
|
||||
|
||||
// Attempts to parse a JSON dictionary into a model object.
|
||||
//
|
||||
// modelClass - The MTLModel subclass to attempt to parse from the JSON.
|
||||
// This class must conform to <MTLJSONSerializing>. This
|
||||
// argument must not be nil.
|
||||
// JSONDictionary - A dictionary representing JSON data. This should match the
|
||||
// format returned by NSJSONSerialization. If this argument is
|
||||
// nil, the method returns nil.
|
||||
// error - If not NULL, this may be set to an error that occurs during
|
||||
// parsing or initializing an instance of `modelClass`.
|
||||
//
|
||||
// Returns an instance of `modelClass` upon success, or nil if a parsing error
|
||||
// occurred.
|
||||
/// Attempts to parse a JSON dictionary into a model object.
|
||||
///
|
||||
/// modelClass - The MTLModel subclass to attempt to parse from the JSON.
|
||||
/// This class must conform to <MTLJSONSerializing>. This
|
||||
/// argument must not be nil.
|
||||
/// JSONDictionary - A dictionary representing JSON data. This should match the
|
||||
/// format returned by NSJSONSerialization. If this argument is
|
||||
/// nil, the method returns nil.
|
||||
/// error - If not NULL, this may be set to an error that occurs during
|
||||
/// parsing or initializing an instance of `modelClass`.
|
||||
///
|
||||
/// Returns an instance of `modelClass` upon success, or nil if a parsing error
|
||||
/// occurred.
|
||||
+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error;
|
||||
|
||||
// Converts a model into a JSON representation.
|
||||
//
|
||||
// model - The model to use for JSON serialization. This argument must not be
|
||||
// nil.
|
||||
//
|
||||
// Returns a JSON dictionary, or nil if a serialization error occurred.
|
||||
+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel<MTLJSONSerializing> *)model;
|
||||
/// Attempts to parse an array of JSON dictionary objects into a model objects
|
||||
/// of a specific class.
|
||||
///
|
||||
/// modelClass - The MTLModel subclass to attempt to parse from the JSON. This
|
||||
/// class must conform to <MTLJSONSerializing>. This argument must
|
||||
/// not be nil.
|
||||
/// JSONArray - A array of dictionaries representing JSON data. This should
|
||||
/// match the format returned by NSJSONSerialization. If this
|
||||
/// argument is nil, the method returns nil.
|
||||
/// error - If not NULL, this may be set to an error that occurs during
|
||||
/// parsing or initializing an any of the instances of
|
||||
/// `modelClass`.
|
||||
///
|
||||
/// Returns an array of `modelClass` instances upon success, or nil if a parsing
|
||||
/// error occurred.
|
||||
+ (NSArray *)modelsOfClass:(Class)modelClass fromJSONArray:(NSArray *)JSONArray error:(NSError **)error;
|
||||
|
||||
// Initializes the receiver by attempting to parse a JSON dictionary into
|
||||
// a model object.
|
||||
//
|
||||
// JSONDictionary - A dictionary representing JSON data. This should match the
|
||||
// format returned by NSJSONSerialization. If this argument is
|
||||
// nil, the method returns nil.
|
||||
// modelClass - The MTLModel subclass to attempt to parse from the JSON.
|
||||
// This class must conform to <MTLJSONSerializing>. This
|
||||
// argument must not be nil.
|
||||
// error - If not NULL, this may be set to an error that occurs during
|
||||
// parsing or initializing an instance of `modelClass`.
|
||||
//
|
||||
// Returns an initialized adapter upon success, or nil if a parsing error
|
||||
// occurred.
|
||||
- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass error:(NSError **)error;
|
||||
/// Converts a model into a JSON representation.
|
||||
///
|
||||
/// model - The model to use for JSON serialization. This argument must not be
|
||||
/// nil.
|
||||
/// error - If not NULL, this may be set to an error that occurs during
|
||||
/// serializing.
|
||||
///
|
||||
/// Returns a JSON dictionary, or nil if a serialization error occurred.
|
||||
+ (NSDictionary *)JSONDictionaryFromModel:(id<MTLJSONSerializing>)model error:(NSError **)error;
|
||||
|
||||
// Initializes the receiver with an existing model.
|
||||
//
|
||||
// model - The model to use for JSON serialization. This argument must not be
|
||||
// nil.
|
||||
- (id)initWithModel:(MTLModel<MTLJSONSerializing> *)model;
|
||||
/// Converts a array of models into a JSON representation.
|
||||
///
|
||||
/// models - The array of models to use for JSON serialization. This argument
|
||||
/// must not be nil.
|
||||
/// error - If not NULL, this may be set to an error that occurs during
|
||||
/// serializing.
|
||||
///
|
||||
/// Returns a JSON array, or nil if a serialization error occurred for any
|
||||
/// model.
|
||||
+ (NSArray *)JSONArrayFromModels:(NSArray *)models error:(NSError **)error;
|
||||
|
||||
// Serializes the receiver's `model` into JSON.
|
||||
//
|
||||
// Returns a JSON dictionary, or nil if a serialization error occurred.
|
||||
- (NSDictionary *)JSONDictionary;
|
||||
/// Initializes the receiver with a given model class.
|
||||
///
|
||||
/// modelClass - The MTLModel subclass to attempt to parse from the JSON and
|
||||
/// back. This class must conform to <MTLJSONSerializing>. This
|
||||
/// argument must not be nil.
|
||||
///
|
||||
/// Returns an initialized adapter.
|
||||
- (id)initWithModelClass:(Class)modelClass;
|
||||
|
||||
/// Deserializes a model from a JSON dictionary.
|
||||
///
|
||||
/// The adapter will call -validate: on the model and consider it an error if the
|
||||
/// validation fails.
|
||||
///
|
||||
/// JSONDictionary - A dictionary representing JSON data. This should match the
|
||||
/// format returned by NSJSONSerialization. This argument must
|
||||
/// not be nil.
|
||||
/// error - If not NULL, this may be set to an error that occurs during
|
||||
/// deserializing or validation.
|
||||
///
|
||||
/// Returns a model object, or nil if a deserialization error occurred or the
|
||||
/// model did not validate successfully.
|
||||
- (id)modelFromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error;
|
||||
|
||||
/// Serializes a model into JSON.
|
||||
///
|
||||
/// model - The model to use for JSON serialization. This argument must not be
|
||||
/// nil.
|
||||
/// error - If not NULL, this may be set to an error that occurs during
|
||||
/// serializing.
|
||||
///
|
||||
/// Returns a model object, or nil if a serialization error occurred.
|
||||
- (NSDictionary *)JSONDictionaryFromModel:(id<MTLJSONSerializing>)model error:(NSError **)error;
|
||||
|
||||
/// Filters the property keys used to serialize a given model.
|
||||
///
|
||||
/// propertyKeys - The property keys for which `model` provides a mapping.
|
||||
/// model - The model being serialized.
|
||||
///
|
||||
/// Subclasses may override this method to determine which property keys should
|
||||
/// be used when serializing `model`. For instance, this method can be used to
|
||||
/// create more efficient updates of server-side resources.
|
||||
///
|
||||
/// The default implementation simply returns `propertyKeys`.
|
||||
///
|
||||
/// Returns a subset of propertyKeys that should be serialized for a given
|
||||
/// model.
|
||||
- (NSSet *)serializablePropertyKeys:(NSSet *)propertyKeys forModel:(id<MTLJSONSerializing>)model;
|
||||
|
||||
/// An optional value transformer that should be used for properties of the given
|
||||
/// class.
|
||||
///
|
||||
/// A value transformer returned by the model's +JSONTransformerForKey: method
|
||||
/// is given precedence over the one returned by this method.
|
||||
///
|
||||
/// The default implementation invokes `+<class>JSONTransformer` on the
|
||||
/// receiver if it's implemented. It supports NSURL conversion through
|
||||
/// +NSURLJSONTransformer.
|
||||
///
|
||||
/// modelClass - The class of the property to serialize. This property must not be
|
||||
/// nil.
|
||||
///
|
||||
/// Returns a value transformer or nil if no transformation should be used.
|
||||
+ (NSValueTransformer *)transformerForModelPropertiesOfClass:(Class)modelClass;
|
||||
|
||||
/// A value transformer that should be used for a properties of the given
|
||||
/// primitive type.
|
||||
///
|
||||
/// If `objCType` matches @encode(id), the value transformer returned by
|
||||
/// +transformerForModelPropertiesOfClass: is used instead.
|
||||
///
|
||||
/// The default implementation transforms properties that match @encode(BOOL)
|
||||
/// using the MTLBooleanValueTransformerName transformer.
|
||||
///
|
||||
/// objCType - The type encoding for the value of this property. This is the type
|
||||
/// as it would be returned by the @encode() directive.
|
||||
///
|
||||
/// Returns a value transformer or nil if no transformation should be used.
|
||||
+ (NSValueTransformer *)transformerForModelPropertiesOfObjCType:(const char *)objCType;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLJSONAdapter (ValueTransformers)
|
||||
|
||||
/// Creates a reversible transformer to convert a JSON dictionary into a MTLModel
|
||||
/// object, and vice-versa.
|
||||
///
|
||||
/// modelClass - The MTLModel subclass to attempt to parse from the JSON. This
|
||||
/// class must conform to <MTLJSONSerializing>. This argument must
|
||||
/// not be nil.
|
||||
///
|
||||
/// Returns a reversible transformer which uses the class of the receiver for
|
||||
/// transforming values back and forth.
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)dictionaryTransformerWithModelClass:(Class)modelClass;
|
||||
|
||||
/// Creates a reversible transformer to convert an array of JSON dictionaries
|
||||
/// into an array of MTLModel objects, and vice-versa.
|
||||
///
|
||||
/// modelClass - The MTLModel subclass to attempt to parse from each JSON
|
||||
/// dictionary. This class must conform to <MTLJSONSerializing>.
|
||||
/// This argument must not be nil.
|
||||
///
|
||||
/// Returns a reversible transformer which uses the class of the receiver for
|
||||
/// transforming array elements back and forth.
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)arrayTransformerWithModelClass:(Class)modelClass;
|
||||
|
||||
/// This value transformer is used by MTLJSONAdapter to automatically convert
|
||||
/// NSURL properties to JSON strings and vice versa.
|
||||
+ (NSValueTransformer *)NSURLJSONTransformer;
|
||||
|
||||
/// This value transformer is used by MTLJSONAdapter to automatically convert
|
||||
/// NSUUID properties to JSON strings and vice versa.
|
||||
+ (NSValueTransformer *)NSUUIDJSONTransformer;
|
||||
|
||||
@end
|
||||
|
||||
@class MTLModel;
|
||||
|
||||
@interface MTLJSONAdapter (Deprecated)
|
||||
|
||||
+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary __attribute__((deprecated("Replaced by +modelOfClass:fromJSONDictionary:error:")));
|
||||
- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass __attribute__((deprecated("Replaced by -initWithJSONDictionary:modelClass:error:")));
|
||||
@property (nonatomic, strong, readonly) id<MTLJSONSerializing> model __attribute__((unavailable("Replaced by -modelFromJSONDictionary:error:")));
|
||||
|
||||
+ (NSArray *)JSONArrayFromModels:(NSArray *)models __attribute__((deprecated("Replaced by +JSONArrayFromModels:error:"))) NS_SWIFT_UNAVAILABLE("Replaced by +JSONArrayFromModels:error:");
|
||||
|
||||
+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel<MTLJSONSerializing> *)model __attribute__((deprecated("Replaced by +JSONDictionaryFromModel:error:"))) NS_SWIFT_UNAVAILABLE("Replaced by +JSONDictionaryFromModel:error:");
|
||||
|
||||
- (NSDictionary *)JSONDictionary __attribute__((unavailable("Replaced by -JSONDictionaryFromModel:error:"))) NS_SWIFT_UNAVAILABLE("Replaced by -JSONDictionaryFromModel:error:");
|
||||
- (NSString *)JSONKeyPathForPropertyKey:(NSString *)key __attribute__((unavailable("Replaced by -serializablePropertyKeys:forModel:")));
|
||||
- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass error:(NSError **)error __attribute__((unavailable("Replaced by -initWithModelClass:")));
|
||||
- (id)initWithModel:(id<MTLJSONSerializing>)model __attribute__((unavailable("Replaced by -initWithModelClass:"))) NS_SWIFT_UNAVAILABLE("Replaced by -initWithModelClass:");
|
||||
- (NSDictionary *)serializeToJSONDictionary:(NSError **)error __attribute__((unavailable("Replaced by -JSONDictionaryFromModel:error:"))) NS_SWIFT_UNAVAILABLE("Replaced by -JSONDictionaryFromModel:error:");
|
||||
|
||||
@end
|
||||
|
||||
@ -6,18 +6,29 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#import "NSDictionary+MTLJSONKeyPath.h"
|
||||
|
||||
#import <Mantle/EXTRuntimeExtensions.h>
|
||||
#import <Mantle/EXTScope.h>
|
||||
#import "MTLJSONAdapter.h"
|
||||
#import "MTLModel.h"
|
||||
#import "MTLTransformerErrorHandling.h"
|
||||
#import "MTLReflection.h"
|
||||
#import "NSValueTransformer+MTLPredefinedTransformerAdditions.h"
|
||||
#import "MTLValueTransformer.h"
|
||||
|
||||
NSString * const MTLJSONAdapterErrorDomain = @"MTLJSONAdapterErrorDomain";
|
||||
const NSInteger MTLJSONAdapterErrorNoClassFound = 2;
|
||||
const NSInteger MTLJSONAdapterErrorInvalidJSONDictionary = 3;
|
||||
const NSInteger MTLJSONAdapterErrorInvalidJSONMapping = 4;
|
||||
|
||||
// An exception was thrown and caught.
|
||||
static const NSInteger MTLJSONAdapterErrorExceptionThrown = 1;
|
||||
const NSInteger MTLJSONAdapterErrorExceptionThrown = 1;
|
||||
|
||||
// Associated with the NSException that was caught.
|
||||
static NSString * const MTLJSONAdapterThrownExceptionErrorKey = @"MTLJSONAdapterThrownException";
|
||||
NSString * const MTLJSONAdapterThrownExceptionErrorKey = @"MTLJSONAdapterThrownException";
|
||||
|
||||
@interface MTLJSONAdapter ()
|
||||
|
||||
@ -28,20 +39,33 @@ static NSString * const MTLJSONAdapterThrownExceptionErrorKey = @"MTLJSONAdapter
|
||||
// A cached copy of the return value of +JSONKeyPathsByPropertyKey.
|
||||
@property (nonatomic, copy, readonly) NSDictionary *JSONKeyPathsByPropertyKey;
|
||||
|
||||
// Looks up the NSValueTransformer that should be used for the given key.
|
||||
//
|
||||
// key - The property key to transform from or to. This argument must not be nil.
|
||||
//
|
||||
// Returns a transformer to use, or nil to not transform the property.
|
||||
- (NSValueTransformer *)JSONTransformerForKey:(NSString *)key;
|
||||
// A cached copy of the return value of -valueTransformersForModelClass:
|
||||
@property (nonatomic, copy, readonly) NSDictionary *valueTransformersByPropertyKey;
|
||||
|
||||
// Looks up the JSON key path that corresponds to the given key.
|
||||
// Used to cache the JSON adapters returned by -JSONAdapterForModelClass:error:.
|
||||
@property (nonatomic, strong, readonly) NSMapTable *JSONAdaptersByModelClass;
|
||||
|
||||
// If +classForParsingJSONDictionary: returns a model class different from the
|
||||
// one this adapter was initialized with, use this method to obtain a cached
|
||||
// instance of a suitable adapter instead.
|
||||
//
|
||||
// key - The property key to retrieve the corresponding JSON key path for. This
|
||||
// argument must not be nil.
|
||||
// modelClass - The class from which to parse the JSON. This class must conform
|
||||
// to <MTLJSONSerializing>. This argument must not be nil.
|
||||
// error - If not NULL, this may be set to an error that occurs during
|
||||
// initializing the adapter.
|
||||
//
|
||||
// Returns a key path to use, or nil to omit the property from JSON.
|
||||
- (NSString *)JSONKeyPathForKey:(NSString *)key;
|
||||
// Returns a JSON adapter for modelClass, creating one of necessary. If no
|
||||
// adapter could be created, nil is returned.
|
||||
- (MTLJSONAdapter *)JSONAdapterForModelClass:(Class)modelClass error:(NSError **)error;
|
||||
|
||||
// Collect all value transformers needed for a given class.
|
||||
//
|
||||
// modelClass - The class from which to parse the JSON. This class must conform
|
||||
// to <MTLJSONSerializing>. This argument must not be nil.
|
||||
//
|
||||
// Returns a dictionary with the properties of modelClass that need
|
||||
// transformation as keys and the value transformers as values.
|
||||
+ (NSDictionary *)valueTransformersForModelClass:(Class)modelClass;
|
||||
|
||||
@end
|
||||
|
||||
@ -50,32 +74,192 @@ static NSString * const MTLJSONAdapterThrownExceptionErrorKey = @"MTLJSONAdapter
|
||||
#pragma mark Convenience methods
|
||||
|
||||
+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error {
|
||||
MTLJSONAdapter *adapter = [[self alloc] initWithJSONDictionary:JSONDictionary modelClass:modelClass error:error];
|
||||
return adapter.model;
|
||||
MTLJSONAdapter *adapter = [[self alloc] initWithModelClass:modelClass];
|
||||
|
||||
return [adapter modelFromJSONDictionary:JSONDictionary error:error];
|
||||
}
|
||||
|
||||
+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel<MTLJSONSerializing> *)model {
|
||||
MTLJSONAdapter *adapter = [[self alloc] initWithModel:model];
|
||||
return adapter.JSONDictionary;
|
||||
+ (NSArray *)modelsOfClass:(Class)modelClass fromJSONArray:(NSArray *)JSONArray error:(NSError **)error {
|
||||
if (JSONArray == nil || ![JSONArray isKindOfClass:NSArray.class]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Missing JSON array", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"%@ could not be created because an invalid JSON array was provided: %@", @""), NSStringFromClass(modelClass), JSONArray.class],
|
||||
};
|
||||
*error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorInvalidJSONDictionary userInfo:userInfo];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray *models = [NSMutableArray arrayWithCapacity:JSONArray.count];
|
||||
for (NSDictionary *JSONDictionary in JSONArray){
|
||||
MTLModel *model = [self modelOfClass:modelClass fromJSONDictionary:JSONDictionary error:error];
|
||||
|
||||
if (model == nil) return nil;
|
||||
|
||||
[models addObject:model];
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
+ (NSDictionary *)JSONDictionaryFromModel:(id<MTLJSONSerializing>)model error:(NSError **)error {
|
||||
MTLJSONAdapter *adapter = [[self alloc] initWithModelClass:model.class];
|
||||
|
||||
return [adapter JSONDictionaryFromModel:model error:error];
|
||||
}
|
||||
|
||||
+ (NSArray *)JSONArrayFromModels:(NSArray *)models error:(NSError **)error {
|
||||
NSParameterAssert(models != nil);
|
||||
NSParameterAssert([models isKindOfClass:NSArray.class]);
|
||||
|
||||
NSMutableArray *JSONArray = [NSMutableArray arrayWithCapacity:models.count];
|
||||
for (MTLModel<MTLJSONSerializing> *model in models) {
|
||||
NSDictionary *JSONDictionary = [self JSONDictionaryFromModel:model error:error];
|
||||
if (JSONDictionary == nil) return nil;
|
||||
|
||||
[JSONArray addObject:JSONDictionary];
|
||||
}
|
||||
|
||||
return JSONArray;
|
||||
}
|
||||
|
||||
#pragma mark Lifecycle
|
||||
|
||||
- (id)init {
|
||||
NSAssert(NO, @"%@ must be initialized with a JSON dictionary or model object", self.class);
|
||||
NSAssert(NO, @"%@ must be initialized with a model class", self.class);
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass error:(NSError **)error {
|
||||
- (id)initWithModelClass:(Class)modelClass {
|
||||
NSParameterAssert(modelClass != nil);
|
||||
NSParameterAssert([modelClass isSubclassOfClass:MTLModel.class]);
|
||||
NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)]);
|
||||
|
||||
if (JSONDictionary == nil) return nil;
|
||||
self = [super init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
if ([modelClass respondsToSelector:@selector(classForParsingJSONDictionary:)]) {
|
||||
modelClass = [modelClass classForParsingJSONDictionary:JSONDictionary];
|
||||
if (modelClass == nil) {
|
||||
_modelClass = modelClass;
|
||||
|
||||
_JSONKeyPathsByPropertyKey = [modelClass JSONKeyPathsByPropertyKey];
|
||||
|
||||
NSSet *propertyKeys = [self.modelClass propertyKeys];
|
||||
|
||||
for (NSString *mappedPropertyKey in _JSONKeyPathsByPropertyKey) {
|
||||
if (![propertyKeys containsObject:mappedPropertyKey]) {
|
||||
NSAssert(NO, @"%@ is not a property of %@.", mappedPropertyKey, modelClass);
|
||||
return nil;
|
||||
}
|
||||
|
||||
id value = _JSONKeyPathsByPropertyKey[mappedPropertyKey];
|
||||
|
||||
if ([value isKindOfClass:NSArray.class]) {
|
||||
for (NSString *keyPath in value) {
|
||||
if ([keyPath isKindOfClass:NSString.class]) continue;
|
||||
|
||||
NSAssert(NO, @"%@ must either map to a JSON key path or a JSON array of key paths, got: %@.", mappedPropertyKey, value);
|
||||
return nil;
|
||||
}
|
||||
} else if (![value isKindOfClass:NSString.class]) {
|
||||
NSAssert(NO, @"%@ must either map to a JSON key path or a JSON array of key paths, got: %@.",mappedPropertyKey, value);
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
_valueTransformersByPropertyKey = [self.class valueTransformersForModelClass:modelClass];
|
||||
|
||||
_JSONAdaptersByModelClass = [NSMapTable strongToStrongObjectsMapTable];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark Serialization
|
||||
|
||||
- (NSDictionary *)JSONDictionaryFromModel:(id<MTLJSONSerializing>)model error:(NSError **)error {
|
||||
NSParameterAssert(model != nil);
|
||||
NSParameterAssert([model isKindOfClass:self.modelClass]);
|
||||
|
||||
if (self.modelClass != model.class) {
|
||||
MTLJSONAdapter *otherAdapter = [self JSONAdapterForModelClass:model.class error:error];
|
||||
|
||||
return [otherAdapter JSONDictionaryFromModel:model error:error];
|
||||
}
|
||||
|
||||
NSSet *propertyKeysToSerialize = [self serializablePropertyKeys:[NSSet setWithArray:self.JSONKeyPathsByPropertyKey.allKeys] forModel:model];
|
||||
|
||||
NSDictionary *dictionaryValue = [model.dictionaryValue dictionaryWithValuesForKeys:propertyKeysToSerialize.allObjects];
|
||||
NSMutableDictionary *JSONDictionary = [[NSMutableDictionary alloc] initWithCapacity:dictionaryValue.count];
|
||||
|
||||
__block BOOL success = YES;
|
||||
__block NSError *tmpError = nil;
|
||||
|
||||
[dictionaryValue enumerateKeysAndObjectsUsingBlock:^(NSString *propertyKey, id value, BOOL *stop) {
|
||||
id JSONKeyPaths = self.JSONKeyPathsByPropertyKey[propertyKey];
|
||||
|
||||
if (JSONKeyPaths == nil) return;
|
||||
|
||||
NSValueTransformer *transformer = self.valueTransformersByPropertyKey[propertyKey];
|
||||
if ([transformer.class allowsReverseTransformation]) {
|
||||
// Map NSNull -> nil for the transformer, and then back for the
|
||||
// dictionaryValue we're going to insert into.
|
||||
if ([value isEqual:NSNull.null]) value = nil;
|
||||
|
||||
if ([transformer respondsToSelector:@selector(reverseTransformedValue:success:error:)]) {
|
||||
id<MTLTransformerErrorHandling> errorHandlingTransformer = (id)transformer;
|
||||
|
||||
value = [errorHandlingTransformer reverseTransformedValue:value success:&success error:&tmpError];
|
||||
|
||||
if (!success) {
|
||||
*stop = YES;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
value = [transformer reverseTransformedValue:value] ?: NSNull.null;
|
||||
}
|
||||
}
|
||||
|
||||
void (^createComponents)(id, NSString *) = ^(id obj, NSString *keyPath) {
|
||||
NSArray *keyPathComponents = [keyPath componentsSeparatedByString:@"."];
|
||||
|
||||
// Set up dictionaries at each step of the key path.
|
||||
for (NSString *component in keyPathComponents) {
|
||||
if ([obj valueForKey:component] == nil) {
|
||||
// Insert an empty mutable dictionary at this spot so that we
|
||||
// can set the whole key path afterward.
|
||||
[obj setValue:[NSMutableDictionary dictionary] forKey:component];
|
||||
}
|
||||
|
||||
obj = [obj valueForKey:component];
|
||||
}
|
||||
};
|
||||
|
||||
if ([JSONKeyPaths isKindOfClass:NSString.class]) {
|
||||
createComponents(JSONDictionary, JSONKeyPaths);
|
||||
|
||||
[JSONDictionary setValue:value forKeyPath:JSONKeyPaths];
|
||||
}
|
||||
|
||||
if ([JSONKeyPaths isKindOfClass:NSArray.class]) {
|
||||
for (NSString *JSONKeyPath in JSONKeyPaths) {
|
||||
createComponents(JSONDictionary, JSONKeyPath);
|
||||
|
||||
[JSONDictionary setValue:value[JSONKeyPath] forKeyPath:JSONKeyPath];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
if (success) {
|
||||
return JSONDictionary;
|
||||
} else {
|
||||
if (error != NULL) *error = tmpError;
|
||||
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (id)modelFromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error {
|
||||
if ([self.modelClass respondsToSelector:@selector(classForParsingJSONDictionary:)]) {
|
||||
Class class = [self.modelClass classForParsingJSONDictionary:JSONDictionary];
|
||||
if (class == nil) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not parse JSON", @""),
|
||||
@ -88,37 +272,70 @@ static NSString * const MTLJSONAdapterThrownExceptionErrorKey = @"MTLJSONAdapter
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSAssert([modelClass isSubclassOfClass:MTLModel.class], @"Class %@ returned from +classForParsingJSONDictionary: is not a subclass of MTLModel", modelClass);
|
||||
NSAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)], @"Class %@ returned from +classForParsingJSONDictionary: does not conform to <MTLJSONSerializing>", modelClass);
|
||||
if (class != self.modelClass) {
|
||||
NSAssert([class conformsToProtocol:@protocol(MTLJSONSerializing)], @"Class %@ returned from +classForParsingJSONDictionary: does not conform to <MTLJSONSerializing>", class);
|
||||
|
||||
MTLJSONAdapter *otherAdapter = [self JSONAdapterForModelClass:class error:error];
|
||||
|
||||
return [otherAdapter modelFromJSONDictionary:JSONDictionary error:error];
|
||||
}
|
||||
}
|
||||
|
||||
self = [super init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
_modelClass = modelClass;
|
||||
_JSONKeyPathsByPropertyKey = [[modelClass JSONKeyPathsByPropertyKey] copy];
|
||||
|
||||
NSMutableDictionary *dictionaryValue = [[NSMutableDictionary alloc] initWithCapacity:JSONDictionary.count];
|
||||
|
||||
for (NSString *propertyKey in [self.modelClass propertyKeys]) {
|
||||
NSString *JSONKeyPath = [self JSONKeyPathForKey:propertyKey];
|
||||
if (JSONKeyPath == nil) continue;
|
||||
id JSONKeyPaths = self.JSONKeyPathsByPropertyKey[propertyKey];
|
||||
|
||||
if (JSONKeyPaths == nil) continue;
|
||||
|
||||
id value;
|
||||
|
||||
if ([JSONKeyPaths isKindOfClass:NSArray.class]) {
|
||||
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
|
||||
|
||||
for (NSString *keyPath in JSONKeyPaths) {
|
||||
BOOL success = NO;
|
||||
id value = [JSONDictionary mtl_valueForJSONKeyPath:keyPath success:&success error:error];
|
||||
|
||||
if (!success) return nil;
|
||||
|
||||
if (value != nil) dictionary[keyPath] = value;
|
||||
}
|
||||
|
||||
value = dictionary;
|
||||
} else {
|
||||
BOOL success = NO;
|
||||
value = [JSONDictionary mtl_valueForJSONKeyPath:JSONKeyPaths success:&success error:error];
|
||||
|
||||
if (!success) return nil;
|
||||
}
|
||||
|
||||
id value = [JSONDictionary valueForKeyPath:JSONKeyPath];
|
||||
if (value == nil) continue;
|
||||
|
||||
@try {
|
||||
NSValueTransformer *transformer = [self JSONTransformerForKey:propertyKey];
|
||||
NSValueTransformer *transformer = self.valueTransformersByPropertyKey[propertyKey];
|
||||
if (transformer != nil) {
|
||||
// Map NSNull -> nil for the transformer, and then back for the
|
||||
// dictionary we're going to insert into.
|
||||
if ([value isEqual:NSNull.null]) value = nil;
|
||||
value = [transformer transformedValue:value] ?: NSNull.null;
|
||||
|
||||
if ([transformer respondsToSelector:@selector(transformedValue:success:error:)]) {
|
||||
id<MTLTransformerErrorHandling> errorHandlingTransformer = (id)transformer;
|
||||
|
||||
BOOL success = YES;
|
||||
value = [errorHandlingTransformer transformedValue:value success:&success error:error];
|
||||
|
||||
if (!success) return nil;
|
||||
} else {
|
||||
value = [transformer transformedValue:value];
|
||||
}
|
||||
|
||||
if (value == nil) value = NSNull.null;
|
||||
}
|
||||
|
||||
dictionaryValue[propertyKey] = value;
|
||||
} @catch (NSException *ex) {
|
||||
NSLog(@"*** Caught exception %@ parsing JSON key path \"%@\" from: %@", ex, JSONKeyPath, JSONDictionary);
|
||||
NSLog(@"*** Caught exception %@ parsing JSON key path \"%@\" from: %@", ex, JSONKeyPaths, JSONDictionary);
|
||||
|
||||
// Fail fast in Debug builds.
|
||||
#if DEBUG
|
||||
@ -126,7 +343,8 @@ static NSString * const MTLJSONAdapterThrownExceptionErrorKey = @"MTLJSONAdapter
|
||||
#else
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: ex.description,
|
||||
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Caught exception parsing JSON key path \"%@\" for model class: %@", JSONKeyPaths, self.modelClass],
|
||||
NSLocalizedRecoverySuggestionErrorKey: ex.description,
|
||||
NSLocalizedFailureReasonErrorKey: ex.reason,
|
||||
MTLJSONAdapterThrownExceptionErrorKey: ex
|
||||
};
|
||||
@ -139,96 +357,295 @@ static NSString * const MTLJSONAdapterThrownExceptionErrorKey = @"MTLJSONAdapter
|
||||
}
|
||||
}
|
||||
|
||||
_model = [self.modelClass modelWithDictionary:dictionaryValue error:error];
|
||||
if (_model == nil) return nil;
|
||||
id model = [self.modelClass modelWithDictionary:dictionaryValue error:error];
|
||||
|
||||
return self;
|
||||
return [model validate:error] ? model : nil;
|
||||
}
|
||||
|
||||
- (id)initWithModel:(MTLModel<MTLJSONSerializing> *)model {
|
||||
NSParameterAssert(model != nil);
|
||||
+ (NSDictionary *)valueTransformersForModelClass:(Class)modelClass {
|
||||
NSParameterAssert(modelClass != nil);
|
||||
NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)]);
|
||||
|
||||
self = [super init];
|
||||
if (self == nil) return nil;
|
||||
NSMutableDictionary *result = [NSMutableDictionary dictionary];
|
||||
|
||||
_model = model;
|
||||
_modelClass = model.class;
|
||||
_JSONKeyPathsByPropertyKey = [[model.class JSONKeyPathsByPropertyKey] copy];
|
||||
for (NSString *key in [modelClass propertyKeys]) {
|
||||
SEL selector = MTLSelectorWithKeyPattern(key, "JSONTransformer");
|
||||
if ([modelClass respondsToSelector:selector]) {
|
||||
IMP imp = [modelClass methodForSelector:selector];
|
||||
NSValueTransformer * (*function)(id, SEL) = (__typeof__(function))imp;
|
||||
NSValueTransformer *transformer = function(modelClass, selector);
|
||||
|
||||
return self;
|
||||
}
|
||||
if (transformer != nil) result[key] = transformer;
|
||||
|
||||
#pragma mark Serialization
|
||||
|
||||
- (NSDictionary *)JSONDictionary {
|
||||
NSDictionary *dictionaryValue = self.model.dictionaryValue;
|
||||
NSMutableDictionary *JSONDictionary = [[NSMutableDictionary alloc] initWithCapacity:dictionaryValue.count];
|
||||
|
||||
[dictionaryValue enumerateKeysAndObjectsUsingBlock:^(NSString *propertyKey, id value, BOOL *stop) {
|
||||
NSString *JSONKeyPath = [self JSONKeyPathForKey:propertyKey];
|
||||
if (JSONKeyPath == nil) return;
|
||||
|
||||
NSValueTransformer *transformer = [self JSONTransformerForKey:propertyKey];
|
||||
if ([transformer.class allowsReverseTransformation]) {
|
||||
// Map NSNull -> nil for the transformer, and then back for the
|
||||
// dictionaryValue we're going to insert into.
|
||||
if ([value isEqual:NSNull.null]) value = nil;
|
||||
value = [transformer reverseTransformedValue:value] ?: NSNull.null;
|
||||
continue;
|
||||
}
|
||||
|
||||
NSArray *keyPathComponents = [JSONKeyPath componentsSeparatedByString:@"."];
|
||||
if ([modelClass respondsToSelector:@selector(JSONTransformerForKey:)]) {
|
||||
NSValueTransformer *transformer = [modelClass JSONTransformerForKey:key];
|
||||
|
||||
// Set up dictionaries at each step of the key path.
|
||||
id obj = JSONDictionary;
|
||||
for (NSString *component in keyPathComponents) {
|
||||
if ([obj valueForKey:component] == nil) {
|
||||
// Insert an empty mutable dictionary at this spot so that we
|
||||
// can set the whole key path afterward.
|
||||
[obj setValue:[NSMutableDictionary dictionary] forKey:component];
|
||||
if (transformer != nil) {
|
||||
result[key] = transformer;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
objc_property_t property = class_getProperty(modelClass, key.UTF8String);
|
||||
|
||||
if (property == NULL) continue;
|
||||
|
||||
mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
|
||||
@onExit {
|
||||
free(attributes);
|
||||
};
|
||||
|
||||
NSValueTransformer *transformer = nil;
|
||||
|
||||
if (*(attributes->type) == *(@encode(id))) {
|
||||
Class propertyClass = attributes->objectClass;
|
||||
|
||||
if (propertyClass != nil) {
|
||||
transformer = [self transformerForModelPropertiesOfClass:propertyClass];
|
||||
}
|
||||
|
||||
obj = [obj valueForKey:component];
|
||||
|
||||
// For user-defined MTLModel, try parse it with dictionaryTransformer.
|
||||
if (nil == transformer && [propertyClass conformsToProtocol:@protocol(MTLJSONSerializing)]) {
|
||||
transformer = [self dictionaryTransformerWithModelClass:propertyClass];
|
||||
}
|
||||
|
||||
if (transformer == nil) transformer = [NSValueTransformer mtl_validatingTransformerForClass:propertyClass ?: NSObject.class];
|
||||
} else {
|
||||
transformer = [self transformerForModelPropertiesOfObjCType:attributes->type] ?: [NSValueTransformer mtl_validatingTransformerForClass:NSValue.class];
|
||||
}
|
||||
|
||||
[JSONDictionary setValue:value forKeyPath:JSONKeyPath];
|
||||
}];
|
||||
|
||||
return JSONDictionary;
|
||||
}
|
||||
|
||||
- (NSValueTransformer *)JSONTransformerForKey:(NSString *)key {
|
||||
NSParameterAssert(key != nil);
|
||||
|
||||
SEL selector = MTLSelectorWithKeyPattern(key, "JSONTransformer");
|
||||
if ([self.modelClass respondsToSelector:selector]) {
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self.modelClass methodSignatureForSelector:selector]];
|
||||
invocation.target = self.modelClass;
|
||||
invocation.selector = selector;
|
||||
[invocation invoke];
|
||||
|
||||
__unsafe_unretained id result = nil;
|
||||
[invocation getReturnValue:&result];
|
||||
return result;
|
||||
if (transformer != nil) result[key] = transformer;
|
||||
}
|
||||
|
||||
if ([self.modelClass respondsToSelector:@selector(JSONTransformerForKey:)]) {
|
||||
return [self.modelClass JSONTransformerForKey:key];
|
||||
return result;
|
||||
}
|
||||
|
||||
- (MTLJSONAdapter *)JSONAdapterForModelClass:(Class)modelClass error:(NSError **)error {
|
||||
NSParameterAssert(modelClass != nil);
|
||||
NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)]);
|
||||
|
||||
@synchronized(self) {
|
||||
MTLJSONAdapter *result = [self.JSONAdaptersByModelClass objectForKey:modelClass];
|
||||
|
||||
if (result != nil) return result;
|
||||
|
||||
result = [[self.class alloc] initWithModelClass:modelClass];
|
||||
|
||||
if (result != nil) {
|
||||
[self.JSONAdaptersByModelClass setObject:result forKey:modelClass];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSSet *)serializablePropertyKeys:(NSSet *)propertyKeys forModel:(id<MTLJSONSerializing>)model {
|
||||
return propertyKeys;
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)transformerForModelPropertiesOfClass:(Class)modelClass {
|
||||
NSParameterAssert(modelClass != nil);
|
||||
|
||||
SEL selector = MTLSelectorWithKeyPattern(NSStringFromClass(modelClass), "JSONTransformer");
|
||||
if (![self respondsToSelector:selector]) return nil;
|
||||
|
||||
IMP imp = [self methodForSelector:selector];
|
||||
NSValueTransformer * (*function)(id, SEL) = (__typeof__(function))imp;
|
||||
NSValueTransformer *result = function(self, selector);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)transformerForModelPropertiesOfObjCType:(const char *)objCType {
|
||||
NSParameterAssert(objCType != NULL);
|
||||
|
||||
if (strcmp(objCType, @encode(BOOL)) == 0) {
|
||||
return [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)JSONKeyPathForKey:(NSString *)key {
|
||||
NSParameterAssert(key != nil);
|
||||
@end
|
||||
|
||||
id JSONKeyPath = self.JSONKeyPathsByPropertyKey[key];
|
||||
if ([JSONKeyPath isEqual:NSNull.null]) return nil;
|
||||
@implementation MTLJSONAdapter (ValueTransformers)
|
||||
|
||||
if (JSONKeyPath == nil) {
|
||||
return key;
|
||||
} else {
|
||||
return JSONKeyPath;
|
||||
}
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)dictionaryTransformerWithModelClass:(Class)modelClass {
|
||||
NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLModel)]);
|
||||
NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)]);
|
||||
__block MTLJSONAdapter *adapter;
|
||||
|
||||
return [MTLValueTransformer
|
||||
transformerUsingForwardBlock:^ id (id JSONDictionary, BOOL *success, NSError **error) {
|
||||
if (JSONDictionary == nil) return nil;
|
||||
|
||||
if (![JSONDictionary isKindOfClass:NSDictionary.class]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert JSON dictionary to model object", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSDictionary, got: %@", @""), JSONDictionary],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey : JSONDictionary
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (!adapter) {
|
||||
adapter = [[self alloc] initWithModelClass:modelClass];
|
||||
}
|
||||
id model = [adapter modelFromJSONDictionary:JSONDictionary error:error];
|
||||
if (model == nil) {
|
||||
*success = NO;
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
reverseBlock:^ NSDictionary * (id model, BOOL *success, NSError **error) {
|
||||
if (model == nil) return nil;
|
||||
|
||||
if (![model conformsToProtocol:@protocol(MTLModel)] || ![model conformsToProtocol:@protocol(MTLJSONSerializing)]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert model object to JSON dictionary", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected a MTLModel object conforming to <MTLJSONSerializing>, got: %@.", @""), model],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey : model
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (!adapter) {
|
||||
adapter = [[self alloc] initWithModelClass:modelClass];
|
||||
}
|
||||
NSDictionary *result = [adapter JSONDictionaryFromModel:model error:error];
|
||||
if (result == nil) {
|
||||
*success = NO;
|
||||
}
|
||||
|
||||
return result;
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)arrayTransformerWithModelClass:(Class)modelClass {
|
||||
id<MTLTransformerErrorHandling> dictionaryTransformer = [self dictionaryTransformerWithModelClass:modelClass];
|
||||
|
||||
return [MTLValueTransformer
|
||||
transformerUsingForwardBlock:^ id (NSArray *dictionaries, BOOL *success, NSError **error) {
|
||||
if (dictionaries == nil) return nil;
|
||||
|
||||
if (![dictionaries isKindOfClass:NSArray.class]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert JSON array to model array", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSArray, got: %@.", @""), dictionaries],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey : dictionaries
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray *models = [NSMutableArray arrayWithCapacity:dictionaries.count];
|
||||
for (id JSONDictionary in dictionaries) {
|
||||
if (JSONDictionary == NSNull.null) {
|
||||
[models addObject:NSNull.null];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (![JSONDictionary isKindOfClass:NSDictionary.class]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert JSON array to model array", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSDictionary or an NSNull, got: %@.", @""), JSONDictionary],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey : JSONDictionary
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
|
||||
id model = [dictionaryTransformer transformedValue:JSONDictionary success:success error:error];
|
||||
|
||||
if (*success == NO) return nil;
|
||||
|
||||
if (model == nil) continue;
|
||||
|
||||
[models addObject:model];
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
reverseBlock:^ id (NSArray *models, BOOL *success, NSError **error) {
|
||||
if (models == nil) return nil;
|
||||
|
||||
if (![models isKindOfClass:NSArray.class]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert model array to JSON array", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSArray, got: %@.", @""), models],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey : models
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray *dictionaries = [NSMutableArray arrayWithCapacity:models.count];
|
||||
for (id model in models) {
|
||||
if (model == NSNull.null) {
|
||||
[dictionaries addObject:NSNull.null];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (![model isKindOfClass:MTLModel.class]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert JSON array to model array", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected a MTLModel or an NSNull, got: %@.", @""), model],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey : model
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSDictionary *dict = [dictionaryTransformer reverseTransformedValue:model success:success error:error];
|
||||
|
||||
if (*success == NO) return nil;
|
||||
|
||||
if (dict == nil) continue;
|
||||
|
||||
[dictionaries addObject:dict];
|
||||
}
|
||||
|
||||
return dictionaries;
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)NSURLJSONTransformer {
|
||||
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)NSUUIDJSONTransformer {
|
||||
return [NSValueTransformer valueTransformerForName:MTLUUIDValueTransformerName];
|
||||
}
|
||||
|
||||
@end
|
||||
@ -238,12 +655,12 @@ static NSString * const MTLJSONAdapterThrownExceptionErrorKey = @"MTLJSONAdapter
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
|
||||
|
||||
+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary {
|
||||
return [self modelOfClass:modelClass fromJSONDictionary:JSONDictionary error:NULL];
|
||||
+ (NSArray *)JSONArrayFromModels:(NSArray *)models {
|
||||
return [self JSONArrayFromModels:models error:NULL];
|
||||
}
|
||||
|
||||
- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass {
|
||||
return [self initWithJSONDictionary:JSONDictionary modelClass:modelClass error:NULL];
|
||||
+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel<MTLJSONSerializing> *)model {
|
||||
return [self JSONDictionaryFromModel:model error:NULL];
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
@ -1,143 +0,0 @@
|
||||
//
|
||||
// MTLManagedObjectAdapter.h
|
||||
// Mantle
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-03-29.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
@class MTLModel;
|
||||
|
||||
// A MTLModel object that supports being serialized to and from Core Data as an
|
||||
// NSManagedObject.
|
||||
@protocol MTLManagedObjectSerializing
|
||||
@required
|
||||
|
||||
// The name of the Core Data entity that the receiver serializes to and
|
||||
// deserializes from.
|
||||
//
|
||||
// This method must not return nil.
|
||||
+ (NSString *)managedObjectEntityName;
|
||||
|
||||
// Specifies how to map property keys to different keys on the receiver's
|
||||
// +managedObjectEntity.
|
||||
//
|
||||
// Entity attributes will be mapped to and from the receiver's properties using
|
||||
// +entityAttributeTransformerForKey:. Entity relationships will be mapped to
|
||||
// and from MTLModel objects using +relationshipModelClassesByPropertyKey.
|
||||
// Fetched properties are not supported.
|
||||
//
|
||||
// Subclasses overriding this method should combine their values with those of
|
||||
// `super`.
|
||||
//
|
||||
// Any property keys not present in the dictionary are assumed to match the
|
||||
// entity key that should be used. Any keys associated with NSNull will not
|
||||
// participate in managed object serialization.
|
||||
//
|
||||
// Returns a dictionary mapping property keys to entity keys (as strings) or
|
||||
// NSNull values.
|
||||
+ (NSDictionary *)managedObjectKeysByPropertyKey;
|
||||
|
||||
@optional
|
||||
|
||||
// Specifies how to convert the given property key to a managed object attribute. If
|
||||
// reversible, the transformer will also be used to convert the managed object
|
||||
// attribute back to the property.
|
||||
//
|
||||
// If the receiver implements a `+<key>EntityAttributeTransformer` method,
|
||||
// MTLManagedObjectAdapter will use the result of that method instead.
|
||||
//
|
||||
// Returns a value transformer, or nil if no transformation should be performed.
|
||||
+ (NSValueTransformer *)entityAttributeTransformerForKey:(NSString *)key;
|
||||
|
||||
// Specifies the MTLModel subclasses that should be deserialized to the
|
||||
// receiver's property keys when a property key corresponds to an entity
|
||||
// relationship.
|
||||
//
|
||||
// In other words, the dictionary returned by this method is used to decode
|
||||
// managed object relationships into MTLModels (or NSArrays or NSSets thereof)
|
||||
// set on the receiver.
|
||||
//
|
||||
// If a property key is omitted from the returned dictionary, but present in
|
||||
// +managedObjectKeysByPropertyKey, and the receiver's +managedObjectEntity has
|
||||
// a relationship at the corresponding key, an exception will be thrown during
|
||||
// deserialization.
|
||||
//
|
||||
// Subclasses overriding this method should combine their values with those of
|
||||
// `super`.
|
||||
//
|
||||
// Returns a dictionary mapping property keys to the Class objects that should
|
||||
// be used.
|
||||
+ (NSDictionary *)relationshipModelClassesByPropertyKey;
|
||||
|
||||
// Overridden to deserialize a different class instead of the receiver, based on
|
||||
// information in the provided object.
|
||||
//
|
||||
// This is mostly useful for class clusters, where the abstract base class would
|
||||
// be passed into +[MTLManagedObjectAdapter
|
||||
// modelOfClass:fromManagedObject:error:], but a subclass should be instantiated
|
||||
// instead.
|
||||
//
|
||||
// managedObject - The object that will be deserialized.
|
||||
//
|
||||
// Returns the class that should be instantiated (which may be the receiver), or
|
||||
// nil to abort parsing (e.g., if the data is invalid).
|
||||
+ (Class)classForDeserializingManagedObject:(NSManagedObject *)managedObject;
|
||||
|
||||
@end
|
||||
|
||||
// The domain for errors originating from MTLManagedObjectAdapter.
|
||||
extern NSString * const MTLManagedObjectAdapterErrorDomain;
|
||||
|
||||
// +classForDeserializingManagedObject: returned nil for the given object.
|
||||
extern const NSInteger MTLManagedObjectAdapterErrorNoClassFound;
|
||||
|
||||
// An NSManagedObject failed to initialize.
|
||||
extern const NSInteger MTLManagedObjectAdapterErrorInitializationFailed;
|
||||
|
||||
// The managed object key specified by +managedObjectKeysByPropertyKey does not
|
||||
// exist in the NSEntityDescription.
|
||||
extern const NSInteger MTLManagedObjectAdapterErrorInvalidManagedObjectKey;
|
||||
|
||||
// The managed object property specified has a type that isn't supported by
|
||||
// MTLManagedObjectAdapter.
|
||||
extern const NSInteger MTLManagedObjectAdapterErrorUnsupportedManagedObjectPropertyType;
|
||||
|
||||
// A MTLModel property cannot be serialized to or deserialized from an
|
||||
// NSManagedObject relationship.
|
||||
//
|
||||
// For a to-one relationship, this means that the property does not contain
|
||||
// a MTLModel, or the MTLModel does not conform to <MTLManagedObjectSerializing>.
|
||||
//
|
||||
// For a to-many relationship, this means that the property does not contain an
|
||||
// NSArray or NSSet of MTLModel<MTLManagedObjectSerializing> instances.
|
||||
extern const NSInteger MTLManagedObjectAdapterErrorUnsupportedRelationshipClass;
|
||||
|
||||
// Converts a MTLModel object to and from an NSManagedObject.
|
||||
@interface MTLManagedObjectAdapter : NSObject
|
||||
|
||||
// Attempts to deserialize an NSManagedObject into a MTLModel object.
|
||||
//
|
||||
// modelClass - The MTLModel subclass to return. This class must conform to
|
||||
// <MTLManagedObjectSerializing>. This argument must not be nil.
|
||||
// managedObject - The managed object to deserialize. If this argument is nil,
|
||||
// the method returns nil.
|
||||
// error - If not NULL, this may be set to an error that occurs during
|
||||
// deserialization or initializing an instance of `modelClass`.
|
||||
//
|
||||
// Returns an instance of `modelClass` upon success, or nil if an error
|
||||
// occurred.
|
||||
+ (id)modelOfClass:(Class)modelClass fromManagedObject:(NSManagedObject *)managedObject error:(NSError **)error;
|
||||
|
||||
// Serializes a MTLModel into an NSManagedObject.
|
||||
//
|
||||
// model - The model object to serialize. This argument must not be nil.
|
||||
// context - The context into which to insert the created managed object. This
|
||||
// argument must not be nil.
|
||||
// error - If not NULL, this may be set to an error that occurs during
|
||||
// serialization or insertion.
|
||||
+ (NSManagedObject *)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model insertingIntoContext:(NSManagedObjectContext *)context error:(NSError **)error;
|
||||
|
||||
@end
|
||||
@ -1,530 +0,0 @@
|
||||
//
|
||||
// MTLManagedObjectAdapter.m
|
||||
// Mantle
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-03-29.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MTLManagedObjectAdapter.h"
|
||||
#import "EXTScope.h"
|
||||
#import "MTLModel.h"
|
||||
#import "MTLReflection.h"
|
||||
|
||||
NSString * const MTLManagedObjectAdapterErrorDomain = @"MTLManagedObjectAdapterErrorDomain";
|
||||
const NSInteger MTLManagedObjectAdapterErrorNoClassFound = 2;
|
||||
const NSInteger MTLManagedObjectAdapterErrorInitializationFailed = 3;
|
||||
const NSInteger MTLManagedObjectAdapterErrorInvalidManagedObjectKey = 4;
|
||||
const NSInteger MTLManagedObjectAdapterErrorUnsupportedManagedObjectPropertyType = 5;
|
||||
const NSInteger MTLManagedObjectAdapterErrorUnsupportedRelationshipClass = 6;
|
||||
|
||||
// Performs the given block in the context's queue, if it has one.
|
||||
static id performInContext(NSManagedObjectContext *context, id (^block)(void)) {
|
||||
if (context.concurrencyType == NSConfinementConcurrencyType) {
|
||||
return block();
|
||||
}
|
||||
|
||||
__block id result = nil;
|
||||
[context performBlockAndWait:^{
|
||||
result = block();
|
||||
}];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// An exception was thrown and caught.
|
||||
static const NSInteger MTLManagedObjectAdapterErrorExceptionThrown = 1;
|
||||
|
||||
@interface MTLManagedObjectAdapter ()
|
||||
|
||||
// The MTLModel subclass being serialized or deserialized.
|
||||
@property (nonatomic, strong, readonly) Class modelClass;
|
||||
|
||||
// A cached copy of the return value of +managedObjectKeysByPropertyKey.
|
||||
@property (nonatomic, copy, readonly) NSDictionary *managedObjectKeysByPropertyKey;
|
||||
|
||||
// A cached copy of the return value of +relationshipModelClassesByPropertyKey.
|
||||
@property (nonatomic, copy, readonly) NSDictionary *relationshipModelClassesByPropertyKey;
|
||||
|
||||
// Initializes the receiver to serialize or deserialize a MTLModel of the given
|
||||
// class.
|
||||
- (id)initWithModelClass:(Class)modelClass;
|
||||
|
||||
// Invoked from +modelOfClass:fromManagedObject:processedObjects:error: after
|
||||
// the receiver's properties have been initialized.
|
||||
- (id)modelFromManagedObject:(NSManagedObject *)managedObject processedObjects:(CFMutableDictionaryRef)processedObjects error:(NSError **)error;
|
||||
|
||||
// Performs the actual work of deserialization. This method is also invoked when
|
||||
// processing relationships, to create a new adapter (if needed) to handle them.
|
||||
//
|
||||
// `processedObjects` is a dictionary mapping NSManagedObjects to the MTLModels
|
||||
// that have been created so far. It should remain alive for the full process
|
||||
// of deserializing the top-level managed object.
|
||||
+ (id)modelOfClass:(Class)modelClass fromManagedObject:(NSManagedObject *)managedObject processedObjects:(CFMutableDictionaryRef)processedObjects error:(NSError **)error;
|
||||
|
||||
// Invoked from
|
||||
// +managedObjectFromModel:insertingIntoContext:processedObjects:error: after
|
||||
// the receiver's properties have been initialized.
|
||||
- (NSManagedObject *)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model insertingIntoContext:(NSManagedObjectContext *)context processedObjects:(CFMutableDictionaryRef)processedObjects error:(NSError **)error;
|
||||
|
||||
// Performs the actual work of serialization. This method is also invoked when
|
||||
// processing relationships, to create a new adapter (if needed) to handle them.
|
||||
//
|
||||
// `processedObjects` is a dictionary mapping MTLModels to the NSManagedObjects
|
||||
// that have been created so far. It should remain alive for the full process
|
||||
// of serializing the top-level MTLModel.
|
||||
+ (NSManagedObject *)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model insertingIntoContext:(NSManagedObjectContext *)context processedObjects:(CFMutableDictionaryRef)processedObjects error:(NSError **)error;
|
||||
|
||||
// Looks up the NSValueTransformer that should be used for any attribute that
|
||||
// corresponds the given property key.
|
||||
//
|
||||
// key - The property key to transform from or to. This argument must not be nil.
|
||||
//
|
||||
// Returns a transformer to use, or nil to not transform the property.
|
||||
- (NSValueTransformer *)entityAttributeTransformerForKey:(NSString *)key;
|
||||
|
||||
// Looks up the managed object key that corresponds to the given key.
|
||||
//
|
||||
// key - The property key to retrieve the corresponding managed object key for.
|
||||
// This argument must not be nil.
|
||||
//
|
||||
// Returns a key to use, or nil to omit the property from managed object
|
||||
// serialization.
|
||||
- (NSString *)managedObjectKeyForKey:(NSString *)key;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLManagedObjectAdapter
|
||||
|
||||
#pragma mark Lifecycle
|
||||
|
||||
- (id)init {
|
||||
NSAssert(NO, @"%@ should not be initialized using -init", self.class);
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (id)initWithModelClass:(Class)modelClass {
|
||||
NSParameterAssert(modelClass != nil);
|
||||
|
||||
self = [super init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
_modelClass = modelClass;
|
||||
_managedObjectKeysByPropertyKey = [[modelClass managedObjectKeysByPropertyKey] copy];
|
||||
|
||||
if ([modelClass respondsToSelector:@selector(relationshipModelClassesByPropertyKey)]) {
|
||||
_relationshipModelClassesByPropertyKey = [[modelClass relationshipModelClassesByPropertyKey] copy];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark Serialization
|
||||
|
||||
- (id)modelFromManagedObject:(NSManagedObject *)managedObject processedObjects:(CFMutableDictionaryRef)processedObjects error:(NSError **)error {
|
||||
NSParameterAssert(managedObject != nil);
|
||||
NSParameterAssert(processedObjects != nil);
|
||||
|
||||
NSEntityDescription *entity = managedObject.entity;
|
||||
NSAssert(entity != nil, @"%@ returned a nil +entity", managedObject);
|
||||
|
||||
NSManagedObjectContext *context = managedObject.managedObjectContext;
|
||||
|
||||
NSDictionary *managedObjectProperties = entity.propertiesByName;
|
||||
MTLModel *model = [[self.modelClass alloc] init];
|
||||
|
||||
// Pre-emptively consider this object processed, so that we don't get into
|
||||
// any cycles when processing its relationships.
|
||||
CFDictionaryAddValue(processedObjects, (__bridge void *)managedObject, (__bridge void *)model);
|
||||
|
||||
BOOL (^setValueForKey)(NSString *, id) = ^(NSString *key, id value) {
|
||||
// Mark this as being autoreleased, because validateValue may return
|
||||
// a new object to be stored in this variable (and we don't want ARC to
|
||||
// double-free or leak the old or new values).
|
||||
__autoreleasing id replaceableValue = value;
|
||||
if (![model validateValue:&replaceableValue forKey:key error:error]) return NO;
|
||||
|
||||
[model setValue:replaceableValue forKey:key];
|
||||
return YES;
|
||||
};
|
||||
|
||||
for (NSString *propertyKey in [self.modelClass propertyKeys]) {
|
||||
NSString *managedObjectKey = [self managedObjectKeyForKey:propertyKey];
|
||||
if (managedObjectKey == nil) continue;
|
||||
|
||||
BOOL (^deserializeAttribute)(NSAttributeDescription *) = ^(NSAttributeDescription *attributeDescription) {
|
||||
id value = performInContext(context, ^{
|
||||
return [managedObject valueForKey:managedObjectKey];
|
||||
});
|
||||
|
||||
NSValueTransformer *attributeTransformer = [self entityAttributeTransformerForKey:propertyKey];
|
||||
if (attributeTransformer != nil) value = [attributeTransformer reverseTransformedValue:value];
|
||||
|
||||
return setValueForKey(propertyKey, value);
|
||||
};
|
||||
|
||||
BOOL (^deserializeRelationship)(NSRelationshipDescription *) = ^(NSRelationshipDescription *relationshipDescription) {
|
||||
Class nestedClass = self.relationshipModelClassesByPropertyKey[propertyKey];
|
||||
if (nestedClass == nil) {
|
||||
[NSException raise:NSInvalidArgumentException format:@"No class specified for decoding relationship at key \"%@\" in managed object %@", managedObjectKey, managedObject];
|
||||
}
|
||||
|
||||
if ([relationshipDescription isToMany]) {
|
||||
id models = performInContext(context, ^ id {
|
||||
id relationshipCollection = [managedObject valueForKey:managedObjectKey];
|
||||
NSMutableArray *models = [NSMutableArray arrayWithCapacity:[relationshipCollection count]];
|
||||
|
||||
for (NSManagedObject *nestedObject in relationshipCollection) {
|
||||
MTLModel *model = [self.class modelOfClass:nestedClass fromManagedObject:nestedObject processedObjects:processedObjects error:error];
|
||||
if (model == nil) return nil;
|
||||
|
||||
[models addObject:model];
|
||||
}
|
||||
|
||||
return models;
|
||||
});
|
||||
|
||||
if (models == nil) return NO;
|
||||
if (![relationshipDescription isOrdered]) models = [NSSet setWithArray:models];
|
||||
|
||||
return setValueForKey(propertyKey, models);
|
||||
} else {
|
||||
NSManagedObject *nestedObject = performInContext(context, ^{
|
||||
return [managedObject valueForKey:managedObjectKey];
|
||||
});
|
||||
|
||||
if (nestedObject == nil) return YES;
|
||||
|
||||
MTLModel *model = [self.class modelOfClass:nestedClass fromManagedObject:nestedObject processedObjects:processedObjects error:error];
|
||||
if (model == nil) return NO;
|
||||
|
||||
return setValueForKey(propertyKey, model);
|
||||
}
|
||||
};
|
||||
|
||||
BOOL (^deserializeProperty)(NSPropertyDescription *) = ^(NSPropertyDescription *propertyDescription) {
|
||||
if (propertyDescription == nil) {
|
||||
if (error != NULL) {
|
||||
NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"No property by name \"%@\" exists on the entity.", @""), managedObjectKey];
|
||||
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not deserialize managed object", @""),
|
||||
NSLocalizedFailureReasonErrorKey: failureReason,
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLManagedObjectAdapterErrorDomain code:MTLManagedObjectAdapterErrorInvalidManagedObjectKey userInfo:userInfo];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Jump through some hoops to avoid referencing classes directly.
|
||||
NSString *propertyClassName = NSStringFromClass(propertyDescription.class);
|
||||
if ([propertyClassName isEqual:@"NSAttributeDescription"]) {
|
||||
return deserializeAttribute((id)propertyDescription);
|
||||
} else if ([propertyClassName isEqual:@"NSRelationshipDescription"]) {
|
||||
return deserializeRelationship((id)propertyDescription);
|
||||
} else {
|
||||
if (error != NULL) {
|
||||
NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"Property descriptions of class %@ are unsupported.", @""), propertyClassName];
|
||||
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not deserialize managed object", @""),
|
||||
NSLocalizedFailureReasonErrorKey: failureReason,
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLManagedObjectAdapterErrorDomain code:MTLManagedObjectAdapterErrorUnsupportedManagedObjectPropertyType userInfo:userInfo];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
};
|
||||
|
||||
if (!deserializeProperty(managedObjectProperties[managedObjectKey])) return nil;
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
+ (id)modelOfClass:(Class)modelClass fromManagedObject:(NSManagedObject *)managedObject error:(NSError **)error {
|
||||
CFMutableDictionaryRef processedObjects = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
if (processedObjects == NULL) return nil;
|
||||
|
||||
@onExit {
|
||||
CFRelease(processedObjects);
|
||||
};
|
||||
|
||||
return [self modelOfClass:modelClass fromManagedObject:managedObject processedObjects:processedObjects error:error];
|
||||
}
|
||||
|
||||
+ (id)modelOfClass:(Class)modelClass fromManagedObject:(NSManagedObject *)managedObject processedObjects:(CFMutableDictionaryRef)processedObjects error:(NSError **)error {
|
||||
NSParameterAssert(modelClass != nil);
|
||||
NSParameterAssert(processedObjects != nil);
|
||||
|
||||
if (managedObject == nil) return nil;
|
||||
|
||||
const void *existingModel = CFDictionaryGetValue(processedObjects, (__bridge void *)managedObject);
|
||||
if (existingModel != NULL) {
|
||||
return (__bridge id)existingModel;
|
||||
}
|
||||
|
||||
if ([modelClass respondsToSelector:@selector(classForDeserializingManagedObject:)]) {
|
||||
modelClass = [modelClass classForDeserializingManagedObject:managedObject];
|
||||
if (modelClass == nil) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not deserialize managed object", @""),
|
||||
NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"No model class could be found to deserialize the object.", @"")
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLManagedObjectAdapterErrorDomain code:MTLManagedObjectAdapterErrorNoClassFound userInfo:userInfo];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
MTLManagedObjectAdapter *adapter = [[self alloc] initWithModelClass:modelClass];
|
||||
return [adapter modelFromManagedObject:managedObject processedObjects:processedObjects error:error];
|
||||
}
|
||||
|
||||
- (NSManagedObject *)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model insertingIntoContext:(NSManagedObjectContext *)context processedObjects:(CFMutableDictionaryRef)processedObjects error:(NSError **)error {
|
||||
NSParameterAssert(model != nil);
|
||||
NSParameterAssert(context != nil);
|
||||
NSParameterAssert(processedObjects != nil);
|
||||
|
||||
NSString *entityName = [model.class managedObjectEntityName];
|
||||
NSAssert(entityName != nil, @"%@ returned a nil +managedObjectEntityName", model.class);
|
||||
|
||||
Class entityDescriptionClass = NSClassFromString(@"NSEntityDescription");
|
||||
NSAssert(entityDescriptionClass != nil, @"CoreData.framework must be linked to use MTLManagedObjectAdapter");
|
||||
|
||||
__block NSManagedObject *managedObject = [entityDescriptionClass insertNewObjectForEntityForName:entityName inManagedObjectContext:context];
|
||||
if (managedObject == nil) {
|
||||
if (error != NULL) {
|
||||
NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"Failed to initialize a managed object from entity named \"%@\".", @""), entityName];
|
||||
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not serialize managed object", @""),
|
||||
NSLocalizedFailureReasonErrorKey: failureReason,
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLManagedObjectAdapterErrorDomain code:MTLManagedObjectAdapterErrorInitializationFailed userInfo:userInfo];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Pre-emptively consider this object processed, so that we don't get into
|
||||
// any cycles when processing its relationships.
|
||||
CFDictionaryAddValue(processedObjects, (__bridge void *)model, (__bridge void *)managedObject);
|
||||
|
||||
NSDictionary *dictionaryValue = model.dictionaryValue;
|
||||
NSDictionary *managedObjectProperties = managedObject.entity.propertiesByName;
|
||||
|
||||
[dictionaryValue enumerateKeysAndObjectsUsingBlock:^(NSString *propertyKey, id value, BOOL *stop) {
|
||||
NSString *managedObjectKey = [self managedObjectKeyForKey:propertyKey];
|
||||
if (managedObjectKey == nil) return;
|
||||
if ([value isEqual:NSNull.null]) value = nil;
|
||||
|
||||
BOOL (^serializeAttribute)(NSAttributeDescription *) = ^(NSAttributeDescription *attributeDescription) {
|
||||
// Mark this as being autoreleased, because validateValue may return
|
||||
// a new object to be stored in this variable (and we don't want ARC to
|
||||
// double-free or leak the old or new values).
|
||||
__autoreleasing id transformedValue = value;
|
||||
|
||||
NSValueTransformer *attributeTransformer = [self entityAttributeTransformerForKey:propertyKey];
|
||||
if (attributeTransformer != nil) transformedValue = [attributeTransformer transformedValue:transformedValue];
|
||||
|
||||
if (![managedObject validateValue:&transformedValue forKey:managedObjectKey error:error]) return NO;
|
||||
[managedObject setValue:transformedValue forKey:managedObjectKey];
|
||||
|
||||
return YES;
|
||||
};
|
||||
|
||||
NSManagedObject * (^objectForRelationshipFromModel)(id) = ^ id (id model) {
|
||||
if (![model isKindOfClass:MTLModel.class] || ![model conformsToProtocol:@protocol(MTLManagedObjectSerializing)]) {
|
||||
if (error != NULL) {
|
||||
NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"Property of class %@ cannot be encoded into an NSManagedObject.", @""), [model class]];
|
||||
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not serialize managed object", @""),
|
||||
NSLocalizedFailureReasonErrorKey: failureReason,
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLManagedObjectAdapterErrorDomain code:MTLManagedObjectAdapterErrorUnsupportedRelationshipClass userInfo:userInfo];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [self.class managedObjectFromModel:model insertingIntoContext:context processedObjects:processedObjects error:error];
|
||||
};
|
||||
|
||||
BOOL (^serializeRelationship)(NSRelationshipDescription *) = ^(NSRelationshipDescription *relationshipDescription) {
|
||||
if (value == nil) return YES;
|
||||
|
||||
if ([relationshipDescription isToMany]) {
|
||||
if (![value conformsToProtocol:@protocol(NSFastEnumeration)]) {
|
||||
if (error != NULL) {
|
||||
NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"Property of class %@ cannot be encoded into a to-many relationship.", @""), [value class]];
|
||||
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not serialize managed object", @""),
|
||||
NSLocalizedFailureReasonErrorKey: failureReason,
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLManagedObjectAdapterErrorDomain code:MTLManagedObjectAdapterErrorUnsupportedRelationshipClass userInfo:userInfo];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
id relationshipCollection;
|
||||
if ([relationshipDescription isOrdered]) {
|
||||
relationshipCollection = [managedObject mutableOrderedSetValueForKey:managedObjectKey];
|
||||
} else {
|
||||
relationshipCollection = [managedObject mutableSetValueForKey:managedObjectKey];
|
||||
}
|
||||
|
||||
for (MTLModel *model in value) {
|
||||
NSManagedObject *nestedObject = objectForRelationshipFromModel(model);
|
||||
if (nestedObject == nil) return NO;
|
||||
|
||||
[relationshipCollection addObject:nestedObject];
|
||||
}
|
||||
} else {
|
||||
NSManagedObject *nestedObject = objectForRelationshipFromModel(value);
|
||||
if (nestedObject == nil) return NO;
|
||||
|
||||
[managedObject setValue:nestedObject forKey:managedObjectKey];
|
||||
}
|
||||
|
||||
return YES;
|
||||
};
|
||||
|
||||
BOOL (^serializeProperty)(NSPropertyDescription *) = ^(NSPropertyDescription *propertyDescription) {
|
||||
if (propertyDescription == nil) {
|
||||
if (error != NULL) {
|
||||
NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"No property by name \"%@\" exists on the entity.", @""), managedObjectKey];
|
||||
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not serialize managed object", @""),
|
||||
NSLocalizedFailureReasonErrorKey: failureReason,
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLManagedObjectAdapterErrorDomain code:MTLManagedObjectAdapterErrorInvalidManagedObjectKey userInfo:userInfo];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Jump through some hoops to avoid referencing classes directly.
|
||||
NSString *propertyClassName = NSStringFromClass(propertyDescription.class);
|
||||
if ([propertyClassName isEqual:@"NSAttributeDescription"]) {
|
||||
return serializeAttribute((id)propertyDescription);
|
||||
} else if ([propertyClassName isEqual:@"NSRelationshipDescription"]) {
|
||||
return serializeRelationship((id)propertyDescription);
|
||||
} else {
|
||||
if (error != NULL) {
|
||||
NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"Property descriptions of class %@ are unsupported.", @""), propertyClassName];
|
||||
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not serialize managed object", @""),
|
||||
NSLocalizedFailureReasonErrorKey: failureReason,
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLManagedObjectAdapterErrorDomain code:MTLManagedObjectAdapterErrorUnsupportedManagedObjectPropertyType userInfo:userInfo];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
};
|
||||
|
||||
if (!serializeProperty(managedObjectProperties[managedObjectKey])) {
|
||||
performInContext(context, ^ id {
|
||||
[context deleteObject:managedObject];
|
||||
return nil;
|
||||
});
|
||||
|
||||
managedObject = nil;
|
||||
*stop = YES;
|
||||
}
|
||||
}];
|
||||
|
||||
if (![managedObject validateForInsert:error]) {
|
||||
performInContext(context, ^ id {
|
||||
[context deleteObject:managedObject];
|
||||
return nil;
|
||||
});
|
||||
}
|
||||
|
||||
return managedObject;
|
||||
}
|
||||
|
||||
+ (NSManagedObject *)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model insertingIntoContext:(NSManagedObjectContext *)context error:(NSError **)error {
|
||||
CFDictionaryKeyCallBacks keyCallbacks = kCFTypeDictionaryKeyCallBacks;
|
||||
|
||||
// Compare MTLModel keys using pointer equality, not -isEqual:.
|
||||
keyCallbacks.equal = NULL;
|
||||
|
||||
CFMutableDictionaryRef processedObjects = CFDictionaryCreateMutable(NULL, 0, &keyCallbacks, &kCFTypeDictionaryValueCallBacks);
|
||||
if (processedObjects == NULL) return nil;
|
||||
|
||||
@onExit {
|
||||
CFRelease(processedObjects);
|
||||
};
|
||||
|
||||
return [self managedObjectFromModel:model insertingIntoContext:context processedObjects:processedObjects error:error];
|
||||
}
|
||||
|
||||
+ (NSManagedObject *)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model insertingIntoContext:(NSManagedObjectContext *)context processedObjects:(CFMutableDictionaryRef)processedObjects error:(NSError **)error {
|
||||
NSParameterAssert(model != nil);
|
||||
NSParameterAssert(context != nil);
|
||||
NSParameterAssert(processedObjects != nil);
|
||||
|
||||
const void *existingManagedObject = CFDictionaryGetValue(processedObjects, (__bridge void *)model);
|
||||
if (existingManagedObject != NULL) {
|
||||
return (__bridge id)existingManagedObject;
|
||||
}
|
||||
|
||||
MTLManagedObjectAdapter *adapter = [[self alloc] initWithModelClass:model.class];
|
||||
return [adapter managedObjectFromModel:model insertingIntoContext:context processedObjects:processedObjects error:error];
|
||||
}
|
||||
|
||||
- (NSValueTransformer *)entityAttributeTransformerForKey:(NSString *)key {
|
||||
NSParameterAssert(key != nil);
|
||||
|
||||
SEL selector = MTLSelectorWithKeyPattern(key, "EntityAttributeTransformer");
|
||||
if ([self.modelClass respondsToSelector:selector]) {
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self.modelClass methodSignatureForSelector:selector]];
|
||||
invocation.target = self.modelClass;
|
||||
invocation.selector = selector;
|
||||
[invocation invoke];
|
||||
|
||||
__unsafe_unretained id result = nil;
|
||||
[invocation getReturnValue:&result];
|
||||
return result;
|
||||
}
|
||||
|
||||
if ([self.modelClass respondsToSelector:@selector(entityAttributeTransformerForKey:)]) {
|
||||
return [self.modelClass entityAttributeTransformerForKey:key];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)managedObjectKeyForKey:(NSString *)key {
|
||||
NSParameterAssert(key != nil);
|
||||
|
||||
id managedObjectKey = self.managedObjectKeysByPropertyKey[key];
|
||||
if ([managedObjectKey isEqual:NSNull.null]) return nil;
|
||||
|
||||
if (managedObjectKey == nil) {
|
||||
return key;
|
||||
} else {
|
||||
return managedObjectKey;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@ -8,121 +8,121 @@
|
||||
|
||||
#import "MTLModel.h"
|
||||
|
||||
// Defines how a MTLModel property key should be encoded into an archive.
|
||||
//
|
||||
// MTLModelEncodingBehaviorExcluded - The property should never be encoded.
|
||||
// MTLModelEncodingBehaviorUnconditional - The property should always be
|
||||
// encoded.
|
||||
// MTLModelEncodingBehaviorConditional - The object should be encoded only
|
||||
// if unconditionally encoded elsewhere.
|
||||
// This should only be used for object
|
||||
// properties.
|
||||
/// Defines how a MTLModel property key should be encoded into an archive.
|
||||
///
|
||||
/// MTLModelEncodingBehaviorExcluded - The property should never be encoded.
|
||||
/// MTLModelEncodingBehaviorUnconditional - The property should always be
|
||||
/// encoded.
|
||||
/// MTLModelEncodingBehaviorConditional - The object should be encoded only
|
||||
/// if unconditionally encoded elsewhere.
|
||||
/// This should only be used for object
|
||||
/// properties.
|
||||
typedef enum : NSUInteger {
|
||||
MTLModelEncodingBehaviorExcluded = 0,
|
||||
MTLModelEncodingBehaviorUnconditional,
|
||||
MTLModelEncodingBehaviorConditional,
|
||||
} MTLModelEncodingBehavior;
|
||||
|
||||
// Implements default archiving and unarchiving behaviors for MTLModel.
|
||||
/// Implements default archiving and unarchiving behaviors for MTLModel.
|
||||
@interface MTLModel (NSCoding) <NSCoding>
|
||||
|
||||
// Initializes the receiver from an archive.
|
||||
//
|
||||
// This will decode the original +modelVersion of the archived object, then
|
||||
// invoke -decodeValueForKey:withCoder:modelVersion: for each of the receiver's
|
||||
// +propertyKeys.
|
||||
//
|
||||
// Returns an initialized model object, or nil if a decoding error occurred.
|
||||
/// Initializes the receiver from an archive.
|
||||
///
|
||||
/// This will decode the original +modelVersion of the archived object, then
|
||||
/// invoke -decodeValueForKey:withCoder:modelVersion: for each of the receiver's
|
||||
/// +propertyKeys.
|
||||
///
|
||||
/// Returns an initialized model object, or nil if a decoding error occurred.
|
||||
- (id)initWithCoder:(NSCoder *)coder;
|
||||
|
||||
// Archives the receiver using the given coder.
|
||||
//
|
||||
// This will encode the receiver's +modelVersion, then the receiver's properties
|
||||
// according to the behaviors specified in +encodingBehaviorsByPropertyKey.
|
||||
/// Archives the receiver using the given coder.
|
||||
///
|
||||
/// This will encode the receiver's +modelVersion, then the receiver's properties
|
||||
/// according to the behaviors specified in +encodingBehaviorsByPropertyKey.
|
||||
- (void)encodeWithCoder:(NSCoder *)coder;
|
||||
|
||||
// Determines how the +propertyKeys of the class are encoded into an archive.
|
||||
// The values of this dictionary should be boxed MTLModelEncodingBehavior
|
||||
// values.
|
||||
//
|
||||
// Any keys not present in the dictionary will be excluded from the archive.
|
||||
//
|
||||
// Subclasses overriding this method should combine their values with those of
|
||||
// `super`.
|
||||
//
|
||||
// Returns a dictionary mapping the receiver's +propertyKeys to default encoding
|
||||
// behaviors. If a property is an object with `weak` semantics, the default
|
||||
// behavior is MTLModelEncodingBehaviorConditional; otherwise, the default is
|
||||
// MTLModelEncodingBehaviorUnconditional.
|
||||
/// Determines how the +propertyKeys of the class are encoded into an archive.
|
||||
/// The values of this dictionary should be boxed MTLModelEncodingBehavior
|
||||
/// values.
|
||||
///
|
||||
/// Any keys not present in the dictionary will be excluded from the archive.
|
||||
///
|
||||
/// Subclasses overriding this method should combine their values with those of
|
||||
/// `super`.
|
||||
///
|
||||
/// Returns a dictionary mapping the receiver's +propertyKeys to default encoding
|
||||
/// behaviors. If a property is an object with `weak` semantics, the default
|
||||
/// behavior is MTLModelEncodingBehaviorConditional; otherwise, the default is
|
||||
/// MTLModelEncodingBehaviorUnconditional.
|
||||
+ (NSDictionary *)encodingBehaviorsByPropertyKey;
|
||||
|
||||
// Determines the classes that are allowed to be decoded for each of the
|
||||
// receiver's properties when using <NSSecureCoding>. The values of this
|
||||
// dictionary should be NSArrays of Class objects.
|
||||
//
|
||||
// If any encodable keys (as determined by +encodingBehaviorsByPropertyKey) are
|
||||
// not present in the dictionary, an exception will be thrown during secure
|
||||
// encoding or decoding.
|
||||
//
|
||||
// Subclasses overriding this method should combine their values with those of
|
||||
// `super`.
|
||||
//
|
||||
// Returns a dictionary mapping the receiver's encodable keys (as determined by
|
||||
// +encodingBehaviorsByPropertyKey) to default allowed classes, based on the
|
||||
// type that each property is declared as. If type of an encodable property
|
||||
// cannot be determined (e.g., it is declared as `id`), it will be omitted from
|
||||
// the dictionary, and subclasses must provide a valid value to prevent an
|
||||
// exception being thrown during encoding/decoding.
|
||||
/// Determines the classes that are allowed to be decoded for each of the
|
||||
/// receiver's properties when using <NSSecureCoding>. The values of this
|
||||
/// dictionary should be NSArrays of Class objects.
|
||||
///
|
||||
/// If any encodable keys (as determined by +encodingBehaviorsByPropertyKey) are
|
||||
/// not present in the dictionary, an exception will be thrown during secure
|
||||
/// encoding or decoding.
|
||||
///
|
||||
/// Subclasses overriding this method should combine their values with those of
|
||||
/// `super`.
|
||||
///
|
||||
/// Returns a dictionary mapping the receiver's encodable keys (as determined by
|
||||
/// +encodingBehaviorsByPropertyKey) to default allowed classes, based on the
|
||||
/// type that each property is declared as. If type of an encodable property
|
||||
/// cannot be determined (e.g., it is declared as `id`), it will be omitted from
|
||||
/// the dictionary, and subclasses must provide a valid value to prevent an
|
||||
/// exception being thrown during encoding/decoding.
|
||||
+ (NSDictionary *)allowedSecureCodingClassesByPropertyKey;
|
||||
|
||||
// Decodes the value of the given property key from an archive.
|
||||
//
|
||||
// By default, this method looks for a `-decode<Key>WithCoder:modelVersion:`
|
||||
// method on the receiver, and invokes it if found.
|
||||
//
|
||||
// If the custom method is not implemented and `coder` does not require secure
|
||||
// coding, `-[NSCoder decodeObjectForKey:]` will be invoked with the given
|
||||
// `key`.
|
||||
//
|
||||
// If the custom method is not implemented and `coder` requires secure coding,
|
||||
// `-[NSCoder decodeObjectOfClasses:forKey:]` will be invoked with the
|
||||
// information from +allowedSecureCodingClassesByPropertyKey and the given `key`. The
|
||||
// receiver must conform to <NSSecureCoding> for this to work correctly.
|
||||
//
|
||||
// key - The property key to decode the value for. This argument cannot
|
||||
// be nil.
|
||||
// coder - The NSCoder representing the archive being decoded. This
|
||||
// argument cannot be nil.
|
||||
// modelVersion - The version of the original model object that was encoded.
|
||||
//
|
||||
// Returns the decoded and boxed value, or nil if the key was not present.
|
||||
/// Decodes the value of the given property key from an archive.
|
||||
///
|
||||
/// By default, this method looks for a `-decode<Key>WithCoder:modelVersion:`
|
||||
/// method on the receiver, and invokes it if found.
|
||||
///
|
||||
/// If the custom method is not implemented and `coder` does not require secure
|
||||
/// coding, `-[NSCoder decodeObjectForKey:]` will be invoked with the given
|
||||
/// `key`.
|
||||
///
|
||||
/// If the custom method is not implemented and `coder` requires secure coding,
|
||||
/// `-[NSCoder decodeObjectOfClasses:forKey:]` will be invoked with the
|
||||
/// information from +allowedSecureCodingClassesByPropertyKey and the given `key`. The
|
||||
/// receiver must conform to <NSSecureCoding> for this to work correctly.
|
||||
///
|
||||
/// key - The property key to decode the value for. This argument cannot
|
||||
/// be nil.
|
||||
/// coder - The NSCoder representing the archive being decoded. This
|
||||
/// argument cannot be nil.
|
||||
/// modelVersion - The version of the original model object that was encoded.
|
||||
///
|
||||
/// Returns the decoded and boxed value, or nil if the key was not present.
|
||||
- (id)decodeValueForKey:(NSString *)key withCoder:(NSCoder *)coder modelVersion:(NSUInteger)modelVersion;
|
||||
|
||||
// The version of this MTLModel subclass.
|
||||
//
|
||||
// This version number is saved in archives so that later model changes can be
|
||||
// made backwards-compatible with old versions.
|
||||
//
|
||||
// Subclasses should override this method to return a higher version number
|
||||
// whenever a breaking change is made to the model.
|
||||
//
|
||||
// Returns 0.
|
||||
/// The version of this MTLModel subclass.
|
||||
///
|
||||
/// This version number is saved in archives so that later model changes can be
|
||||
/// made backwards-compatible with old versions.
|
||||
///
|
||||
/// Subclasses should override this method to return a higher version number
|
||||
/// whenever a breaking change is made to the model.
|
||||
///
|
||||
/// Returns 0.
|
||||
+ (NSUInteger)modelVersion;
|
||||
|
||||
@end
|
||||
|
||||
// This method must be overridden to support archives created by older versions
|
||||
// of Mantle (before the `MTLModel+NSCoding` interface existed).
|
||||
/// This method must be overridden to support archives created by older versions
|
||||
/// of Mantle (before the `MTLModel+NSCoding` interface existed).
|
||||
@interface MTLModel (OldArchiveSupport)
|
||||
|
||||
// Converts an archived external representation to a dictionary suitable for
|
||||
// passing to -initWithDictionary:.
|
||||
//
|
||||
// externalRepresentation - The decoded external representation of the receiver.
|
||||
// fromVersion - The model version at the time the external
|
||||
// representation was encoded.
|
||||
//
|
||||
// Returns nil by default, indicating that conversion failed.
|
||||
/// Converts an archived external representation to a dictionary suitable for
|
||||
/// passing to -initWithDictionary:.
|
||||
///
|
||||
/// externalRepresentation - The decoded external representation of the receiver.
|
||||
/// fromVersion - The model version at the time the external
|
||||
/// representation was encoded.
|
||||
///
|
||||
/// Returns nil by default, indicating that conversion failed.
|
||||
+ (NSDictionary *)dictionaryValueFromArchivedExternalRepresentation:(NSDictionary *)externalRepresentation version:(NSUInteger)fromVersion;
|
||||
|
||||
@end
|
||||
|
||||
@ -7,10 +7,9 @@
|
||||
//
|
||||
|
||||
#import "MTLModel+NSCoding.h"
|
||||
#import "EXTRuntimeExtensions.h"
|
||||
#import "EXTScope.h"
|
||||
#import <Mantle/EXTRuntimeExtensions.h>
|
||||
#import <Mantle/EXTScope.h>
|
||||
#import "MTLReflection.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
// Used in archives to store the modelVersion of the archived instance.
|
||||
static NSString * const MTLModelVersionKey = @"MTLModelVersion";
|
||||
@ -129,25 +128,25 @@ static void verifyAllowedClassesByPropertyKey(Class modelClass) {
|
||||
|
||||
SEL selector = MTLSelectorWithCapitalizedKeyPattern("decode", key, "WithCoder:modelVersion:");
|
||||
if ([self respondsToSelector:selector]) {
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]];
|
||||
invocation.target = self;
|
||||
invocation.selector = selector;
|
||||
[invocation setArgument:&coder atIndex:2];
|
||||
[invocation setArgument:&modelVersion atIndex:3];
|
||||
[invocation invoke];
|
||||
|
||||
__unsafe_unretained id result = nil;
|
||||
[invocation getReturnValue:&result];
|
||||
IMP imp = [self methodForSelector:selector];
|
||||
id (*function)(id, SEL, NSCoder *, NSUInteger) = (__typeof__(function))imp;
|
||||
id result = function(self, selector, coder, modelVersion);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (coderRequiresSecureCoding(coder)) {
|
||||
NSArray *allowedClasses = self.class.allowedSecureCodingClassesByPropertyKey[key];
|
||||
NSAssert(allowedClasses != nil, @"No allowed classes specified for securely decoding key \"%@\" on %@", key, self.class);
|
||||
|
||||
return [coder decodeObjectOfClasses:[NSSet setWithArray:allowedClasses] forKey:key];
|
||||
} else {
|
||||
return [coder decodeObjectForKey:key];
|
||||
@try {
|
||||
if (coderRequiresSecureCoding(coder)) {
|
||||
NSArray *allowedClasses = self.class.allowedSecureCodingClassesByPropertyKey[key];
|
||||
NSAssert(allowedClasses != nil, @"No allowed classes specified for securely decoding key \"%@\" on %@", key, self.class);
|
||||
|
||||
return [coder decodeObjectOfClasses:[NSSet setWithArray:allowedClasses] forKey:key];
|
||||
} else {
|
||||
return [coder decodeObjectForKey:key];
|
||||
}
|
||||
} @catch (NSException *ex) {
|
||||
NSLog(@"*** Caught exception decoding value for key \"%@\" on class %@: %@", key, self.class, ex);
|
||||
@throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,24 +211,29 @@ static void verifyAllowedClassesByPropertyKey(Class modelClass) {
|
||||
|
||||
NSDictionary *encodingBehaviors = self.class.encodingBehaviorsByPropertyKey;
|
||||
[self.dictionaryValue enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
|
||||
// Skip nil values.
|
||||
if ([value isEqual:NSNull.null]) return;
|
||||
|
||||
switch ([encodingBehaviors[key] unsignedIntegerValue]) {
|
||||
// This will also match a nil behavior.
|
||||
case MTLModelEncodingBehaviorExcluded:
|
||||
break;
|
||||
|
||||
case MTLModelEncodingBehaviorUnconditional:
|
||||
[coder encodeObject:value forKey:key];
|
||||
break;
|
||||
|
||||
case MTLModelEncodingBehaviorConditional:
|
||||
[coder encodeConditionalObject:value forKey:key];
|
||||
break;
|
||||
|
||||
default:
|
||||
NSAssert(NO, @"Unrecognized encoding behavior %@ for key \"%@\"", encodingBehaviors[key], key);
|
||||
@try {
|
||||
// Skip nil values.
|
||||
if ([value isEqual:NSNull.null]) return;
|
||||
|
||||
switch ([encodingBehaviors[key] unsignedIntegerValue]) {
|
||||
// This will also match a nil behavior.
|
||||
case MTLModelEncodingBehaviorExcluded:
|
||||
break;
|
||||
|
||||
case MTLModelEncodingBehaviorUnconditional:
|
||||
[coder encodeObject:value forKey:key];
|
||||
break;
|
||||
|
||||
case MTLModelEncodingBehaviorConditional:
|
||||
[coder encodeConditionalObject:value forKey:key];
|
||||
break;
|
||||
|
||||
default:
|
||||
NSAssert(NO, @"Unrecognized encoding behavior %@ on class %@ for key \"%@\"", self.class, encodingBehaviors[key], key);
|
||||
}
|
||||
} @catch (NSException *ex) {
|
||||
NSLog(@"*** Caught exception encoding value for key \"%@\" on class %@: %@", key, self.class, ex);
|
||||
@throw ex;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@ -8,94 +8,172 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// An abstract base class for model objects, using reflection to provide
|
||||
// sensible default behaviors.
|
||||
//
|
||||
// The default implementations of <NSCopying>, -hash, and -isEqual: make use of
|
||||
// the +propertyKeys method.
|
||||
@interface MTLModel : NSObject <NSCopying>
|
||||
/// Defines a property's storage behavior, which affects how it will be copied,
|
||||
/// compared, and persisted.
|
||||
///
|
||||
/// MTLPropertyStorageNone - This property is not included in -description,
|
||||
/// -hash, or anything else.
|
||||
/// MTLPropertyStorageTransitory - This property is included in one-off
|
||||
/// operations like -copy and -dictionaryValue but
|
||||
/// does not affect -isEqual: or -hash.
|
||||
/// It may disappear at any time.
|
||||
/// MTLPropertyStoragePermanent - The property is included in serialization
|
||||
/// (like `NSCoding`) and equality, since it can
|
||||
/// be expected to stick around.
|
||||
typedef enum : NSUInteger {
|
||||
MTLPropertyStorageNone,
|
||||
MTLPropertyStorageTransitory,
|
||||
MTLPropertyStoragePermanent,
|
||||
} MTLPropertyStorage;
|
||||
|
||||
// Returns a new instance of the receiver initialized using
|
||||
// -initWithDictionary:error:.
|
||||
/// This protocol defines the minimal interface that classes need to implement to
|
||||
/// interact with Mantle adapters.
|
||||
///
|
||||
/// It is intended for scenarios where inheriting from MTLModel is not feasible.
|
||||
/// However, clients are encouraged to subclass the MTLModel class if they can.
|
||||
///
|
||||
/// Clients that wish to implement their own adapters should target classes
|
||||
/// conforming to this protocol rather than subclasses of MTLModel to ensure
|
||||
/// maximum compatibility.
|
||||
@protocol MTLModel <NSObject, NSCopying>
|
||||
|
||||
/// Initializes a new instance of the receiver using key-value coding, setting
|
||||
/// the keys and values in the given dictionary.
|
||||
///
|
||||
/// dictionaryValue - Property keys and values to set on the instance. Any NSNull
|
||||
/// values will be converted to nil before being used. KVC
|
||||
/// validation methods will automatically be invoked for all of
|
||||
/// the properties given.
|
||||
/// error - If not NULL, this may be set to any error that occurs
|
||||
/// (like a KVC validation error).
|
||||
///
|
||||
/// Returns an initialized model object, or nil if validation failed.
|
||||
+ (instancetype)modelWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;
|
||||
|
||||
// Initializes the receiver with default values.
|
||||
//
|
||||
// This is the designated initializer for this class.
|
||||
- (instancetype)init;
|
||||
|
||||
// Initializes the receiver using key-value coding, setting the keys and values
|
||||
// in the given dictionary.
|
||||
//
|
||||
// dictionaryValue - Property keys and values to set on the receiver. Any NSNull
|
||||
// values will be converted to nil before being used. KVC
|
||||
// validation methods will automatically be invoked for all of
|
||||
// the properties given. If nil, this method is equivalent to
|
||||
// -init.
|
||||
// error - If not NULL, this may be set to any error that occurs
|
||||
// (like a KVC validation error).
|
||||
//
|
||||
// Returns an initialized model object, or nil if validation failed.
|
||||
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;
|
||||
|
||||
// Returns the keys for all @property declarations, except for `readonly`
|
||||
// properties without ivars, or properties on MTLModel itself.
|
||||
+ (NSSet *)propertyKeys;
|
||||
|
||||
// A dictionary representing the properties of the receiver.
|
||||
//
|
||||
// The default implementation combines the values corresponding to all
|
||||
// +propertyKeys into a dictionary, with any nil values represented by NSNull.
|
||||
//
|
||||
// This property must never be nil.
|
||||
/// A dictionary representing the properties of the receiver.
|
||||
///
|
||||
/// Combines the values corresponding to all +propertyKeys into a dictionary,
|
||||
/// with any nil values represented by NSNull.
|
||||
///
|
||||
/// This property must never be nil.
|
||||
@property (nonatomic, copy, readonly) NSDictionary *dictionaryValue;
|
||||
|
||||
// Merges the value of the given key on the receiver with the value of the same
|
||||
// key from the given model object, giving precedence to the other model object.
|
||||
//
|
||||
// By default, this method looks for a `-merge<Key>FromModel:` method on the
|
||||
// receiver, and invokes it if found. If not found, and `model` is not nil, the
|
||||
// value for the given key is taken from `model`.
|
||||
- (void)mergeValueForKey:(NSString *)key fromModel:(MTLModel *)model;
|
||||
/// Initializes the receiver using key-value coding, setting the keys and values
|
||||
/// in the given dictionary.
|
||||
///
|
||||
/// Subclass implementations may override this method, calling the super
|
||||
/// implementation, in order to perform further processing and initialization
|
||||
/// after deserialization.
|
||||
///
|
||||
/// dictionaryValue - Property keys and values to set on the receiver. Any NSNull
|
||||
/// values will be converted to nil before being used. KVC
|
||||
/// validation methods will automatically be invoked for all of
|
||||
/// the properties given. If nil, this method is equivalent to
|
||||
/// -init.
|
||||
/// error - If not NULL, this may be set to any error that occurs
|
||||
/// (like a KVC validation error).
|
||||
///
|
||||
/// Returns an initialized model object, or nil if validation failed.
|
||||
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;
|
||||
|
||||
// Merges the values of the given model object into the receiver, using
|
||||
// -mergeValueForKey:fromModel: for each key in +propertyKeys.
|
||||
//
|
||||
// `model` must be an instance of the receiver's class or a subclass thereof.
|
||||
- (void)mergeValuesForKeysFromModel:(MTLModel *)model;
|
||||
/// Merges the value of the given key on the receiver with the value of the same
|
||||
/// key from the given model object, giving precedence to the other model object.
|
||||
- (void)mergeValueForKey:(NSString *)key fromModel:(id<MTLModel>)model;
|
||||
|
||||
@end
|
||||
/// Returns the keys for all @property declarations, except for `readonly`
|
||||
/// properties without ivars, or properties on MTLModel itself.
|
||||
+ (NSSet *)propertyKeys;
|
||||
|
||||
// Implements validation logic for MTLModel.
|
||||
@interface MTLModel (Validation)
|
||||
|
||||
// Validates the model.
|
||||
//
|
||||
// The default implementation simply invokes -validateValue:forKey:error: with
|
||||
// all +propertyKeys and their current value. If -validateValue:forKey:error:
|
||||
// returns a new value, the property is set to that new value.
|
||||
//
|
||||
// error - If not NULL, this may be set to any error that occurs during
|
||||
// validation
|
||||
//
|
||||
// Returns YES if the model is valid, or NO if the validation failed.
|
||||
/// Validates the model.
|
||||
///
|
||||
/// error - If not NULL, this may be set to any error that occurs during
|
||||
/// validation
|
||||
///
|
||||
/// Returns YES if the model is valid, or NO if the validation failed.
|
||||
- (BOOL)validate:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLModel (Unavailable)
|
||||
/// An abstract base class for model objects, using reflection to provide
|
||||
/// sensible default behaviors.
|
||||
///
|
||||
/// The default implementations of <NSCopying>, -hash, and -isEqual: make use of
|
||||
/// the +propertyKeys method.
|
||||
@interface MTLModel : NSObject <MTLModel>
|
||||
|
||||
+ (instancetype)modelWithDictionary:(NSDictionary *)dictionaryValue __attribute__((deprecated("Replaced by +modelWithDictionary:error:")));
|
||||
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue __attribute__((deprecated("Replaced by -initWithDictionary:error:")));
|
||||
/// Initializes the receiver using key-value coding, setting the keys and values
|
||||
/// in the given dictionary.
|
||||
///
|
||||
/// dictionaryValue - Property keys and values to set on the receiver. Any NSNull
|
||||
/// values will be converted to nil before being used. KVC
|
||||
/// validation methods will automatically be invoked for all of
|
||||
/// the properties given. If nil, this method is equivalent to
|
||||
/// -init.
|
||||
/// error - If not NULL, this may be set to any error that occurs
|
||||
/// (like a KVC validation error).
|
||||
///
|
||||
/// Returns an initialized model object, or nil if validation failed.
|
||||
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;
|
||||
|
||||
+ (instancetype)modelWithExternalRepresentation:(NSDictionary *)externalRepresentation __attribute__((deprecated("Replaced by -[MTLJSONAdapter initWithJSONDictionary:modelClass:]")));
|
||||
- (instancetype)initWithExternalRepresentation:(NSDictionary *)externalRepresentation __attribute__((deprecated("Replaced by -[MTLJSONAdapter initWithJSONDictionary:modelClass:]")));
|
||||
/// Initializes the receiver with default values.
|
||||
///
|
||||
/// This is the designated initializer for this class.
|
||||
- (instancetype)init;
|
||||
|
||||
@property (nonatomic, copy, readonly) NSDictionary *externalRepresentation __attribute__((deprecated("Replaced by MTLJSONAdapter.JSONDictionary")));
|
||||
/// By default, this method looks for a `-merge<Key>FromModel:` method on the
|
||||
/// receiver, and invokes it if found. If not found, and `model` is not nil, the
|
||||
/// value for the given key is taken from `model`.
|
||||
- (void)mergeValueForKey:(NSString *)key fromModel:(id<MTLModel>)model;
|
||||
|
||||
+ (NSDictionary *)externalRepresentationKeyPathsByPropertyKey __attribute__((deprecated("Replaced by +JSONKeyPathsByPropertyKey in <MTLJSONSerializing>")));
|
||||
+ (NSValueTransformer *)transformerForKey:(NSString *)key __attribute__((deprecated("Replaced by +JSONTransformerForKey: in <MTLJSONSerializing>")));
|
||||
/// Merges the values of the given model object into the receiver, using
|
||||
/// -mergeValueForKey:fromModel: for each key in +propertyKeys.
|
||||
///
|
||||
/// `model` must be an instance of the receiver's class or a subclass thereof.
|
||||
- (void)mergeValuesForKeysFromModel:(id<MTLModel>)model;
|
||||
|
||||
+ (NSDictionary *)migrateExternalRepresentation:(NSDictionary *)externalRepresentation fromVersion:(NSUInteger)fromVersion __attribute__((deprecated("Replaced by -decodeValueForKey:withCoder:modelVersion:")));
|
||||
/// The storage behavior of a given key.
|
||||
///
|
||||
/// The default implementation returns MTLPropertyStorageNone for properties that
|
||||
/// are readonly and not backed by an instance variable and
|
||||
/// MTLPropertyStoragePermanent otherwise.
|
||||
///
|
||||
/// Subclasses can use this method to prevent MTLModel from resolving circular
|
||||
/// references by returning MTLPropertyStorageTransitory.
|
||||
///
|
||||
/// Returns the storage behavior for a given key on the receiver.
|
||||
+ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey;
|
||||
|
||||
/// Compares the receiver with another object for equality.
|
||||
///
|
||||
/// The default implementation is equivalent to comparing all properties of both
|
||||
/// models for which +storageBehaviorForPropertyWithKey: returns
|
||||
/// MTLPropertyStoragePermanent.
|
||||
///
|
||||
/// Returns YES if the two models are considered equal, NO otherwise.
|
||||
- (BOOL)isEqual:(id)object;
|
||||
|
||||
/// A string that describes the contents of the receiver.
|
||||
///
|
||||
/// The default implementation is based on the receiver's class and all its
|
||||
/// properties for which +storageBehaviorForPropertyWithKey: returns
|
||||
/// MTLPropertyStoragePermanent.
|
||||
- (NSString *)description;
|
||||
|
||||
@end
|
||||
|
||||
/// Implements validation logic for MTLModel.
|
||||
@interface MTLModel (Validation)
|
||||
|
||||
/// Validates the model.
|
||||
///
|
||||
/// The default implementation simply invokes -validateValue:forKey:error: with
|
||||
/// all +propertyKeys and their current value. If -validateValue:forKey:error:
|
||||
/// returns a new value, the property is set to that new value.
|
||||
///
|
||||
/// error - If not NULL, this may be set to any error that occurs during
|
||||
/// validation
|
||||
///
|
||||
/// Returns YES if the model is valid, or NO if the validation failed.
|
||||
- (BOOL)validate:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
@ -8,19 +8,22 @@
|
||||
|
||||
#import "NSError+MTLModelException.h"
|
||||
#import "MTLModel.h"
|
||||
#import "EXTRuntimeExtensions.h"
|
||||
#import "EXTScope.h"
|
||||
#import <Mantle/EXTRuntimeExtensions.h>
|
||||
#import <Mantle/EXTScope.h>
|
||||
#import "MTLReflection.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
// This coupling is needed for backwards compatibility in MTLModel's deprecated
|
||||
// methods.
|
||||
#import "MTLJSONAdapter.h"
|
||||
#import "MTLModel+NSCoding.h"
|
||||
|
||||
// Used to cache the reflection performed in +propertyKeys.
|
||||
static void *MTLModelCachedPropertyKeysKey = &MTLModelCachedPropertyKeysKey;
|
||||
|
||||
// Associated in +generateAndCachePropertyKeys with a set of all transitory
|
||||
// property keys.
|
||||
static void *MTLModelCachedTransitoryPropertyKeysKey = &MTLModelCachedTransitoryPropertyKeysKey;
|
||||
|
||||
// Associated in +generateAndCachePropertyKeys with a set of all permanent
|
||||
// property keys.
|
||||
static void *MTLModelCachedPermanentPropertyKeysKey = &MTLModelCachedPermanentPropertyKeysKey;
|
||||
|
||||
// Validates a value for an object and sets it if necessary.
|
||||
//
|
||||
// obj - The object for which the value is being validated. This value
|
||||
@ -67,6 +70,18 @@ static BOOL MTLValidateAndSetValue(id obj, NSString *key, id value, BOOL forceUp
|
||||
|
||||
@interface MTLModel ()
|
||||
|
||||
// Inspects all properties of returned by +propertyKeys using
|
||||
// +storageBehaviorForPropertyWithKey and caches the results.
|
||||
+ (void)generateAndCacheStorageBehaviors;
|
||||
|
||||
// Returns a set of all property keys for which
|
||||
// +storageBehaviorForPropertyWithKey returned MTLPropertyStorageTransitory.
|
||||
+ (NSSet *)transitoryPropertyKeys;
|
||||
|
||||
// Returns a set of all property keys for which
|
||||
// +storageBehaviorForPropertyWithKey returned MTLPropertyStoragePermanent.
|
||||
+ (NSSet *)permanentPropertyKeys;
|
||||
|
||||
// Enumerates all properties of the receiver's class hierarchy, starting at the
|
||||
// receiver, and continuing up until (but not including) MTLModel.
|
||||
//
|
||||
@ -80,6 +95,31 @@ static BOOL MTLValidateAndSetValue(id obj, NSString *key, id value, BOOL forceUp
|
||||
|
||||
#pragma mark Lifecycle
|
||||
|
||||
+ (void)generateAndCacheStorageBehaviors {
|
||||
NSMutableSet *transitoryKeys = [NSMutableSet set];
|
||||
NSMutableSet *permanentKeys = [NSMutableSet set];
|
||||
|
||||
for (NSString *propertyKey in self.propertyKeys) {
|
||||
switch ([self storageBehaviorForPropertyWithKey:propertyKey]) {
|
||||
case MTLPropertyStorageNone:
|
||||
break;
|
||||
|
||||
case MTLPropertyStorageTransitory:
|
||||
[transitoryKeys addObject:propertyKey];
|
||||
break;
|
||||
|
||||
case MTLPropertyStoragePermanent:
|
||||
[permanentKeys addObject:propertyKey];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// It doesn't really matter if we replace another thread's work, since we do
|
||||
// it atomically and the result should be the same.
|
||||
objc_setAssociatedObject(self, MTLModelCachedTransitoryPropertyKeysKey, transitoryKeys, OBJC_ASSOCIATION_COPY);
|
||||
objc_setAssociatedObject(self, MTLModelCachedPermanentPropertyKeysKey, permanentKeys, OBJC_ASSOCIATION_COPY);
|
||||
}
|
||||
|
||||
+ (instancetype)modelWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
|
||||
return [[self alloc] initWithDictionary:dictionary error:error];
|
||||
}
|
||||
@ -98,7 +138,7 @@ static BOOL MTLValidateAndSetValue(id obj, NSString *key, id value, BOOL forceUp
|
||||
// a new object to be stored in this variable (and we don't want ARC to
|
||||
// double-free or leak the old or new values).
|
||||
__autoreleasing id value = [dictionary objectForKey:key];
|
||||
|
||||
|
||||
if ([value isEqual:NSNull.null]) value = nil;
|
||||
|
||||
BOOL success = MTLValidateAndSetValue(self, key, value, YES, error);
|
||||
@ -139,15 +179,11 @@ static BOOL MTLValidateAndSetValue(id obj, NSString *key, id value, BOOL forceUp
|
||||
NSMutableSet *keys = [NSMutableSet set];
|
||||
|
||||
[self enumeratePropertiesUsingBlock:^(objc_property_t property, BOOL *stop) {
|
||||
mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
|
||||
@onExit {
|
||||
free(attributes);
|
||||
};
|
||||
|
||||
if (attributes->readonly && attributes->ivar == NULL) return;
|
||||
|
||||
NSString *key = @(property_getName(property));
|
||||
[keys addObject:key];
|
||||
|
||||
if ([self storageBehaviorForPropertyWithKey:key] != MTLPropertyStorageNone) {
|
||||
[keys addObject:key];
|
||||
}
|
||||
}];
|
||||
|
||||
// It doesn't really matter if we replace another thread's work, since we do
|
||||
@ -157,13 +193,64 @@ static BOOL MTLValidateAndSetValue(id obj, NSString *key, id value, BOOL forceUp
|
||||
return keys;
|
||||
}
|
||||
|
||||
+ (NSSet *)transitoryPropertyKeys {
|
||||
NSSet *transitoryPropertyKeys = objc_getAssociatedObject(self, MTLModelCachedTransitoryPropertyKeysKey);
|
||||
|
||||
if (transitoryPropertyKeys == nil) {
|
||||
[self generateAndCacheStorageBehaviors];
|
||||
transitoryPropertyKeys = objc_getAssociatedObject(self, MTLModelCachedTransitoryPropertyKeysKey);
|
||||
}
|
||||
|
||||
return transitoryPropertyKeys;
|
||||
}
|
||||
|
||||
+ (NSSet *)permanentPropertyKeys {
|
||||
NSSet *permanentPropertyKeys = objc_getAssociatedObject(self, MTLModelCachedPermanentPropertyKeysKey);
|
||||
|
||||
if (permanentPropertyKeys == nil) {
|
||||
[self generateAndCacheStorageBehaviors];
|
||||
permanentPropertyKeys = objc_getAssociatedObject(self, MTLModelCachedPermanentPropertyKeysKey);
|
||||
}
|
||||
|
||||
return permanentPropertyKeys;
|
||||
}
|
||||
|
||||
- (NSDictionary *)dictionaryValue {
|
||||
return [self dictionaryWithValuesForKeys:self.class.propertyKeys.allObjects];
|
||||
NSSet *keys = [self.class.transitoryPropertyKeys setByAddingObjectsFromSet:self.class.permanentPropertyKeys];
|
||||
|
||||
return [self dictionaryWithValuesForKeys:keys.allObjects];
|
||||
}
|
||||
|
||||
+ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey {
|
||||
objc_property_t property = class_getProperty(self.class, propertyKey.UTF8String);
|
||||
|
||||
if (property == NULL) return MTLPropertyStorageNone;
|
||||
|
||||
mtl_propertyAttributes *attributes = mtl_copyPropertyAttributes(property);
|
||||
@onExit {
|
||||
free(attributes);
|
||||
};
|
||||
|
||||
BOOL hasGetter = [self instancesRespondToSelector:attributes->getter];
|
||||
BOOL hasSetter = [self instancesRespondToSelector:attributes->setter];
|
||||
if (!attributes->dynamic && attributes->ivar == NULL && !hasGetter && !hasSetter) {
|
||||
return MTLPropertyStorageNone;
|
||||
} else if (attributes->readonly && attributes->ivar == NULL) {
|
||||
if ([self isEqual:MTLModel.class]) {
|
||||
return MTLPropertyStorageNone;
|
||||
} else {
|
||||
// Check superclass in case the subclass redeclares a property that
|
||||
// falls through
|
||||
return [self.superclass storageBehaviorForPropertyWithKey:propertyKey];
|
||||
}
|
||||
} else {
|
||||
return MTLPropertyStoragePermanent;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Merging
|
||||
|
||||
- (void)mergeValueForKey:(NSString *)key fromModel:(MTLModel *)model {
|
||||
- (void)mergeValueForKey:(NSString *)key fromModel:(NSObject<MTLModel> *)model {
|
||||
NSParameterAssert(key != nil);
|
||||
|
||||
SEL selector = MTLSelectorWithCapitalizedKeyPattern("merge", key, "FromModel:");
|
||||
@ -175,16 +262,17 @@ static BOOL MTLValidateAndSetValue(id obj, NSString *key, id value, BOOL forceUp
|
||||
return;
|
||||
}
|
||||
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]];
|
||||
invocation.target = self;
|
||||
invocation.selector = selector;
|
||||
|
||||
[invocation setArgument:&model atIndex:2];
|
||||
[invocation invoke];
|
||||
IMP imp = [self methodForSelector:selector];
|
||||
void (*function)(id, SEL, id<MTLModel>) = (__typeof__(function))imp;
|
||||
function(self, selector, model);
|
||||
}
|
||||
|
||||
- (void)mergeValuesForKeysFromModel:(MTLModel *)model {
|
||||
- (void)mergeValuesForKeysFromModel:(id<MTLModel>)model {
|
||||
NSSet *propertyKeys = model.class.propertyKeys;
|
||||
|
||||
for (NSString *key in self.class.propertyKeys) {
|
||||
if (![propertyKeys containsObject:key]) continue;
|
||||
|
||||
[self mergeValueForKey:key fromModel:model];
|
||||
}
|
||||
}
|
||||
@ -205,19 +293,23 @@ static BOOL MTLValidateAndSetValue(id obj, NSString *key, id value, BOOL forceUp
|
||||
#pragma mark NSCopying
|
||||
|
||||
- (instancetype)copyWithZone:(NSZone *)zone {
|
||||
return [[self.class allocWithZone:zone] initWithDictionary:self.dictionaryValue error:NULL];
|
||||
MTLModel *copy = [[self.class allocWithZone:zone] init];
|
||||
[copy setValuesForKeysWithDictionary:self.dictionaryValue];
|
||||
return copy;
|
||||
}
|
||||
|
||||
#pragma mark NSObject
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"<%@: %p> %@", self.class, self, self.dictionaryValue];
|
||||
NSDictionary *permanentProperties = [self dictionaryWithValuesForKeys:self.class.permanentPropertyKeys.allObjects];
|
||||
|
||||
return [NSString stringWithFormat:@"<%@: %p> %@", self.class, self, permanentProperties];
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
NSUInteger value = 0;
|
||||
|
||||
for (NSString *key in self.class.propertyKeys) {
|
||||
for (NSString *key in self.class.permanentPropertyKeys) {
|
||||
value ^= [[self valueForKey:key] hash];
|
||||
}
|
||||
|
||||
@ -228,7 +320,7 @@ static BOOL MTLValidateAndSetValue(id obj, NSString *key, id value, BOOL forceUp
|
||||
if (self == model) return YES;
|
||||
if (![model isMemberOfClass:self.class]) return NO;
|
||||
|
||||
for (NSString *key in self.class.propertyKeys) {
|
||||
for (NSString *key in self.class.permanentPropertyKeys) {
|
||||
id selfValue = [self valueForKey:key];
|
||||
id modelValue = [model valueForKey:key];
|
||||
|
||||
|
||||
@ -8,24 +8,24 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// Creates a selector from a key and a constant string.
|
||||
//
|
||||
// key - The key to insert into the generated selector. This key should be in
|
||||
// its natural case.
|
||||
// suffix - A string to append to the key as part of the selector.
|
||||
//
|
||||
// Returns a selector, or NULL if the input strings cannot form a valid
|
||||
// selector.
|
||||
/// Creates a selector from a key and a constant string.
|
||||
///
|
||||
/// key - The key to insert into the generated selector. This key should be in
|
||||
/// its natural case.
|
||||
/// suffix - A string to append to the key as part of the selector.
|
||||
///
|
||||
/// Returns a selector, or NULL if the input strings cannot form a valid
|
||||
/// selector.
|
||||
SEL MTLSelectorWithKeyPattern(NSString *key, const char *suffix) __attribute__((pure, nonnull(1, 2)));
|
||||
|
||||
// Creates a selector from a key and a constant prefix and suffix.
|
||||
//
|
||||
// prefix - A string to prepend to the key as part of the selector.
|
||||
// key - The key to insert into the generated selector. This key should be in
|
||||
// its natural case, and will have its first letter capitalized when
|
||||
// inserted.
|
||||
// suffix - A string to append to the key as part of the selector.
|
||||
//
|
||||
// Returns a selector, or NULL if the input strings cannot form a valid
|
||||
// selector.
|
||||
/// Creates a selector from a key and a constant prefix and suffix.
|
||||
///
|
||||
/// prefix - A string to prepend to the key as part of the selector.
|
||||
/// key - The key to insert into the generated selector. This key should be in
|
||||
/// its natural case, and will have its first letter capitalized when
|
||||
/// inserted.
|
||||
/// suffix - A string to append to the key as part of the selector.
|
||||
///
|
||||
/// Returns a selector, or NULL if the input strings cannot form a valid
|
||||
/// selector.
|
||||
SEL MTLSelectorWithCapitalizedKeyPattern(const char *prefix, NSString *key, const char *suffix) __attribute__((pure, nonnull(1, 2, 3)));
|
||||
|
||||
66
Mantle/MTLTransformerErrorHandling.h
Normal file
66
Mantle/MTLTransformerErrorHandling.h
Normal file
@ -0,0 +1,66 @@
|
||||
//
|
||||
// MTLTransformerErrorHandling.h
|
||||
// Mantle
|
||||
//
|
||||
// Created by Robert Böhnke on 10/6/13.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/// The domain for errors originating from the MTLTransformerErrorHandling
|
||||
/// protocol.
|
||||
///
|
||||
/// Transformers conforming to this protocol are expected to use this error
|
||||
/// domain if the transformation fails.
|
||||
extern NSString * const MTLTransformerErrorHandlingErrorDomain;
|
||||
|
||||
/// Used to indicate that the input value was illegal.
|
||||
///
|
||||
/// Transformers conforming to this protocol are expected to use this error code
|
||||
/// if the transformation fails due to an invalid input value.
|
||||
extern const NSInteger MTLTransformerErrorHandlingErrorInvalidInput;
|
||||
|
||||
/// Associated with the invalid input value.
|
||||
///
|
||||
/// Transformers conforming to this protocol are expected to associate this key
|
||||
/// with the invalid input in the userInfo dictionary.
|
||||
extern NSString * const MTLTransformerErrorHandlingInputValueErrorKey;
|
||||
|
||||
/// This protocol can be implemented by NSValueTransformer subclasses to
|
||||
/// communicate errors that occur during transformation.
|
||||
@protocol MTLTransformerErrorHandling <NSObject>
|
||||
@required
|
||||
|
||||
/// Transforms a value, returning any error that occurred during transformation.
|
||||
///
|
||||
/// value - The value to transform.
|
||||
/// success - If not NULL, this will be set to a boolean indicating whether the
|
||||
/// transformation was successful.
|
||||
/// error - If not NULL, this may be set to an error that occurs during
|
||||
/// transforming the value.
|
||||
///
|
||||
/// Returns the result of the transformation which may be nil. Clients should
|
||||
/// inspect the success parameter to decide how to proceed with the result.
|
||||
- (id)transformedValue:(id)value success:(BOOL *)success error:(NSError **)error;
|
||||
|
||||
@optional
|
||||
|
||||
/// Reverse-transforms a value, returning any error that occurred during
|
||||
/// transformation.
|
||||
///
|
||||
/// Transformers conforming to this protocol are expected to implemented this
|
||||
/// method if they support reverse transformation.
|
||||
///
|
||||
/// value - The value to transform.
|
||||
/// success - If not NULL, this will be set to a boolean indicating whether the
|
||||
/// transformation was successful.
|
||||
/// error - If not NULL, this may be set to an error that occurs during
|
||||
/// transforming the value.
|
||||
///
|
||||
/// Returns the result of the reverse transformation which may be nil. Clients
|
||||
/// should inspect the success parameter to decide how to proceed with the
|
||||
/// result.
|
||||
- (id)reverseTransformedValue:(id)value success:(BOOL *)success error:(NSError **)error;
|
||||
|
||||
@end
|
||||
15
Mantle/MTLTransformerErrorHandling.m
Normal file
15
Mantle/MTLTransformerErrorHandling.m
Normal file
@ -0,0 +1,15 @@
|
||||
//
|
||||
// MTLTransformerErrorHandling.h
|
||||
// Mantle
|
||||
//
|
||||
// Created by Robert Böhnke on 10/6/13.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MTLTransformerErrorHandling.h"
|
||||
|
||||
NSString * const MTLTransformerErrorHandlingErrorDomain = @"MTLTransformerErrorHandlingErrorDomain";
|
||||
|
||||
const NSInteger MTLTransformerErrorHandlingErrorInvalidInput = 1;
|
||||
|
||||
NSString * const MTLTransformerErrorHandlingInputValueErrorKey = @"MTLTransformerErrorHandlingInputValueErrorKey";
|
||||
@ -8,29 +8,45 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef id (^MTLValueTransformerBlock)(id);
|
||||
#import "MTLTransformerErrorHandling.h"
|
||||
|
||||
//
|
||||
// A value transformer supporting block-based transformation.
|
||||
//
|
||||
@interface MTLValueTransformer : NSValueTransformer
|
||||
/// A block that represents a transformation.
|
||||
///
|
||||
/// value - The value to transform.
|
||||
/// success - The block must set this parameter to indicate whether the
|
||||
/// transformation was successful.
|
||||
/// MTLValueTransformer will always call this block with *success
|
||||
/// initialized to YES.
|
||||
/// error - If not NULL, this may be set to an error that occurs during
|
||||
/// transforming the value.
|
||||
///
|
||||
/// Returns the result of the transformation, which may be nil.
|
||||
typedef id (^MTLValueTransformerBlock)(id value, BOOL *success, NSError **error);
|
||||
|
||||
// Returns a transformer which transforms values using the given block. Reverse
|
||||
// transformations will not be allowed.
|
||||
+ (instancetype)transformerWithBlock:(MTLValueTransformerBlock)transformationBlock;
|
||||
///
|
||||
/// A value transformer supporting block-based transformation.
|
||||
///
|
||||
@interface MTLValueTransformer : NSValueTransformer <MTLTransformerErrorHandling>
|
||||
|
||||
// Returns a transformer which transforms values using the given block, for
|
||||
// forward or reverse transformations.
|
||||
+ (instancetype)reversibleTransformerWithBlock:(MTLValueTransformerBlock)transformationBlock;
|
||||
/// Returns a transformer which transforms values using the given block. Reverse
|
||||
/// transformations will not be allowed.
|
||||
+ (instancetype)transformerUsingForwardBlock:(MTLValueTransformerBlock)transformation;
|
||||
|
||||
// Returns a transformer which transforms values using the given blocks.
|
||||
+ (instancetype)reversibleTransformerWithForwardBlock:(MTLValueTransformerBlock)forwardBlock reverseBlock:(MTLValueTransformerBlock)reverseBlock;
|
||||
/// Returns a transformer which transforms values using the given block, for
|
||||
/// forward or reverse transformations.
|
||||
+ (instancetype)transformerUsingReversibleBlock:(MTLValueTransformerBlock)transformation;
|
||||
|
||||
/// Returns a transformer which transforms values using the given blocks.
|
||||
+ (instancetype)transformerUsingForwardBlock:(MTLValueTransformerBlock)forwardTransformation reverseBlock:(MTLValueTransformerBlock)reverseTransformation;
|
||||
|
||||
@end
|
||||
|
||||
//
|
||||
// Any MTLValueTransformer supporting reverse transformation. Necessary because
|
||||
// +allowsReverseTransformation is a class method.
|
||||
//
|
||||
@interface MTLReversibleValueTransformer : MTLValueTransformer
|
||||
@interface MTLValueTransformer (Deprecated)
|
||||
|
||||
+ (NSValueTransformer *)transformerWithBlock:(id (^)(id))transformationBlock __attribute__((deprecated("Replaced by +transformerUsingForwardBlock:")));
|
||||
|
||||
+ (NSValueTransformer *)reversibleTransformerWithBlock:(id (^)(id))transformationBlock __attribute__((deprecated("Replaced by +transformerUsingReversibleBlock:")));
|
||||
|
||||
+ (NSValueTransformer *)reversibleTransformerWithForwardBlock:(id (^)(id))forwardBlock reverseBlock:(id (^)(id))reverseBlock __attribute__((deprecated("Replaced by +transformerUsingForwardBlock:reverseBlock:")));
|
||||
|
||||
@end
|
||||
|
||||
@ -8,6 +8,13 @@
|
||||
|
||||
#import "MTLValueTransformer.h"
|
||||
|
||||
//
|
||||
// Any MTLValueTransformer supporting reverse transformation. Necessary because
|
||||
// +allowsReverseTransformation is a class method.
|
||||
//
|
||||
@interface MTLReversibleValueTransformer : MTLValueTransformer
|
||||
@end
|
||||
|
||||
@interface MTLValueTransformer ()
|
||||
|
||||
@property (nonatomic, copy, readonly) MTLValueTransformerBlock forwardBlock;
|
||||
@ -19,15 +26,15 @@
|
||||
|
||||
#pragma mark Lifecycle
|
||||
|
||||
+ (instancetype)transformerWithBlock:(MTLValueTransformerBlock)transformationBlock {
|
||||
return [[self alloc] initWithForwardBlock:transformationBlock reverseBlock:nil];
|
||||
+ (instancetype)transformerUsingForwardBlock:(MTLValueTransformerBlock)forwardBlock {
|
||||
return [[self alloc] initWithForwardBlock:forwardBlock reverseBlock:nil];
|
||||
}
|
||||
|
||||
+ (instancetype)reversibleTransformerWithBlock:(MTLValueTransformerBlock)transformationBlock {
|
||||
return [self reversibleTransformerWithForwardBlock:transformationBlock reverseBlock:transformationBlock];
|
||||
+ (instancetype)transformerUsingReversibleBlock:(MTLValueTransformerBlock)reversibleBlock {
|
||||
return [self transformerUsingForwardBlock:reversibleBlock reverseBlock:reversibleBlock];
|
||||
}
|
||||
|
||||
+ (instancetype)reversibleTransformerWithForwardBlock:(MTLValueTransformerBlock)forwardBlock reverseBlock:(MTLValueTransformerBlock)reverseBlock {
|
||||
+ (instancetype)transformerUsingForwardBlock:(MTLValueTransformerBlock)forwardBlock reverseBlock:(MTLValueTransformerBlock)reverseBlock {
|
||||
return [[MTLReversibleValueTransformer alloc] initWithForwardBlock:forwardBlock reverseBlock:reverseBlock];
|
||||
}
|
||||
|
||||
@ -50,11 +57,26 @@
|
||||
}
|
||||
|
||||
+ (Class)transformedValueClass {
|
||||
return [NSObject class];
|
||||
return NSObject.class;
|
||||
}
|
||||
|
||||
- (id)transformedValue:(id)value {
|
||||
return self.forwardBlock(value);
|
||||
NSError *error = nil;
|
||||
BOOL success = YES;
|
||||
|
||||
return self.forwardBlock(value, &success, &error);
|
||||
}
|
||||
|
||||
- (id)transformedValue:(id)value success:(BOOL *)outerSuccess error:(NSError **)outerError {
|
||||
NSError *error = nil;
|
||||
BOOL success = YES;
|
||||
|
||||
id transformedValue = self.forwardBlock(value, &success, &error);
|
||||
|
||||
if (outerSuccess != NULL) *outerSuccess = success;
|
||||
if (outerError != NULL) *outerError = error;
|
||||
|
||||
return transformedValue;
|
||||
}
|
||||
|
||||
@end
|
||||
@ -75,7 +97,54 @@
|
||||
}
|
||||
|
||||
- (id)reverseTransformedValue:(id)value {
|
||||
return self.reverseBlock(value);
|
||||
NSError *error = nil;
|
||||
BOOL success = YES;
|
||||
|
||||
return self.reverseBlock(value, &success, &error);
|
||||
}
|
||||
|
||||
- (id)reverseTransformedValue:(id)value success:(BOOL *)outerSuccess error:(NSError **)outerError {
|
||||
NSError *error = nil;
|
||||
BOOL success = YES;
|
||||
|
||||
id transformedValue = self.reverseBlock(value, &success, &error);
|
||||
|
||||
if (outerSuccess != NULL) *outerSuccess = success;
|
||||
if (outerError != NULL) *outerError = error;
|
||||
|
||||
return transformedValue;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation MTLValueTransformer (Deprecated)
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
|
||||
|
||||
+ (instancetype)transformerWithBlock:(id (^)(id))transformationBlock {
|
||||
return [self transformerUsingForwardBlock:^(id value, BOOL *success, NSError **error) {
|
||||
return transformationBlock(value);
|
||||
}];
|
||||
}
|
||||
|
||||
+ (instancetype)reversibleTransformerWithBlock:(id (^)(id))transformationBlock {
|
||||
return [self transformerUsingReversibleBlock:^(id value, BOOL *success, NSError **error) {
|
||||
return transformationBlock(value);
|
||||
}];
|
||||
}
|
||||
|
||||
+ (instancetype)reversibleTransformerWithForwardBlock:(id (^)(id))forwardBlock reverseBlock:(id (^)(id))reverseBlock {
|
||||
return [self
|
||||
transformerUsingForwardBlock:^(id value, BOOL *success, NSError **error) {
|
||||
return forwardBlock(value);
|
||||
}
|
||||
reverseBlock:^(id value, BOOL *success, NSError **error) {
|
||||
return reverseBlock(value);
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
@end
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
//
|
||||
// Prefix header for all source files of the 'Mantle' target in the 'Mantle' project
|
||||
//
|
||||
|
||||
#ifdef __OBJC__
|
||||
#import <Foundation/Foundation.h>
|
||||
#endif
|
||||
@ -6,13 +6,22 @@
|
||||
// Copyright (c) 2012 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for Mantle.
|
||||
FOUNDATION_EXPORT double MantleVersionNumber;
|
||||
|
||||
//! Project version string for Mantle.
|
||||
FOUNDATION_EXPORT const unsigned char MantleVersionString[];
|
||||
|
||||
#import <Mantle/MTLJSONAdapter.h>
|
||||
#import <Mantle/MTLManagedObjectAdapter.h>
|
||||
#import <Mantle/MTLModel.h>
|
||||
#import <Mantle/MTLModel+NSCoding.h>
|
||||
#import <Mantle/MTLValueTransformer.h>
|
||||
#import <Mantle/MTLTransformerErrorHandling.h>
|
||||
#import <Mantle/NSArray+MTLManipulationAdditions.h>
|
||||
#import <Mantle/NSDictionary+MTLManipulationAdditions.h>
|
||||
#import <Mantle/NSDictionary+MTLMappingAdditions.h>
|
||||
#import <Mantle/NSObject+MTLComparisonAdditions.h>
|
||||
#import <Mantle/NSValueTransformer+MTLInversionAdditions.h>
|
||||
#import <Mantle/NSValueTransformer+MTLPredefinedTransformerAdditions.h>
|
||||
|
||||
@ -10,19 +10,19 @@
|
||||
|
||||
@interface NSArray (MTLManipulationAdditions)
|
||||
|
||||
// The first object in the array or nil if the array is empty.
|
||||
// Forwards to `firstObject` which has been first declared in iOS7, but works with iOS4/10.6.
|
||||
/// The first object in the array or nil if the array is empty.
|
||||
/// Forwards to `firstObject` which has been first declared in iOS7, but works with iOS4/10.6.
|
||||
@property (nonatomic, readonly, strong) id mtl_firstObject;
|
||||
|
||||
// Returns a new array without all instances of the given object.
|
||||
/// Returns a new array without all instances of the given object.
|
||||
- (NSArray *)mtl_arrayByRemovingObject:(id)object;
|
||||
|
||||
// Returns a new array without the first object. If the array is empty, it
|
||||
// returns the empty array.
|
||||
/// Returns a new array without the first object. If the array is empty, it
|
||||
/// returns the empty array.
|
||||
- (NSArray *)mtl_arrayByRemovingFirstObject;
|
||||
|
||||
// Returns a new array without the last object. If the array is empty, it
|
||||
// returns the empty array.
|
||||
/// Returns a new array without the last object. If the array is empty, it
|
||||
/// returns the empty array.
|
||||
- (NSArray *)mtl_arrayByRemovingLastObject;
|
||||
|
||||
@end
|
||||
|
||||
27
Mantle/NSDictionary+MTLJSONKeyPath.h
Normal file
27
Mantle/NSDictionary+MTLJSONKeyPath.h
Normal file
@ -0,0 +1,27 @@
|
||||
//
|
||||
// NSDictionary+MTLJSONKeyPath.h
|
||||
// Mantle
|
||||
//
|
||||
// Created by Robert Böhnke on 19/03/14.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface NSDictionary (MTLJSONKeyPath)
|
||||
|
||||
/// Looks up the value of a key path in the receiver.
|
||||
///
|
||||
/// JSONKeyPath - The key path that should be resolved. Every element along this
|
||||
/// key path needs to be an instance of NSDictionary for the
|
||||
/// resolving to be successful.
|
||||
/// success - If not NULL, this will be set to a boolean indicating whether
|
||||
/// the key path was resolved successfully.
|
||||
/// error - If not NULL, this may be set to an error that occurs during
|
||||
/// resolving the value.
|
||||
///
|
||||
/// Returns the value for the key path which may be nil. Clients should inspect
|
||||
/// the success parameter to decide how to proceed with the result.
|
||||
- (id)mtl_valueForJSONKeyPath:(NSString *)JSONKeyPath success:(BOOL *)success error:(NSError **)error;
|
||||
|
||||
@end
|
||||
47
Mantle/NSDictionary+MTLJSONKeyPath.m
Normal file
47
Mantle/NSDictionary+MTLJSONKeyPath.m
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// NSDictionary+MTLJSONKeyPath.m
|
||||
// Mantle
|
||||
//
|
||||
// Created by Robert Böhnke on 19/03/14.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSDictionary+MTLJSONKeyPath.h"
|
||||
|
||||
#import "MTLJSONAdapter.h"
|
||||
|
||||
@implementation NSDictionary (MTLJSONKeyPath)
|
||||
|
||||
- (id)mtl_valueForJSONKeyPath:(NSString *)JSONKeyPath success:(BOOL *)success error:(NSError **)error {
|
||||
NSArray *components = [JSONKeyPath componentsSeparatedByString:@"."];
|
||||
|
||||
id result = self;
|
||||
for (NSString *component in components) {
|
||||
// Check the result before resolving the key path component to not
|
||||
// affect the last value of the path.
|
||||
if (result == nil || result == NSNull.null) break;
|
||||
|
||||
if (![result isKindOfClass:NSDictionary.class]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Invalid JSON dictionary", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"JSON key path %1$@ could not resolved because an incompatible JSON dictionary was supplied: \"%2$@\"", @""), JSONKeyPath, self]
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorInvalidJSONDictionary userInfo:userInfo];
|
||||
}
|
||||
|
||||
if (success != NULL) *success = NO;
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
result = result[component];
|
||||
}
|
||||
|
||||
if (success != NULL) *success = YES;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
@ -10,16 +10,22 @@
|
||||
|
||||
@interface NSDictionary (MTLManipulationAdditions)
|
||||
|
||||
// Merges the keys and values from the given dictionary into the receiver. If
|
||||
// both the receiver and `dictionary` have a given key, the value from
|
||||
// `dictionary` is used.
|
||||
//
|
||||
// Returns a new dictionary containing the entries of the receiver combined with
|
||||
// those of `dictionary`.
|
||||
/// Merges the keys and values from the given dictionary into the receiver. If
|
||||
/// both the receiver and `dictionary` have a given key, the value from
|
||||
/// `dictionary` is used.
|
||||
///
|
||||
/// Returns a new dictionary containing the entries of the receiver combined with
|
||||
/// those of `dictionary`.
|
||||
- (NSDictionary *)mtl_dictionaryByAddingEntriesFromDictionary:(NSDictionary *)dictionary;
|
||||
|
||||
// Creates a new dictionary with all the entries for the given keys removed from
|
||||
// the receiver.
|
||||
- (NSDictionary *)mtl_dictionaryByRemovingEntriesWithKeys:(NSSet *)keys;
|
||||
/// Creates a new dictionary with all the entries for the given keys removed from
|
||||
/// the receiver.
|
||||
- (NSDictionary *)mtl_dictionaryByRemovingValuesForKeys:(NSArray *)keys;
|
||||
|
||||
@end
|
||||
|
||||
@interface NSDictionary (MTLManipulationAdditions_Deprecated)
|
||||
|
||||
- (NSDictionary *)mtl_dictionaryByRemovingEntriesWithKeys:(NSSet *)keys __attribute__((deprecated("Replaced by -mtl_dictionaryByRemovingValuesForKeys:")));
|
||||
|
||||
@end
|
||||
|
||||
@ -16,10 +16,23 @@
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSDictionary *)mtl_dictionaryByRemovingEntriesWithKeys:(NSSet *)keys {
|
||||
- (NSDictionary *)mtl_dictionaryByRemovingValuesForKeys:(NSArray *)keys {
|
||||
NSMutableDictionary *result = [self mutableCopy];
|
||||
[result removeObjectsForKeys:keys.allObjects];
|
||||
[result removeObjectsForKeys:keys];
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSDictionary (MTLManipulationAdditions_Deprecated)
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated"
|
||||
|
||||
- (NSDictionary *)mtl_dictionaryByRemovingEntriesWithKeys:(NSSet *)keys {
|
||||
return [self mtl_dictionaryByRemovingValuesForKeys:keys.allObjects];
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
@end
|
||||
|
||||
21
Mantle/NSDictionary+MTLMappingAdditions.h
Normal file
21
Mantle/NSDictionary+MTLMappingAdditions.h
Normal file
@ -0,0 +1,21 @@
|
||||
//
|
||||
// NSDictionary+MTLMappingAdditions.h
|
||||
// Mantle
|
||||
//
|
||||
// Created by Robert Böhnke on 10/31/13.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface NSDictionary (MTLMappingAdditions)
|
||||
|
||||
/// Creates an identity mapping for serialization.
|
||||
///
|
||||
/// class - A subclass of MTLModel.
|
||||
///
|
||||
/// Returns a dictionary that maps all properties of the given class to
|
||||
/// themselves.
|
||||
+ (NSDictionary *)mtl_identityPropertyMapWithModel:(Class)modelClass;
|
||||
|
||||
@end
|
||||
23
Mantle/NSDictionary+MTLMappingAdditions.m
Normal file
23
Mantle/NSDictionary+MTLMappingAdditions.m
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// NSDictionary+MTLMappingAdditions.m
|
||||
// Mantle
|
||||
//
|
||||
// Created by Robert Böhnke on 10/31/13.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MTLModel.h"
|
||||
|
||||
#import "NSDictionary+MTLMappingAdditions.h"
|
||||
|
||||
@implementation NSDictionary (MTLMappingAdditions)
|
||||
|
||||
+ (NSDictionary *)mtl_identityPropertyMapWithModel:(Class)modelClass {
|
||||
NSCParameterAssert([modelClass conformsToProtocol:@protocol(MTLModel)]);
|
||||
|
||||
NSArray *propertyKeys = [modelClass propertyKeys].allObjects;
|
||||
|
||||
return [NSDictionary dictionaryWithObjects:propertyKeys forKeys:propertyKeys];
|
||||
}
|
||||
|
||||
@end
|
||||
@ -10,14 +10,14 @@
|
||||
|
||||
@interface NSError (MTLModelException)
|
||||
|
||||
// Creates a new error for an exception that occured during updating an
|
||||
// MTLModel.
|
||||
//
|
||||
// exception - The exception that was thrown while updating the model.
|
||||
// This argument must not be nil.
|
||||
//
|
||||
// Returns an error that takes its localized description and failure reason
|
||||
// from the exception.
|
||||
/// Creates a new error for an exception that occurred during updating an
|
||||
/// MTLModel.
|
||||
///
|
||||
/// exception - The exception that was thrown while updating the model.
|
||||
/// This argument must not be nil.
|
||||
///
|
||||
/// Returns an error that takes its localized description and failure reason
|
||||
/// from the exception.
|
||||
+ (instancetype)mtl_modelErrorWithException:(NSException *)exception;
|
||||
|
||||
@end
|
||||
|
||||
@ -11,5 +11,5 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// Returns whether both objects are identical or equal via -isEqual:
|
||||
/// Returns whether both objects are identical or equal via -isEqual:
|
||||
BOOL MTLEqualObjects(id obj1, id obj2);
|
||||
|
||||
@ -10,12 +10,12 @@
|
||||
|
||||
@interface NSValueTransformer (MTLInversionAdditions)
|
||||
|
||||
// Flips the direction of the receiver's transformation, such that
|
||||
// -transformedValue: will become -reverseTransformedValue:, and vice-versa.
|
||||
//
|
||||
// The receiver must allow reverse transformation.
|
||||
//
|
||||
// Returns an inverted transformer.
|
||||
/// Flips the direction of the receiver's transformation, such that
|
||||
/// -transformedValue: will become -reverseTransformedValue:, and vice-versa.
|
||||
///
|
||||
/// The receiver must allow reverse transformation.
|
||||
///
|
||||
/// Returns an inverted transformer.
|
||||
- (NSValueTransformer *)mtl_invertedTransformer;
|
||||
|
||||
@end
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
#import "NSValueTransformer+MTLInversionAdditions.h"
|
||||
#import "MTLTransformerErrorHandling.h"
|
||||
#import "MTLValueTransformer.h"
|
||||
|
||||
@implementation NSValueTransformer (MTLInversionAdditions)
|
||||
@ -14,11 +15,23 @@
|
||||
- (NSValueTransformer *)mtl_invertedTransformer {
|
||||
NSParameterAssert(self.class.allowsReverseTransformation);
|
||||
|
||||
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(id value) {
|
||||
return [self reverseTransformedValue:value];
|
||||
} reverseBlock:^(id value) {
|
||||
return [self transformedValue:value];
|
||||
}];
|
||||
if ([self conformsToProtocol:@protocol(MTLTransformerErrorHandling)]) {
|
||||
NSParameterAssert([self respondsToSelector:@selector(reverseTransformedValue:success:error:)]);
|
||||
|
||||
id<MTLTransformerErrorHandling> errorHandlingSelf = (id)self;
|
||||
|
||||
return [MTLValueTransformer transformerUsingForwardBlock:^(id value, BOOL *success, NSError **error) {
|
||||
return [errorHandlingSelf reverseTransformedValue:value success:success error:error];
|
||||
} reverseBlock:^(id value, BOOL *success, NSError **error) {
|
||||
return [errorHandlingSelf transformedValue:value success:success error:error];
|
||||
}];
|
||||
} else {
|
||||
return [MTLValueTransformer transformerUsingForwardBlock:^(id value, BOOL *success, NSError **error) {
|
||||
return [self reverseTransformedValue:value];
|
||||
} reverseBlock:^(id value, BOOL *success, NSError **error) {
|
||||
return [self transformedValue:value];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@ -8,47 +8,111 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// The name for a value transformer that converts strings into URLs and back.
|
||||
#import "MTLTransformerErrorHandling.h"
|
||||
|
||||
/// The name for a value transformer that converts strings into URLs and back.
|
||||
extern NSString * const MTLURLValueTransformerName;
|
||||
|
||||
// Ensure an NSNumber is backed by __NSCFBoolean/CFBooleanRef
|
||||
//
|
||||
// NSJSONSerialization, and likely other serialization libraries, ordinarily
|
||||
// serialize NSNumbers as numbers, and thus booleans would be serialized as
|
||||
// 0/1. The exception is when the NSNumber is backed by __NSCFBoolean, which,
|
||||
// though very much an implementation detail, is detected and serialized as a
|
||||
// proper boolean.
|
||||
/// The name for a value transformer that converts strings into NSUUIDs and back.
|
||||
extern NSString * const MTLUUIDValueTransformerName;
|
||||
|
||||
/// Ensure an NSNumber is backed by __NSCFBoolean/CFBooleanRef
|
||||
///
|
||||
/// NSJSONSerialization, and likely other serialization libraries, ordinarily
|
||||
/// serialize NSNumbers as numbers, and thus booleans would be serialized as
|
||||
/// 0/1. The exception is when the NSNumber is backed by __NSCFBoolean, which,
|
||||
/// though very much an implementation detail, is detected and serialized as a
|
||||
/// proper boolean.
|
||||
extern NSString * const MTLBooleanValueTransformerName;
|
||||
|
||||
@interface NSValueTransformer (MTLPredefinedTransformerAdditions)
|
||||
|
||||
// Creates a reversible transformer to convert a JSON dictionary into a MTLModel
|
||||
// object, and vice-versa.
|
||||
//
|
||||
// modelClass - The MTLModel subclass to attempt to parse from the JSON. This
|
||||
// class must conform to <MTLJSONSerializing>. This argument must
|
||||
// not be nil.
|
||||
//
|
||||
// Returns a reversible transformer which uses MTLJSONAdapter for transforming
|
||||
// values back and forth.
|
||||
+ (NSValueTransformer *)mtl_JSONDictionaryTransformerWithModelClass:(Class)modelClass;
|
||||
/// An optionally reversible transformer which applies the given transformer to
|
||||
/// each element of an array.
|
||||
///
|
||||
/// transformer - The transformer to apply to each element. If the transformer
|
||||
/// is reversible, the transformer returned by this method will be
|
||||
/// reversible. This argument must not be nil.
|
||||
///
|
||||
/// Returns a transformer which applies a transformation to each element of an
|
||||
/// array.
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_arrayMappingTransformerWithTransformer:(NSValueTransformer *)transformer;
|
||||
|
||||
// Creates a reversible transformer to convert an array of JSON dictionaries
|
||||
// into an array of MTLModel objects, and vice-versa.
|
||||
//
|
||||
// modelClass - The MTLModel subclass to attempt to parse from each JSON
|
||||
// dictionary. This class must conform to <MTLJSONSerializing>.
|
||||
// This argument must not be nil.
|
||||
//
|
||||
// Returns a reversible transformer which uses MTLJSONAdapter for transforming
|
||||
// array elements back and forth.
|
||||
+ (NSValueTransformer *)mtl_JSONArrayTransformerWithModelClass:(Class)modelClass;
|
||||
|
||||
@end
|
||||
|
||||
@interface NSValueTransformer (UnavailableMTLPredefinedTransformerAdditions)
|
||||
|
||||
+ (NSValueTransformer *)mtl_externalRepresentationTransformerWithModelClass:(Class)modelClass __attribute__((deprecated("Replaced by +mtl_JSONDictionaryTransformerWithModelClass:")));
|
||||
+ (NSValueTransformer *)mtl_externalRepresentationArrayTransformerWithModelClass:(Class)modelClass __attribute__((deprecated("Replaced by +mtl_JSONArrayTransformerWithModelClass:")));
|
||||
/// A reversible value transformer to transform between the keys and objects of a
|
||||
/// dictionary.
|
||||
///
|
||||
/// dictionary - The dictionary whose keys and values should be
|
||||
/// transformed between. This argument must not be nil.
|
||||
/// defaultValue - The result to fall back to, in case no key matching the
|
||||
/// input value was found during a forward transformation.
|
||||
/// reverseDefaultValue - The result to fall back to, in case no value matching
|
||||
/// the input value was found during a reverse
|
||||
/// transformation.
|
||||
///
|
||||
/// Can for example be used for transforming between enum values and their string
|
||||
/// representation.
|
||||
///
|
||||
/// NSValueTransformer *valueTransformer = [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{
|
||||
/// @"foo": @(EnumDataTypeFoo),
|
||||
/// @"bar": @(EnumDataTypeBar),
|
||||
/// } defaultValue: @(EnumDataTypeUndefined) reverseDefaultValue: @"undefined"];
|
||||
///
|
||||
/// Returns a transformer which will map from keys to objects for forward
|
||||
/// transformations, and from objects to keys for reverse transformations.
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_valueMappingTransformerWithDictionary:(NSDictionary *)dictionary defaultValue:(id)defaultValue reverseDefaultValue:(id)reverseDefaultValue;
|
||||
|
||||
/// Returns a value transformer created by calling
|
||||
/// `+mtl_valueMappingTransformerWithDictionary:defaultValue:reverseDefaultValue:`
|
||||
/// with a default value of `nil` and a reverse default value of `nil`.
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_valueMappingTransformerWithDictionary:(NSDictionary *)dictionary;
|
||||
|
||||
/// A reversible value transformer to transform between a date and its string
|
||||
/// representation
|
||||
///
|
||||
/// dateFormat - The date format used by the date formatter (http://www.unicode.org/reports/tr35/tr35-31/tr35-dates.html#Date_Field_Symbol_Table)
|
||||
/// calendar - The calendar used by the date formatter
|
||||
/// locale - The locale used by the date formatter
|
||||
/// timeZone - The time zone used by the date formatter
|
||||
///
|
||||
/// Returns a transformer which will map from strings to dates for forward
|
||||
/// transformations, and from dates to strings for reverse transformations.
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_dateTransformerWithDateFormat:(NSString *)dateFormat calendar:(NSCalendar *)calendar locale:(NSLocale *)locale timeZone:(NSTimeZone *)timeZone defaultDate:(NSDate *)defaultDate;
|
||||
|
||||
/// Returns a value transformer created by calling
|
||||
/// `+mtl_dateTransformerWithDateFormat:calendar:locale:timeZone:defaultDate:`
|
||||
/// with a calendar, locale, time zone and default date of `nil`.
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_dateTransformerWithDateFormat:(NSString *)dateFormat locale:(NSLocale *)locale;
|
||||
|
||||
/// A reversible value transformer to transform between a number and its string
|
||||
/// representation
|
||||
///
|
||||
/// numberStyle - The number style used by the number formatter
|
||||
///
|
||||
/// Returns a transformer which will map from strings to numbers for forward
|
||||
/// transformations, and from numbers to strings for reverse transformations.
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_numberTransformerWithNumberStyle:(NSNumberFormatterStyle)numberStyle locale:(NSLocale *)locale;
|
||||
|
||||
/// A reversible value transformer to transform between an object and its string
|
||||
/// representation
|
||||
///
|
||||
/// formatter - The formatter used to perform the transformation
|
||||
/// objectClass - The class of object that the formatter operates on
|
||||
///
|
||||
/// Returns a transformer which will map from strings to objects for forward
|
||||
/// transformations, and from objects to strings for reverse transformations.
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_transformerWithFormatter:(NSFormatter *)formatter forObjectClass:(Class)objectClass;
|
||||
|
||||
/// A value transformer that errors if the transformed value are not of the given
|
||||
/// class.
|
||||
///
|
||||
/// class - The expected class. This argument must not be nil.
|
||||
///
|
||||
/// Returns a transformer which will return an error if the transformed in value
|
||||
/// is not a member of class. Otherwise, the value is simply passed through.
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_validatingTransformerForClass:(Class)modelClass;
|
||||
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_JSONDictionaryTransformerWithModelClass:(Class)modelClass __attribute__((deprecated("Replaced by +[MTLJSONAdapter dictionaryTransformerWithModelClass:]")));
|
||||
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_JSONArrayTransformerWithModelClass:(Class)modelClass __attribute__((deprecated("Replaced by +[MTLJSONAdapter arrayTransformerWithModelClass:]")));
|
||||
|
||||
@end
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
#import "MTLValueTransformer.h"
|
||||
|
||||
NSString * const MTLURLValueTransformerName = @"MTLURLValueTransformerName";
|
||||
NSString * const MTLUUIDValueTransformerName = @"MTLUUIDValueTransformerName";
|
||||
NSString * const MTLBooleanValueTransformerName = @"MTLBooleanValueTransformerName";
|
||||
|
||||
@implementation NSValueTransformer (MTLPredefinedTransformerAdditions)
|
||||
@ -21,20 +22,132 @@ NSString * const MTLBooleanValueTransformerName = @"MTLBooleanValueTransformerNa
|
||||
+ (void)load {
|
||||
@autoreleasepool {
|
||||
MTLValueTransformer *URLValueTransformer = [MTLValueTransformer
|
||||
reversibleTransformerWithForwardBlock:^ id (NSString *str) {
|
||||
if (![str isKindOfClass:NSString.class]) return nil;
|
||||
return [NSURL URLWithString:str];
|
||||
transformerUsingForwardBlock:^ id (NSString *str, BOOL *success, NSError **error) {
|
||||
if (str == nil) return nil;
|
||||
|
||||
if (![str isKindOfClass:NSString.class]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert string to URL", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSString, got: %@.", @""), str],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey : str
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSURL *result = [NSURL URLWithString:str];
|
||||
|
||||
if (result == nil) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert string to URL", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Input URL string %@ was malformed", @""), str],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey : str
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
reverseBlock:^ id (NSURL *URL) {
|
||||
if (![URL isKindOfClass:NSURL.class]) return nil;
|
||||
reverseBlock:^ id (NSURL *URL, BOOL *success, NSError **error) {
|
||||
if (URL == nil) return nil;
|
||||
|
||||
if (![URL isKindOfClass:NSURL.class]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert URL to string", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSURL, got: %@.", @""), URL],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey : URL
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
return URL.absoluteString;
|
||||
}];
|
||||
|
||||
|
||||
[NSValueTransformer setValueTransformer:URLValueTransformer forName:MTLURLValueTransformerName];
|
||||
|
||||
MTLValueTransformer *UUIDValueTransformer = [MTLValueTransformer
|
||||
transformerUsingForwardBlock:^id(NSString *string, BOOL *success, NSError **error) {
|
||||
if (string == nil) return nil;
|
||||
|
||||
if (![string isKindOfClass:NSString.class]) {
|
||||
if (error) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert string to UUID", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSString, got: %@.", @""), string],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey : string
|
||||
};
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSUUID *result = [[NSUUID alloc] initWithUUIDString:string];
|
||||
|
||||
if (result == nil) {
|
||||
if (error) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert string to UUID", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Input UUID string %@ was malformed", @""), string],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey : string
|
||||
};
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
reverseBlock:^id(NSUUID *uuid, BOOL *success, NSError **error) {
|
||||
if (uuid == nil) return nil;
|
||||
|
||||
if (![uuid isKindOfClass:NSUUID.class]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert UUID to string", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSUUID, got: %@.", @""), uuid],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey : uuid};
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
|
||||
return uuid.UUIDString;
|
||||
}];
|
||||
|
||||
[NSValueTransformer setValueTransformer:UUIDValueTransformer forName:MTLUUIDValueTransformerName];
|
||||
|
||||
MTLValueTransformer *booleanValueTransformer = [MTLValueTransformer
|
||||
reversibleTransformerWithBlock:^ id (NSNumber *boolean) {
|
||||
if (![boolean isKindOfClass:NSNumber.class]) return nil;
|
||||
transformerUsingReversibleBlock:^ id (NSNumber *boolean, BOOL *success, NSError **error) {
|
||||
if (boolean == nil) return nil;
|
||||
|
||||
if (![boolean isKindOfClass:NSNumber.class]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not convert number to boolean-backed number or vice-versa", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSNumber, got: %@.", @""), boolean],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey : boolean
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
return (NSNumber *)(boolean.boolValue ? kCFBooleanTrue : kCFBooleanFalse);
|
||||
}];
|
||||
|
||||
@ -44,62 +157,291 @@ NSString * const MTLBooleanValueTransformerName = @"MTLBooleanValueTransformerNa
|
||||
|
||||
#pragma mark Customizable Transformers
|
||||
|
||||
+ (NSValueTransformer *)mtl_JSONDictionaryTransformerWithModelClass:(Class)modelClass {
|
||||
NSParameterAssert([modelClass isSubclassOfClass:MTLModel.class]);
|
||||
NSParameterAssert([modelClass conformsToProtocol:@protocol(MTLJSONSerializing)]);
|
||||
|
||||
return [MTLValueTransformer
|
||||
reversibleTransformerWithForwardBlock:^ id (NSDictionary *JSONDictionary) {
|
||||
if (JSONDictionary == nil) return nil;
|
||||
|
||||
NSAssert([JSONDictionary isKindOfClass:NSDictionary.class], @"Expected a dictionary, got: %@", JSONDictionary);
|
||||
|
||||
return [MTLJSONAdapter modelOfClass:modelClass fromJSONDictionary:JSONDictionary error:NULL];
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_arrayMappingTransformerWithTransformer:(NSValueTransformer *)transformer {
|
||||
NSParameterAssert(transformer != nil);
|
||||
|
||||
id (^forwardBlock)(NSArray *values, BOOL *success, NSError **error) = ^ id (NSArray *values, BOOL *success, NSError **error) {
|
||||
if (values == nil) return nil;
|
||||
|
||||
if (![values isKindOfClass:NSArray.class]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not transform non-array type", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSArray, got: %@.", @""), values],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey: values
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
reverseBlock:^ id (MTLModel<MTLJSONSerializing> *model) {
|
||||
if (model == nil) return nil;
|
||||
|
||||
NSMutableArray *transformedValues = [NSMutableArray arrayWithCapacity:values.count];
|
||||
NSInteger index = -1;
|
||||
for (id value in values) {
|
||||
index++;
|
||||
if (value == NSNull.null) {
|
||||
[transformedValues addObject:NSNull.null];
|
||||
continue;
|
||||
}
|
||||
|
||||
id transformedValue = nil;
|
||||
if ([transformer conformsToProtocol:@protocol(MTLTransformerErrorHandling)]) {
|
||||
NSError *underlyingError = nil;
|
||||
transformedValue = [(id<MTLTransformerErrorHandling>)transformer transformedValue:value success:success error:&underlyingError];
|
||||
|
||||
if (*success == NO) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not transform array", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Could not transform value at index %d", @""), index],
|
||||
NSUnderlyingErrorKey: underlyingError,
|
||||
MTLTransformerErrorHandlingInputValueErrorKey: values
|
||||
};
|
||||
|
||||
NSAssert([model isKindOfClass:MTLModel.class], @"Expected a MTLModel object, got %@", model);
|
||||
NSAssert([model conformsToProtocol:@protocol(MTLJSONSerializing)], @"Expected a model object conforming to <MTLJSONSerializing>, got %@", model);
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
} else {
|
||||
transformedValue = [transformer transformedValue:value];
|
||||
}
|
||||
|
||||
if (transformedValue == nil) continue;
|
||||
|
||||
[transformedValues addObject:transformedValue];
|
||||
}
|
||||
|
||||
return transformedValues;
|
||||
};
|
||||
|
||||
id (^reverseBlock)(NSArray *values, BOOL *success, NSError **error) = nil;
|
||||
if (transformer.class.allowsReverseTransformation) {
|
||||
reverseBlock = ^ id (NSArray *values, BOOL *success, NSError **error) {
|
||||
if (values == nil) return nil;
|
||||
|
||||
if (![values isKindOfClass:NSArray.class]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not transform non-array type", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSArray, got: %@.", @""), values],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey: values
|
||||
};
|
||||
|
||||
return [MTLJSONAdapter JSONDictionaryFromModel:model];
|
||||
}];
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray *transformedValues = [NSMutableArray arrayWithCapacity:values.count];
|
||||
NSInteger index = -1;
|
||||
for (id value in values) {
|
||||
index++;
|
||||
if (value == NSNull.null) {
|
||||
[transformedValues addObject:NSNull.null];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
id transformedValue = nil;
|
||||
if ([transformer respondsToSelector:@selector(reverseTransformedValue:success:error:)]) {
|
||||
NSError *underlyingError = nil;
|
||||
transformedValue = [(id<MTLTransformerErrorHandling>)transformer reverseTransformedValue:value success:success error:&underlyingError];
|
||||
|
||||
if (*success == NO) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not transform array", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Could not transform value at index %d", @""), index],
|
||||
NSUnderlyingErrorKey: underlyingError,
|
||||
MTLTransformerErrorHandlingInputValueErrorKey: values
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
} else {
|
||||
transformedValue = [transformer reverseTransformedValue:value];
|
||||
}
|
||||
|
||||
if (transformedValue == nil) continue;
|
||||
|
||||
[transformedValues addObject:transformedValue];
|
||||
}
|
||||
|
||||
return transformedValues;
|
||||
};
|
||||
}
|
||||
if (reverseBlock != nil) {
|
||||
return [MTLValueTransformer transformerUsingForwardBlock:forwardBlock reverseBlock:reverseBlock];
|
||||
} else {
|
||||
return [MTLValueTransformer transformerUsingForwardBlock:forwardBlock];
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)mtl_JSONArrayTransformerWithModelClass:(Class)modelClass {
|
||||
NSValueTransformer *dictionaryTransformer = [self mtl_JSONDictionaryTransformerWithModelClass:modelClass];
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_validatingTransformerForClass:(Class)modelClass {
|
||||
NSParameterAssert(modelClass != nil);
|
||||
|
||||
return [MTLValueTransformer transformerUsingForwardBlock:^ id (id value, BOOL *success, NSError **error) {
|
||||
if (value != nil && ![value isKindOfClass:modelClass]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Value did not match expected type", @""),
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected %1$@ to be of class %2$@ but got %3$@", @""), value, modelClass, [value class]],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey : value
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
|
||||
return value;
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)mtl_valueMappingTransformerWithDictionary:(NSDictionary *)dictionary defaultValue:(id)defaultValue reverseDefaultValue:(id)reverseDefaultValue {
|
||||
NSParameterAssert(dictionary != nil);
|
||||
NSParameterAssert(dictionary.count == [[NSSet setWithArray:dictionary.allValues] count]);
|
||||
|
||||
return [MTLValueTransformer
|
||||
reversibleTransformerWithForwardBlock:^ id (NSArray *dictionaries) {
|
||||
if (dictionaries == nil) return nil;
|
||||
|
||||
NSAssert([dictionaries isKindOfClass:NSArray.class], @"Expected a array of dictionaries, got: %@", dictionaries);
|
||||
|
||||
NSMutableArray *models = [NSMutableArray arrayWithCapacity:dictionaries.count];
|
||||
for (NSDictionary *JSONDictionary in dictionaries) {
|
||||
id model = [dictionaryTransformer transformedValue:JSONDictionary];
|
||||
if (model == nil) continue;
|
||||
|
||||
[models addObject:model];
|
||||
transformerUsingForwardBlock:^ id (id <NSCopying> key, BOOL *success, NSError **error) {
|
||||
return dictionary[key ?: NSNull.null] ?: defaultValue;
|
||||
}
|
||||
reverseBlock:^ id (id value, BOOL *success, NSError **error) {
|
||||
__block id result = nil;
|
||||
[dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id anObject, BOOL *stop) {
|
||||
if ([value isEqual:anObject]) {
|
||||
result = key;
|
||||
*stop = YES;
|
||||
}
|
||||
}];
|
||||
|
||||
return models;
|
||||
}
|
||||
reverseBlock:^ id (NSArray *models) {
|
||||
if (models == nil) return nil;
|
||||
|
||||
NSAssert([models isKindOfClass:NSArray.class], @"Expected a array of MTLModels, got: %@", models);
|
||||
|
||||
NSMutableArray *dictionaries = [NSMutableArray arrayWithCapacity:models.count];
|
||||
for (MTLModel *model in models) {
|
||||
NSDictionary *dict = [dictionaryTransformer reverseTransformedValue:model];
|
||||
if (dict == nil) continue;
|
||||
|
||||
[dictionaries addObject:dict];
|
||||
}
|
||||
|
||||
return dictionaries;
|
||||
}];
|
||||
return result ?: reverseDefaultValue;
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)mtl_valueMappingTransformerWithDictionary:(NSDictionary *)dictionary {
|
||||
return [self mtl_valueMappingTransformerWithDictionary:dictionary defaultValue:nil reverseDefaultValue:nil];
|
||||
}
|
||||
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_dateTransformerWithDateFormat:(NSString *)dateFormat calendar:(NSCalendar *)calendar locale:(NSLocale *)locale timeZone:(NSTimeZone *)timeZone defaultDate:(NSDate *)defaultDate {
|
||||
NSParameterAssert(dateFormat.length);
|
||||
|
||||
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
||||
dateFormatter.dateFormat = dateFormat;
|
||||
dateFormatter.calendar = calendar;
|
||||
dateFormatter.locale = locale;
|
||||
dateFormatter.timeZone = timeZone;
|
||||
dateFormatter.defaultDate = defaultDate;
|
||||
|
||||
return [NSValueTransformer mtl_transformerWithFormatter:dateFormatter forObjectClass:NSDate.class];
|
||||
}
|
||||
|
||||
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_dateTransformerWithDateFormat:(NSString *)dateFormat locale:(NSLocale *)locale {
|
||||
return [self mtl_dateTransformerWithDateFormat:dateFormat calendar:nil locale:locale timeZone:nil defaultDate:nil];
|
||||
}
|
||||
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_numberTransformerWithNumberStyle:(NSNumberFormatterStyle)numberStyle locale:(NSLocale *)locale {
|
||||
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
|
||||
numberFormatter.numberStyle = numberStyle;
|
||||
numberFormatter.locale = locale;
|
||||
|
||||
return [self mtl_transformerWithFormatter:numberFormatter forObjectClass:NSNumber.class];
|
||||
}
|
||||
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_transformerWithFormatter:(NSFormatter *)formatter forObjectClass:(Class)objectClass {
|
||||
NSParameterAssert(formatter != nil);
|
||||
NSParameterAssert(objectClass != nil);
|
||||
return [MTLValueTransformer
|
||||
transformerUsingForwardBlock:^ id (NSString *str, BOOL *success, NSError *__autoreleasing *error) {
|
||||
if (str == nil) return nil;
|
||||
|
||||
if (![str isKindOfClass:NSString.class]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"Could not convert string to %@", @""), objectClass],
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an NSString as input, got: %@.", @""), str],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey : str
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
|
||||
id object = nil;
|
||||
NSString *errorDescription = nil;
|
||||
*success = [formatter getObjectValue:&object forString:str errorDescription:&errorDescription];
|
||||
|
||||
if (errorDescription != nil) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"Could not convert string to %@", @""), objectClass],
|
||||
NSLocalizedFailureReasonErrorKey: errorDescription,
|
||||
MTLTransformerErrorHandlingInputValueErrorKey : str
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![object isKindOfClass:objectClass]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"Could not convert string to %@", @""), objectClass],
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an %@ as output from the formatter, got: %@.", @""), objectClass, object],
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFormattingError userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
|
||||
return object;
|
||||
} reverseBlock:^id(id object, BOOL *success, NSError *__autoreleasing *error) {
|
||||
if (object == nil) return nil;
|
||||
|
||||
if (![object isKindOfClass:objectClass]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"Could not convert %@ to string", @""), objectClass],
|
||||
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Expected an %@ as input, got: %@.", @""), objectClass, object],
|
||||
MTLTransformerErrorHandlingInputValueErrorKey : object
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:MTLTransformerErrorHandlingErrorDomain code:MTLTransformerErrorHandlingErrorInvalidInput userInfo:userInfo];
|
||||
}
|
||||
*success = NO;
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *string = [formatter stringForObjectValue:object];
|
||||
*success = (string != nil);
|
||||
return string;
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
|
||||
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_JSONDictionaryTransformerWithModelClass:(Class)modelClass {
|
||||
return [MTLJSONAdapter dictionaryTransformerWithModelClass:modelClass];
|
||||
}
|
||||
|
||||
+ (NSValueTransformer<MTLTransformerErrorHandling> *)mtl_JSONArrayTransformerWithModelClass:(Class)modelClass {
|
||||
return [MTLJSONAdapter arrayTransformerWithModelClass:modelClass];
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
@end
|
||||
|
||||
@ -50,10 +50,10 @@ NSString *lowercaseStringPath = @keypath(NSString.new, lowercaseString);
|
||||
*
|
||||
* @code
|
||||
|
||||
NSString *employessFirstNamePath = @collectionKeypath(department.employees, Employee.new, firstName)
|
||||
NSString *employeesFirstNamePath = @collectionKeypath(department.employees, Employee.new, firstName)
|
||||
// => @"employees.firstName"
|
||||
|
||||
NSString *employessFirstNamePath = @collectionKeypath(Department.new, employees, Employee.new, firstName)
|
||||
NSString *employeesFirstNamePath = @collectionKeypath(Department.new, employees, Employee.new, firstName)
|
||||
// => @"employees.firstName"
|
||||
|
||||
* @endcode
|
||||
|
||||
@ -9,6 +9,8 @@
|
||||
|
||||
#import "EXTRuntimeExtensions.h"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
mtl_propertyAttributes *mtl_copyPropertyAttributes (objc_property_t property) {
|
||||
const char * const attrString = property_getAttributes(property);
|
||||
if (!attrString) {
|
||||
|
||||
@ -88,7 +88,7 @@
|
||||
_Pragma("clang diagnostic pop")
|
||||
|
||||
/*** implementation details follow ***/
|
||||
typedef void (^mtl_cleanupBlock_t)();
|
||||
typedef void (^mtl_cleanupBlock_t)(void);
|
||||
|
||||
void mtl_executeCleanupBlock (__strong mtl_cleanupBlock_t *block);
|
||||
|
||||
|
||||
@ -5,11 +5,13 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.github.${PRODUCT_NAME:rfc1034identifier}</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
@ -6,17 +6,21 @@
|
||||
// Copyright (c) 2012 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
SpecBegin(MTLArrayManipulationAdditions)
|
||||
#import <Mantle/Mantle.h>
|
||||
#import <Nimble/Nimble.h>
|
||||
#import <Quick/Quick.h>
|
||||
|
||||
QuickSpecBegin(MTLArrayManipulationAdditions)
|
||||
|
||||
describe(@"-mtl_firstObject", ^{
|
||||
it(@"should return the first object", ^{
|
||||
NSArray *array = @[ @1, @2, @3 ];
|
||||
expect(array.mtl_firstObject).to.equal(@1);
|
||||
expect(array.mtl_firstObject).to(equal(@1));
|
||||
});
|
||||
|
||||
it(@"should return nil for an empty array", ^{
|
||||
NSArray *array = @[];
|
||||
expect(array.mtl_firstObject).to.beNil();
|
||||
expect(array.mtl_firstObject).to(beNil());
|
||||
});
|
||||
});
|
||||
|
||||
@ -24,18 +28,18 @@ describe(@"-mtl_arrayByRemovingObject:", ^{
|
||||
it(@"should return a new array without the object", ^{
|
||||
NSArray *array = @[ @1, @2, @3 ];
|
||||
NSArray *expected = @[ @2, @3 ];
|
||||
expect([array mtl_arrayByRemovingObject:@1]).to.equal(expected);
|
||||
expect([array mtl_arrayByRemovingObject:@1]).to(equal(expected));
|
||||
});
|
||||
|
||||
it(@"should return a new array without all occurrences of the object", ^{
|
||||
NSArray *array = @[ @1, @2, @3, @1, @1 ];
|
||||
NSArray *expected = @[ @2, @3 ];
|
||||
expect([array mtl_arrayByRemovingObject:@1]).to.equal(expected);
|
||||
expect([array mtl_arrayByRemovingObject:@1]).to(equal(expected));
|
||||
});
|
||||
|
||||
|
||||
it(@"should return an equivalent array if it doesn't contain the object", ^{
|
||||
NSArray *array = @[ @1, @2, @3 ];
|
||||
expect([array mtl_arrayByRemovingObject:@42]).to.equal(array);
|
||||
expect([array mtl_arrayByRemovingObject:@42]).to(equal(array));
|
||||
});
|
||||
});
|
||||
|
||||
@ -43,12 +47,12 @@ describe(@"-mtl_arrayByRemovingFirstObject", ^{
|
||||
it(@"should return the array without the first object", ^{
|
||||
NSArray *array = @[ @1, @2, @3 ];
|
||||
NSArray *expected = @[ @2, @3 ];
|
||||
expect(array.mtl_arrayByRemovingFirstObject).to.equal(expected);
|
||||
expect(array.mtl_arrayByRemovingFirstObject).to(equal(expected));
|
||||
});
|
||||
|
||||
it(@"should return the same array if it's empty", ^{
|
||||
NSArray *array = @[];
|
||||
expect(array.mtl_arrayByRemovingFirstObject).to.equal(array);
|
||||
expect(array.mtl_arrayByRemovingFirstObject).to(equal(array));
|
||||
});
|
||||
});
|
||||
|
||||
@ -56,13 +60,13 @@ describe(@"-mtl_arrayByRemovingLastObject", ^{
|
||||
it(@"should return the array without the last object", ^{
|
||||
NSArray *array = @[ @1, @2, @3 ];
|
||||
NSArray *expected = @[ @1, @2 ];
|
||||
expect(array.mtl_arrayByRemovingLastObject).to.equal(expected);
|
||||
expect(array.mtl_arrayByRemovingLastObject).to(equal(expected));
|
||||
});
|
||||
|
||||
it(@"should return the same array if it's empty", ^{
|
||||
NSArray *array = @[];
|
||||
expect(array.mtl_arrayByRemovingLastObject).to.equal(array);
|
||||
expect(array.mtl_arrayByRemovingLastObject).to(equal(array));
|
||||
});
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
QuickSpecEnd
|
||||
|
||||
@ -9,32 +9,36 @@
|
||||
// See the LICENSE file for more information.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
#import <Nimble/Nimble.h>
|
||||
#import <Quick/Quick.h>
|
||||
|
||||
#import "NSObject+MTLComparisonAdditions.h"
|
||||
|
||||
SpecBegin(MTLComparisonAdditions)
|
||||
QuickSpecBegin(MTLComparisonAdditions)
|
||||
|
||||
describe(@"MTLEqualObjects", ^{
|
||||
id obj1 = @"Test1";
|
||||
id obj2 = @"Test2";
|
||||
|
||||
it(@"returns true when given two values of nil", ^{
|
||||
expect(MTLEqualObjects(nil, nil)).to.beTruthy();
|
||||
expect(@(MTLEqualObjects(nil, nil))).to(beTruthy());
|
||||
});
|
||||
|
||||
it(@"returns true when given two equal objects", ^{
|
||||
expect(MTLEqualObjects(obj1, obj1)).to.beTruthy();
|
||||
expect(@(MTLEqualObjects(obj1, obj1))).to(beTruthy());
|
||||
});
|
||||
|
||||
it(@"returns false when given two inequal objects", ^{
|
||||
expect(MTLEqualObjects(obj1, obj2)).to.beFalsy();
|
||||
expect(@(MTLEqualObjects(obj1, obj2))).to(beFalsy());
|
||||
});
|
||||
|
||||
it(@"returns false when given an object and nil", ^{
|
||||
expect(MTLEqualObjects(obj1, nil)).to.beFalsy();
|
||||
expect(@(MTLEqualObjects(obj1, nil))).to(beFalsy());
|
||||
});
|
||||
|
||||
it(@"returns the same value when given symmetric arguments", ^{
|
||||
expect(MTLEqualObjects(obj2, obj1)).to.equal(MTLEqualObjects(obj1, obj2));
|
||||
expect(@(MTLEqualObjects(obj2, obj1))).to(equal(@(MTLEqualObjects(obj1, obj2))));
|
||||
});
|
||||
|
||||
describe(@"when comparing mutable objects", ^{
|
||||
@ -42,9 +46,9 @@ describe(@"MTLEqualObjects", ^{
|
||||
id mutableObj2 = [obj1 mutableCopy];
|
||||
|
||||
it(@"returns true when given two equal but not identical objects", ^{
|
||||
expect(MTLEqualObjects(mutableObj1, mutableObj2)).to.beTruthy();
|
||||
expect(@(MTLEqualObjects(mutableObj1, mutableObj2))).to(beTruthy());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
QuickSpecEnd
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
//
|
||||
// MTLCoreDataTestModels.h
|
||||
// Mantle
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-04-05.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
// Corresponds to the `Parent` entity.
|
||||
@interface MTLParentTestModel : MTLModel <MTLManagedObjectSerializing>
|
||||
|
||||
// Associated with the `number` attribute.
|
||||
@property (nonatomic, copy) NSString *numberString;
|
||||
|
||||
@property (nonatomic, copy) NSDate *date;
|
||||
@property (nonatomic, copy) NSString *requiredString;
|
||||
|
||||
@property (nonatomic, copy) NSArray *orderedChildren;
|
||||
@property (nonatomic, copy) NSSet *unorderedChildren;
|
||||
|
||||
@end
|
||||
|
||||
// Corresponds to the `Child` entity.
|
||||
@interface MTLChildTestModel : MTLModel <MTLManagedObjectSerializing>
|
||||
|
||||
// Associated with the `id` attribute.
|
||||
@property (nonatomic, assign) NSUInteger childID;
|
||||
|
||||
@property (nonatomic, weak) MTLParentTestModel *parent1;
|
||||
@property (nonatomic, weak) MTLParentTestModel *parent2;
|
||||
|
||||
@end
|
||||
@ -1,60 +0,0 @@
|
||||
//
|
||||
// MTLCoreDataTestModels.m
|
||||
// Mantle
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-04-05.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MTLCoreDataTestModels.h"
|
||||
|
||||
@implementation MTLParentTestModel
|
||||
|
||||
+ (NSString *)managedObjectEntityName {
|
||||
return @"Parent";
|
||||
}
|
||||
|
||||
+ (NSDictionary *)managedObjectKeysByPropertyKey {
|
||||
return @{
|
||||
@"numberString": @"number",
|
||||
@"requiredString": @"string"
|
||||
};
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)numberStringEntityAttributeTransformer {
|
||||
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *str) {
|
||||
return [NSDecimalNumber decimalNumberWithString:str];
|
||||
} reverseBlock:^(NSNumber *num) {
|
||||
return num.stringValue;
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSDictionary *)relationshipModelClassesByPropertyKey {
|
||||
return @{
|
||||
@"orderedChildren": MTLChildTestModel.class,
|
||||
@"unorderedChildren": MTLChildTestModel.class,
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLChildTestModel
|
||||
|
||||
+ (NSString *)managedObjectEntityName {
|
||||
return @"Child";
|
||||
}
|
||||
|
||||
+ (NSDictionary *)managedObjectKeysByPropertyKey {
|
||||
return @{
|
||||
@"childID": @"id"
|
||||
};
|
||||
}
|
||||
|
||||
+ (NSDictionary *)relationshipModelClassesByPropertyKey {
|
||||
return @{
|
||||
@"parent1": MTLParentTestModel.class,
|
||||
@"parent2": MTLParentTestModel.class,
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
@ -6,58 +6,62 @@
|
||||
// Copyright (c) 2012 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
SpecBegin(MTLDictionaryManipulationAdditions)
|
||||
#import <Mantle/Mantle.h>
|
||||
#import <Nimble/Nimble.h>
|
||||
#import <Quick/Quick.h>
|
||||
|
||||
QuickSpecBegin(MTLDictionaryManipulationAdditions)
|
||||
|
||||
describe(@"-mtl_dictionaryByAddingEntriesFromDictionary:", ^{
|
||||
NSDictionary *dict = @{ @"foo": @"bar", @(5): NSNull.null };
|
||||
|
||||
it(@"should return the same dictionary when adding from an empty dictionary", ^{
|
||||
NSDictionary *combined = [dict mtl_dictionaryByAddingEntriesFromDictionary:@{}];
|
||||
expect(combined).to.equal(dict);
|
||||
expect(combined).to(equal(dict));
|
||||
});
|
||||
|
||||
it(@"should return the same dictionary when adding from nil", ^{
|
||||
NSDictionary *combined = [dict mtl_dictionaryByAddingEntriesFromDictionary:nil];
|
||||
expect(combined).to.equal(dict);
|
||||
expect(combined).to(equal(dict));
|
||||
});
|
||||
|
||||
it(@"should add any new keys", ^{
|
||||
NSDictionary *combined = [dict mtl_dictionaryByAddingEntriesFromDictionary:@{ @"buzz": @(10), @"baz": NSNull.null }];
|
||||
NSDictionary *expected = @{ @"foo": @"bar", @(5): NSNull.null, @"buzz": @(10), @"baz": NSNull.null };
|
||||
expect(combined).to.equal(expected);
|
||||
expect(combined).to(equal(expected));
|
||||
});
|
||||
|
||||
it(@"should replace any existing keys", ^{
|
||||
NSDictionary *combined = [dict mtl_dictionaryByAddingEntriesFromDictionary:@{ @(5): @(10), @"buzz": @"baz" }];
|
||||
NSDictionary *expected = @{ @"foo": @"bar", @(5): @(10), @"buzz": @"baz" };
|
||||
expect(combined).to.equal(expected);
|
||||
expect(combined).to(equal(expected));
|
||||
});
|
||||
});
|
||||
|
||||
describe(@"-mtl_dictionaryByRemovingEntriesWithKeys:", ^{
|
||||
describe(@"-mtl_dictionaryByRemovingValuesForKeys:", ^{
|
||||
NSDictionary *dict = @{ @"foo": @"bar", @(5): NSNull.null };
|
||||
|
||||
it(@"should return the same dictionary when removing keys that don't exist in the receiver", ^{
|
||||
NSDictionary *removed = [dict mtl_dictionaryByRemovingEntriesWithKeys:[NSSet setWithObject:@"hi"]];
|
||||
expect(removed).to.equal(dict);
|
||||
NSDictionary *removed = [dict mtl_dictionaryByRemovingValuesForKeys:@[ @"hi"]];
|
||||
expect(removed).to(equal(dict));
|
||||
});
|
||||
|
||||
it(@"should return the same dictionary when given a nil array of keys", ^{
|
||||
NSDictionary *removed = [dict mtl_dictionaryByRemovingEntriesWithKeys:nil];
|
||||
expect(removed).to.equal(dict);
|
||||
NSDictionary *removed = [dict mtl_dictionaryByRemovingValuesForKeys:nil];
|
||||
expect(removed).to(equal(dict));
|
||||
});
|
||||
|
||||
it(@"should remove all the entries for the given keys", ^{
|
||||
NSDictionary *removed = [dict mtl_dictionaryByRemovingEntriesWithKeys:[NSSet setWithObject:@(5)]];
|
||||
NSDictionary *removed = [dict mtl_dictionaryByRemovingValuesForKeys:@[ @5 ]];
|
||||
NSDictionary *expected = @{ @"foo": @"bar" };
|
||||
expect(removed).to.equal(expected);
|
||||
expect(removed).to(equal(expected));
|
||||
});
|
||||
|
||||
it(@"should return an empty dictionary when it removes all its keys", ^{
|
||||
NSDictionary *removed = [dict mtl_dictionaryByRemovingEntriesWithKeys:[NSSet setWithArray:dict.allKeys]];
|
||||
NSDictionary *removed = [dict mtl_dictionaryByRemovingValuesForKeys:dict.allKeys];
|
||||
NSDictionary *expected = @{};
|
||||
expect(removed).to.equal(expected);
|
||||
expect(removed).to(equal(expected));
|
||||
});
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
QuickSpecEnd
|
||||
|
||||
28
MantleTests/MTLDictionaryMappingSpec.m
Normal file
28
MantleTests/MTLDictionaryMappingSpec.m
Normal file
@ -0,0 +1,28 @@
|
||||
//
|
||||
// MTLDictionaryMappingSpec.m
|
||||
// Mantle
|
||||
//
|
||||
// Created by Robert Böhnke on 10/23/13.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Quick/Quick.h>
|
||||
#import <Nimble/Nimble.h>
|
||||
#import "MTLTestModel.h"
|
||||
|
||||
#import "NSDictionary+MTLMappingAdditions.h"
|
||||
|
||||
QuickSpecBegin(MTLDictionaryMappingAdditions)
|
||||
|
||||
it(@"should return a mapping", ^{
|
||||
NSDictionary *mapping = @{
|
||||
@"name": @"name",
|
||||
@"count": @"count",
|
||||
@"nestedName": @"nestedName",
|
||||
@"weakModel": @"weakModel"
|
||||
};
|
||||
|
||||
expect([NSDictionary mtl_identityPropertyMapWithModel:MTLTestModel.class]).to(equal(mapping));
|
||||
});
|
||||
|
||||
QuickSpecEnd
|
||||
@ -6,9 +6,13 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
#import <Nimble/Nimble.h>
|
||||
#import <Quick/Quick.h>
|
||||
|
||||
#import "NSError+MTLModelException.h"
|
||||
|
||||
SpecBegin(MTLErrorModelException)
|
||||
QuickSpecBegin(MTLErrorModelException)
|
||||
|
||||
describe(@"+mtl_modelErrorWithException:", ^{
|
||||
it(@"should return a new error for that exception", ^{
|
||||
@ -16,10 +20,10 @@ describe(@"+mtl_modelErrorWithException:", ^{
|
||||
|
||||
NSError *error = [NSError mtl_modelErrorWithException:exception];
|
||||
|
||||
expect(error).toNot.beNil();
|
||||
expect(error.localizedDescription).to.equal(@"Just Testing");
|
||||
expect(error.localizedFailureReason).to.equal(@"Just Testing");
|
||||
expect(error).notTo(beNil());
|
||||
expect(error.localizedDescription).to(equal(@"Just Testing"));
|
||||
expect(error.localizedFailureReason).to(equal(@"Just Testing"));
|
||||
});
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
QuickSpecEnd
|
||||
|
||||
@ -6,52 +6,57 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
#import <Nimble/Nimble.h>
|
||||
#import <Quick/Quick.h>
|
||||
|
||||
#import "MTLTestJSONAdapter.h"
|
||||
#import "MTLTestModel.h"
|
||||
#import "MTLTransformerErrorExamples.h"
|
||||
|
||||
SpecBegin(MTLJSONAdapter)
|
||||
@interface MTLJSONAdapter (SpecExtensions)
|
||||
|
||||
it(@"should initialize from JSON", ^{
|
||||
// Used for testing transformer lifetimes.
|
||||
+ (NSValueTransformer *)NSDateJSONTransformer;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLJSONAdapter (SpecExtensions)
|
||||
|
||||
+ (NSValueTransformer *)NSDateJSONTransformer {
|
||||
return [[NSValueTransformer alloc] init];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
QuickSpecBegin(MTLJSONAdapterSpec)
|
||||
|
||||
it(@"should initialize with a model class", ^{
|
||||
NSDictionary *values = @{
|
||||
@"username": NSNull.null,
|
||||
@"count": @"5",
|
||||
};
|
||||
|
||||
NSError *error = nil;
|
||||
MTLJSONAdapter *adapter = [[MTLJSONAdapter alloc] initWithJSONDictionary:values modelClass:MTLTestModel.class error:&error];
|
||||
expect(adapter).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
MTLJSONAdapter *adapter = [[MTLJSONAdapter alloc] initWithModelClass:MTLTestModel.class];
|
||||
expect(adapter).notTo(beNil());
|
||||
|
||||
NSError *error = nil;
|
||||
MTLTestModel *model = [adapter modelFromJSONDictionary:values error:&error];
|
||||
expect(error).to(beNil());
|
||||
|
||||
expect(model).notTo(beNil());
|
||||
expect(model.name).to(beNil());
|
||||
expect(@(model.count)).to(equal(@5));
|
||||
|
||||
MTLTestModel *model = (id)adapter.model;
|
||||
expect(model).notTo.beNil();
|
||||
expect(model.name).to.beNil();
|
||||
expect(model.count).to.equal(5);
|
||||
|
||||
NSDictionary *JSONDictionary = @{
|
||||
@"username": NSNull.null,
|
||||
@"count": @"5",
|
||||
@"nested": @{ @"name": NSNull.null },
|
||||
};
|
||||
|
||||
expect(adapter.JSONDictionary).to.equal(JSONDictionary);
|
||||
});
|
||||
|
||||
it(@"should initialize from a model", ^{
|
||||
MTLTestModel *model = [MTLTestModel modelWithDictionary:@{
|
||||
@"name": @"foobar",
|
||||
@"count": @5,
|
||||
} error:NULL];
|
||||
|
||||
MTLJSONAdapter *adapter = [[MTLJSONAdapter alloc] initWithModel:model];
|
||||
expect(adapter).notTo.beNil();
|
||||
expect(adapter.model).to.beIdenticalTo(model);
|
||||
|
||||
NSDictionary *JSONDictionary = @{
|
||||
@"username": @"foobar",
|
||||
@"count": @"5",
|
||||
@"nested": @{ @"name": NSNull.null },
|
||||
};
|
||||
|
||||
expect(adapter.JSONDictionary).to.equal(JSONDictionary);
|
||||
__block NSError *serializationError;
|
||||
expect([adapter JSONDictionaryFromModel:model error:&serializationError]).to(equal(JSONDictionary));
|
||||
expect(serializationError).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should initialize nested key paths from JSON", ^{
|
||||
@ -63,21 +68,98 @@ it(@"should initialize nested key paths from JSON", ^{
|
||||
|
||||
NSError *error = nil;
|
||||
MTLTestModel *model = [MTLJSONAdapter modelOfClass:MTLTestModel.class fromJSONDictionary:values error:&error];
|
||||
expect(model).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
expect(model).notTo(beNil());
|
||||
expect(error).to(beNil());
|
||||
|
||||
expect(model.name).to.equal(@"foo");
|
||||
expect(model.count).to.equal(0);
|
||||
expect(model.nestedName).to.equal(@"bar");
|
||||
expect(model.name).to(equal(@"foo"));
|
||||
expect(@(model.count)).to(equal(@0));
|
||||
expect(model.nestedName).to(equal(@"bar"));
|
||||
|
||||
expect([MTLJSONAdapter JSONDictionaryFromModel:model]).to.equal(values);
|
||||
__block NSError *serializationError;
|
||||
expect([MTLJSONAdapter JSONDictionaryFromModel:model error:&serializationError]).to(equal(values));
|
||||
expect(serializationError).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should return nil with a nil JSON dictionary, but no error", ^{
|
||||
it(@"it should initialize properties with multiple key paths from JSON", ^{
|
||||
NSDictionary *values = @{
|
||||
@"location": @20,
|
||||
@"length": @12,
|
||||
@"nested": @{
|
||||
@"location": @12,
|
||||
@"length": @34
|
||||
}
|
||||
};
|
||||
|
||||
NSError *error = nil;
|
||||
MTLJSONAdapter *adapter = [[MTLJSONAdapter alloc] initWithJSONDictionary:nil modelClass:MTLTestModel.class error:&error];
|
||||
expect(adapter).to.beNil();
|
||||
expect(error).to.beNil();
|
||||
MTLMultiKeypathModel *model = [MTLJSONAdapter modelOfClass:MTLMultiKeypathModel.class fromJSONDictionary:values error:&error];
|
||||
expect(model).notTo(beNil());
|
||||
expect(error).to(beNil());
|
||||
|
||||
expect(@(model.range.location)).to(equal(@20));
|
||||
expect(@(model.range.length)).to(equal(@12));
|
||||
|
||||
expect(@(model.nestedRange.location)).to(equal(@12));
|
||||
expect(@(model.nestedRange.length)).to(equal(@34));
|
||||
|
||||
__block NSError *serializationError;
|
||||
expect([MTLJSONAdapter JSONDictionaryFromModel:model error:&serializationError]).to(equal(values));
|
||||
expect(serializationError).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should return nil and error with an invalid key path from JSON",^{
|
||||
NSDictionary *values = @{
|
||||
@"username": @"foo",
|
||||
@"nested": @"bar",
|
||||
@"count": @"0"
|
||||
};
|
||||
|
||||
NSError *error = nil;
|
||||
MTLTestModel *model = [MTLJSONAdapter modelOfClass:MTLTestModel.class fromJSONDictionary:values error:&error];
|
||||
expect(model).to(beNil());
|
||||
expect(error).notTo(beNil());
|
||||
expect(error.domain).to(equal(MTLJSONAdapterErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(MTLJSONAdapterErrorInvalidJSONDictionary)));
|
||||
});
|
||||
|
||||
it(@"should support key paths across arrays", ^{
|
||||
NSDictionary *values = @{
|
||||
@"users": @[
|
||||
@{
|
||||
@"name": @"foo"
|
||||
},
|
||||
@{
|
||||
@"name": @"bar"
|
||||
},
|
||||
@{
|
||||
@"name": @"baz"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
NSError *error = nil;
|
||||
MTLArrayTestModel *model = [MTLJSONAdapter modelOfClass:MTLArrayTestModel.class fromJSONDictionary:values error:&error];
|
||||
expect(model).to(beNil());
|
||||
expect(error).notTo(beNil());
|
||||
|
||||
expect(error.domain).to(equal(MTLJSONAdapterErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(MTLJSONAdapterErrorInvalidJSONDictionary)));
|
||||
});
|
||||
|
||||
it(@"should initialize without returning any error when using a JSON dictionary which Null.null as value",^{
|
||||
NSDictionary *values = @{
|
||||
@"username": @"foo",
|
||||
@"nested": NSNull.null,
|
||||
@"count": @"0"
|
||||
};
|
||||
|
||||
NSError *error = nil;
|
||||
MTLTestModel *model = [MTLJSONAdapter modelOfClass:MTLTestModel.class fromJSONDictionary:values error:&error];
|
||||
expect(model).notTo(beNil());
|
||||
expect(error).to(beNil());
|
||||
|
||||
expect(model.name).to(equal(@"foo"));
|
||||
expect(@(model.count)).to(equal(@0));
|
||||
expect(model.nestedName).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should ignore unrecognized JSON keys", ^{
|
||||
@ -91,12 +173,12 @@ it(@"should ignore unrecognized JSON keys", ^{
|
||||
|
||||
NSError *error = nil;
|
||||
MTLTestModel *model = [MTLJSONAdapter modelOfClass:MTLTestModel.class fromJSONDictionary:values error:&error];
|
||||
expect(model).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
expect(model).notTo(beNil());
|
||||
expect(error).to(beNil());
|
||||
|
||||
expect(model.name).to.equal(@"buzz");
|
||||
expect(model.count).to.equal(2);
|
||||
expect(model.nestedName).to.equal(@"bar");
|
||||
expect(model.name).to(equal(@"buzz"));
|
||||
expect(@(model.count)).to(equal(@2));
|
||||
expect(model.nestedName).to(equal(@"bar"));
|
||||
});
|
||||
|
||||
it(@"should fail to initialize if JSON dictionary validation fails", ^{
|
||||
@ -106,9 +188,175 @@ it(@"should fail to initialize if JSON dictionary validation fails", ^{
|
||||
|
||||
NSError *error = nil;
|
||||
MTLTestModel *model = [MTLJSONAdapter modelOfClass:MTLTestModel.class fromJSONDictionary:values error:&error];
|
||||
expect(model).to.beNil();
|
||||
expect(error.domain).to.equal(MTLTestModelErrorDomain);
|
||||
expect(error.code).to.equal(MTLTestModelNameTooLong);
|
||||
expect(model).to(beNil());
|
||||
expect(error.domain).to(equal(MTLTestModelErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(MTLTestModelNameTooLong)));
|
||||
});
|
||||
|
||||
it(@"should implicitly transform NSStrings to URLs", ^{
|
||||
NSDictionary *values = @{
|
||||
@"URL": @"http://github.com/1",
|
||||
@"otherURL": @"http://github.com/2",
|
||||
};
|
||||
|
||||
NSError *error = nil;
|
||||
MTLURLSubclassModel *model = [MTLJSONAdapter modelOfClass:MTLURLSubclassModel.class fromJSONDictionary:values error:&error];
|
||||
expect(model.URL).to(equal([NSURL URLWithString:@"http://github.com/1"]));
|
||||
expect(model.otherURL).to(equal([NSURL URLWithString:@"http://github.com/2"]));
|
||||
expect(error).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should implicitly transform URLs", ^{
|
||||
MTLURLModel *model = [[MTLURLModel alloc] init];
|
||||
|
||||
NSError *error = nil;
|
||||
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:model error:&error];
|
||||
|
||||
expect(JSONDictionary[@"URL"]).to(equal(@"http://github.com"));
|
||||
expect(error).to(beNil());
|
||||
});
|
||||
|
||||
|
||||
it(@"should implicitly transform NSStrings to UUIDs", ^{
|
||||
NSDictionary *values = @{
|
||||
@"UUID": @"D278C472-6DC3-4EE1-A947-861E6AF311C3",
|
||||
@"otherUUID": @"24E1E56A-3F37-4ECE-8310-931F6ACD401A",
|
||||
};
|
||||
|
||||
NSError *error = nil;
|
||||
MTLUUIDSubclassModel *model = [MTLJSONAdapter modelOfClass:MTLUUIDSubclassModel.class fromJSONDictionary:values error:&error];
|
||||
expect(model.UUID).to(equal([[NSUUID alloc] initWithUUIDString:@"D278C472-6DC3-4EE1-A947-861E6AF311C3"]));
|
||||
expect(model.otherUUID).to(equal([[NSUUID alloc] initWithUUIDString:@"24E1E56A-3F37-4ECE-8310-931F6ACD401A"]));
|
||||
expect(error).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should implicitly transform UUIDs", ^{
|
||||
MTLUUIDModel *model = [[MTLUUIDModel alloc] init];
|
||||
|
||||
NSError *error = nil;
|
||||
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:model error:&error];
|
||||
|
||||
expect(JSONDictionary[@"UUID"]).to(equal(@"4A275FBD-8217-4397-964B-403F4C2B8545"));
|
||||
expect(error).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should implicitly transform BOOLs", ^{
|
||||
MTLBoolModel *model = [[MTLBoolModel alloc] init];
|
||||
|
||||
NSError *error = nil;
|
||||
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:model error:&error];
|
||||
|
||||
expect(JSONDictionary[@"flag"]).to(beIdenticalTo((id)kCFBooleanFalse));
|
||||
expect(error).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should not invoke implicit transformers for property keys not actually backed by properties", ^{
|
||||
MTLNonPropertyModel *model = [[MTLNonPropertyModel alloc] init];
|
||||
|
||||
NSError *error = nil;
|
||||
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:model error:&error];
|
||||
|
||||
expect(error).to(beNil());
|
||||
expect(JSONDictionary[@"homepage"]).to(equal(model.homepage));
|
||||
});
|
||||
|
||||
it(@"should fail to initialize if JSON transformer fails", ^{
|
||||
NSDictionary *values = @{
|
||||
@"URL": @666,
|
||||
};
|
||||
|
||||
NSError *error = nil;
|
||||
MTLModel *model = [MTLJSONAdapter modelOfClass:MTLURLModel.class fromJSONDictionary:values error:&error];
|
||||
expect(model).to(beNil());
|
||||
expect(error.domain).to(equal(MTLTransformerErrorHandlingErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(MTLTransformerErrorHandlingErrorInvalidInput)));
|
||||
expect(error.userInfo[MTLTransformerErrorHandlingInputValueErrorKey]).to(equal(@666));
|
||||
});
|
||||
|
||||
it(@"should fail to deserialize if the JSON types don't match the primitive properties", ^{
|
||||
NSDictionary *values = @{
|
||||
@"flag": @"Potentially"
|
||||
};
|
||||
|
||||
NSError *error = nil;
|
||||
MTLModel *model = [MTLJSONAdapter modelOfClass:MTLBoolModel.class fromJSONDictionary:values error:&error];
|
||||
expect(model).to(beNil());
|
||||
|
||||
expect(error.domain).to(equal(MTLTransformerErrorHandlingErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(MTLTransformerErrorHandlingErrorInvalidInput)));
|
||||
expect(error.userInfo[MTLTransformerErrorHandlingInputValueErrorKey]).to(equal(@"Potentially"));
|
||||
});
|
||||
|
||||
it(@"should fail to deserialize if the JSON types don't match the properties", ^{
|
||||
NSDictionary *values = @{
|
||||
@"string": @666
|
||||
};
|
||||
|
||||
NSError *error = nil;
|
||||
MTLModel *model = [MTLJSONAdapter modelOfClass:MTLStringModel.class fromJSONDictionary:values error:&error];
|
||||
expect(model).to(beNil());
|
||||
|
||||
expect(error.domain).to(equal(MTLTransformerErrorHandlingErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(MTLTransformerErrorHandlingErrorInvalidInput)));
|
||||
expect(error.userInfo[MTLTransformerErrorHandlingInputValueErrorKey]).to(equal(@666));
|
||||
});
|
||||
|
||||
it(@"should allow subclasses to filter serialized property keys", ^{
|
||||
NSDictionary *values = @{
|
||||
@"username": @"foo",
|
||||
@"count": @"5",
|
||||
@"nested": @{ @"name": NSNull.null }
|
||||
};
|
||||
|
||||
MTLTestJSONAdapter *adapter = [[MTLTestJSONAdapter alloc] initWithModelClass:MTLTestModel.class];
|
||||
|
||||
NSError *error;
|
||||
MTLTestModel *model = [adapter modelFromJSONDictionary:values error:&error];
|
||||
expect(model).notTo(beNil());
|
||||
expect(error).to(beNil());
|
||||
|
||||
NSDictionary *complete = [adapter JSONDictionaryFromModel:model error:&error];
|
||||
NSDictionary *expected = [values mtl_dictionaryByAddingEntriesFromDictionary:@{ @"test": @YES }];
|
||||
|
||||
expect(complete).to(equal(expected));
|
||||
expect(error).to(beNil());
|
||||
|
||||
adapter.ignoredPropertyKeys = [NSSet setWithObjects:@"count", @"nestedName", nil];
|
||||
|
||||
NSDictionary *partial = [adapter JSONDictionaryFromModel:model error:&error];
|
||||
expected = @{
|
||||
@"username": @"foo",
|
||||
@"test": @YES,
|
||||
};
|
||||
|
||||
expect(partial).to(equal(expected));
|
||||
expect(error).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should accept any object for id properties", ^{
|
||||
NSDictionary *values = @{
|
||||
@"anyObject": @"Not an NSValue"
|
||||
};
|
||||
|
||||
NSError *error = nil;
|
||||
MTLIDModel *model = [MTLJSONAdapter modelOfClass:MTLIDModel.class fromJSONDictionary:values error:&error];
|
||||
expect(model).notTo(beNil());
|
||||
expect(model.anyObject).to(equal(@"Not an NSValue"));
|
||||
|
||||
expect(error.domain).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should fail to serialize if a JSON transformer errors", ^{
|
||||
MTLURLModel *model = [[MTLURLModel alloc] init];
|
||||
|
||||
[model setValue:@"totallyNotAnNSURL" forKey:@"URL"];
|
||||
|
||||
NSError *error;
|
||||
NSDictionary *dictionary = [MTLJSONAdapter JSONDictionaryFromModel:model error:&error];
|
||||
expect(dictionary).to(beNil());
|
||||
expect(error.domain).to(equal(MTLTransformerErrorHandlingErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(MTLTransformerErrorHandlingErrorInvalidInput)));
|
||||
expect(error.userInfo[MTLTransformerErrorHandlingInputValueErrorKey]).to(equal(@"totallyNotAnNSURL"));
|
||||
});
|
||||
|
||||
it(@"should parse a different model class", ^{
|
||||
@ -120,24 +368,325 @@ it(@"should parse a different model class", ^{
|
||||
|
||||
NSError *error = nil;
|
||||
MTLTestModel *model = [MTLJSONAdapter modelOfClass:MTLSubstitutingTestModel.class fromJSONDictionary:values error:&error];
|
||||
expect(model).to.beKindOf(MTLTestModel.class);
|
||||
expect(error).to.beNil();
|
||||
expect(model).to(beAnInstanceOf(MTLTestModel.class));
|
||||
expect(error).to(beNil());
|
||||
|
||||
expect(model.name).to.equal(@"foo");
|
||||
expect(model.count).to.equal(0);
|
||||
expect(model.nestedName).to.equal(@"bar");
|
||||
expect(model.name).to(equal(@"foo"));
|
||||
expect(@(model.count)).to(equal(@0));
|
||||
expect(model.nestedName).to(equal(@"bar"));
|
||||
|
||||
expect([MTLJSONAdapter JSONDictionaryFromModel:model]).to.equal(values);
|
||||
__block NSError *serializationError;
|
||||
expect([MTLJSONAdapter JSONDictionaryFromModel:model error:&serializationError]).to(equal(values));
|
||||
expect(serializationError).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should serialize different model classes", ^{
|
||||
MTLJSONAdapter *adapter = [[MTLJSONAdapter alloc] initWithModelClass:MTLClassClusterModel.class];
|
||||
|
||||
MTLChocolateClassClusterModel *chocolate = [MTLChocolateClassClusterModel modelWithDictionary:@{
|
||||
@"bitterness": @100
|
||||
} error:NULL];
|
||||
|
||||
NSError *error = nil;
|
||||
NSDictionary *chocolateValues = [adapter JSONDictionaryFromModel:chocolate error:&error];
|
||||
|
||||
expect(error).to(beNil());
|
||||
expect(chocolateValues).to(equal((@{
|
||||
@"flavor": @"chocolate",
|
||||
@"chocolate_bitterness": @"100"
|
||||
})));
|
||||
|
||||
MTLStrawberryClassClusterModel *strawberry = [MTLStrawberryClassClusterModel modelWithDictionary:@{
|
||||
@"freshness": @20
|
||||
} error:NULL];
|
||||
|
||||
NSDictionary *strawberryValues = [adapter JSONDictionaryFromModel:strawberry error:&error];
|
||||
|
||||
expect(error).to(beNil());
|
||||
expect(strawberryValues).to(equal((@{
|
||||
@"flavor": @"strawberry",
|
||||
@"strawberry_freshness": @20
|
||||
})));
|
||||
});
|
||||
|
||||
it(@"should parse model classes not inheriting from MTLModel", ^{
|
||||
NSDictionary *values = @{
|
||||
@"name": @"foo",
|
||||
};
|
||||
|
||||
NSError *error = nil;
|
||||
MTLConformingModel *model = [MTLJSONAdapter modelOfClass:MTLConformingModel.class fromJSONDictionary:values error:&error];
|
||||
expect(model).to(beAnInstanceOf(MTLConformingModel.class));
|
||||
expect(error).to(beNil());
|
||||
|
||||
expect(model.name).to(equal(@"foo"));
|
||||
});
|
||||
|
||||
it(@"should return an error when no suitable model class is found", ^{
|
||||
NSError *error = nil;
|
||||
MTLTestModel *model = [MTLJSONAdapter modelOfClass:MTLSubstitutingTestModel.class fromJSONDictionary:@{} error:&error];
|
||||
expect(model).to.beNil();
|
||||
expect(model).to(beNil());
|
||||
|
||||
expect(error).notTo.beNil();
|
||||
expect(error.domain).to.equal(MTLJSONAdapterErrorDomain);
|
||||
expect(error.code).to.equal(MTLJSONAdapterErrorNoClassFound);
|
||||
expect(error).notTo(beNil());
|
||||
expect(error.domain).to(equal(MTLJSONAdapterErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(MTLJSONAdapterErrorNoClassFound)));
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
it(@"should validate models", ^{
|
||||
NSError *error = nil;
|
||||
MTLValidationModel *model = [MTLJSONAdapter modelOfClass:MTLValidationModel.class fromJSONDictionary:@{} error:&error];
|
||||
|
||||
expect(model).to(beNil());
|
||||
|
||||
expect(error).notTo(beNil());
|
||||
expect(error.domain).to(equal(MTLTestModelErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(MTLTestModelNameMissing)));
|
||||
});
|
||||
|
||||
describe(@"JSON transformers", ^{
|
||||
describe(@"dictionary transformer", ^{
|
||||
__block NSValueTransformer *transformer;
|
||||
|
||||
__block MTLTestModel *model;
|
||||
__block NSDictionary *JSONDictionary;
|
||||
|
||||
beforeEach(^{
|
||||
model = [[MTLTestModel alloc] init];
|
||||
JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:model error:NULL];
|
||||
|
||||
transformer = [MTLJSONAdapter dictionaryTransformerWithModelClass:MTLTestModel.class];
|
||||
expect(transformer).notTo(beNil());
|
||||
});
|
||||
|
||||
it(@"should transform a JSON dictionary into a model", ^{
|
||||
expect([transformer transformedValue:JSONDictionary]).to(equal(model));
|
||||
});
|
||||
|
||||
it(@"should transform a model into a JSON dictionary", ^{
|
||||
expect(@([transformer.class allowsReverseTransformation])).to(beTruthy());
|
||||
expect([transformer reverseTransformedValue:model]).to(equal(JSONDictionary));
|
||||
});
|
||||
|
||||
itBehavesLike(MTLTransformerErrorExamples, ^{
|
||||
return @{
|
||||
MTLTransformerErrorExamplesTransformer: transformer,
|
||||
MTLTransformerErrorExamplesInvalidTransformationInput: NSNull.null,
|
||||
MTLTransformerErrorExamplesInvalidReverseTransformationInput: NSNull.null
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
describe(@"external representation array transformer", ^{
|
||||
__block NSValueTransformer *transformer;
|
||||
|
||||
__block NSArray *models;
|
||||
__block NSArray *JSONDictionaries;
|
||||
|
||||
beforeEach(^{
|
||||
NSMutableArray *uniqueModels = [NSMutableArray array];
|
||||
NSMutableArray *mutableDictionaries = [NSMutableArray array];
|
||||
|
||||
for (NSUInteger i = 0; i < 10; i++) {
|
||||
MTLTestModel *model = [[MTLTestModel alloc] init];
|
||||
model.count = i;
|
||||
|
||||
[uniqueModels addObject:model];
|
||||
|
||||
NSDictionary *dict = [MTLJSONAdapter JSONDictionaryFromModel:model error:NULL];
|
||||
expect(dict).notTo(beNil());
|
||||
|
||||
[mutableDictionaries addObject:dict];
|
||||
}
|
||||
|
||||
uniqueModels[2] = NSNull.null;
|
||||
mutableDictionaries[2] = NSNull.null;
|
||||
|
||||
models = [uniqueModels copy];
|
||||
JSONDictionaries = [mutableDictionaries copy];
|
||||
|
||||
transformer = [MTLJSONAdapter arrayTransformerWithModelClass:MTLTestModel.class];
|
||||
expect(transformer).notTo(beNil());
|
||||
});
|
||||
|
||||
it(@"should transform JSON dictionaries into models", ^{
|
||||
expect([transformer transformedValue:JSONDictionaries]).to(equal(models));
|
||||
});
|
||||
|
||||
it(@"should transform models into JSON dictionaries", ^{
|
||||
expect(@([transformer.class allowsReverseTransformation])).to(beTruthy());
|
||||
expect([transformer reverseTransformedValue:models]).to(equal(JSONDictionaries));
|
||||
});
|
||||
|
||||
itBehavesLike(MTLTransformerErrorExamples, ^{
|
||||
return @{
|
||||
MTLTransformerErrorExamplesTransformer: transformer,
|
||||
MTLTransformerErrorExamplesInvalidTransformationInput: NSNull.null,
|
||||
MTLTransformerErrorExamplesInvalidReverseTransformationInput: NSNull.null
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it(@"should use receiving class for serialization", ^{
|
||||
NSDictionary *values = @{
|
||||
@"username": @"foo",
|
||||
@"count": @"5",
|
||||
@"nested": @{ @"name": NSNull.null }
|
||||
};
|
||||
|
||||
NSValueTransformer *transformer = [MTLTestJSONAdapter dictionaryTransformerWithModelClass:MTLTestModel.class];
|
||||
|
||||
MTLTestModel *model = [transformer transformedValue:values];
|
||||
expect(model).to(beAKindOf(MTLTestModel.class));
|
||||
expect(model).notTo(beNil());
|
||||
|
||||
NSDictionary *serialized = [transformer reverseTransformedValue:model];
|
||||
expect(serialized).notTo(beNil());
|
||||
expect(serialized[@"test"]).to(beTruthy());
|
||||
});
|
||||
});
|
||||
|
||||
describe(@"Deserializing multiple models", ^{
|
||||
NSDictionary *value1 = @{
|
||||
@"username": @"foo"
|
||||
};
|
||||
|
||||
NSDictionary *value2 = @{
|
||||
@"username": @"bar"
|
||||
};
|
||||
|
||||
NSArray *JSONModels = @[ value1, value2 ];
|
||||
|
||||
it(@"should initialize models from an array of JSON dictionaries", ^{
|
||||
NSError *error = nil;
|
||||
NSArray *mantleModels = [MTLJSONAdapter modelsOfClass:MTLTestModel.class fromJSONArray:JSONModels error:&error];
|
||||
|
||||
expect(error).to(beNil());
|
||||
expect(mantleModels).notTo(beNil());
|
||||
expect(@(mantleModels.count)).to(equal(@2));
|
||||
expect([mantleModels[0] name]).to(equal(@"foo"));
|
||||
expect([mantleModels[1] name]).to(equal(@"bar"));
|
||||
});
|
||||
|
||||
it(@"should not be affected by a NULL error parameter", ^{
|
||||
NSError *error = nil;
|
||||
NSArray *expected = [MTLJSONAdapter modelsOfClass:MTLTestModel.class fromJSONArray:JSONModels error:&error];
|
||||
NSArray *models = [MTLJSONAdapter modelsOfClass:MTLTestModel.class fromJSONArray:JSONModels error:NULL];
|
||||
|
||||
expect(models).to(equal(expected));
|
||||
});
|
||||
});
|
||||
|
||||
it(@"should return nil and an error if it fails to initialize any model from an array", ^{
|
||||
NSDictionary *value1 = @{
|
||||
@"username": @"foo",
|
||||
@"count": @"1",
|
||||
};
|
||||
|
||||
NSDictionary *value2 = @{
|
||||
@"count": @[ @"This won't parse" ],
|
||||
};
|
||||
|
||||
NSArray *JSONModels = @[ value1, value2 ];
|
||||
|
||||
NSError *error = nil;
|
||||
NSArray *mantleModels = [MTLJSONAdapter modelsOfClass:MTLSubstitutingTestModel.class fromJSONArray:JSONModels error:&error];
|
||||
|
||||
expect(error).notTo(beNil());
|
||||
expect(error.domain).to(equal(MTLJSONAdapterErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(MTLJSONAdapterErrorNoClassFound)));
|
||||
expect(mantleModels).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should return an array of dictionaries from models", ^{
|
||||
MTLTestModel *model1 = [[MTLTestModel alloc] init];
|
||||
model1.name = @"foo";
|
||||
|
||||
MTLTestModel *model2 = [[MTLTestModel alloc] init];
|
||||
model2.name = @"bar";
|
||||
|
||||
NSError *error;
|
||||
NSArray *JSONArray = [MTLJSONAdapter JSONArrayFromModels:@[ model1, model2 ] error:&error];
|
||||
|
||||
expect(error).to(beNil());
|
||||
|
||||
expect(JSONArray).notTo(beNil());
|
||||
expect(@(JSONArray.count)).to(equal(@2));
|
||||
expect(JSONArray[0][@"username"]).to(equal(@"foo"));
|
||||
expect(JSONArray[1][@"username"]).to(equal(@"bar"));
|
||||
});
|
||||
|
||||
it(@"should not leak transformers", ^{
|
||||
__weak id weakTransformer;
|
||||
|
||||
@autoreleasepool {
|
||||
id transformer = [MTLJSONAdapter transformerForModelPropertiesOfClass:NSDate.class];
|
||||
weakTransformer = transformer;
|
||||
|
||||
expect(transformer).notTo(beNil());
|
||||
}
|
||||
|
||||
expect(weakTransformer).toEventually(beNil());
|
||||
});
|
||||
|
||||
it(@"should support recursive models", ^{
|
||||
NSDictionary *dictionary = @{
|
||||
@"owner": @{ @"name": @"Cameron" },
|
||||
@"users": @[
|
||||
@{ @"name": @"Dimitri" },
|
||||
@{ @"name": @"John" },
|
||||
],
|
||||
};
|
||||
|
||||
NSError *error = nil;
|
||||
MTLRecursiveGroupModel *group = [MTLJSONAdapter modelOfClass:MTLRecursiveGroupModel.class fromJSONDictionary:dictionary error:&error];
|
||||
expect(group).notTo(beNil());
|
||||
expect(@(group.users.count)).to(equal(@2));
|
||||
});
|
||||
|
||||
it(@"should automatically transform a property that conforms to MTLJSONSerializing", ^{
|
||||
NSDictionary *JSONDictionary = @{
|
||||
@"property": @"property",
|
||||
@"conformingMTLJSONSerializingProperty":@{
|
||||
@"username": @"testName",
|
||||
@"count": @"5",
|
||||
},
|
||||
@"nonConformingMTLJSONSerializingProperty": NSNull.null
|
||||
};
|
||||
|
||||
MTLJSONAdapter *adapter = [[MTLJSONAdapter alloc] initWithModelClass:MTLPropertyDefaultAdapterModel.class];
|
||||
expect(adapter).notTo(beNil());
|
||||
|
||||
NSError *error = nil;
|
||||
MTLPropertyDefaultAdapterModel *model = [MTLJSONAdapter modelOfClass:MTLPropertyDefaultAdapterModel.class fromJSONDictionary:JSONDictionary error:&error];
|
||||
expect(model).notTo(beNil());
|
||||
expect(model.conformingMTLJSONSerializingProperty).notTo(beNil());
|
||||
expect(model.conformingMTLJSONSerializingProperty.name).to(equal(@"testName"));
|
||||
expect(model.nonConformingMTLJSONSerializingProperty).to(beNil());
|
||||
expect(model.property).to(equal(@"property"));
|
||||
expect(error).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should not automatically transform a property that conforms to MTLModel but not MTLJSONSerializing", ^{
|
||||
NSDictionary *JSONDictionary = @{
|
||||
@"property": @"property",
|
||||
@"conformingMTLJSONSerializingProperty":@{
|
||||
@"username": @"testName",
|
||||
@"count": @"5",
|
||||
},
|
||||
/// Triggers an error since the dictionary is not automatically parsed
|
||||
/// and no transformer is supplied.
|
||||
@"nonConformingMTLJSONSerializingProperty": @{}
|
||||
};
|
||||
|
||||
MTLJSONAdapter *adapter = [[MTLJSONAdapter alloc] initWithModelClass:MTLPropertyDefaultAdapterModel.class];
|
||||
expect(adapter).notTo(beNil());
|
||||
|
||||
NSError *error = nil;
|
||||
MTLPropertyDefaultAdapterModel *model = [adapter modelFromJSONDictionary:JSONDictionary error:&error];
|
||||
expect(model).to(beNil());
|
||||
expect(error).notTo(beNil());
|
||||
expect(error.domain).to(equal(MTLTransformerErrorHandlingErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(MTLTransformerErrorHandlingErrorInvalidInput)));
|
||||
});
|
||||
|
||||
QuickSpecEnd
|
||||
|
||||
@ -1,236 +0,0 @@
|
||||
//
|
||||
// MTLManagedObjectAdapterSpec.m
|
||||
// Mantle
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-05-17.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MTLCoreDataTestModels.h"
|
||||
|
||||
SpecBegin(MTLManagedObjectAdapter)
|
||||
|
||||
__block NSPersistentStoreCoordinator *persistentStoreCoordinator;
|
||||
|
||||
beforeEach(^{
|
||||
NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:@[ [NSBundle bundleForClass:self.class] ]];
|
||||
expect(model).notTo.beNil();
|
||||
|
||||
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model];
|
||||
expect(persistentStoreCoordinator).notTo.beNil();
|
||||
expect([persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:NULL]).notTo.beNil();
|
||||
});
|
||||
|
||||
describe(@"with a confined context", ^{
|
||||
__block NSManagedObjectContext *context;
|
||||
|
||||
__block NSEntityDescription *parentEntity;
|
||||
__block NSEntityDescription *childEntity;
|
||||
|
||||
beforeEach(^{
|
||||
context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
|
||||
expect(context).notTo.beNil();
|
||||
|
||||
context.undoManager = nil;
|
||||
context.persistentStoreCoordinator = persistentStoreCoordinator;
|
||||
|
||||
parentEntity = [NSEntityDescription entityForName:@"Parent" inManagedObjectContext:context];
|
||||
expect(parentEntity).notTo.beNil();
|
||||
|
||||
childEntity = [NSEntityDescription entityForName:@"Child" inManagedObjectContext:context];
|
||||
expect(childEntity).notTo.beNil();
|
||||
});
|
||||
|
||||
describe(@"+modelOfClass:fromManagedObject:error:", ^{
|
||||
__block NSManagedObject *parent;
|
||||
|
||||
__block NSDate *date;
|
||||
__block NSString *numberString;
|
||||
__block NSString *requiredString;
|
||||
|
||||
beforeEach(^{
|
||||
date = [NSDate date];
|
||||
numberString = @"1234";
|
||||
requiredString = @"foobar";
|
||||
|
||||
parent = [[NSManagedObject alloc] initWithEntity:parentEntity insertIntoManagedObjectContext:context];
|
||||
expect(parent).notTo.beNil();
|
||||
|
||||
for (NSUInteger i = 0; i < 3; i++) {
|
||||
NSManagedObject *child = [[NSManagedObject alloc] initWithEntity:childEntity insertIntoManagedObjectContext:context];
|
||||
expect(child).notTo.beNil();
|
||||
|
||||
[child setValue:@(i) forKey:@"id"];
|
||||
[[parent mutableOrderedSetValueForKey:@"orderedChildren"] addObject:child];
|
||||
}
|
||||
|
||||
for (NSUInteger i = 3; i < 6; i++) {
|
||||
NSManagedObject *child = [[NSManagedObject alloc] initWithEntity:childEntity insertIntoManagedObjectContext:context];
|
||||
expect(child).notTo.beNil();
|
||||
|
||||
[child setValue:@(i) forKey:@"id"];
|
||||
[[parent mutableSetValueForKey:@"unorderedChildren"] addObject:child];
|
||||
}
|
||||
|
||||
[parent setValue:requiredString forKey:@"string"];
|
||||
|
||||
__block NSError *error = nil;
|
||||
expect([context save:&error]).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
// Make sure that pending changes are picked up too.
|
||||
[parent setValue:@(numberString.integerValue) forKey:@"number"];
|
||||
[parent setValue:date forKey:@"date"];
|
||||
});
|
||||
|
||||
it(@"should initialize a MTLParentTestModel with children", ^{
|
||||
NSError *error = nil;
|
||||
MTLParentTestModel *parentModel = [MTLManagedObjectAdapter modelOfClass:MTLParentTestModel.class fromManagedObject:parent error:&error];
|
||||
expect(parentModel).to.beKindOf(MTLParentTestModel.class);
|
||||
expect(error).to.beNil();
|
||||
|
||||
expect(parentModel.date).to.equal(date);
|
||||
expect(parentModel.numberString).to.equal(numberString);
|
||||
expect(parentModel.requiredString).to.equal(requiredString);
|
||||
|
||||
expect(parentModel.orderedChildren.count).to.equal(3);
|
||||
expect(parentModel.unorderedChildren.count).to.equal(3);
|
||||
|
||||
for (NSUInteger i = 0; i < 3; i++) {
|
||||
MTLChildTestModel *child = parentModel.orderedChildren[i];
|
||||
expect(child).to.beKindOf(MTLChildTestModel.class);
|
||||
|
||||
expect(child.childID).to.equal(i);
|
||||
expect(child.parent1).to.beNil();
|
||||
expect(child.parent2).to.beIdenticalTo(parentModel);
|
||||
}
|
||||
|
||||
for (MTLChildTestModel *child in parentModel.unorderedChildren) {
|
||||
expect(child).to.beKindOf(MTLChildTestModel.class);
|
||||
|
||||
expect(child.childID).to.beGreaterThanOrEqualTo(3);
|
||||
expect(child.childID).to.beLessThan(6);
|
||||
|
||||
expect(child.parent1).to.beIdenticalTo(parentModel);
|
||||
expect(child.parent2).to.beNil();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe(@"+managedObjectFromModel:insertingIntoContext:error:", ^{
|
||||
__block MTLParentTestModel *parentModel;
|
||||
|
||||
beforeEach(^{
|
||||
parentModel = [MTLParentTestModel modelWithDictionary:@{
|
||||
@"date": [NSDate date],
|
||||
@"numberString": @"1234",
|
||||
@"requiredString": @"foobar"
|
||||
} error:NULL];
|
||||
expect(parentModel).notTo.beNil();
|
||||
|
||||
NSMutableArray *orderedChildren = [NSMutableArray array];
|
||||
NSMutableSet *unorderedChildren = [NSMutableSet set];
|
||||
|
||||
for (NSUInteger i = 0; i < 3; i++) {
|
||||
MTLChildTestModel *child = [MTLChildTestModel modelWithDictionary:@{
|
||||
@"childID": @(i),
|
||||
@"parent2": parentModel
|
||||
} error:NULL];
|
||||
expect(child).notTo.beNil();
|
||||
|
||||
[orderedChildren addObject:child];
|
||||
}
|
||||
|
||||
for (NSUInteger i = 3; i < 6; i++) {
|
||||
MTLChildTestModel *child = [MTLChildTestModel modelWithDictionary:@{
|
||||
@"childID": @(i),
|
||||
@"parent1": parentModel
|
||||
} error:NULL];
|
||||
expect(child).notTo.beNil();
|
||||
|
||||
[unorderedChildren addObject:child];
|
||||
}
|
||||
|
||||
parentModel.orderedChildren = orderedChildren;
|
||||
parentModel.unorderedChildren = unorderedChildren;
|
||||
});
|
||||
|
||||
it(@"should insert a managed object with children", ^{
|
||||
__block NSError *error = nil;
|
||||
NSManagedObject *parent = [MTLManagedObjectAdapter managedObjectFromModel:parentModel insertingIntoContext:context error:&error];
|
||||
expect(parent).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
expect(parent.entity).to.equal(parentEntity);
|
||||
expect(context.insertedObjects).to.contain(parent);
|
||||
|
||||
expect([parent valueForKey:@"date"]).to.equal(parentModel.date);
|
||||
expect([[parent valueForKey:@"number"] stringValue]).to.equal(parentModel.numberString);
|
||||
expect([parent valueForKey:@"string"]).to.equal(parentModel.requiredString);
|
||||
|
||||
NSOrderedSet *orderedChildren = [parent valueForKey:@"orderedChildren"];
|
||||
expect(orderedChildren).notTo.beNil();
|
||||
expect(orderedChildren.count).to.equal(3);
|
||||
|
||||
NSSet *unorderedChildren = [parent valueForKey:@"unorderedChildren"];
|
||||
expect(unorderedChildren).notTo.beNil();
|
||||
expect(unorderedChildren.count).to.equal(3);
|
||||
|
||||
for (NSUInteger i = 0; i < 3; i++) {
|
||||
NSManagedObject *child = orderedChildren[i];
|
||||
expect(child.entity).to.equal(childEntity);
|
||||
expect(context.insertedObjects).to.contain(child);
|
||||
|
||||
expect([[child valueForKey:@"id"] unsignedIntegerValue]).to.equal(i);
|
||||
expect([child valueForKey:@"parent1"]).to.beNil();
|
||||
expect([child valueForKey:@"parent2"]).to.equal(parent);
|
||||
}
|
||||
|
||||
for (NSManagedObject *child in unorderedChildren) {
|
||||
expect(child.entity).to.equal(childEntity);
|
||||
expect(context.insertedObjects).to.contain(child);
|
||||
|
||||
NSUInteger childID = [[child valueForKey:@"id"] unsignedIntegerValue];
|
||||
expect(childID).to.beGreaterThanOrEqualTo(3);
|
||||
expect(childID).to.beLessThan(6);
|
||||
|
||||
expect([child valueForKey:@"parent1"]).to.equal(parent);
|
||||
expect([child valueForKey:@"parent2"]).to.beNil();
|
||||
}
|
||||
|
||||
expect([context save:&error]).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(@"with a main queue context", ^{
|
||||
__block NSManagedObjectContext *context;
|
||||
|
||||
__block NSEntityDescription *parentEntity;
|
||||
|
||||
beforeEach(^{
|
||||
context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
|
||||
expect(context).notTo.beNil();
|
||||
|
||||
context.undoManager = nil;
|
||||
context.persistentStoreCoordinator = persistentStoreCoordinator;
|
||||
|
||||
parentEntity = [NSEntityDescription entityForName:@"Parent" inManagedObjectContext:context];
|
||||
expect(parentEntity).notTo.beNil();
|
||||
});
|
||||
|
||||
it(@"should not deadlock on the main thread", ^{
|
||||
NSManagedObject *parent = [[NSManagedObject alloc] initWithEntity:parentEntity insertIntoManagedObjectContext:context];
|
||||
expect(parent).notTo.beNil();
|
||||
|
||||
[parent setValue:@"foobar" forKey:@"string"];
|
||||
|
||||
NSError *error = nil;
|
||||
MTLParentTestModel *parentModel = [MTLManagedObjectAdapter modelOfClass:MTLParentTestModel.class fromManagedObject:parent error:&error];
|
||||
expect(parentModel).to.beKindOf(MTLParentTestModel.class);
|
||||
expect(error).to.beNil();
|
||||
});
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
@ -6,35 +6,39 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
#import <Nimble/Nimble.h>
|
||||
#import <Quick/Quick.h>
|
||||
|
||||
#import "MTLTestModel.h"
|
||||
|
||||
SpecBegin(MTLModelNSCoding)
|
||||
QuickSpecBegin(MTLModelNSCoding)
|
||||
|
||||
it(@"should have default encoding behaviors", ^{
|
||||
NSDictionary *behaviors = MTLTestModel.encodingBehaviorsByPropertyKey;
|
||||
expect(behaviors).notTo.beNil();
|
||||
expect(behaviors).notTo(beNil());
|
||||
|
||||
expect(behaviors[@"name"]).to.equal(@(MTLModelEncodingBehaviorUnconditional));
|
||||
expect(behaviors[@"count"]).to.equal(@(MTLModelEncodingBehaviorUnconditional));
|
||||
expect(behaviors[@"weakModel"]).to.equal(@(MTLModelEncodingBehaviorConditional));
|
||||
expect(behaviors[@"dynamicName"]).to.beNil();
|
||||
expect(behaviors[@"name"]).to(equal(@(MTLModelEncodingBehaviorUnconditional)));
|
||||
expect(behaviors[@"count"]).to(equal(@(MTLModelEncodingBehaviorUnconditional)));
|
||||
expect(behaviors[@"weakModel"]).to(equal(@(MTLModelEncodingBehaviorConditional)));
|
||||
expect(behaviors[@"dynamicName"]).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should have default allowed classes", ^{
|
||||
NSDictionary *allowedClasses = MTLTestModel.allowedSecureCodingClassesByPropertyKey;
|
||||
expect(allowedClasses).notTo.beNil();
|
||||
expect(allowedClasses).notTo(beNil());
|
||||
|
||||
expect(allowedClasses[@"name"]).to(equal(@[ NSString.class ]));
|
||||
expect(allowedClasses[@"count"]).to(equal(@[ NSValue.class ]));
|
||||
expect(allowedClasses[@"weakModel"]).to(equal(@[ MTLEmptyTestModel.class ]));
|
||||
|
||||
expect(allowedClasses[@"name"]).to.equal(@[ NSString.class ]);
|
||||
expect(allowedClasses[@"count"]).to.equal(@[ NSValue.class ]);
|
||||
expect(allowedClasses[@"weakModel"]).to.equal(@[ MTLEmptyTestModel.class ]);
|
||||
|
||||
// Not encoded into archives.
|
||||
expect(allowedClasses[@"nestedName"]).to.beNil();
|
||||
expect(allowedClasses[@"dynamicName"]).to.beNil();
|
||||
expect(allowedClasses[@"nestedName"]).to(beNil());
|
||||
expect(allowedClasses[@"dynamicName"]).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should default to version 0", ^{
|
||||
expect(MTLEmptyTestModel.modelVersion).to.equal(0);
|
||||
expect(@(MTLEmptyTestModel.modelVersion)).to(equal(@0));
|
||||
});
|
||||
|
||||
describe(@"archiving", ^{
|
||||
@ -46,7 +50,7 @@ describe(@"archiving", ^{
|
||||
|
||||
beforeEach(^{
|
||||
emptyModel = [[MTLEmptyTestModel alloc] init];
|
||||
expect(emptyModel).notTo.beNil();
|
||||
expect(emptyModel).notTo(beNil());
|
||||
|
||||
values = @{
|
||||
@"name": @"foobar",
|
||||
@ -55,77 +59,77 @@ describe(@"archiving", ^{
|
||||
|
||||
NSError *error = nil;
|
||||
model = [[MTLTestModel alloc] initWithDictionary:values error:&error];
|
||||
expect(model).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
expect(model).notTo(beNil());
|
||||
expect(error).to(beNil());
|
||||
|
||||
archiveAndUnarchiveModel = [^{
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:model];
|
||||
expect(data).notTo.beNil();
|
||||
expect(data).notTo(beNil());
|
||||
|
||||
MTLTestModel *unarchivedModel = [NSKeyedUnarchiver unarchiveObjectWithData:data];
|
||||
expect(unarchivedModel).notTo.beNil();
|
||||
expect(unarchivedModel).notTo(beNil());
|
||||
|
||||
return unarchivedModel;
|
||||
} copy];
|
||||
});
|
||||
|
||||
it(@"should archive unconditional properties", ^{
|
||||
expect(archiveAndUnarchiveModel()).to.equal(model);
|
||||
expect(archiveAndUnarchiveModel()).to(equal(model));
|
||||
});
|
||||
|
||||
it(@"should not archive excluded properties", ^{
|
||||
model.nestedName = @"foobar";
|
||||
|
||||
|
||||
MTLTestModel *unarchivedModel = archiveAndUnarchiveModel();
|
||||
expect(unarchivedModel.nestedName).to.beNil();
|
||||
expect(unarchivedModel).notTo.equal(model);
|
||||
expect(unarchivedModel.nestedName).to(beNil());
|
||||
expect(unarchivedModel).notTo(equal(model));
|
||||
|
||||
model.nestedName = nil;
|
||||
expect(unarchivedModel).to.equal(model);
|
||||
expect(unarchivedModel).to(equal(model));
|
||||
});
|
||||
|
||||
it(@"should not archive conditional properties if not encoded elsewhere", ^{
|
||||
model.weakModel = emptyModel;
|
||||
|
||||
|
||||
MTLTestModel *unarchivedModel = archiveAndUnarchiveModel();
|
||||
expect(unarchivedModel.weakModel).to.beNil();
|
||||
expect(unarchivedModel.weakModel).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should archive conditional properties if encoded elsewhere", ^{
|
||||
model.weakModel = emptyModel;
|
||||
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:@[ model, emptyModel ]];
|
||||
expect(data).notTo.beNil();
|
||||
expect(data).notTo(beNil());
|
||||
|
||||
NSArray *objects = [NSKeyedUnarchiver unarchiveObjectWithData:data];
|
||||
expect(objects.count).to.equal(2);
|
||||
expect(objects[1]).to.equal(emptyModel);
|
||||
|
||||
expect(@(objects.count)).to(equal(@2));
|
||||
expect(objects[1]).to(equal(emptyModel));
|
||||
|
||||
MTLTestModel *unarchivedModel = objects[0];
|
||||
expect(unarchivedModel).to.equal(model);
|
||||
expect(unarchivedModel.weakModel).to.equal(emptyModel);
|
||||
expect(unarchivedModel).to(equal(model));
|
||||
expect(unarchivedModel.weakModel).to(equal(emptyModel));
|
||||
});
|
||||
|
||||
it(@"should invoke custom decoding logic", ^{
|
||||
MTLTestModel.modelVersion = 0;
|
||||
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:model];
|
||||
expect(data).notTo.beNil();
|
||||
expect(data).notTo(beNil());
|
||||
|
||||
MTLTestModel.modelVersion = 1;
|
||||
|
||||
MTLTestModel *unarchivedModel = [NSKeyedUnarchiver unarchiveObjectWithData:data];
|
||||
expect(unarchivedModel).notTo.beNil();
|
||||
expect(unarchivedModel.name).to.equal(@"M: foobar");
|
||||
expect(unarchivedModel.count).to.equal(@5);
|
||||
expect(unarchivedModel).notTo(beNil());
|
||||
expect(unarchivedModel.name).to(equal(@"M: foobar"));
|
||||
expect(@(unarchivedModel.count)).to(equal(@5));
|
||||
});
|
||||
|
||||
it(@"should unarchive an external representation from the old model format", ^{
|
||||
NSURL *archiveURL = [[NSBundle bundleForClass:self.class] URLForResource:@"MTLTestModel-OldArchive" withExtension:@"plist"];
|
||||
expect(archiveURL).notTo.beNil();
|
||||
expect(archiveURL).notTo(beNil());
|
||||
|
||||
MTLTestModel *unarchivedModel = [NSKeyedUnarchiver unarchiveObjectWithFile:archiveURL.path];
|
||||
expect(unarchivedModel).notTo.beNil();
|
||||
expect(unarchivedModel).notTo(beNil());
|
||||
|
||||
NSDictionary *expectedValues = @{
|
||||
@"name": @"foobar",
|
||||
@ -133,9 +137,9 @@ describe(@"archiving", ^{
|
||||
@"nestedName": @"fuzzbuzz",
|
||||
@"weakModel": NSNull.null,
|
||||
};
|
||||
|
||||
expect(unarchivedModel.dictionaryValue).to.equal(expectedValues);
|
||||
|
||||
expect(unarchivedModel.dictionaryValue).to(equal(expectedValues));
|
||||
});
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
QuickSpecEnd
|
||||
|
||||
@ -6,25 +6,29 @@
|
||||
// Copyright (c) 2012 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
#import <Nimble/Nimble.h>
|
||||
#import <Quick/Quick.h>
|
||||
|
||||
#import "MTLTestModel.h"
|
||||
|
||||
SpecBegin(MTLModel)
|
||||
QuickSpecBegin(MTLModelSpec)
|
||||
|
||||
it(@"should not loop infinitely in +propertyKeys without any properties", ^{
|
||||
expect(MTLEmptyTestModel.propertyKeys).to.equal([NSSet set]);
|
||||
expect(MTLEmptyTestModel.propertyKeys).to(equal([NSSet set]));
|
||||
});
|
||||
|
||||
it(@"should not include dynamic readonly properties in +propertyKeys", ^{
|
||||
NSSet *expectedKeys = [NSSet setWithObjects:@"name", @"count", @"nestedName", @"weakModel", nil];
|
||||
expect(MTLTestModel.propertyKeys).to.equal(expectedKeys);
|
||||
expect(MTLTestModel.propertyKeys).to(equal(expectedKeys));
|
||||
});
|
||||
|
||||
it(@"should initialize with default values", ^{
|
||||
MTLTestModel *model = [[MTLTestModel alloc] init];
|
||||
expect(model).notTo.beNil();
|
||||
expect(model).notTo(beNil());
|
||||
|
||||
expect(model.name).to.beNil();
|
||||
expect(model.count).to.equal(1);
|
||||
expect(model.name).to(beNil());
|
||||
expect(@(model.count)).to(equal(@1));
|
||||
|
||||
NSDictionary *expectedValues = @{
|
||||
@"name": NSNull.null,
|
||||
@ -33,18 +37,18 @@ it(@"should initialize with default values", ^{
|
||||
@"weakModel": NSNull.null,
|
||||
};
|
||||
|
||||
expect(model.dictionaryValue).to.equal(expectedValues);
|
||||
expect([model dictionaryWithValuesForKeys:expectedValues.allKeys]).to.equal(expectedValues);
|
||||
expect(model.dictionaryValue).to(equal(expectedValues));
|
||||
expect([model dictionaryWithValuesForKeys:expectedValues.allKeys]).to(equal(expectedValues));
|
||||
});
|
||||
|
||||
it(@"should initialize to default values with a nil dictionary", ^{
|
||||
NSError *error = nil;
|
||||
MTLTestModel *dictionaryModel = [[MTLTestModel alloc] initWithDictionary:nil error:&error];
|
||||
expect(dictionaryModel).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
expect(dictionaryModel).notTo(beNil());
|
||||
expect(error).to(beNil());
|
||||
|
||||
MTLTestModel *defaultModel = [[MTLTestModel alloc] init];
|
||||
expect(dictionaryModel).to.equal(defaultModel);
|
||||
expect(dictionaryModel).to(equal(defaultModel));
|
||||
});
|
||||
|
||||
describe(@"with a dictionary of values", ^{
|
||||
@ -54,7 +58,7 @@ describe(@"with a dictionary of values", ^{
|
||||
|
||||
beforeEach(^{
|
||||
emptyModel = [[MTLEmptyTestModel alloc] init];
|
||||
expect(emptyModel).notTo.beNil();
|
||||
expect(emptyModel).notTo(beNil());
|
||||
|
||||
values = @{
|
||||
@"name": @"foobar",
|
||||
@ -65,63 +69,136 @@ describe(@"with a dictionary of values", ^{
|
||||
|
||||
NSError *error = nil;
|
||||
model = [[MTLTestModel alloc] initWithDictionary:values error:&error];
|
||||
expect(model).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
expect(model).notTo(beNil());
|
||||
expect(error).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should initialize with the given values", ^{
|
||||
expect(model.name).to.equal(@"foobar");
|
||||
expect(model.count).to.equal(5);
|
||||
expect(model.nestedName).to.equal(@"fuzzbuzz");
|
||||
expect(model.weakModel).to.equal(emptyModel);
|
||||
expect(model.name).to(equal(@"foobar"));
|
||||
expect(@(model.count)).to(equal(@5));
|
||||
expect(model.nestedName).to(equal(@"fuzzbuzz"));
|
||||
expect(model.weakModel).to(equal(emptyModel));
|
||||
|
||||
expect(model.dictionaryValue).to.equal(values);
|
||||
expect([model dictionaryWithValuesForKeys:values.allKeys]).to.equal(values);
|
||||
expect(model.dictionaryValue).to(equal(values));
|
||||
expect([model dictionaryWithValuesForKeys:values.allKeys]).to(equal(values));
|
||||
});
|
||||
|
||||
it(@"should compare equal to a matching model", ^{
|
||||
expect(model).to.equal(model);
|
||||
expect(model).to(equal(model));
|
||||
|
||||
MTLTestModel *matchingModel = [[MTLTestModel alloc] initWithDictionary:values error:NULL];
|
||||
expect(model).to.equal(matchingModel);
|
||||
expect(model.hash).to.equal(matchingModel.hash);
|
||||
expect(model.dictionaryValue).to.equal(matchingModel.dictionaryValue);
|
||||
expect(model).to(equal(matchingModel));
|
||||
expect(@(model.hash)).to(equal(@(matchingModel.hash)));
|
||||
expect(model.dictionaryValue).to(equal(matchingModel.dictionaryValue));
|
||||
});
|
||||
|
||||
it(@"should not compare equal to different model", ^{
|
||||
MTLTestModel *differentModel = [[MTLTestModel alloc] init];
|
||||
expect(model).notTo.equal(differentModel);
|
||||
expect(model.dictionaryValue).notTo.equal(differentModel.dictionaryValue);
|
||||
expect(model).notTo(equal(differentModel));
|
||||
expect(model.dictionaryValue).notTo(equal(differentModel.dictionaryValue));
|
||||
});
|
||||
|
||||
it(@"should implement <NSCopying>", ^{
|
||||
MTLTestModel *copiedModel = [model copy];
|
||||
expect(copiedModel).to.equal(model);
|
||||
expect(copiedModel).notTo.beIdenticalTo(model);
|
||||
expect(copiedModel).to(equal(model));
|
||||
expect(copiedModel).notTo(beIdenticalTo(model));
|
||||
});
|
||||
|
||||
it(@"should not consider -weakModel for equality", ^{
|
||||
MTLTestModel *copiedModel = [model copy];
|
||||
copiedModel.weakModel = nil;
|
||||
|
||||
expect(model).to(equal(copiedModel));
|
||||
});
|
||||
});
|
||||
|
||||
it(@"should fail to initialize if dictionary validation fails", ^{
|
||||
NSError *error = nil;
|
||||
MTLTestModel *model = [[MTLTestModel alloc] initWithDictionary:@{ @"name": @"this is too long a name" } error:&error];
|
||||
expect(model).to.beNil();
|
||||
expect(model).to(beNil());
|
||||
|
||||
expect(error).notTo.beNil();
|
||||
expect(error.domain).to.equal(MTLTestModelErrorDomain);
|
||||
expect(error.code).to.equal(MTLTestModelNameTooLong);
|
||||
expect(error).notTo(beNil());
|
||||
expect(error.domain).to(equal(MTLTestModelErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(MTLTestModelNameTooLong)));
|
||||
});
|
||||
|
||||
it(@"should merge two models together", ^{
|
||||
MTLTestModel *target = [[MTLTestModel alloc] initWithDictionary:@{ @"name": @"foo", @"count": @(5) } error:NULL];
|
||||
expect(target).notTo.beNil();
|
||||
expect(target).notTo(beNil());
|
||||
|
||||
MTLTestModel *source = [[MTLTestModel alloc] initWithDictionary:@{ @"name": @"bar", @"count": @(3) } error:NULL];
|
||||
expect(source).notTo.beNil();
|
||||
expect(source).notTo(beNil());
|
||||
|
||||
[target mergeValuesForKeysFromModel:source];
|
||||
|
||||
expect(target.name).to.equal(@"bar");
|
||||
expect(target.count).to.equal(8);
|
||||
expect(target.name).to(equal(@"bar"));
|
||||
expect(@(target.count)).to(equal(@8));
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
it(@"should consider primitive properties permanent", ^{
|
||||
expect(@([MTLStorageBehaviorModel storageBehaviorForPropertyWithKey:@"primitive"])).to(equal(@(MTLPropertyStoragePermanent)));
|
||||
});
|
||||
|
||||
it(@"should consider object-type assign properties permanent", ^{
|
||||
expect(@([MTLStorageBehaviorModel storageBehaviorForPropertyWithKey:@"assignProperty"])).to(equal(@(MTLPropertyStoragePermanent)));
|
||||
});
|
||||
|
||||
it(@"should consider object-type strong properties permanent", ^{
|
||||
expect(@([MTLStorageBehaviorModel storageBehaviorForPropertyWithKey:@"strongProperty"])).to(equal(@(MTLPropertyStoragePermanent)));
|
||||
});
|
||||
|
||||
it(@"should ignore readonly properties without backing ivar", ^{
|
||||
expect(@([MTLStorageBehaviorModel storageBehaviorForPropertyWithKey:@"notIvarBacked"])).to(equal(@(MTLPropertyStorageNone)));
|
||||
});
|
||||
|
||||
it(@"should consider properties declared in subclass with storage in superclass permanent", ^{
|
||||
expect(@([MTLStorageBehaviorModelSubclass storageBehaviorForPropertyWithKey:@"shadowedInSubclass"])).to(equal(@(MTLPropertyStoragePermanent)));
|
||||
expect(@([MTLStorageBehaviorModelSubclass storageBehaviorForPropertyWithKey:@"declaredInProtocol"])).to(equal(@(MTLPropertyStoragePermanent)));
|
||||
});
|
||||
|
||||
it(@"should ignore optional protocol properties not implemented", ^{
|
||||
expect(@([MTLOptionalPropertyModel storageBehaviorForPropertyWithKey:@"optionalUnimplementedProperty"])).to(equal(@(MTLPropertyStorageNone)));
|
||||
expect(@([MTLOptionalPropertyModel storageBehaviorForPropertyWithKey:@"optionalImplementedProperty"])).to(equal(@(MTLPropertyStoragePermanent)));
|
||||
});
|
||||
|
||||
describe(@"merging with model subclasses", ^{
|
||||
__block MTLTestModel *superclass;
|
||||
__block MTLSubclassTestModel *subclass;
|
||||
|
||||
beforeEach(^{
|
||||
superclass = [MTLTestModel modelWithDictionary:@{
|
||||
@"name": @"foo",
|
||||
@"count": @5
|
||||
} error:NULL];
|
||||
|
||||
expect(superclass).notTo(beNil());
|
||||
|
||||
subclass = [MTLSubclassTestModel modelWithDictionary:@{
|
||||
@"name": @"bar",
|
||||
@"count": @3,
|
||||
@"generation": @1,
|
||||
@"role": @"subclass"
|
||||
} error:NULL];
|
||||
|
||||
expect(subclass).notTo(beNil());
|
||||
});
|
||||
|
||||
it(@"should merge from subclass model", ^{
|
||||
[superclass mergeValuesForKeysFromModel:subclass];
|
||||
|
||||
expect(superclass.name).to(equal(@"bar"));
|
||||
expect(@(superclass.count)).to(equal(@8));
|
||||
});
|
||||
|
||||
it(@"should merge from superclass model", ^{
|
||||
[subclass mergeValuesForKeysFromModel:superclass];
|
||||
|
||||
expect(subclass.name).to(equal(@"foo"));
|
||||
expect(@(subclass.count)).to(equal(@8));
|
||||
expect(subclass.generation).to(equal(@1));
|
||||
expect(subclass.role).to(equal(@"subclass"));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
QuickSpecEnd
|
||||
|
||||
@ -6,22 +6,26 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
#import <Nimble/Nimble.h>
|
||||
#import <Quick/Quick.h>
|
||||
|
||||
#import "MTLTestModel.h"
|
||||
|
||||
#import "MTLModel.h"
|
||||
|
||||
SpecBegin(MTLModelValidation)
|
||||
QuickSpecBegin(MTLModelValidation)
|
||||
|
||||
it(@"should fail with incorrect values", ^{
|
||||
MTLValidationModel *model = [[MTLValidationModel alloc] init];
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success = [model validate:&error];
|
||||
expect(success).to.beFalsy();
|
||||
expect(@(success)).to(beFalsy());
|
||||
|
||||
expect(error).notTo.beNil();
|
||||
expect(error.domain).to.equal(MTLTestModelErrorDomain);
|
||||
expect(error.code).to.equal(MTLTestModelNameMissing);
|
||||
expect(error).notTo(beNil());
|
||||
expect(error.domain).to(equal(MTLTestModelErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(MTLTestModelNameMissing)));
|
||||
});
|
||||
|
||||
it(@"should succeed with correct values", ^{
|
||||
@ -29,9 +33,9 @@ it(@"should succeed with correct values", ^{
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success = [model validate:&error];
|
||||
expect(success).to.beTruthy();
|
||||
expect(@(success)).to(beTruthy());
|
||||
|
||||
expect(error).to.beNil();
|
||||
expect(error).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should apply values returned from -validateValue:error:", ^{
|
||||
@ -39,11 +43,11 @@ it(@"should apply values returned from -validateValue:error:", ^{
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success = [model validate:&error];
|
||||
expect(success).to.beTruthy();
|
||||
expect(@(success)).to(beTruthy());
|
||||
|
||||
expect(model.name).to.equal(@"foobar");
|
||||
expect(model.name).to(equal(@"foobar"));
|
||||
|
||||
expect(error).to.beNil();
|
||||
expect(error).to(beNil());
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
QuickSpecEnd
|
||||
|
||||
@ -6,108 +6,299 @@
|
||||
// Copyright (c) 2012 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
#import <Nimble/Nimble.h>
|
||||
#import <Quick/Quick.h>
|
||||
#import "MTLTransformerErrorExamples.h"
|
||||
|
||||
#import "MTLTestModel.h"
|
||||
|
||||
SpecBegin(MTLPredefinedTransformerAdditions)
|
||||
enum : NSInteger {
|
||||
MTLPredefinedTransformerAdditionsSpecEnumNegative = -1,
|
||||
MTLPredefinedTransformerAdditionsSpecEnumZero = 0,
|
||||
MTLPredefinedTransformerAdditionsSpecEnumPositive = 1,
|
||||
MTLPredefinedTransformerAdditionsSpecEnumDefault = 42,
|
||||
} MTLPredefinedTransformerAdditionsSpecEnum;
|
||||
|
||||
it(@"should define a URL value transformer", ^{
|
||||
NSValueTransformer *transformer = [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
|
||||
expect(transformer).notTo.beNil();
|
||||
expect([transformer.class allowsReverseTransformation]).to.beTruthy();
|
||||
QuickSpecBegin(MTLPredefinedTransformerAdditions)
|
||||
|
||||
NSString *URLString = @"http://www.github.com/";
|
||||
expect([transformer transformedValue:URLString]).to.equal([NSURL URLWithString:URLString]);
|
||||
expect([transformer reverseTransformedValue:[NSURL URLWithString:URLString]]).to.equal(URLString);
|
||||
describe(@"The URL transformer", ^{
|
||||
__block NSValueTransformer *transformer;
|
||||
|
||||
expect([transformer transformedValue:nil]).to.beNil();
|
||||
expect([transformer reverseTransformedValue:nil]).to.beNil();
|
||||
beforeEach(^{
|
||||
transformer = [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
|
||||
|
||||
expect(transformer).notTo(beNil());
|
||||
expect(@([transformer.class allowsReverseTransformation])).to(beTruthy());
|
||||
});
|
||||
|
||||
it(@"should convert NSStrings to NSURLs and back", ^{
|
||||
NSString *URLString = @"http://www.github.com/";
|
||||
expect([transformer transformedValue:URLString]).to(equal([NSURL URLWithString:URLString]));
|
||||
expect([transformer reverseTransformedValue:[NSURL URLWithString:URLString]]).to(equal(URLString));
|
||||
|
||||
expect([transformer transformedValue:nil]).to(beNil());
|
||||
expect([transformer reverseTransformedValue:nil]).to(beNil());
|
||||
});
|
||||
|
||||
itBehavesLike(MTLTransformerErrorExamples, ^{
|
||||
return @{
|
||||
MTLTransformerErrorExamplesTransformer: transformer,
|
||||
MTLTransformerErrorExamplesInvalidTransformationInput: @"not a valid URL",
|
||||
MTLTransformerErrorExamplesInvalidReverseTransformationInput: NSNull.null
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it(@"should define an NSNumber boolean value transformer", ^{
|
||||
// Back these NSNumbers with ints, rather than booleans,
|
||||
// to ensure that the value transformers are actually transforming.
|
||||
NSNumber *booleanYES = @(1);
|
||||
NSNumber *booleanNO = @(0);
|
||||
|
||||
NSValueTransformer *transformer = [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName];
|
||||
expect(transformer).notTo.beNil();
|
||||
expect([transformer.class allowsReverseTransformation]).to.beTruthy();
|
||||
|
||||
expect([transformer transformedValue:booleanYES]).to.equal([NSNumber numberWithBool:YES]);
|
||||
expect([transformer transformedValue:booleanYES]).to.equal((id)kCFBooleanTrue);
|
||||
|
||||
expect([transformer reverseTransformedValue:booleanYES]).to.equal([NSNumber numberWithBool:YES]);
|
||||
expect([transformer reverseTransformedValue:booleanYES]).to.equal((id)kCFBooleanTrue);
|
||||
|
||||
expect([transformer transformedValue:booleanNO]).to.equal([NSNumber numberWithBool:NO]);
|
||||
expect([transformer transformedValue:booleanNO]).to.equal((id)kCFBooleanFalse);
|
||||
|
||||
expect([transformer reverseTransformedValue:booleanNO]).to.equal([NSNumber numberWithBool:NO]);
|
||||
expect([transformer reverseTransformedValue:booleanNO]).to.equal((id)kCFBooleanFalse);
|
||||
|
||||
expect([transformer transformedValue:nil]).to.beNil();
|
||||
expect([transformer reverseTransformedValue:nil]).to.beNil();
|
||||
});
|
||||
|
||||
describe(@"JSON transformers", ^{
|
||||
__block NSArray *models;
|
||||
__block NSArray *JSONDictionaries;
|
||||
describe(@"The UUID transformer", ^{
|
||||
__block NSValueTransformer *transformer;
|
||||
|
||||
beforeEach(^{
|
||||
NSMutableArray *uniqueModels = [NSMutableArray array];
|
||||
NSMutableArray *mutableDictionaries = [NSMutableArray array];
|
||||
transformer = [NSValueTransformer valueTransformerForName:MTLUUIDValueTransformerName];
|
||||
|
||||
expect(transformer).notTo(beNil());
|
||||
expect(@([transformer.class allowsReverseTransformation])).to(beTruthy());
|
||||
});
|
||||
|
||||
it(@"should convert NSStrings to NSUUIDs and back", ^{
|
||||
NSString *UUIDString = @"4A275FBD-8217-4397-964B-403F4C2B8545";
|
||||
expect([transformer transformedValue:UUIDString]).to(equal([[NSUUID alloc] initWithUUIDString:UUIDString]));
|
||||
expect([transformer reverseTransformedValue:[[NSUUID alloc] initWithUUIDString:UUIDString]]).to(equal(UUIDString));
|
||||
|
||||
expect([transformer transformedValue:nil]).to(beNil());
|
||||
expect([transformer reverseTransformedValue:nil]).to(beNil());
|
||||
});
|
||||
|
||||
itBehavesLike(MTLTransformerErrorExamples, ^{
|
||||
return @{
|
||||
MTLTransformerErrorExamplesTransformer: transformer,
|
||||
MTLTransformerErrorExamplesInvalidTransformationInput: @"not a valid UUID",
|
||||
MTLTransformerErrorExamplesInvalidReverseTransformationInput: NSNull.null
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
for (NSUInteger i = 0; i < 10; i++) {
|
||||
MTLTestModel *model = [[MTLTestModel alloc] init];
|
||||
model.count = i;
|
||||
describe(@"The number transformer", ^{
|
||||
__block NSValueTransformer *transformer;
|
||||
|
||||
[uniqueModels addObject:model];
|
||||
|
||||
NSDictionary *dict = [MTLJSONAdapter JSONDictionaryFromModel:model];
|
||||
expect(dict).notTo.beNil();
|
||||
|
||||
[mutableDictionaries addObject:dict];
|
||||
}
|
||||
|
||||
models = [uniqueModels copy];
|
||||
JSONDictionaries = [mutableDictionaries copy];
|
||||
beforeEach(^{
|
||||
transformer = [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName];
|
||||
expect(transformer).notTo(beNil());
|
||||
expect(@([transformer.class allowsReverseTransformation])).to(beTruthy());
|
||||
});
|
||||
|
||||
describe(@"dictionary transformer", ^{
|
||||
__block NSValueTransformer *transformer;
|
||||
it(@"it convert int- to boolean-backed NSNumbers and back", ^{
|
||||
// Back these NSNumbers with ints, rather than booleans,
|
||||
// to ensure that the value transformers are actually transforming.
|
||||
NSNumber *booleanYES = @(1);
|
||||
NSNumber *booleanNO = @(0);
|
||||
|
||||
before(^{
|
||||
transformer = [NSValueTransformer mtl_JSONDictionaryTransformerWithModelClass:MTLTestModel.class];
|
||||
expect(transformer).notTo.beNil();
|
||||
expect([transformer transformedValue:booleanYES]).to(equal([NSNumber numberWithBool:YES]));
|
||||
expect([transformer transformedValue:booleanYES]).to(beIdenticalTo((id)kCFBooleanTrue));
|
||||
|
||||
expect([transformer reverseTransformedValue:booleanYES]).to(equal([NSNumber numberWithBool:YES]));
|
||||
expect([transformer reverseTransformedValue:booleanYES]).to(beIdenticalTo((id)kCFBooleanTrue));
|
||||
|
||||
expect([transformer transformedValue:booleanNO]).to(equal([NSNumber numberWithBool:NO]));
|
||||
expect([transformer transformedValue:booleanNO]).to(beIdenticalTo((id)kCFBooleanFalse));
|
||||
|
||||
expect([transformer reverseTransformedValue:booleanNO]).to(equal([NSNumber numberWithBool:NO]));
|
||||
expect([transformer reverseTransformedValue:booleanNO]).to(beIdenticalTo((id)kCFBooleanFalse));
|
||||
|
||||
expect([transformer transformedValue:nil]).to(beNil());
|
||||
expect([transformer reverseTransformedValue:nil]).to(beNil());
|
||||
});
|
||||
|
||||
itBehavesLike(MTLTransformerErrorExamples, ^{
|
||||
return @{
|
||||
MTLTransformerErrorExamplesTransformer: transformer,
|
||||
MTLTransformerErrorExamplesInvalidTransformationInput: NSNull.null,
|
||||
MTLTransformerErrorExamplesInvalidReverseTransformationInput: NSNull.null
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
describe(@"+mtl_arrayMappingTransformerWithTransformer:", ^{
|
||||
__block NSValueTransformer *transformer;
|
||||
|
||||
NSArray *URLStrings = @[
|
||||
@"https://github.com/",
|
||||
@"https://github.com/MantleFramework",
|
||||
@"http://apple.com"
|
||||
];
|
||||
NSArray *URLs = @[
|
||||
[NSURL URLWithString:@"https://github.com/"],
|
||||
[NSURL URLWithString:@"https://github.com/MantleFramework"],
|
||||
[NSURL URLWithString:@"http://apple.com"]
|
||||
];
|
||||
|
||||
describe(@"when called with a reversible transformer", ^{
|
||||
beforeEach(^{
|
||||
NSValueTransformer *appliedTransformer = [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
|
||||
transformer = [NSValueTransformer mtl_arrayMappingTransformerWithTransformer:appliedTransformer];
|
||||
expect(transformer).notTo(beNil());
|
||||
});
|
||||
|
||||
it(@"should transform a JSON dictionary into a model", ^{
|
||||
expect([transformer transformedValue:JSONDictionaries.lastObject]).to.equal(models.lastObject);
|
||||
it(@"should allow reverse transformation", ^{
|
||||
expect(@([transformer.class allowsReverseTransformation])).to(beTruthy());
|
||||
});
|
||||
|
||||
it(@"should transform a model into a JSON dictionary", ^{
|
||||
expect([transformer.class allowsReverseTransformation]).to.beTruthy();
|
||||
expect([transformer reverseTransformedValue:models.lastObject]).to.equal(JSONDictionaries.lastObject);
|
||||
it(@"should apply the transformer to each element", ^{
|
||||
expect([transformer transformedValue:URLStrings]).to(equal(URLs));
|
||||
});
|
||||
|
||||
it(@"should apply the transformer to each element in reverse", ^{
|
||||
expect([transformer reverseTransformedValue:URLs]).to(equal(URLStrings));
|
||||
});
|
||||
});
|
||||
|
||||
describe(@"external representation array transformer", ^{
|
||||
__block NSValueTransformer *transformer;
|
||||
|
||||
before(^{
|
||||
transformer = [NSValueTransformer mtl_JSONArrayTransformerWithModelClass:MTLTestModel.class];
|
||||
expect(transformer).notTo.beNil();
|
||||
describe(@"when called with a non-reversible transformer", ^{
|
||||
beforeEach(^{
|
||||
NSValueTransformer *appliedTransformer = [MTLValueTransformer transformerUsingForwardBlock:^(NSString *str, BOOL *success, NSError **error) {
|
||||
return [NSURL URLWithString:str];
|
||||
}];
|
||||
transformer = [NSValueTransformer mtl_arrayMappingTransformerWithTransformer:appliedTransformer];
|
||||
expect(transformer).notTo(beNil());
|
||||
});
|
||||
|
||||
it(@"should transform JSON dictionaries into models", ^{
|
||||
expect([transformer transformedValue:JSONDictionaries]).to.equal(models);
|
||||
it(@"should not allow reverse transformation", ^{
|
||||
expect(@([transformer.class allowsReverseTransformation])).to(beFalsy());
|
||||
});
|
||||
|
||||
it(@"should transform models into JSON dictionaries", ^{
|
||||
expect([transformer.class allowsReverseTransformation]).to.beTruthy();
|
||||
expect([transformer reverseTransformedValue:models]).to.equal(JSONDictionaries);
|
||||
it(@"should apply the transformer to each element", ^{
|
||||
expect([transformer transformedValue:URLStrings]).to(equal(URLs));
|
||||
});
|
||||
});
|
||||
|
||||
itBehavesLike(MTLTransformerErrorExamples, ^{
|
||||
return @{
|
||||
MTLTransformerErrorExamplesTransformer: transformer,
|
||||
MTLTransformerErrorExamplesInvalidTransformationInput: NSNull.null,
|
||||
MTLTransformerErrorExamplesInvalidReverseTransformationInput: NSNull.null
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
describe(@"value mapping transformer", ^{
|
||||
__block NSValueTransformer *transformer;
|
||||
|
||||
NSDictionary *dictionary = @{
|
||||
@"negative": @(MTLPredefinedTransformerAdditionsSpecEnumNegative),
|
||||
@[ @"zero" ]: @(MTLPredefinedTransformerAdditionsSpecEnumZero),
|
||||
@"positive": @(MTLPredefinedTransformerAdditionsSpecEnumPositive),
|
||||
};
|
||||
|
||||
beforeEach(^{
|
||||
transformer = [NSValueTransformer mtl_valueMappingTransformerWithDictionary:dictionary];
|
||||
});
|
||||
|
||||
it(@"should transform enum values into strings", ^{
|
||||
expect([transformer transformedValue:@"negative"]).to(equal(@(MTLPredefinedTransformerAdditionsSpecEnumNegative)));
|
||||
expect([transformer transformedValue:@[ @"zero" ]]).to(equal(@(MTLPredefinedTransformerAdditionsSpecEnumZero)));
|
||||
expect([transformer transformedValue:@"positive"]).to(equal(@(MTLPredefinedTransformerAdditionsSpecEnumPositive)));
|
||||
});
|
||||
|
||||
it(@"should transform strings into enum values", ^{
|
||||
expect(@([transformer.class allowsReverseTransformation])).to(beTruthy());
|
||||
|
||||
expect([transformer reverseTransformedValue:@(MTLPredefinedTransformerAdditionsSpecEnumNegative)]).to(equal(@"negative"));
|
||||
expect([transformer reverseTransformedValue:@(MTLPredefinedTransformerAdditionsSpecEnumZero)]).to(equal(@[ @"zero" ]));
|
||||
expect([transformer reverseTransformedValue:@(MTLPredefinedTransformerAdditionsSpecEnumPositive)]).to(equal(@"positive"));
|
||||
});
|
||||
|
||||
describe(@"default values", ^{
|
||||
beforeEach(^{
|
||||
transformer = [NSValueTransformer mtl_valueMappingTransformerWithDictionary:dictionary defaultValue:@(MTLPredefinedTransformerAdditionsSpecEnumDefault) reverseDefaultValue:@"default"];
|
||||
});
|
||||
|
||||
it(@"should transform unknown strings into the default enum value", ^{
|
||||
expect([transformer transformedValue:@"unknown"]).to(equal(@(MTLPredefinedTransformerAdditionsSpecEnumDefault)));
|
||||
});
|
||||
|
||||
it(@"should transform the default enum value into the default string", ^{
|
||||
expect([transformer reverseTransformedValue:@(MTLPredefinedTransformerAdditionsSpecEnumDefault)]).to(equal(@"default"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
describe(@"date format transformer", ^{
|
||||
__block NSValueTransformer<MTLTransformerErrorHandling> *transformer;
|
||||
|
||||
beforeEach(^{
|
||||
transformer = [NSValueTransformer mtl_dateTransformerWithDateFormat:@"MMMM d, yyyy" calendar:[NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian] locale:[NSLocale localeWithLocaleIdentifier:@"en_US"] timeZone:[NSTimeZone timeZoneWithName:@"America/Los_Angeles"] defaultDate:nil];
|
||||
expect(transformer).notTo(beNil());
|
||||
expect(@([transformer.class allowsReverseTransformation])).to(beTruthy());
|
||||
expect([transformer transformedValue:nil]).to(beNil());
|
||||
expect([transformer reverseTransformedValue:nil]).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should transform strings into dates", ^{
|
||||
expect([transformer transformedValue:@"September 25, 2015"]).to(equal([NSDate dateWithTimeIntervalSince1970:1443164400]));
|
||||
});
|
||||
|
||||
it(@"should transform dates into strings", ^{
|
||||
expect([transformer reverseTransformedValue:[NSDate dateWithTimeIntervalSince1970:1183135260]]).to(equal(@"June 29, 2007"));
|
||||
});
|
||||
|
||||
it(@"should surface date formatter error descriptions", ^{
|
||||
__block NSError *error;
|
||||
__block BOOL success = NO;
|
||||
|
||||
expect([transformer transformedValue:@"September 37, 2015" success:&success error:&error]).to(beNil());
|
||||
expect(@(success)).to(beFalsy());
|
||||
expect(error).notTo(beNil());
|
||||
expect(error.domain).to(equal(MTLTransformerErrorHandlingErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(MTLTransformerErrorHandlingErrorInvalidInput)));
|
||||
expect(error.userInfo[NSLocalizedFailureReasonErrorKey]).notTo(beNil());
|
||||
});
|
||||
|
||||
itBehavesLike(MTLTransformerErrorExamples, ^{
|
||||
return @{
|
||||
MTLTransformerErrorExamplesTransformer: transformer,
|
||||
MTLTransformerErrorExamplesInvalidTransformationInput: NSNull.null,
|
||||
MTLTransformerErrorExamplesInvalidReverseTransformationInput: NSNull.null
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
describe(@"number format transformer", ^{
|
||||
__block NSValueTransformer<MTLTransformerErrorHandling> *transformer;
|
||||
|
||||
beforeEach(^{
|
||||
transformer = [NSValueTransformer mtl_numberTransformerWithNumberStyle:NSNumberFormatterDecimalStyle locale:[NSLocale localeWithLocaleIdentifier:@"en_US"]];
|
||||
expect(transformer).notTo(beNil());
|
||||
expect(@([transformer.class allowsReverseTransformation])).to(beTruthy());
|
||||
expect([transformer transformedValue:nil]).to(beNil());
|
||||
expect([transformer reverseTransformedValue:nil]).to(beNil());
|
||||
});
|
||||
|
||||
it(@"should transform strings into numbers", ^{
|
||||
expect([transformer transformedValue:@"0.12345"]).to(equal(@0.12345));
|
||||
});
|
||||
|
||||
it(@"should transform numbers into strings", ^{
|
||||
expect([transformer reverseTransformedValue:@12345.678]).to(equal(@"12,345.678"));
|
||||
});
|
||||
|
||||
it(@"should surface number formatter error descriptions", ^{
|
||||
__block NSError *error;
|
||||
__block BOOL success = NO;
|
||||
|
||||
expect([transformer transformedValue:@"Apple" success:&success error:&error]).to(beNil());
|
||||
expect(@(success)).to(beFalsy());
|
||||
expect(error).notTo(beNil());
|
||||
expect(error.domain).to(equal(MTLTransformerErrorHandlingErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(MTLTransformerErrorHandlingErrorInvalidInput)));
|
||||
expect(error.userInfo[NSLocalizedFailureReasonErrorKey]).notTo(beNil());
|
||||
});
|
||||
|
||||
itBehavesLike(MTLTransformerErrorExamples, ^{
|
||||
return @{
|
||||
MTLTransformerErrorExamplesTransformer: transformer,
|
||||
MTLTransformerErrorExamplesInvalidTransformationInput: NSNull.null,
|
||||
MTLTransformerErrorExamplesInvalidReverseTransformationInput: NSNull.null
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
QuickSpecEnd
|
||||
|
||||
17
MantleTests/MTLTestJSONAdapter.h
Normal file
17
MantleTests/MTLTestJSONAdapter.h
Normal file
@ -0,0 +1,17 @@
|
||||
//
|
||||
// MTLTestJSONAdapter.h
|
||||
// Mantle
|
||||
//
|
||||
// Created by Robert Böhnke on 03/04/14.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
|
||||
// Adds a custom key "test" to constructed JSON.
|
||||
@interface MTLTestJSONAdapter : MTLJSONAdapter
|
||||
|
||||
// These property keys are not serialized.
|
||||
@property (readwrite, nonatomic, strong) NSSet *ignoredPropertyKeys;
|
||||
|
||||
@end
|
||||
28
MantleTests/MTLTestJSONAdapter.m
Normal file
28
MantleTests/MTLTestJSONAdapter.m
Normal file
@ -0,0 +1,28 @@
|
||||
//
|
||||
// MTLTestJSONAdapter.m
|
||||
// Mantle
|
||||
//
|
||||
// Created by Robert Böhnke on 03/04/14.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MTLTestJSONAdapter.h"
|
||||
|
||||
@implementation MTLTestJSONAdapter
|
||||
|
||||
- (NSSet *)serializablePropertyKeys:(NSSet *)propertyKeys forModel:(id<MTLJSONSerializing>)model {
|
||||
NSMutableSet *copy = [propertyKeys mutableCopy];
|
||||
|
||||
[copy minusSet:self.ignoredPropertyKeys];
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
- (NSDictionary *)JSONDictionaryFromModel:(id<MTLJSONSerializing>)model error:(NSError **)error {
|
||||
NSDictionary *dictionary = [super JSONDictionaryFromModel:model error:error];
|
||||
return [dictionary mtl_dictionaryByAddingEntriesFromDictionary:@{
|
||||
@"test": @YES
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
@ -6,10 +6,14 @@
|
||||
// Copyright (c) 2012 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
|
||||
extern NSString * const MTLTestModelErrorDomain;
|
||||
extern const NSInteger MTLTestModelNameTooLong;
|
||||
extern const NSInteger MTLTestModelNameMissing;
|
||||
|
||||
|
||||
|
||||
@interface MTLEmptyTestModel : MTLModel
|
||||
@end
|
||||
|
||||
@ -36,16 +40,31 @@ extern const NSInteger MTLTestModelNameMissing;
|
||||
// Should not be stored in the dictionary value or JSON.
|
||||
@property (nonatomic, copy, readonly) NSString *dynamicName;
|
||||
|
||||
// Should not be stored in JSON.
|
||||
// Should not be stored in JSON, has MTLPropertyStorageTransitory.
|
||||
@property (nonatomic, weak) MTLEmptyTestModel *weakModel;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLSubclassTestModel : MTLTestModel
|
||||
|
||||
// Properties to test merging between subclass and superclass
|
||||
@property (nonatomic, copy) NSString *role;
|
||||
@property (nonatomic, copy) NSNumber *generation;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLArrayTestModel : MTLModel <MTLJSONSerializing>
|
||||
|
||||
// This property is associated with a "users.username" key in JSON.
|
||||
@property (nonatomic, copy) NSString *names;
|
||||
|
||||
@end
|
||||
|
||||
// Parses MTLTestModel objects from JSON instead.
|
||||
@interface MTLSubstitutingTestModel : MTLModel <MTLJSONSerializing>
|
||||
@end
|
||||
|
||||
@interface MTLValidationModel : MTLModel
|
||||
@interface MTLValidationModel : MTLModel <MTLJSONSerializing>
|
||||
|
||||
// Defaults to nil, which is not considered valid.
|
||||
@property (nonatomic, copy) NSString *name;
|
||||
@ -55,3 +74,156 @@ extern const NSInteger MTLTestModelNameMissing;
|
||||
// Returns a default name of 'foobar' when validateName:error: is invoked
|
||||
@interface MTLSelfValidatingModel : MTLValidationModel
|
||||
@end
|
||||
|
||||
@interface MTLURLModel : MTLModel <MTLJSONSerializing>
|
||||
|
||||
// Defaults to http://github.com.
|
||||
@property (nonatomic, strong) NSURL *URL;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLURLSubclassModel : MTLURLModel
|
||||
|
||||
// Defaults to http://github.com/Mantle/Mantle.
|
||||
@property (nonatomic, strong) NSURL *otherURL;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLUUIDModel : MTLModel <MTLJSONSerializing>
|
||||
|
||||
// Defaults to 4A275FBD-8217-4397-964B-403F4C2B8545
|
||||
@property (nonatomic, strong) NSUUID *UUID;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLUUIDSubclassModel : MTLUUIDModel
|
||||
|
||||
// Defaults to 593246D2-A290-43D5-9070-A299A489AE29
|
||||
@property (nonatomic, strong) NSUUID *otherUUID;
|
||||
|
||||
@end
|
||||
|
||||
// Conforms to MTLJSONSerializing but does not inherit from the MTLModel class.
|
||||
@interface MTLConformingModel : NSObject <MTLJSONSerializing>
|
||||
|
||||
@property (nonatomic, copy) NSString *name;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLStorageBehaviorModel : MTLModel
|
||||
|
||||
@property (readonly, nonatomic, assign) BOOL primitive;
|
||||
|
||||
@property (readonly, nonatomic, assign) id assignProperty;
|
||||
@property (readonly, nonatomic, weak) id weakProperty;
|
||||
@property (readonly, nonatomic, strong) id strongProperty;
|
||||
|
||||
@property (readonly, nonatomic, strong) id shadowedInSubclass;
|
||||
@property (readonly, nonatomic, strong) id declaredInProtocol;
|
||||
|
||||
@end
|
||||
|
||||
@protocol MTLDateProtocol <NSObject>
|
||||
|
||||
@property (readonly, nonatomic, strong) id declaredInProtocol;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLStorageBehaviorModelSubclass : MTLStorageBehaviorModel <MTLDateProtocol>
|
||||
|
||||
@property (readonly, nonatomic, strong) id shadowedInSubclass;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLBoolModel : MTLModel <MTLJSONSerializing>
|
||||
|
||||
@property (nonatomic, assign) BOOL flag;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLStringModel : MTLModel <MTLJSONSerializing>
|
||||
|
||||
@property (readwrite, nonatomic, copy) NSString *string;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLIDModel : MTLModel <MTLJSONSerializing>
|
||||
|
||||
@property (nonatomic, strong) id anyObject;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLNonPropertyModel : MTLModel <MTLJSONSerializing>
|
||||
|
||||
- (NSURL *)homepage;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLMultiKeypathModel : MTLModel <MTLJSONSerializing>
|
||||
|
||||
// This property is associated with the "location" and "length" keys in JSON.
|
||||
@property (readonly, nonatomic, assign) NSRange range;
|
||||
|
||||
// This property is associated with the "nested.location" and "nested.length"
|
||||
// keys in JSON.
|
||||
@property (readonly, nonatomic, assign) NSRange nestedRange;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLClassClusterModel : MTLModel <MTLJSONSerializing>
|
||||
|
||||
@property (readonly, nonatomic, copy) NSString *flavor;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLChocolateClassClusterModel : MTLClassClusterModel
|
||||
|
||||
// Associated with the "chocolate_bitterness" JSON key and transformed to a
|
||||
// string.
|
||||
@property (readwrite, nonatomic, assign) NSUInteger bitterness;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLStrawberryClassClusterModel : MTLClassClusterModel
|
||||
|
||||
// Associated with the "strawberry_freshness" JSON key.
|
||||
@property (readwrite, nonatomic, assign) NSUInteger freshness;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol MTLOptionalPropertyProtocol
|
||||
|
||||
@optional
|
||||
@property (readwrite, nonatomic, strong) id optionalUnimplementedProperty;
|
||||
@property (readwrite, nonatomic, strong) id optionalImplementedProperty;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLOptionalPropertyModel : MTLModel <MTLOptionalPropertyProtocol>
|
||||
|
||||
@property (readwrite, nonatomic, strong) id optionalImplementedProperty;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface MTLRecursiveUserModel : MTLModel <MTLJSONSerializing>
|
||||
|
||||
@property (nonatomic, copy, readonly) NSString *name;
|
||||
@property (nonatomic, copy, readonly) NSArray *groups;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLRecursiveGroupModel : MTLModel <MTLJSONSerializing>
|
||||
|
||||
@property (nonatomic, readonly) MTLRecursiveUserModel *owner;
|
||||
@property (nonatomic, readonly) NSArray *users;
|
||||
@end
|
||||
|
||||
@interface MTLPropertyDefaultAdapterModel : MTLModel<MTLJSONSerializing>
|
||||
|
||||
@property (readwrite, nonatomic, strong) MTLEmptyTestModel *nonConformingMTLJSONSerializingProperty;
|
||||
@property (readwrite, nonatomic, strong) MTLTestModel *conformingMTLJSONSerializingProperty;
|
||||
@property (readwrite, nonatomic, strong) NSString *property;
|
||||
|
||||
@end
|
||||
|
||||
@ -6,7 +6,10 @@
|
||||
// Copyright (c) 2012 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSDictionary+MTLManipulationAdditions.h"
|
||||
|
||||
#import "MTLTestModel.h"
|
||||
#import "NSDictionary+MTLMappingAdditions.h"
|
||||
|
||||
NSString * const MTLTestModelErrorDomain = @"MTLTestModelErrorDomain";
|
||||
const NSInteger MTLTestModelNameTooLong = 1;
|
||||
@ -57,19 +60,23 @@ static NSUInteger modelVersion = 1;
|
||||
#pragma mark MTLJSONSerializing
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return @{
|
||||
NSMutableDictionary *mapping = [[NSDictionary mtl_identityPropertyMapWithModel:self] mutableCopy];
|
||||
|
||||
[mapping removeObjectForKey:@"weakModel"];
|
||||
[mapping addEntriesFromDictionary:@{
|
||||
@"name": @"username",
|
||||
@"nestedName": @"nested.name",
|
||||
@"weakModel": NSNull.null,
|
||||
};
|
||||
@"nestedName": @"nested.name"
|
||||
}];
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)countJSONTransformer {
|
||||
return [MTLValueTransformer
|
||||
reversibleTransformerWithForwardBlock:^(NSString *str) {
|
||||
transformerUsingForwardBlock:^(NSString *str, BOOL *success, NSError **error) {
|
||||
return @(str.integerValue);
|
||||
}
|
||||
reverseBlock:^(NSNumber *num) {
|
||||
reverseBlock:^(NSNumber *num, BOOL *success, NSError **error) {
|
||||
return num.stringValue;
|
||||
}];
|
||||
}
|
||||
@ -112,6 +119,16 @@ static NSUInteger modelVersion = 1;
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark Property Storage Behavior
|
||||
|
||||
+ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey {
|
||||
if ([propertyKey isEqual:@"weakModel"]) {
|
||||
return MTLPropertyStorageTransitory;
|
||||
} else {
|
||||
return [super storageBehaviorForPropertyWithKey:propertyKey];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Merging
|
||||
|
||||
- (void)mergeCountFromModel:(MTLTestModel *)model {
|
||||
@ -120,10 +137,23 @@ static NSUInteger modelVersion = 1;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLSubclassTestModel
|
||||
@end
|
||||
|
||||
@implementation MTLArrayTestModel
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return @{
|
||||
@"names": @"users.name"
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLSubstitutingTestModel
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return @{};
|
||||
return [NSDictionary mtl_identityPropertyMapWithModel:self];
|
||||
}
|
||||
|
||||
+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary {
|
||||
@ -140,6 +170,12 @@ static NSUInteger modelVersion = 1;
|
||||
|
||||
@implementation MTLValidationModel
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return @{
|
||||
@"name": @"name"
|
||||
};
|
||||
}
|
||||
|
||||
- (BOOL)validateName:(NSString **)name error:(NSError **)error {
|
||||
if (*name != nil) return YES;
|
||||
if (error != NULL) {
|
||||
@ -162,3 +198,377 @@ static NSUInteger modelVersion = 1;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLURLModel
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
self.URL = [NSURL URLWithString:@"http://github.com"];
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return [NSDictionary mtl_identityPropertyMapWithModel:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLURLSubclassModel
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
self.otherURL = [NSURL URLWithString:@"http://github.com/Mantle/Mantle"];
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key {
|
||||
return @{
|
||||
// Not provided transformer for self.URL
|
||||
@"otherURL": [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName],
|
||||
}[key];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLUUIDModel
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
self.UUID = [[NSUUID alloc] initWithUUIDString:@"4A275FBD-8217-4397-964B-403F4C2B8545"];
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return [NSDictionary mtl_identityPropertyMapWithModel:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLUUIDSubclassModel
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
self.otherUUID = [[NSUUID alloc] initWithUUIDString:@"593246D2-A290-43D5-9070-A299A489AE29"];
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key {
|
||||
return @{
|
||||
// Not provided transformer for self.UUID
|
||||
@"otherUUID": [NSValueTransformer valueTransformerForName:MTLUUIDValueTransformerName],
|
||||
}[key];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLBoolModel
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return [NSDictionary mtl_identityPropertyMapWithModel:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLStringModel
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return [NSDictionary mtl_identityPropertyMapWithModel:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLIDModel
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return [NSDictionary mtl_identityPropertyMapWithModel:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLNonPropertyModel
|
||||
|
||||
+ (NSSet *)propertyKeys {
|
||||
return [NSSet setWithObject:@"homepage"];
|
||||
}
|
||||
|
||||
- (NSURL *)homepage {
|
||||
return [NSURL URLWithString:@"about:blank"];
|
||||
}
|
||||
|
||||
+ (MTLPropertyStorage)storageBehaviorForPropertyWithKey:(NSString *)propertyKey {
|
||||
if ([propertyKey isEqual:@"homepage"]) {
|
||||
return MTLPropertyStoragePermanent;
|
||||
}
|
||||
|
||||
return [super storageBehaviorForPropertyWithKey:propertyKey];
|
||||
}
|
||||
|
||||
#pragma mark - MTLJSONSerializing
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return @{
|
||||
@"homepage": @"homepage"
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLConformingModel ()
|
||||
|
||||
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLConformingModel
|
||||
|
||||
#pragma mark Lifecycle
|
||||
|
||||
+ (instancetype)modelWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
|
||||
return [[self alloc] initWithDictionary:dictionaryValue error:error];
|
||||
}
|
||||
|
||||
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
|
||||
self = [super init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
_name = dictionaryValue[@"name"];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)validate:(NSError **)error {
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark MTLModel
|
||||
|
||||
- (NSDictionary *)dictionaryValue {
|
||||
if (self.name == nil) return @{};
|
||||
|
||||
return @{
|
||||
@"name": self.name
|
||||
};
|
||||
}
|
||||
|
||||
+ (NSSet *)propertyKeys {
|
||||
return [NSSet setWithObject:@"name"];
|
||||
}
|
||||
|
||||
- (void)mergeValueForKey:(NSString *)key fromModel:(id<MTLModel>)model {
|
||||
if ([key isEqualToString:@"name"]) {
|
||||
self.name = [model dictionaryValue][@"name"];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mergeValuesForKeysFromModel:(id<MTLModel>)model {
|
||||
self.name = [model dictionaryValue][@"name"];
|
||||
}
|
||||
|
||||
#pragma mark MTLJSONSerializing
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return @{
|
||||
@"name": @"name"
|
||||
};
|
||||
}
|
||||
|
||||
#pragma mark NSObject
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return self.name.hash;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(MTLConformingModel *)model {
|
||||
if (self == model) return YES;
|
||||
if (![model isMemberOfClass:self.class]) return NO;
|
||||
|
||||
return self.name == model.name || [self.name isEqual:model.name];
|
||||
}
|
||||
|
||||
#pragma mark NSCopying
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone {
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLStorageBehaviorModel
|
||||
|
||||
- (id)notIvarBacked {
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLStorageBehaviorModelSubclass
|
||||
|
||||
@dynamic shadowedInSubclass;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLMultiKeypathModel
|
||||
|
||||
#pragma mark MTLJSONSerializing
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return @{
|
||||
@"range": @[ @"location", @"length" ],
|
||||
@"nestedRange": @[ @"nested.location", @"nested.length" ]
|
||||
};
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)rangeJSONTransformer {
|
||||
return [MTLValueTransformer
|
||||
transformerUsingForwardBlock:^(NSDictionary *value, BOOL *success, NSError **error) {
|
||||
NSUInteger location = [value[@"location"] unsignedIntegerValue];
|
||||
NSUInteger length = [value[@"length"] unsignedIntegerValue];
|
||||
|
||||
return [NSValue valueWithRange:NSMakeRange(location, length)];
|
||||
} reverseBlock:^(NSValue *value, BOOL *success, NSError **error) {
|
||||
NSRange range = value.rangeValue;
|
||||
|
||||
return @{
|
||||
@"location": @(range.location),
|
||||
@"length": @(range.length)
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)nestedRangeJSONTransformer {
|
||||
return [MTLValueTransformer
|
||||
transformerUsingForwardBlock:^(NSDictionary *value, BOOL *success, NSError **error) {
|
||||
NSUInteger location = [value[@"nested.location"] unsignedIntegerValue];
|
||||
NSUInteger length = [value[@"nested.length"] unsignedIntegerValue];
|
||||
|
||||
return [NSValue valueWithRange:NSMakeRange(location, length)];
|
||||
} reverseBlock:^(NSValue *value, BOOL *success, NSError **error) {
|
||||
NSRange range = value.rangeValue;
|
||||
|
||||
return @{
|
||||
@"nested.location": @(range.location),
|
||||
@"nested.length": @(range.length)
|
||||
};
|
||||
}];
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation MTLClassClusterModel : MTLModel
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return @{
|
||||
@"flavor": @"flavor"
|
||||
};
|
||||
}
|
||||
|
||||
+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary {
|
||||
if ([JSONDictionary[@"flavor"] isEqualToString:@"chocolate"]) {
|
||||
return MTLChocolateClassClusterModel.class;
|
||||
}
|
||||
|
||||
if ([JSONDictionary[@"flavor"] isEqualToString:@"strawberry"]) {
|
||||
return MTLStrawberryClassClusterModel.class;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLChocolateClassClusterModel : MTLClassClusterModel
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return [[super JSONKeyPathsByPropertyKey] mtl_dictionaryByAddingEntriesFromDictionary:@{
|
||||
@"bitterness": @"chocolate_bitterness"
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSString *)flavor {
|
||||
return @"chocolate";
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)bitternessJSONTransformer {
|
||||
return [MTLValueTransformer
|
||||
transformerUsingForwardBlock:^(NSString *string, BOOL *success, NSError **error) {
|
||||
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
|
||||
|
||||
return [formatter numberFromString:string];
|
||||
}
|
||||
reverseBlock:^(NSNumber *value, BOOL *success, NSError **error) {
|
||||
return [value description];
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLStrawberryClassClusterModel
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return [[super JSONKeyPathsByPropertyKey] mtl_dictionaryByAddingEntriesFromDictionary:@{
|
||||
@"freshness": @"strawberry_freshness"
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSString *)flavor {
|
||||
return @"strawberry";
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLOptionalPropertyModel
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLRecursiveUserModel
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return @{
|
||||
@"name": @"name",
|
||||
@"groups": @"groups",
|
||||
};
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)groupsJSONTransformer {
|
||||
return [MTLJSONAdapter arrayTransformerWithModelClass:MTLRecursiveGroupModel.class];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLRecursiveGroupModel
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return @{
|
||||
@"owner": @"owner",
|
||||
@"users": @"users",
|
||||
};
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)ownerJSONTransformer {
|
||||
return [MTLJSONAdapter dictionaryTransformerWithModelClass:MTLRecursiveUserModel.class];
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)usersJSONTransformer {
|
||||
return [MTLJSONAdapter arrayTransformerWithModelClass:MTLRecursiveUserModel.class];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MTLPropertyDefaultAdapterModel
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return @{
|
||||
@"conformingMTLJSONSerializingProperty": @"conformingMTLJSONSerializingProperty",
|
||||
@"nonConformingMTLJSONSerializingProperty": @"nonConformingMTLJSONSerializingProperty",
|
||||
@"property": @"property"
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
//
|
||||
// MTLTestNotificationObserver.h
|
||||
// Mantle
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2012-09-26.
|
||||
// Copyright (c) 2012 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface MTLTestNotificationObserver : NSObject
|
||||
|
||||
@property (nonatomic, assign, readonly) BOOL receivedNotification;
|
||||
|
||||
// Should only be invoked once.
|
||||
- (void)notificationPosted:(NSNotification *)notification;
|
||||
|
||||
@end
|
||||
@ -1,24 +0,0 @@
|
||||
//
|
||||
// MTLTestNotificationObserver.m
|
||||
// Mantle
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2012-09-26.
|
||||
// Copyright (c) 2012 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MTLTestNotificationObserver.h"
|
||||
|
||||
@interface MTLTestNotificationObserver ()
|
||||
@property (nonatomic, assign, readwrite) BOOL receivedNotification;
|
||||
@end
|
||||
|
||||
@implementation MTLTestNotificationObserver
|
||||
|
||||
- (void)notificationPosted:(NSNotification *)notification {
|
||||
NSParameterAssert(notification != nil);
|
||||
|
||||
NSAssert(!self.receivedNotification, @"Should receive a single notification: %@", notification);
|
||||
self.receivedNotification = YES;
|
||||
}
|
||||
|
||||
@end
|
||||
15
MantleTests/MTLTransformerErrorExamples.h
Normal file
15
MantleTests/MTLTransformerErrorExamples.h
Normal file
@ -0,0 +1,15 @@
|
||||
//
|
||||
// MTLTransformerErrorExamples.h
|
||||
// Mantle
|
||||
//
|
||||
// Created by Robert Böhnke on 10/9/13.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
extern NSString * const MTLTransformerErrorExamples;
|
||||
|
||||
extern NSString * const MTLTransformerErrorExamplesTransformer;
|
||||
extern NSString * const MTLTransformerErrorExamplesInvalidTransformationInput;
|
||||
extern NSString * const MTLTransformerErrorExamplesInvalidReverseTransformationInput;
|
||||
65
MantleTests/MTLTransformerErrorExamples.m
Normal file
65
MantleTests/MTLTransformerErrorExamples.m
Normal file
@ -0,0 +1,65 @@
|
||||
//
|
||||
// MTLTransformerErrorExamples.m
|
||||
// Mantle
|
||||
//
|
||||
// Created by Robert Böhnke on 10/9/13.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Quick/Quick.h>
|
||||
#import <Nimble/Nimble.h>
|
||||
#import "MTLTransformerErrorExamples.h"
|
||||
|
||||
#import "MTLTransformerErrorHandling.h"
|
||||
|
||||
NSString * const MTLTransformerErrorExamples = @"MTLTransformerErrorExamples";
|
||||
|
||||
NSString * const MTLTransformerErrorExamplesTransformer = @"MTLTransformerErrorExamplesTransformer";
|
||||
NSString * const MTLTransformerErrorExamplesInvalidTransformationInput = @"MTLTransformerErrorExamplesInvalidTransformationInput";
|
||||
NSString * const MTLTransformerErrorExamplesInvalidReverseTransformationInput = @"MTLTransformerErrorExamplesInvalidReverseTransformationInput";
|
||||
|
||||
QuickConfigurationBegin(MTLTransformerErrorExamplesConfiguration)
|
||||
|
||||
+ (void)configure:(Configuration *)configuration {
|
||||
sharedExamples(MTLTransformerErrorExamples, ^(QCKDSLSharedExampleContext data) {
|
||||
__block NSValueTransformer<MTLTransformerErrorHandling> *transformer;
|
||||
__block id invalidTransformationInput;
|
||||
__block id invalidReverseTransformationInput;
|
||||
|
||||
beforeEach(^{
|
||||
transformer = data()[MTLTransformerErrorExamplesTransformer];
|
||||
invalidTransformationInput = data()[MTLTransformerErrorExamplesInvalidTransformationInput];
|
||||
invalidReverseTransformationInput = data()[MTLTransformerErrorExamplesInvalidReverseTransformationInput];
|
||||
|
||||
expect(@([transformer conformsToProtocol:@protocol(MTLTransformerErrorHandling)])).to(beTruthy());
|
||||
});
|
||||
|
||||
it(@"should return errors occurring during transformation", ^{
|
||||
__block NSError *error;
|
||||
__block BOOL success = NO;
|
||||
|
||||
expect([transformer transformedValue:invalidTransformationInput success:&success error:&error]).to(beNil());
|
||||
expect(@(success)).to(beFalsy());
|
||||
expect(error).notTo(beNil());
|
||||
expect(error.domain).to(equal(MTLTransformerErrorHandlingErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(MTLTransformerErrorHandlingErrorInvalidInput)));
|
||||
expect(error.userInfo[MTLTransformerErrorHandlingInputValueErrorKey]).to(equal(invalidTransformationInput));
|
||||
});
|
||||
|
||||
it(@"should return errors occurring during reverse transformation", ^{
|
||||
if (![transformer.class allowsReverseTransformation]) return;
|
||||
|
||||
__block NSError *error;
|
||||
__block BOOL success = NO;
|
||||
|
||||
expect([transformer reverseTransformedValue:invalidReverseTransformationInput success:&success error:&error]).to(beNil());
|
||||
expect(@(success)).to(beFalsy());
|
||||
expect(error).notTo(beNil());
|
||||
expect(error.domain).to(equal(MTLTransformerErrorHandlingErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(MTLTransformerErrorHandlingErrorInvalidInput)));
|
||||
expect(error.userInfo[MTLTransformerErrorHandlingInputValueErrorKey]).to(equal(invalidReverseTransformationInput));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
QuickConfigurationEnd
|
||||
@ -6,6 +6,10 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
#import <Nimble/Nimble.h>
|
||||
#import <Quick/Quick.h>
|
||||
|
||||
@interface TestTransformer : NSValueTransformer
|
||||
@end
|
||||
|
||||
@ -29,29 +33,29 @@
|
||||
|
||||
@end
|
||||
|
||||
SpecBegin(MTLValueTransformerInversionAdditions)
|
||||
QuickSpecBegin(MTLValueTransformerInversionAdditions)
|
||||
|
||||
__block TestTransformer *transformer;
|
||||
|
||||
beforeEach(^{
|
||||
transformer = [[TestTransformer alloc] init];
|
||||
expect(transformer).notTo.beNil();
|
||||
expect(transformer).notTo(beNil());
|
||||
});
|
||||
|
||||
it(@"should invert a transformer", ^{
|
||||
NSValueTransformer *inverted = transformer.mtl_invertedTransformer;
|
||||
expect(inverted).notTo.beNil();
|
||||
expect(inverted).notTo(beNil());
|
||||
|
||||
expect([inverted transformedValue:nil]).to.equal(@"reverse");
|
||||
expect([inverted reverseTransformedValue:nil]).to.equal(@"forward");
|
||||
expect([inverted transformedValue:nil]).to(equal(@"reverse"));
|
||||
expect([inverted reverseTransformedValue:nil]).to(equal(@"forward"));
|
||||
});
|
||||
|
||||
it(@"should invert an inverted transformer", ^{
|
||||
NSValueTransformer *inverted = transformer.mtl_invertedTransformer.mtl_invertedTransformer;
|
||||
expect(inverted).notTo.beNil();
|
||||
expect(inverted).notTo(beNil());
|
||||
|
||||
expect([inverted transformedValue:nil]).to.equal(@"forward");
|
||||
expect([inverted reverseTransformedValue:nil]).to.equal(@"reverse");
|
||||
expect([inverted transformedValue:nil]).to(equal(@"forward"));
|
||||
expect([inverted reverseTransformedValue:nil]).to(equal(@"reverse"));
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
QuickSpecEnd
|
||||
|
||||
@ -6,46 +6,50 @@
|
||||
// Copyright (c) 2012 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
SpecBegin(MTLValueTransformer)
|
||||
#import <Mantle/Mantle.h>
|
||||
#import <Nimble/Nimble.h>
|
||||
#import <Quick/Quick.h>
|
||||
|
||||
QuickSpecBegin(MTLValueTransformerSpec)
|
||||
|
||||
it(@"should return a forward transformer with a block", ^{
|
||||
MTLValueTransformer *transformer = [MTLValueTransformer transformerWithBlock:^(NSString *str) {
|
||||
MTLValueTransformer *transformer = [MTLValueTransformer transformerUsingForwardBlock:^(NSString *str, BOOL *success, NSError **error) {
|
||||
return [str stringByAppendingString:@"bar"];
|
||||
}];
|
||||
|
||||
expect(transformer).notTo.beNil();
|
||||
expect([transformer.class allowsReverseTransformation]).to.beFalsy();
|
||||
expect(transformer).notTo(beNil());
|
||||
expect(@([transformer.class allowsReverseTransformation])).to(beFalsy());
|
||||
|
||||
expect([transformer transformedValue:@"foo"]).to.equal(@"foobar");
|
||||
expect([transformer transformedValue:@"bar"]).to.equal(@"barbar");
|
||||
expect([transformer transformedValue:@"foo"]).to(equal(@"foobar"));
|
||||
expect([transformer transformedValue:@"bar"]).to(equal(@"barbar"));
|
||||
});
|
||||
|
||||
it(@"should return a reversible transformer with a block", ^{
|
||||
MTLValueTransformer *transformer = [MTLValueTransformer reversibleTransformerWithBlock:^(NSString *str) {
|
||||
MTLValueTransformer *transformer = [MTLValueTransformer transformerUsingReversibleBlock:^(NSString *str, BOOL *success, NSError **error) {
|
||||
return [str stringByAppendingString:@"bar"];
|
||||
}];
|
||||
|
||||
expect(transformer).notTo.beNil();
|
||||
expect([transformer.class allowsReverseTransformation]).to.beTruthy();
|
||||
expect(transformer).notTo(beNil());
|
||||
expect(@([transformer.class allowsReverseTransformation])).to(beTruthy());
|
||||
|
||||
expect([transformer transformedValue:@"foo"]).to.equal(@"foobar");
|
||||
expect([transformer reverseTransformedValue:@"foo"]).to.equal(@"foobar");
|
||||
expect([transformer transformedValue:@"foo"]).to(equal(@"foobar"));
|
||||
expect([transformer reverseTransformedValue:@"foo"]).to(equal(@"foobar"));
|
||||
});
|
||||
|
||||
it(@"should return a reversible transformer with forward and reverse blocks", ^{
|
||||
MTLValueTransformer *transformer = [MTLValueTransformer
|
||||
reversibleTransformerWithForwardBlock:^(NSString *str) {
|
||||
transformerUsingForwardBlock:^(NSString *str, BOOL *success, NSError **error) {
|
||||
return [str stringByAppendingString:@"bar"];
|
||||
}
|
||||
reverseBlock:^(NSString *str) {
|
||||
reverseBlock:^(NSString *str, BOOL *success, NSError **error) {
|
||||
return [str substringToIndex:str.length - 3];
|
||||
}];
|
||||
|
||||
expect(transformer).notTo.beNil();
|
||||
expect([transformer.class allowsReverseTransformation]).to.beTruthy();
|
||||
expect(transformer).notTo(beNil());
|
||||
expect(@([transformer.class allowsReverseTransformation])).to(beTruthy());
|
||||
|
||||
expect([transformer transformedValue:@"foo"]).to.equal(@"foobar");
|
||||
expect([transformer reverseTransformedValue:@"foobar"]).to.equal(@"foo");
|
||||
expect([transformer transformedValue:@"foo"]).to(equal(@"foobar"));
|
||||
expect([transformer reverseTransformedValue:@"foobar"]).to(equal(@"foo"));
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
QuickSpecEnd
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
//
|
||||
// Prefix header for all source files of the 'MantleTests' target in the 'Mantle' project
|
||||
//
|
||||
|
||||
#ifdef __OBJC__
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Mantle/Mantle.h>
|
||||
|
||||
#import "Specta.h"
|
||||
|
||||
#define EXP_SHORTHAND
|
||||
#import "Expecta.h"
|
||||
#endif
|
||||
19
MantleTests/SwiftSpec.swift
Normal file
19
MantleTests/SwiftSpec.swift
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// SwiftSpec.swift
|
||||
// Archimedes
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2014-10-02.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Nimble
|
||||
import Quick
|
||||
|
||||
// Without this, the Swift stdlib won't be linked into the test target (even if
|
||||
// “Embedded Content Contains Swift Code” is enabled).
|
||||
class SwiftSpec: QuickSpec {
|
||||
override func spec() {
|
||||
expect(true).to(beTruthy())
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="2061" systemVersion="12D78" minimumToolsVersion="Xcode 4.3" macOSVersion="Automatic" iOSVersion="Automatic">
|
||||
<entity name="Child" syncable="YES">
|
||||
<attribute name="id" attributeType="Integer 16" defaultValueString="0" indexed="YES" syncable="YES"/>
|
||||
<relationship name="parent1" optional="YES" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="Parent" inverseName="unorderedChildren" inverseEntity="Parent" syncable="YES"/>
|
||||
<relationship name="parent2" optional="YES" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="Parent" inverseName="orderedChildren" inverseEntity="Parent" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Parent" syncable="YES">
|
||||
<attribute name="date" optional="YES" attributeType="Date" syncable="YES"/>
|
||||
<attribute name="number" optional="YES" attributeType="Integer 64" defaultValueString="0" syncable="YES"/>
|
||||
<attribute name="string" attributeType="String" syncable="YES"/>
|
||||
<relationship name="orderedChildren" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="Child" inverseName="parent2" inverseEntity="Child" syncable="YES"/>
|
||||
<relationship name="unorderedChildren" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Child" inverseName="parent1" inverseEntity="Child" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Child" positionX="405" positionY="324" width="128" height="88"/>
|
||||
<element name="Parent" positionX="160" positionY="192" width="128" height="118"/>
|
||||
</elements>
|
||||
</model>
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit dc270cfa343a4d1482a5b0c221106086eb8134d4
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit aebf44e527aebc411bb456bcac40bbae46d14a7a
|
||||
273
README.md
273
README.md
@ -1,13 +1,8 @@
|
||||
# Mantle
|
||||
# Mantle [](https://github.com/Carthage/Carthage)
|
||||
|
||||
Mantle makes it easy to write a simple model layer for your Cocoa or Cocoa Touch
|
||||
application.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To start building the framework, clone this repository and then run `script/bootstrap`.
|
||||
This will automatically pull down any dependencies.
|
||||
|
||||
## The Typical Model Object
|
||||
|
||||
What's wrong with the way model objects are usually written in Objective-C?
|
||||
@ -32,6 +27,7 @@ typedef enum : NSUInteger {
|
||||
@property (nonatomic, copy, readonly) NSString *reporterLogin;
|
||||
@property (nonatomic, copy, readonly) NSDate *updatedAt;
|
||||
@property (nonatomic, strong, readonly) GHUser *assignee;
|
||||
@property (nonatomic, copy, readonly) NSDate *retrievedAt;
|
||||
|
||||
@property (nonatomic, copy) NSString *title;
|
||||
@property (nonatomic, copy) NSString *body;
|
||||
@ -58,7 +54,7 @@ typedef enum : NSUInteger {
|
||||
_URL = [NSURL URLWithString:dictionary[@"url"]];
|
||||
_HTMLURL = [NSURL URLWithString:dictionary[@"html_url"]];
|
||||
_number = dictionary[@"number"];
|
||||
|
||||
|
||||
if ([dictionary[@"state"] isEqualToString:@"open"]) {
|
||||
_state = GHIssueStateOpen;
|
||||
} else if ([dictionary[@"state"] isEqualToString:@"closed"]) {
|
||||
@ -66,6 +62,7 @@ typedef enum : NSUInteger {
|
||||
}
|
||||
|
||||
_title = [dictionary[@"title"] copy];
|
||||
_retrievedAt = [NSDate date];
|
||||
_body = [dictionary[@"body"] copy];
|
||||
_reporterLogin = [dictionary[@"user"][@"login"] copy];
|
||||
_assignee = [[GHUser alloc] initWithDictionary:dictionary[@"assignee"]];
|
||||
@ -84,6 +81,7 @@ typedef enum : NSUInteger {
|
||||
_number = [coder decodeObjectForKey:@"number"];
|
||||
_state = [coder decodeUnsignedIntegerForKey:@"state"];
|
||||
_title = [coder decodeObjectForKey:@"title"];
|
||||
_retrievedAt = [NSDate date];
|
||||
_body = [coder decodeObjectForKey:@"body"];
|
||||
_reporterLogin = [coder decodeObjectForKey:@"reporterLogin"];
|
||||
_assignee = [coder decodeObjectForKey:@"assignee"];
|
||||
@ -116,7 +114,10 @@ typedef enum : NSUInteger {
|
||||
issue->_updatedAt = self.updatedAt;
|
||||
|
||||
issue.title = self.title;
|
||||
issue->_retrievedAt = [NSDate date];
|
||||
issue.body = self.body;
|
||||
|
||||
return issue;
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
@ -135,11 +136,12 @@ typedef enum : NSUInteger {
|
||||
Whew, that's a lot of boilerplate for something so simple! And, even then, there
|
||||
are some problems that this example doesn't address:
|
||||
|
||||
* If the `url` or `html_url` field is missing, `+[NSURL URLWithString:]` will throw an exception.
|
||||
* There's no way to update a `GHIssue` with new data from the server.
|
||||
* There's no way to turn a `GHIssue` _back_ into JSON.
|
||||
* `GHIssueState` shouldn't be encoded as-is. If the enum changes in the future, existing archives might break.
|
||||
* If the interface of `GHIssue` changes down the road, existing archives might break.
|
||||
* `GHIssueState` shouldn't be encoded as-is. If the enum changes in the future,
|
||||
existing archives might break.
|
||||
* If the interface of `GHIssue` changes down the road, existing archives might
|
||||
break.
|
||||
|
||||
## Why Not Use Core Data?
|
||||
|
||||
@ -160,8 +162,8 @@ If you're just trying to access some JSON objects, Core Data can be a lot of
|
||||
work for little gain.
|
||||
|
||||
Nonetheless, if you're using or want to use Core Data in your app already,
|
||||
Mantle can still be a convenient translation layer between the API and your managed
|
||||
model objects.
|
||||
Mantle can still be a convenient translation layer between the API and your
|
||||
managed model objects.
|
||||
|
||||
## MTLModel
|
||||
|
||||
@ -188,6 +190,8 @@ typedef enum : NSUInteger {
|
||||
@property (nonatomic, copy) NSString *title;
|
||||
@property (nonatomic, copy) NSString *body;
|
||||
|
||||
@property (nonatomic, copy, readonly) NSDate *retrievedAt;
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
@ -205,6 +209,8 @@ typedef enum : NSUInteger {
|
||||
return @{
|
||||
@"URL": @"url",
|
||||
@"HTMLURL": @"html_url",
|
||||
@"number": @"number",
|
||||
@"state": @"state",
|
||||
@"reporterLogin": @"user.login",
|
||||
@"assignee": @"assignee",
|
||||
@"updatedAt": @"updated_at"
|
||||
@ -220,30 +226,34 @@ typedef enum : NSUInteger {
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)stateJSONTransformer {
|
||||
NSDictionary *states = @{
|
||||
return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{
|
||||
@"open": @(GHIssueStateOpen),
|
||||
@"closed": @(GHIssueStateClosed)
|
||||
};
|
||||
|
||||
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *str) {
|
||||
return states[str];
|
||||
} reverseBlock:^(NSNumber *state) {
|
||||
return [states allKeysForObject:state].lastObject;
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)assigneeJSONTransformer {
|
||||
return [NSValueTransformer mtl_JSONDictionaryTransformerWithModelClass:GHUser.class];
|
||||
return [MTLJSONAdapter dictionaryTransformerWithModelClass:GHUser.class];
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)updatedAtJSONTransformer {
|
||||
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *str) {
|
||||
return [self.dateFormatter dateFromString:str];
|
||||
} reverseBlock:^(NSDate *date) {
|
||||
return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {
|
||||
return [self.dateFormatter dateFromString:dateString];
|
||||
} reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {
|
||||
return [self.dateFormatter stringFromDate:date];
|
||||
}];
|
||||
}
|
||||
|
||||
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
|
||||
self = [super initWithDictionary:dictionaryValue error:error];
|
||||
if (self == nil) return nil;
|
||||
|
||||
// Store a value that needs to be determined locally upon initialization.
|
||||
_retrievedAt = [NSDate date];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
@ -254,11 +264,6 @@ implementations for all these methods.
|
||||
|
||||
The problems with the original example all happen to be fixed as well:
|
||||
|
||||
> If the `url` or `html_url` field is missing, `+[NSURL URLWithString:]` will throw an exception.
|
||||
|
||||
The URL transformer we used (included in Mantle) returns `nil` if given a `nil`
|
||||
string.
|
||||
|
||||
> There's no way to update a `GHIssue` with new data from the server.
|
||||
|
||||
`MTLModel` has an extensible `-mergeValuesForKeysFromModel:` method, which makes
|
||||
@ -267,8 +272,9 @@ it easy to specify how new model data should be integrated.
|
||||
> There's no way to turn a `GHIssue` _back_ into JSON.
|
||||
|
||||
This is where reversible transformers really come in handy. `+[MTLJSONAdapter
|
||||
JSONDictionaryFromModel:]` can transform any model object conforming to
|
||||
`<MTLJSONSerializing>` back into a JSON dictionary.
|
||||
JSONDictionaryFromModel:error:]` can transform any model object conforming to
|
||||
`<MTLJSONSerializing>` back into a JSON dictionary. `+[MTLJSONAdapter
|
||||
JSONArrayFromModels:error:]` is the same but turns an array of model objects into an JSON array of dictionaries.
|
||||
|
||||
> If the interface of `GHIssue` changes down the road, existing archives might break.
|
||||
|
||||
@ -276,6 +282,185 @@ JSONDictionaryFromModel:]` can transform any model object conforming to
|
||||
archival. When unarchiving, `-decodeValueForKey:withCoder:modelVersion:` will
|
||||
be invoked if overridden, giving you a convenient hook to upgrade old data.
|
||||
|
||||
## MTLJSONSerializing
|
||||
|
||||
In order to serialize your model objects from or into JSON, you need to
|
||||
implement `<MTLJSONSerializing>` in your `MTLModel` subclass. This allows you to
|
||||
use `MTLJSONAdapter` to convert your model objects from JSON and back:
|
||||
|
||||
```objc
|
||||
NSError *error = nil;
|
||||
XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
|
||||
```
|
||||
|
||||
```objc
|
||||
NSError *error = nil;
|
||||
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:user error:&error];
|
||||
```
|
||||
|
||||
### `+JSONKeyPathsByPropertyKey`
|
||||
|
||||
The dictionary returned by this method specifies how your model object's
|
||||
properties map to the keys in the JSON representation, for example:
|
||||
|
||||
```objc
|
||||
|
||||
@interface XYUser : MTLModel
|
||||
|
||||
@property (readonly, nonatomic, copy) NSString *name;
|
||||
@property (readonly, nonatomic, strong) NSDate *createdAt;
|
||||
|
||||
@property (readonly, nonatomic, assign, getter = isMeUser) BOOL meUser;
|
||||
@property (readonly, nonatomic, strong) XYHelper *helper;
|
||||
|
||||
@end
|
||||
|
||||
@implementation XYUser
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return @{
|
||||
@"name": @"name",
|
||||
@"createdAt": @"created_at"
|
||||
};
|
||||
}
|
||||
|
||||
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
|
||||
self = [super initWithDictionary:dictionaryValue error:error];
|
||||
if (self == nil) return nil;
|
||||
|
||||
_helper = [XYHelper helperWithName:self.name createdAt:self.createdAt];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
In this example, the `XYUser` class declares four properties that Mantle
|
||||
handles in different ways:
|
||||
|
||||
- `name` is mapped to a key of the same name in the JSON representation.
|
||||
- `createdAt` is converted to its snake case equivalent.
|
||||
- `meUser` is not serialized into JSON.
|
||||
- `helper` is initialized exactly once after JSON deserialization.
|
||||
|
||||
Use `-[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:]` if your
|
||||
model's superclass also implements `MTLJSONSerializing` to merge their mappings.
|
||||
|
||||
If you'd like to map all properties of a Model class to themselves, you can use
|
||||
the `+[NSDictionary mtl_identityPropertyMapWithModel:]` helper method.
|
||||
|
||||
When deserializing JSON using
|
||||
`+[MTLJSONAdapter modelOfClass:fromJSONDictionary:error:]`, JSON keys that don't
|
||||
correspond to a property name or have an explicit mapping are ignored:
|
||||
|
||||
```objc
|
||||
NSDictionary *JSONDictionary = @{
|
||||
@"name": @"john",
|
||||
@"created_at": @"2013/07/02 16:40:00 +0000",
|
||||
@"plan": @"lite"
|
||||
};
|
||||
|
||||
XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
|
||||
```
|
||||
|
||||
Here, the `plan` would be ignored since it neither matches a property name of
|
||||
`XYUser` nor is it otherwise mapped in `+JSONKeyPathsByPropertyKey`.
|
||||
|
||||
### `+JSONTransformerForKey:`
|
||||
|
||||
Implement this optional method to convert a property from a different type when
|
||||
deserializing from JSON.
|
||||
|
||||
```
|
||||
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key {
|
||||
if ([key isEqualToString:@"createdAt"]) {
|
||||
return [NSValueTransformer valueTransformerForName:XYDateValueTransformerName];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
```
|
||||
|
||||
`key` is the key that applies to your model object; not the original JSON key. Keep this in mind if you transform the key names using `+JSONKeyPathsByPropertyKey`.
|
||||
|
||||
For added convenience, if you implement `+<key>JSONTransformer`,
|
||||
`MTLJSONAdapter` will use the result of that method instead. For example, dates
|
||||
that are commonly represented as strings in JSON can be transformed to `NSDate`s
|
||||
like so:
|
||||
|
||||
```objc
|
||||
return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {
|
||||
return [self.dateFormatter dateFromString:dateString];
|
||||
} reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {
|
||||
return [self.dateFormatter stringFromDate:date];
|
||||
}];
|
||||
}
|
||||
```
|
||||
|
||||
If the transformer is reversible, it will also be used when serializing the
|
||||
object into JSON.
|
||||
|
||||
### `+classForParsingJSONDictionary:`
|
||||
|
||||
If you are implementing a class cluster, implement this optional method to
|
||||
determine which subclass of your base class should be used when deserializing an
|
||||
object from JSON.
|
||||
|
||||
```objc
|
||||
@interface XYMessage : MTLModel
|
||||
|
||||
@end
|
||||
|
||||
@interface XYTextMessage: XYMessage
|
||||
|
||||
@property (readonly, nonatomic, copy) NSString *body;
|
||||
|
||||
@end
|
||||
|
||||
@interface XYPictureMessage : XYMessage
|
||||
|
||||
@property (readonly, nonatomic, strong) NSURL *imageURL;
|
||||
|
||||
@end
|
||||
|
||||
@implementation XYMessage
|
||||
|
||||
+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary {
|
||||
if (JSONDictionary[@"image_url"] != nil) {
|
||||
return XYPictureMessage.class;
|
||||
}
|
||||
|
||||
if (JSONDictionary[@"body"] != nil) {
|
||||
return XYTextMessage.class;
|
||||
}
|
||||
|
||||
NSAssert(NO, @"No matching class for the JSON dictionary '%@'.", JSONDictionary);
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
```
|
||||
|
||||
`MTLJSONAdapter` will then pick the class based on the JSON dictionary you pass
|
||||
in:
|
||||
|
||||
```objc
|
||||
NSDictionary *textMessage = @{
|
||||
@"id": @1,
|
||||
@"body": @"Hello World!"
|
||||
};
|
||||
|
||||
NSDictionary *pictureMessage = @{
|
||||
@"id": @2,
|
||||
@"image_url": @"http://example.com/lolcat.gif"
|
||||
};
|
||||
|
||||
XYTextMessage *messageA = [MTLJSONAdapter modelOfClass:XYMessage.class fromJSONDictionary:textMessage error:NULL];
|
||||
|
||||
XYPictureMessage *messageB = [MTLJSONAdapter modelOfClass:XYMessage.class fromJSONDictionary:pictureMessage error:NULL];
|
||||
```
|
||||
|
||||
## Persistence
|
||||
|
||||
Mantle doesn't automatically persist your objects for you. However, `MTLModel`
|
||||
@ -285,7 +470,35 @@ does conform to `<NSCoding>`, so model objects can be archived to disk using
|
||||
If you need something more powerful, or want to avoid keeping your whole model
|
||||
in memory at once, Core Data may be a better choice.
|
||||
|
||||
## System Requirements
|
||||
|
||||
Mantle supports OS X 10.9+ and iOS 8.0+.
|
||||
|
||||
## Importing Mantle
|
||||
|
||||
To add Mantle to your application:
|
||||
|
||||
1. Add the Mantle repository as a submodule of your application's repository.
|
||||
1. Run `git submodule update --init --recursive` from within the Mantle folder.
|
||||
1. Drag and drop `Mantle.xcodeproj` into your application's Xcode project.
|
||||
1. On the "General" tab of your application target, add `Mantle.framework` to the "Embedded Binaries".
|
||||
|
||||
[Carthage](https://github.com/Carthage/Carthage) users can simply add Mantle to their `Cartfile`:
|
||||
```
|
||||
github "Mantle/Mantle"
|
||||
```
|
||||
|
||||
If you would prefer to use [CocoaPods](http://cocoapods.org), there are some
|
||||
[Mantle podspecs](https://github.com/CocoaPods/Specs/tree/master/Specs/5/d/c/Mantle) that
|
||||
have been generously contributed by third parties.
|
||||
|
||||
If you’re instead developing Mantle on its own, use the `Mantle.xcworkspace` file.
|
||||
|
||||
## License
|
||||
|
||||
Mantle is released under the MIT license. See
|
||||
[LICENSE.md](https://github.com/github/Mantle/blob/master/LICENSE.md).
|
||||
|
||||
## More Info
|
||||
|
||||
Have a question? Please [open an issue](https://github.com/Mantle/Mantle/issues/new)!
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_DIR=$(dirname "$0")
|
||||
cd "$SCRIPT_DIR/.."
|
||||
|
||||
echo "*** Updating submodules..."
|
||||
git submodule sync --quiet || exit $?
|
||||
git submodule update --init || exit $?
|
||||
git submodule foreach --recursive --quiet "git submodule sync --quiet && git submodule update --init" || exit $?
|
||||
114
script/cibuild
114
script/cibuild
@ -1,114 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_DIR=$(dirname "$0")
|
||||
cd "$SCRIPT_DIR/.."
|
||||
|
||||
##
|
||||
## Configuration Variables
|
||||
##
|
||||
|
||||
# The build configuration to use.
|
||||
if [ -z "$XCCONFIGURATION" ]
|
||||
then
|
||||
XCCONFIGURATION="Release"
|
||||
fi
|
||||
|
||||
# The workspace to build.
|
||||
#
|
||||
# If not set and no workspace is found, the -workspace flag will not be passed
|
||||
# to xcodebuild.
|
||||
if [ -z "$XCWORKSPACE" ]
|
||||
then
|
||||
XCWORKSPACE=$(ls -d *.xcworkspace 2>/dev/null | head -n 1)
|
||||
fi
|
||||
|
||||
# A bootstrap script to run before building.
|
||||
#
|
||||
# If this file does not exist, it is not considered an error.
|
||||
BOOTSTRAP="$SCRIPT_DIR/bootstrap"
|
||||
|
||||
# A whitespace-separated list of default targets or schemes to build, if none
|
||||
# are specified on the command line.
|
||||
#
|
||||
# Individual names can be quoted to avoid word splitting.
|
||||
DEFAULT_TARGETS=
|
||||
|
||||
# Extra build settings to pass to xcodebuild.
|
||||
XCODEBUILD_SETTINGS="TEST_AFTER_BUILD=YES"
|
||||
|
||||
##
|
||||
## Build Process
|
||||
##
|
||||
|
||||
if [ -z "$*" ]
|
||||
then
|
||||
# lol recursive shell script
|
||||
if [ -n "$DEFAULT_TARGETS" ]
|
||||
then
|
||||
echo "$DEFAULT_TARGETS" | xargs "$SCRIPT_DIR/cibuild"
|
||||
else
|
||||
xcodebuild -list | awk -f "$SCRIPT_DIR/targets.awk" | xargs "$SCRIPT_DIR/cibuild"
|
||||
fi
|
||||
|
||||
exit $?
|
||||
fi
|
||||
|
||||
if [ -f "$BOOTSTRAP" ]
|
||||
then
|
||||
echo "*** Bootstrapping..."
|
||||
bash "$BOOTSTRAP"
|
||||
fi
|
||||
|
||||
echo "*** The following targets will be built:"
|
||||
|
||||
for target in "$@"
|
||||
do
|
||||
echo "$target"
|
||||
done
|
||||
|
||||
echo "*** Cleaning all targets..."
|
||||
xcodebuild -alltargets clean OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
|
||||
|
||||
run_xcodebuild ()
|
||||
{
|
||||
local scheme=$1
|
||||
|
||||
if [ -n "$XCWORKSPACE" ]
|
||||
then
|
||||
xcodebuild -workspace "$XCWORKSPACE" -scheme "$scheme" -configuration "$XCCONFIGURATION" build OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
|
||||
else
|
||||
xcodebuild -scheme "$scheme" -configuration "$XCCONFIGURATION" build OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
|
||||
fi
|
||||
|
||||
local status=$?
|
||||
|
||||
return $status
|
||||
}
|
||||
|
||||
build_scheme ()
|
||||
{
|
||||
local scheme=$1
|
||||
|
||||
run_xcodebuild "$scheme" 2>&1 | awk -f "$SCRIPT_DIR/xcodebuild.awk"
|
||||
|
||||
local awkstatus=$?
|
||||
local xcstatus=${PIPESTATUS[0]}
|
||||
|
||||
if [ "$xcstatus" -eq "65" ]
|
||||
then
|
||||
# This probably means that there's no scheme by that name. Give up.
|
||||
echo "*** Error building scheme $scheme -- perhaps it doesn't exist"
|
||||
elif [ "$awkstatus" -eq "1" ]
|
||||
then
|
||||
return $awkstatus
|
||||
fi
|
||||
|
||||
return $xcstatus
|
||||
}
|
||||
|
||||
echo "*** Building..."
|
||||
|
||||
for scheme in "$@"
|
||||
do
|
||||
build_scheme "$scheme" || exit $?
|
||||
done
|
||||
@ -1,12 +0,0 @@
|
||||
BEGIN {
|
||||
FS = "\n";
|
||||
}
|
||||
|
||||
/Targets:/ {
|
||||
while (getline && $0 != "") {
|
||||
if ($0 ~ /Tests/) continue;
|
||||
|
||||
sub(/^ +/, "");
|
||||
print "'" $0 "'";
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
# Exit statuses:
|
||||
#
|
||||
# 0 - No errors found.
|
||||
# 1 - Build or test failure. Errors will be logged automatically.
|
||||
# 2 - Untestable target. Retry with the "build" action.
|
||||
|
||||
BEGIN {
|
||||
status = 0;
|
||||
}
|
||||
|
||||
{
|
||||
print;
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
/is not valid for Testing/ {
|
||||
exit 2;
|
||||
}
|
||||
|
||||
/[0-9]+: (error|warning):/ {
|
||||
errors = errors $0 "\n";
|
||||
}
|
||||
|
||||
/(TEST|BUILD) FAILED/ {
|
||||
status = 1;
|
||||
}
|
||||
|
||||
END {
|
||||
if (length(errors) > 0) {
|
||||
print "\n*** All errors:\n" errors;
|
||||
}
|
||||
|
||||
fflush(stdout);
|
||||
exit status;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user