Compare commits

..

441 Commits

Author SHA1 Message Date
Robert Böhnke
098746782c
Merge pull request #785 from Konshin/documentation_fix
suppress documentation warnings
2018-08-07 04:33:41 +09:00
David Snabel-Caunt
db043e35df
Merge pull request #822 from mogelbuster/master
Block Definition Fix for mtl_cleanupBlock_t
2018-04-29 17:17:22 +01:00
KennethConnors
8d88f85a6b Block definition fix
This is to fix the xcode warning: "This block declaration is not a prototype".
2018-04-29 10:01:51 -04:00
David Snabel-Caunt
6dae4d4617 Merge pull request #809 from alexpezzi/master
Carthage + Xcode 9 compatibility
2017-09-27 11:02:00 +01:00
alexpezzi
751c6bb089 Carthage + Xcode 9 compatibility 2017-09-26 12:22:15 -04:00
David Caunt
813bb27a07 Merge pull request #804 from Mantle/xcode9
Add Xcode9 CI jobs
2017-07-18 13:17:33 +01:00
David Snabel-Caunt
bb2025276a Use Xcode 8.3 CI image 2017-07-16 14:56:06 +01:00
David Snabel-Caunt
99b6217a42 Add Xcode 9 CI jobs 2017-07-16 14:51:28 +01:00
Алексей Коньшин
50d8d250f4 added invisible symbol between @ and encode to suppress documentation warnings 2017-03-09 17:34:14 +03:00
David Caunt
fd46656f86 Merge pull request #767 from Mantle/update-readme
Update readme
2016-12-04 19:20:06 +00:00
Dave Caunt
ebe9ef9986 Remove outdated bug warning
Works in Xcode 8
2016-12-02 11:03:24 +00:00
Dave Caunt
87a7bd851c Fix README link to podspecs 2016-12-02 10:58:57 +00:00
David Caunt
d6b0128cab Merge pull request #758 from Mantle/xcode8
Update for Xcode 8 Support
2016-10-12 14:56:41 +01:00
Dave Caunt
408bde2425 Update for Xcode 8 Support 2016-10-11 17:54:17 +01:00
David Caunt
3098f8e7bb Merge pull request #749 from bruun/fix-changelog-spelling-error
Fix incorrect method name in CHANGELOG.md
2016-08-24 11:05:54 +01:00
Thomas Bruun
674de0c342 Fix incorrect method name in CHANGELOG.md
There is no method called JSONKeyPathsForPropertyKey, it is called
JSONKeyPathsByPropertyKey.
2016-08-24 09:13:36 +02:00
Robert Böhnke
b1606ead5f Merge pull request #723 from Mantle/ck/uuid-transformer
Added implicit NSUUID JSON value transformer
2016-07-12 12:02:07 +02:00
David Caunt
50d10a6853 Merge pull request #731 from ikesyo/tvos-target
Add tvOS target
2016-07-11 21:57:21 +01:00
Syo Ikeda
c713a23bf5 Add tvOS target 2016-07-12 01:29:43 +09:00
David Caunt
a08ba893d6 Merge pull request #730 from ikesyo/watchos-target
Add watchOS target
2016-07-11 15:03:17 +01:00
Syo Ikeda
3930492012 Add watchOS target 2016-07-11 19:43:14 +09:00
Syo Ikeda
9d34a114e0 Update xcconfigs 2016-07-11 19:32:09 +09:00
David Caunt
eec57547b6 Merge pull request #729 from ikesyo/xcode7.3
Xcode 7.3
2016-07-11 10:17:57 +01:00
Syo Ikeda
465826427e Make the project testable with Xcode 7.3 2016-07-11 10:31:38 +09:00
David Caunt
9f27293364 Merge pull request #728 from ikesyo/travis-plain-xcodebuild
[Travis] Replace xctool with plain xcodebuild
2016-07-10 18:18:34 +01:00
Syo Ikeda
8dbb8b547c Explicit dependencies 2016-07-10 18:29:27 +09:00
Syo Ikeda
399713dbfd [Travis] Replace xctool with plain xcodebuild 2016-07-10 18:12:17 +09:00
Conrad Kramer
8ae87acabc Added implicit NSUUID JSON value transformer 2016-07-01 14:18:30 -07:00
Robert Böhnke
a16d18f635 Merge pull request #694 from getaaron/patch-1
Add extra context to JSONTransformerForKey method
2016-04-21 17:26:19 +02:00
Aaron Brager
7e640910ef Add extra context to JSONTransformerForKey method 2016-04-21 09:19:01 -05:00
Robert Böhnke
cdb82df5dd Merge pull request #685 from k06a/fix/free-nsurl-transform-to-work-always
Made automatic NSURL transfom to work even if JSONTransformerForKey method defined
2016-03-17 13:02:37 +01:00
Anton Bukov
9f860bafa8 Added test case to demonstrate fix 2016-03-11 15:20:19 +03:00
Anton Bukov
691277c14f Changed identation from spaces to tabs 2016-03-11 11:27:05 +03:00
Anton Bukov
09a1f31c32 Made automatic NRURL transfom to work even if JSONTransformerForKey method defined 2016-03-10 13:43:53 +03:00
Robert Böhnke
51e4f70224 Merge pull request #680 from Mantle/fix-swift-warnings
Fix warnings when using MTLJSONAdapter from Swift
2016-02-15 20:14:16 +01:00
Michael Sanders
0f18db37d0 Fix warnings when using MTLJSONAdapter from Swift 2016-02-15 20:06:18 +01:00
Robert Böhnke
c32280d4dc Merge pull request #667 from Mantle/update-xcode-on-travis
Update Xcode on travis
2016-02-15 19:56:00 +01:00
Robert Böhnke
eaa7cc3536 Trial and error 2016-02-15 19:49:33 +01:00
Robert Böhnke
6a4141363f Merge pull request #658 from magnusolafur/master
Add NS_SWIFT_UNAVAILABLE
2016-01-15 17:58:51 -08:00
Robert Böhnke
e0eb39df7b Update dependencies 2016-01-12 10:03:31 -08:00
Robert Böhnke
c624c6ec73 Merge pull request #641 from conradev/formatter-transformers
Add NSFormatter value transformer additions
2016-01-11 17:43:40 -08:00
Conrad Kramer
8a1d9f8c48 Add NSFormatter value transformer additions 2016-01-11 12:31:32 -08:00
Robert Böhnke
7ac1d0e571 Bump Xcode on travis 2016-01-11 08:25:15 -08:00
Conrad Kramer
e781ead0d4 Update testing dependencies 2016-01-09 14:19:53 +01:00
Conrad Kramer
54d82b7417 Update Xcode project for Xcode 7.1 2016-01-09 14:19:53 +01:00
Robert Böhnke
20ac7b96d3 Update Xcode on travis 2016-01-09 14:16:06 +01:00
Robert Böhnke
afa50afc30 Merge pull request #661 from kypselia/improved_error
Improved error descriptions
2015-12-29 21:03:42 +01:00
Karim-Pierre Maalej
ea61b4ee8c Code review fix 2015-12-28 16:14:54 +01:00
Karim-Pierre Maalej
dd655697a7 Another fix after rebase 2015-12-26 16:54:36 +01:00
Karim-Pierre Maalej
99dba73b0d Fix typo after rebase 2015-12-26 16:46:09 +01:00
Karim-Pierre Maalej
05a2c99849 Added recovery suggestion to exception error 2015-12-26 16:32:36 +01:00
Karim-Pierre Maalej
2707b0bc01 Exported two missing error constants 2015-12-26 16:32:36 +01:00
Magnus Olafur Magnusson
0f811cbd47 Add NS_SWIFT_UNAVAILABLE macro to prevent Swift from using deprecated methods 2015-12-18 09:46:39 +00:00
David Caunt
234b69846b Merge pull request #631 from priteshshah1983/only-protocol-conformance
Only check for MTLModel protocol conformance
2015-10-28 09:48:11 +00:00
Pritesh Shah
857c82691d Only check for MTLModel protocol conformance 2015-10-27 19:07:27 -07:00
Robert Böhnke
66af2bf345 Merge pull request #617 from startupthekid/master
fixed model assertion from MTLModel subclass to protocol
2015-09-29 10:43:41 +02:00
startupthekid
956bfaf190 fixed model assertion from MTLModel subclass to protocol 2015-09-27 19:24:56 -04:00
Robert Böhnke
7690ba7944 Merge pull request #611 from ksuther/xcode7-upgrade-check
Xcode 7 really wants to run these upgrade checks.
2015-09-13 13:26:10 +02:00
Robert Böhnke
c456fd5e8c Merge pull request #610 from neonichu/scope-private-libextobjc-headers
Scope usage of private libextobjc headers
2015-09-13 13:26:00 +02:00
Kent Sutherland
5a9cc70ee0 Xcode 7 really wants to run these upgrade checks. 2015-09-11 23:03:23 -04:00
Boris Bügling
958dbd8c9c Scope usage of private libextobjc headers. 2015-09-11 19:11:03 +02:00
Robert Böhnke
0cb962c52b Merge pull request #605 from Mantle/implicit-parsing
Implicit parsing
2015-08-31 10:39:30 +02:00
Robert Böhnke
c48ae3572e Minor cleanup 2015-08-31 10:11:00 +02:00
Andrew(Zhiyong) Yang
a2b4a96776 Parse MTLJSONSerializing conforming property with JSONAdaptor by default. 2015-08-24 11:29:26 +08:00
Robert Böhnke
b11cd11dd1 Merge pull request #593 from Mantle/initialize-variables
Initialize variables
2015-08-20 13:50:59 +02:00
Robert Böhnke
b83f2c60a1 Initialize variables 2015-08-20 13:38:25 +02:00
Robert Böhnke
e5d59be0dc Merge pull request #590 from Coeur/patch-1
spelling
2015-08-16 12:08:40 +02:00
Antoine Cœur
633b653b80 spelling 2015-08-16 16:20:47 +08:00
Robert Böhnke
6a9bd2687b Merge pull request #582 from jmah/jmah/581-recursive-models
Lazily create MTLJSONAdapter instance to allow recursive definitions
2015-08-08 19:16:06 +02:00
Jonathon Mah
dcca4baf8f Source code style consistency 2015-08-08 10:10:21 -07:00
Jonathon Mah
09001acec7 Lazily create MTLJSONAdapter instance to allow recursive definitions 2015-08-05 14:13:29 -07:00
Jonathon Mah
4f00e0f97a Add test case for recursive models from @cspickert 2015-08-05 14:09:03 -07:00
Robert Böhnke
4ca6073945 Merge pull request #580 from dcaunt/test-class-fix
Correct variable type in unit test
2015-08-05 15:58:25 +02:00
Dave Caunt
af8dbe89dc Correct variable type in unit test 2015-08-05 12:57:00 +01:00
Robert Böhnke
1b37d354b8 Merge pull request #568 from jmah/jmah/protocol-storage
Check superclass for property persistence
2015-07-31 11:44:21 +02:00
Jonathon Mah
0747420c3e Clarify test property name to avoid comment 2015-07-30 12:36:53 -07:00
Jonathon Mah
7a9c012fdd Add test for optional-and-implemented property 2015-07-30 12:32:32 -07:00
Jonathon Mah
daab4ea561 Use NSObject method to check method existence 2015-07-30 12:30:38 -07:00
Jonathon Mah
52c30e08da Wrap comment to 80 chars 2015-07-30 12:28:23 -07:00
Jonathon Mah
973bc03066 Clarify subclass property storage test 2015-07-30 12:27:40 -07:00
Jonathon Mah
459bfb863d Replace arbitrary type with id to avoid being misleading 2015-07-30 12:25:59 -07:00
Jonathon Mah
26f0e7e384 Merge remote-tracking branch 'mantle/master' into jmah/protocol-storage 2015-07-30 12:23:26 -07:00
Robert Böhnke
d41e8d3c8c Merge pull request #535 from kevin-traansmission/master
Unimplemented, unmapped, optional protocol properties return MTLStoragePropertyNone from MTLModel +storageBehaviorForPropertyWithKeys:
2015-07-30 09:45:27 +02:00
Jonathon Mah
93c0a5c3e4 Check superclass for property persistence
A subclass can shadow a parent's property declaration, either explicitly
with a `@property` directive or implicitly by conforming to a protocol
that requires a particular property. The result is that the subclass
property record has no associated ivar (or methods), and calls to get /
set the property will fall up to the superclass's methods (backed by the
superclass's ivar).
2015-07-29 16:44:59 -07:00
Robert Böhnke
ec98991888 Merge pull request #567 from jmah/jmah/one-adapter-per-transformer
Capture a model-specific adapter when creating transformer
2015-07-28 08:37:28 +02:00
Jonathon Mah
f03df68add Capture a model-specific adapter when creating transformer
This can significantly reduce the amount of (re-)parsing of a model's
JSON mapping.
2015-07-27 15:28:06 -07:00
Robert Böhnke
704673bbe0 Merge pull request #556 from hengw/master
Fix MTLJSONAdapter subclass not being used in method JSONAdapterForMo…
2015-07-26 15:55:50 +02:00
Robert Böhnke
c9ce8e70f1 Merge pull request #562 from alikaragoz/master
Updated README to match current version of the API
2015-07-25 15:42:41 +02:00
Ali Karagoz
4a909589bc Updated README to match current version of the API 2015-07-24 22:42:39 +02:00
Robert Böhnke
8f937de2fc Merge pull request #548 from zadr/skip-validation-on-copy
skip re-validation in -[MTLModel copyWithZone:]
2015-07-21 13:28:33 +02:00
Zachary Drayer
6cc31af211 skip re-validation in -copy 2015-07-17 12:51:20 -07:00
weijian
bcabb0e024 Change [self class] to self.class 2015-07-15 14:15:08 +08:00
weijian
9338df7a65 Fix missing bracket 2015-07-14 15:09:53 +08:00
weijian
e508420c9d Fix MTLJSONAdapter subclass not being used in method JSONAdapterForModelClass:error: 2015-07-14 14:35:05 +08:00
Robert Böhnke
67c0a49c43 Merge pull request #552 from kmikael/kmikael-patch-changelog
Fix incorrect information in the changelog
2015-07-02 16:20:05 +02:00
Mikael Konutgan
b7cf3e9cdd Fix incorrect information in the changelog
About mapping multiple JSON fields to a single property,
the changelog states that a dictionary of key paths must be set,
but actually an array of key paths should be set.
2015-07-02 15:55:10 +02:00
Robert Böhnke
88f4d8e399 Merge pull request #537 from danielgalasko/master
Remove deprecated transformers from README
2015-06-17 22:01:04 +02:00
Daniel Galasko
dca0524825 Merge pull request #1 from danielgalasko/danielgalasko-hotfix-readmedeprecationfixes
Remove deprecated MTLValueTransformer methods in README
2015-06-17 16:32:48 +02:00
Daniel Galasko
c247fb8bd1 Remove deprecated MTLValueTransformer methods in README
The current README uses  `+[MTLValueTransformer reversibleTransformerWithForwardBlock:reverseBlock:]` which has been deprecated. This commit updates the README to use the newer constructor `+[MTLValueTransformer transformerUsingForwardBlock:reverseBlock:]`
2015-06-17 16:31:27 +02:00
Kevin Y. Kim
853e569b7a Issue #523 (Unimplemented, Unmapped, Optional Protocol Properties crash MTLModel -validate:). Unimplemented, unmapped, optional protocol properties return MTLStoragePropertyNone from MTLModel +storageBehaviorForPropertyWithKeys: 2015-06-10 11:05:56 -04:00
Justin Spahr-Summers
d236caa864 Merge pull request #532 from Mantle/property-validation
Actually validate the property class
2015-06-05 23:44:51 -05:00
Robert Böhnke
1c8bc90ae6 Actually validate the property class 2015-06-04 13:12:17 +02:00
Robert Böhnke
c55800e6a2 Merge pull request #528 from ikesyo/restore-travis
Bring back Travis with Xcode 6.3 support
2015-06-04 13:02:19 +02:00
Syo Ikeda
b7974f64b3 Bring back Travis with Xcode 6.3 support 2015-06-02 09:16:37 +09:00
Justin Spahr-Summers
dc859b6906 Merge pull request #525 from Mantle/fix-memory-crash
Fix __unsafe_unretained crashes in IMP returns
2015-05-22 21:49:44 -05:00
Justin Spahr-Summers
76a83fa781 Avoid __unsafe_unretained in IMP calls 2015-05-22 19:47:36 -07:00
Justin Spahr-Summers
e21bafdbb2 Add a test that NSValueTransformers don't leak 2015-05-22 19:47:26 -07:00
Justin Spahr-Summers
5665290afc Run tests in Release to catch memory crasher 2015-05-22 19:35:15 -07:00
Justin Spahr-Summers
02b40b1ec8 Merge pull request #520 from adamkaplan/faster-method-calls
Replaced use of NSInvocation with direct IMP calls for performance.
2015-05-21 09:53:06 -07:00
adamkaplan
526b6a24db Cleaned up IMP signatures per code review feedback. 2015-05-20 11:18:48 -04:00
adamkaplan
e176a9d44e Merge remote-tracking branch 'main/master' 2015-05-20 10:38:46 -04:00
Justin Spahr-Summers
61023465ae Merge pull request #516 from dcaunt/xcode6.3-tests
Xcode6.3 tests
2015-05-19 15:57:50 -07:00
adamkaplan
097177c8cd Replaced use of NSInvocation with direct IMP calls for performance. 2015-05-18 16:36:43 -04:00
Robert Böhnke
79ae468c2c Merge pull request #519 from Coeur/patch-1
Spelling fix
2015-05-18 18:00:14 +02:00
Antoine Cœur
9077997b3c spelling fix 2015-05-18 10:54:02 +08:00
David Caunt
6765b64cee Remove Travis config pending Xcode 6.3 support 2015-05-15 14:02:07 +01:00
David Caunt
1da815e969 Ignore Carthage Build directory 2015-05-11 13:55:10 +01:00
David Caunt
7f1a69b194 Bump test dependencies for Xcode 6.3 2015-05-11 13:54:09 +01:00
Justin Spahr-Summers
8848226647 Merge pull request #498 from dcaunt/ios-install-instructions
Updated README for iOS installation instructions
2015-05-10 15:11:37 -07:00
David Caunt
676f5f556f Update README integration for workspace bug 2015-05-08 11:55:04 +01:00
Justin Spahr-Summers
66b2f61cc2 Merge pull request #505 from chipp/master
Update README.md System Requirements section
2015-05-02 20:31:36 -07:00
David Caunt
989479c9b5 Removed unnecessary install steps 2015-04-21 17:35:44 +01:00
Vladimir Burdukov
56226e27c5 Update README.md System Requirements section
Mantle supports OS X 10.9+ and iOS 8.0+.
2015-04-21 09:44:41 +03:00
Justin Spahr-Summers
5d864afc4c Merge pull request #503 from Mantle/improve-error-message
Improve error message for +mtl_validatingTransformerForClass:
2015-04-17 13:12:19 -07:00
Robert Böhnke
a24ed455f9 Merge branch 'master' into improve-error-message 2015-04-17 15:32:51 +02:00
Robert Böhnke
34eae472dc Improve error message for +mtl_validatingTransformerForClass: 2015-04-17 15:31:34 +02:00
David Caunt
4fc19e04bb Added Carthage install instructions to README 2015-04-16 20:23:16 +01:00
David Caunt
bcad84d700 Amend instructions for Mac framework embedding 2015-04-16 20:17:53 +01:00
Robert Böhnke
86e5cb30fd Merge pull request #499 from johanrydenstam/renamed-class-to-classModel
Changed name of class parameter to modelClass
2015-04-16 16:41:25 +02:00
Johan Rydenstam
39ee0f7f0e Changed name of Class parameter to modelClass 2015-04-16 15:40:48 +02:00
David Caunt
f9f91eaef7 Updated README for iOS installation instructions 2015-04-14 15:39:29 +01:00
Robert Böhnke
92bafa11f8 Merge pull request #495 from dcaunt/changelog-mapping
Added note about multiple JSON field mapping
2015-04-13 19:17:51 +02:00
David Caunt
8f233cfa9b Added note about multiple JSON field mapping 2015-04-13 17:03:38 +01:00
Robert Böhnke
91e49881e6 Merge pull request #489 from zsk425/master
Fix illegal parameter's name
2015-04-10 12:15:18 +02:00
Shaokang Zhao
ddf43709de Fix illegal parameter's name 2015-04-10 16:08:46 +08:00
Justin Spahr-Summers
050121a64b Merge pull request #465 from Mantle/2.0-development
2.0
2015-04-01 09:01:27 -07:00
Justin Spahr-Summers
c50019b93e Merge pull request #481 from shepting/patch-1
Updated year for copyright
2015-03-20 17:28:36 -07:00
Steven Hepting
ed1a76b493 Remove the year to be consistent regardless of current date. 2015-03-20 17:06:32 -07:00
Steven Hepting
1da84b5731 Updated year for copyright 2015-03-20 14:53:28 -07:00
Justin Spahr-Summers
620f824c03 Put Carthage badge on README header line 2015-03-17 14:21:52 -07:00
Justin Spahr-Summers
8a5080ddb7 Merge pull request #478 from Mantle/adapters-changelog
Add changelog note for transformers on the JSON adapter
2015-03-17 13:35:15 -07:00
Robert Böhnke
42648b5577 Merge pull request #477 from Mantle/triple-slash-comments
Use triple-slash comments for all interfaces
2015-03-17 21:28:29 +01:00
Justin Spahr-Summers
7688e1db93 CHANGELOG for #474 2015-03-17 13:26:47 -07:00
Robert Böhnke
8d8ce021f5 Merge pull request #475 from Mantle/changelog
2.0 changelog
2015-03-17 20:56:44 +01:00
Robert Böhnke
cb245e05e6 Merge pull request #474 from Mantle/pr/258
Move JSON transformers to MTLJSONAdapter (redux)
2015-03-17 20:53:36 +01:00
Robert Böhnke
36df3c5157 Merge pull request #472 from Mantle/handle-dictionary-adapter-errors
Set *success to NO upon dictionary transformer failure
2015-03-17 20:51:46 +01:00
Justin Spahr-Summers
2c03c34f33 Suggest mtl_invertedTransformer for migrating transformers
h/t @robb
2015-03-17 12:51:45 -07:00
Justin Spahr-Summers
c76c30aa7f Reorder JSON type checking section to the end 2015-03-17 10:50:58 -07:00
Justin Spahr-Summers
801aaf834f CHANGELOG for #210 2015-03-17 10:49:38 -07:00
Justin Spahr-Summers
1b239f3317 CHANGELOG for #153 2015-03-17 10:45:30 -07:00
Justin Spahr-Summers
e3b059b718 CHANGELOG for d0638371d3 2015-03-17 10:37:49 -07:00
Justin Spahr-Summers
b10f2d5b9d CHANGELOG for #275 2015-03-17 09:11:46 -07:00
Justin Spahr-Summers
d4d1a7f993 Use triple-slash comments for all interfaces 2015-03-17 09:04:42 -07:00
Justin Spahr-Summers
58c66eec7d CHANGELOG for #251 2015-03-16 12:48:09 -07:00
Justin Spahr-Summers
cc5f79d9b9 Link to <MTLModel> pull request 2015-03-16 12:45:09 -07:00
Justin Spahr-Summers
5ac1eb38a2 CHANGELOG for #219 2015-03-16 12:40:32 -07:00
Justin Spahr-Summers
67bfc9770d Fix link to deployment target heading 2015-03-16 12:32:42 -07:00
Justin Spahr-Summers
19ffa8851b CHANGELOG for new deployment target requirement 2015-03-16 12:31:59 -07:00
Justin Spahr-Summers
f43c4f5848 CHANGELOG for #374 2015-03-16 12:28:28 -07:00
Justin Spahr-Summers
eeb9a4bfb1 CHANGELOG for #170 2015-03-16 11:47:57 -07:00
Justin Spahr-Summers
a7722b950e Stub a CHANGELOG 2015-03-16 11:40:40 -07:00
Justin Spahr-Summers
0430c5b580 Test that JSON transformers use their adapter class 2015-03-16 11:23:34 -07:00
Justin Spahr-Summers
2dc5573b80 Merge branch 'handle-dictionary-adapter-errors' into pr/258 2015-03-16 11:10:39 -07:00
Justin Spahr-Summers
74edd2e56b Set *success to NO upon dictionary transformer failure
Resolves #455.
2015-03-16 10:56:44 -07:00
Justin Spahr-Summers
e1a81094aa Merge branch 'master' into 2.0-development 2015-03-16 10:48:16 -07:00
Robert Böhnke
a4a26feb8f Merge pull request #469 from Jachobsen/jachobsen/carthage_badge
Add Carthage compatibility badge to the README
2015-03-12 16:21:52 +01:00
Henrik Jachobsen
7076652fb2 Add Carthage compatibility badge to the README 2015-03-12 09:42:28 +01:00
Robert Böhnke
7f0176b6e4 Merge branch 'master' into 2.0-development 2015-02-23 16:44:25 -08:00
Robert Böhnke
6f14d652cf Merge pull request #410 from Mantle/2.0-xcode6
Xcode 6.1 support
2014-11-14 21:57:16 +01:00
Justin Spahr-Summers
ccf0f27778 Merge branch 'master' into 2.0-xcode6
Conflicts:
	Mantle.xcodeproj/project.pbxproj
	MantleTests/MTLCoreDataTestModels.m
	MantleTests/MTLDictionaryManipulationSpec.m
	MantleTests/MTLJSONAdapterSpec.m
	MantleTests/MTLManagedObjectAdapterSpec.m
	MantleTests/MTLModelSpec.m
	MantleTests/MTLPredefinedTransformerAdditionsSpec.m
2014-11-14 11:31:33 -08:00
Justin Spahr-Summers
a5018746d9 Merge pull request #374 from Mantle/coredata-removal
Remove Core Data code
2014-07-29 10:27:55 -07:00
Robert Böhnke
7e489a447c Remove CoreData code 2014-07-27 14:52:30 +02:00
Robert Böhnke
4c9767df80 Merge branch 'master' into 2.0-development
Conflicts:
	Mantle/MTLJSONAdapter.m
	Mantle/NSValueTransformer+MTLPredefinedTransformerAdditions.m
	MantleTests/MTLJSONAdapterSpec.m
2014-07-27 14:50:14 +02:00
Justin Spahr-Summers
c93d7d5e59 Merge pull request #320 from Mantle/nil-transformers
Handle nil transformers
2014-07-22 23:40:34 -07:00
Robert Böhnke
4cffd20ccb Handle nil from JSONTransformerForKey:
and entityAttributeTransformerForKey:, too
2014-07-20 15:09:19 -07:00
Robert Böhnke
f8f9f80d90 Revert "Assert against nil transformers"
This reverts commit 1ca75a9d6c.
2014-07-20 15:00:14 -07:00
Robert Böhnke
b8c3e2095a Revert "Document non-nil requirement"
This reverts commit f3f62de1cb.
2014-07-20 15:00:12 -07:00
Robert Böhnke
5d789de7c9 Revert "WS"
This reverts commit 69ac21101a.
2014-07-20 15:00:11 -07:00
Robert Böhnke
5273cf675f Revert "Make transformer for key consistent with convenience methods"
This reverts commit 598d5caf96.
2014-07-20 15:00:10 -07:00
Robert Böhnke
4cabb8a142 Revert "Add MTLIdentityValueTransformerName"
This reverts commit 8e80404b3d.
2014-07-20 15:00:05 -07:00
Robert Böhnke
6b87fb85e6 Merge branch 'master' into 2.0-development 2014-06-25 12:57:20 +02:00
Justin Spahr-Summers
c9a6414631 Merge pull request #345 from Mantle/docs-fix
Docs fix
2014-06-11 09:28:19 -07:00
Robert Böhnke
984dfef8fa Fix method signature in documentation 2014-06-11 07:37:16 +02:00
Robert Böhnke
8e80404b3d Add MTLIdentityValueTransformerName 2014-06-11 07:28:04 +02:00
Robert Böhnke
598d5caf96 Make transformer for key consistent with convenience methods 2014-06-10 14:19:40 -07:00
Justin Spahr-Summers
ee0c95cf2b Merge pull request #338 from Mantle/removing-keys
Correctly deprecate -mtl_dictionaryByRemovingEntriesWithKeys:
2014-06-10 14:14:18 -07:00
Robert Böhnke
aa045a0568 😒 2014-06-10 14:10:56 -07:00
Robert Böhnke
ee881f9931 Ignore -Wdeprecated instead 2014-06-09 16:32:53 -07:00
Robert Böhnke
216c6113f4 Fix copypasta 🍝 2014-06-09 16:32:32 -07:00
Robert Böhnke
69ac21101a WS 2014-06-09 16:30:04 -07:00
Robert Böhnke
f3f62de1cb Document non-nil requirement 2014-06-09 16:28:53 -07:00
Robert Böhnke
1ca75a9d6c Assert against nil transformers 2014-06-09 16:27:39 -07:00
Justin Spahr-Summers
807c00e965 Merge pull request #335 from Mantle/auto-validating
Automatically validate models from JSON data
2014-06-09 16:05:20 -07:00
Robert Böhnke
6052a7dde9 Moving docs 2014-06-09 15:37:10 -07:00
Robert Böhnke
68d537ab2c Document validation 2014-06-09 12:19:57 -07:00
Robert Böhnke
2a21b686c1 Add mtl_dictionaryByRemovingValuesForKeys: 2014-05-27 22:01:48 +02:00
Robert Böhnke
78a0ebfaeb Revert "Use arrays when removing dictionary entries"
This reverts commit 502175f690.

Conflicts:
	MantleTests/MTLTestModel.m
2014-05-27 21:54:57 +02:00
Robert Böhnke
2b7ac7b016 Add JSON model validation 2014-05-27 11:00:33 +02:00
Justin Spahr-Summers
1fc58a488e Merge pull request #325 from Mantle/mapping-exception
Throw an exception for illegal JSON mappings
2014-05-14 09:13:12 -07:00
Robert Böhnke
aa047f71cf Remove MTLIllegalJSONMappingModel 2014-05-14 01:42:44 +02:00
Robert Böhnke
8fc086fdc5 Don't test for exceptions 2014-05-14 01:42:41 +02:00
Robert Böhnke
c58a89a0b7 Don't localize 2014-05-14 01:42:34 +02:00
Robert Böhnke
209e19dcd6 Also throw exceptions for mappings to NULL or other objects 2014-05-13 16:11:26 +02:00
Robert Böhnke
c271353120 Throw an exception for illegal JSON mappings 2014-05-13 15:40:56 +02:00
Robert Böhnke
5692bbdc00 Handle nil value transformers 2014-05-07 17:24:26 +02:00
Justin Spahr-Summers
d0834c5c67 Merge pull request #278 from Mantle/longer-lived-adapters
Longer lived JSON adapters
2014-05-06 16:07:43 -07:00
Robert Böhnke
b4bf1242e2 Illegal mappings should fail when creating adapters 2014-05-06 23:25:57 +02:00
Robert Böhnke
6f2e85c7ae Merge branch '2.0-development' into longer-lived-adapters
Conflicts:
	Mantle.xcodeproj/project.pbxproj
	Mantle/MTLJSONAdapter.h
	Mantle/MTLJSONAdapter.m
	MantleTests/MTLTestModel.h
2014-05-06 23:20:29 +02:00
Robert Böhnke
e28b12af4e Merge branch 'master' into 2.0-development
Conflicts:
	Mantle/MTLJSONAdapter.m
	Mantle/MTLManagedObjectAdapter.m
	Mantle/MTLModel.m
	Mantle/NSValueTransformer+MTLPredefinedTransformerAdditions.h
	Mantle/NSValueTransformer+MTLPredefinedTransformerAdditions.m
	MantleTests/MTLJSONAdapterSpec.m
	MantleTests/MTLModelSpec.m
	MantleTests/MTLTestModel.h
	MantleTests/MTLTestModel.m
2014-05-06 23:05:43 +02:00
Robert Böhnke
97be74e576 Merge branch '2.0-development' into longer-lived-adapters 2014-05-06 22:43:07 +02:00
Robert Böhnke
652ce9be1f WS 2014-04-24 11:25:56 +02:00
Robert Böhnke
020d3cc91b Remove unnecessary cast 2014-04-24 11:16:59 +02:00
Robert Böhnke
06d10eaf22 Fix spec wording 2014-04-24 11:16:15 +02:00
Robert Böhnke
d882faff57 Revert "Bump Deployment Target to 10.8/6.0"
This reverts commit 19981517ea.
2014-04-22 22:05:20 +02:00
Robert Böhnke
a44c73b1a6 Improve docs for -serializablePropertyKeys:forModel: 2014-04-22 22:04:44 +02:00
Robert Böhnke
d4ecaf36cd Use @synchronized instead of GCD 2014-04-22 21:59:45 +02:00
Robert Böhnke
3d3ac1de55 Make NSURLEntityAttributeTransformer a class method 2014-04-22 21:58:13 +02:00
Robert Böhnke
d496494327 Handle transformer lookup in class methods 2014-04-22 21:54:28 +02:00
Robert Böhnke
658951199d Restore valueTransformersForModelClass: declaration 2014-04-22 15:09:07 +02:00
Robert Böhnke
159cd2179c WS 2014-04-22 15:03:39 +02:00
Robert Böhnke
f6644252cc and back! 2014-04-22 15:02:27 +02:00
Robert Böhnke
cf82e6aa3d Serialize class clusters correctly 2014-04-22 14:55:46 +02:00
Robert Böhnke
6eb3e51920 Rename to serializablePropertyKeys:forModel: 2014-04-22 14:08:32 +02:00
Robert Böhnke
769069d46c Cache whole adapters instead 2014-04-22 14:06:37 +02:00
Robert Böhnke
8e36f6d666 Merge branch 'master' into 2.0-development 2014-04-16 23:05:55 +02:00
Robert Böhnke
fc1009a01f Don't advertise private methods 2014-04-03 14:07:45 +02:00
Robert Böhnke
5cfd2fec5f Cache transformers and mappings in MTLJSONAdapter 2014-04-03 14:01:16 +02:00
Robert Böhnke
19981517ea Bump Deployment Target to 10.8/6.0 2014-04-03 13:46:03 +02:00
Robert Böhnke
db43557fd0 Add -filterPropertyKeys:forModel: 2014-04-03 13:33:47 +02:00
Robert Böhnke
c2955052c4 Use id as return value for models 2014-04-03 13:33:16 +02:00
Robert Böhnke
ae14eb762a WS 2014-04-03 12:58:07 +02:00
Robert Böhnke
4de1334bac Merge branch '2.0-development' into longer-lived-adapters 2014-04-03 12:57:40 +02:00
Robert Böhnke
5c74d82970 Merge branch 'master' into 2.0-development 2014-04-03 12:57:03 +02:00
Robert Böhnke
066ce61fe5 Add longer lived adapters 2014-03-21 12:49:04 +01:00
Justin Spahr-Summers
1bec4fc2d3 Merge pull request #275 from MantleFramework/dictionary-paths
Require every element along a JSON key path to be a NSDictionary
2014-03-20 14:06:00 -07:00
Robert Böhnke
f3dc78ec2d Actually rename to -mtl_valueForJSONKeyPath:success:error: 2014-03-20 22:01:37 +01:00
Robert Böhnke
fa5be2cee6 Document key path lookup behavior 2014-03-20 21:59:37 +01:00
Robert Böhnke
2571a72727 Reorder imports 2014-03-19 23:14:14 +00:00
Robert Böhnke
a2418b851c Use subscripting syntax 2014-03-19 23:14:08 +00:00
Robert Böhnke
44525fa53f Rename variables 2014-03-19 23:13:52 +00:00
Robert Böhnke
193a6cbc2b Rename to -mtl_valueForJSONKeyPath:success:error: 2014-03-19 23:06:54 +00:00
Robert Böhnke
5f88beaee1 Remove headers from targets 2014-03-19 23:05:59 +00:00
Robert Böhnke
fb7b9011a7 Require all values along JSON key paths to be dictionaries 2014-03-19 11:57:45 +00:00
Justin Spahr-Summers
ec1f44a0ff Merge pull request #270 from MantleFramework/multiple-keypaths
Map properties to multiple JSON key paths
2014-03-18 10:39:55 -07:00
Robert Böhnke
73c8ace698 Update +JSONKeyPathsByPropertyKey documentation 2014-03-18 17:20:06 +00:00
Robert Böhnke
ed29ca476d Remove cast 2014-03-18 17:11:54 +00:00
Robert Böhnke
b7f7065902 Un-focus spec 2014-03-18 17:08:38 +00:00
Robert Böhnke
4c784807a9 Fix typo 2014-03-18 12:28:54 +00:00
Robert Böhnke
5e0d955e6f Add example for +JSONKeyPathsByPropertyKey 2014-03-18 12:05:11 +00:00
Robert Böhnke
ae322bf125 Move componentsSeparatedByString: 2014-03-18 11:49:18 +00:00
Robert Böhnke
0fa5ccdc44 Add support for multiple, nested key paths 2014-03-18 11:25:22 +00:00
Robert Böhnke
ed15668ced WS 2014-03-18 11:12:07 +00:00
Robert Böhnke
99b48e9cb9 WS 2014-03-14 19:47:32 +01:00
Robert Böhnke
4fb6d2a908 Allow mapping to multiple JSON key paths 2014-03-14 19:46:31 +01:00
Robert Böhnke
f4069f8e06 Fix __attribute__ 2014-03-14 18:54:06 +01:00
Justin Spahr-Summers
d34a138a5f Merge pull request #251 from MantleFramework/implicit-validation
Implicit validation
2014-03-14 10:08:23 -07:00
Robert Böhnke
ed477e2f79 Don't compare all of attributes->type 2014-03-14 02:29:37 +01:00
Justin Spahr-Summers
c7ffb811cb Merge pull request #219 from MantleFramework/model-protocol
Extract the MTLModel protocol
2014-03-13 18:08:21 -07:00
Robert Böhnke
af1ea58570 Move MTLConformingModel initializer to class extension 2014-03-14 02:06:14 +01:00
Robert Böhnke
bf0a5e7236 Handle properties having a class but no type 2014-03-14 01:55:47 +01:00
Robert Böhnke
3fd838e18c Merge branch '2.0-development' into model-protocol
Conflicts:
	Mantle/MTLModel.h
	MantleTests/MTLTestModel.h
	MantleTests/MTLTestModel.m
2014-03-14 01:48:38 +01:00
Robert Böhnke
d641f119bb Un-fit spec 2014-03-14 01:44:40 +01:00
Robert Böhnke
d0cc019ab1 Move +modelWithDictionary:error: to the MTLModel protocol 2014-03-14 01:42:28 +01:00
Robert Böhnke
1e2e25bc89 Merge branch '2.0-development' into model-protocol 2014-03-14 01:36:16 +01:00
Robert Böhnke
d5739b1ed0 Remove -mergeValuesForKeysFromModel: from MTLModel protocol 2014-03-14 01:35:33 +01:00
Justin Spahr-Summers
1a4507e9b1 Merge pull request #210 from MantleFramework/property-behavior
Add storage behavior for property keys
2014-03-13 17:26:55 -07:00
Robert Böhnke
61d026b490 Add documentation to the MTLModel protocol 2014-03-14 00:30:17 +01:00
Robert Böhnke
9beadc85a6 Fix spelling 2014-03-14 00:14:36 +01:00
Robert Böhnke
36a79809ff Remove unnecessary imports 2014-03-14 00:12:50 +01:00
Robert Böhnke
b677ad6196 Change variable type 2014-03-14 00:12:49 +01:00
Robert Böhnke
4971735f93 WS 2014-03-14 00:08:01 +01:00
Robert Böhnke
0597f7934f Use -isEqual: 2014-03-13 23:52:42 +01:00
Robert Böhnke
199b34e3da Fix failure reason 2014-03-13 23:45:27 +01:00
Robert Böhnke
873fc8603e WS 2014-03-13 23:45:05 +01:00
Robert Böhnke
efdf5e783a Document that validating transformers should not be nil 2014-03-13 23:44:26 +01:00
Robert Böhnke
3f9d37ebea Don't expect NSValue instances for id properties 2014-03-13 23:43:07 +01:00
Robert Böhnke
29e23b8489 Merge branch 'master' into 2.0-development 2014-03-11 21:57:07 +01:00
Robert Böhnke
ecf3d76f9c Merge pull request #248 from dcaunt/array-mapping-transformer
[WIP] Added array mapping transformer and refactored JSON array transformer
2014-03-09 20:04:01 +01:00
Dave Caunt
2478828a82 Header doc for arguments that must not be nil 2014-03-09 18:45:21 +00:00
Dave Caunt
0370a83fcf Changed description of array mapping transformer test 2014-03-09 18:38:34 +00:00
Dave Caunt
3e1438d773 Separated out array mapping transformer tests by reversible and non-reversible conditions 2014-03-09 17:34:09 +00:00
Dave Caunt
4aea0d4835 Improved test readability 2014-03-09 10:00:47 +00:00
Dave Caunt
481b7a611f Added tests for array mapping transformer 2014-03-08 20:08:33 +00:00
Dave Caunt
44e5615885 Added header note for subclasses 2014-03-06 19:27:42 +00:00
Dave Caunt
e546dc09d9 Moved JSON dictionary and array transformers to MTLJSONAdapter 2014-03-06 11:21:08 +00:00
Dave Caunt
fffba6f84d End header comment sentences with a full stop. 2014-03-03 18:50:37 +00:00
Robert Böhnke
9295ab638a Use implicit value transformers to validate properties 2014-03-03 02:24:25 +01:00
Robert Böhnke
e43e3b464e Add class validation transformer 2014-03-03 02:13:42 +01:00
Dave Caunt
ab6b277deb Whitespace and indentation fixes 2014-03-01 10:53:25 +00:00
Robert Böhnke
86b502d053 Fix accidental use of -isEqualTo: 2014-02-27 12:57:04 +01:00
Robert Böhnke
49e2bcb5b8 Improve documentation 2014-02-26 16:28:57 -05:00
Robert Böhnke
f5b1739b99 Merge branch '2.0-development' into model-protocol
Conflicts:
	Mantle/MTLManagedObjectAdapter.m
	MantleTests/MTLTestModel.h
2014-02-26 16:21:43 -05:00
Robert Böhnke
6d205da711 Allow subclasses to override +propertyKeys 2014-02-26 16:17:48 -05:00
Dave Caunt
939dfbf062 Check reverseBlock is not nil 2014-02-26 10:36:50 +00:00
Dave Caunt
793b5fa570 Whitespace fixes 2014-02-26 10:35:48 +00:00
Dave Caunt
571d511a47 Added array mapping transformer and refactored mtl_JSONArrayTransformerWithModelClass: to use it 2014-02-22 20:29:37 +00:00
Robert Böhnke
652a2a57f1 Merge branch '2.0-development' into property-behavior
Conflicts:
	MantleTests/MTLTestModel.h
2014-02-15 19:01:48 +01:00
Justin Spahr-Summers
de2f2bbc23 Merge pull request #188 from MantleFramework/implicit-transformers
Add implicit transformers
2014-02-14 12:26:29 -08:00
Robert Böhnke
70cd8d032a Reuse property attributes, remove inspection hooks 2014-02-14 00:54:51 +01:00
Robert Böhnke
79697df4b5 Rename mtl_copyObjCTypeOfPropertyWithKey 2014-02-14 00:41:52 +01:00
Robert Böhnke
24c6e13481 Consider weak properties permanent
Until we find a way to reliably detect readonly weak properties
😢
2014-02-14 00:10:58 +01:00
Robert Böhnke
d9d8f97394 Merge branch '2.0-development' into property-behavior 2014-02-13 01:30:24 +01:00
Robert Böhnke
5537d67580 Only consider weak properties transitory 2014-02-13 01:22:53 +01:00
Robert Böhnke
ae161c5246 Fix TomDoc style 2014-02-13 01:11:39 +01:00
Robert Böhnke
6e148c59fd Remove attribute argument 2014-02-13 01:03:45 +01:00
Robert Böhnke
55c264734b Look up associated objects less frequently 2014-02-12 02:22:56 +01:00
Robert Böhnke
c4812ac2b3 Pass in attributes to determine storage behavior 2014-02-12 02:18:21 +01:00
Robert Böhnke
b7c3544347 WS 2014-02-12 01:38:39 +01:00
Robert Böhnke
f69daa4330 Improve documentation of MTLModel 2014-02-12 01:38:32 +01:00
Robert Böhnke
8eed7558b6 Rename generateAndCachePropertyKeys method 2014-02-12 01:37:33 +01:00
Robert Böhnke
ac6feeb36e Improve documentation of MTLModel 2014-02-12 01:27:34 +01:00
Robert Böhnke
7f3e346b20 Merge branch 'master' into 2.0-development 2014-02-12 01:21:01 +01:00
Robert Böhnke
06746f3649 Document property storage behavior 2014-02-01 16:39:36 +01:00
Robert Böhnke
b3fc341bd0 Add spec for conforming models 2014-01-22 13:13:48 +01:00
Robert Böhnke
e51c31f765 Extract the MTLModel protocol 2014-01-22 13:01:22 +01:00
Robert Böhnke
34626329ba Merge branch 'master' into 2.0-development
Conflicts:
	Mantle/NSValueTransformer+MTLPredefinedTransformerAdditions.m
2014-01-20 16:42:14 +01:00
Robert Böhnke
338a6607df Defer reflection 2014-01-10 20:22:33 +01:00
Robert Böhnke
b7d96f174f Only look up transformers for actual properties 2014-01-08 19:08:08 +01:00
Robert Böhnke
bcda70bfe0 Remove focused spec 2014-01-08 18:54:31 +01:00
Robert Böhnke
005fddfeab Negate spec description 2014-01-06 00:46:49 +01:00
Robert Böhnke
5e1398101f Move property key reflection to +initialize 2014-01-06 00:40:34 +01:00
Robert Böhnke
7753325d73 Implement +storageBehaviorForPropertyWithKey: 2014-01-06 00:31:39 +01:00
Robert Böhnke
f5d02a7b4f Improve and spec MTLPropertyInspection 2014-01-04 03:18:07 +01:00
Robert Böhnke
78a885af73 Mention URL transformers in -transformerForModelPropertiesOfClass: 2014-01-04 02:31:01 +01:00
Robert Böhnke
cba71b4695 Make class based transformers instrance methods 2014-01-04 02:25:18 +01:00
Robert Böhnke
0937988d92 Document +NSURLEntityAttributeTransformer 2014-01-04 02:21:04 +01:00
Robert Böhnke
08dd953e22 Pluralize valueTransformersByPropertyKey 2014-01-04 02:08:45 +01:00
Robert Böhnke
b4ddc0d779 Remove unneeded imports 2013-12-31 00:25:41 +01:00
Robert Böhnke
b523dbd04a Extract property inspection into category 2013-12-31 00:22:37 +01:00
Robert Böhnke
537a64ba87 Add implicit transformers to MTLManagedObjectAdapter 2013-12-29 17:31:51 +01:00
Robert Böhnke
8fd0a3ef02 Merge branch '2.0-development' into implicit-transformers
Conflicts:
	Mantle/MTLJSONAdapter.h
	Mantle/MTLJSONAdapter.m
2013-12-13 00:28:05 +01:00
Robert Böhnke
1cd58fb6fa Merge branch 'master' into 2.0-development
Conflicts:
	Mantle/MTLJSONAdapter.m
2013-12-13 00:05:26 +01:00
Robert Böhnke
fff1f75b68 Cache value transformers on the property key 2013-12-12 23:50:55 +01:00
Robert Böhnke
b5a8f7281f Improve documentation 2013-12-12 00:17:35 +01:00
Robert Böhnke
9f0736cc00 Rename to -transformerForModelPropertiesOfObjCType: 2013-12-12 00:17:20 +01:00
Robert Böhnke
e963954227 Document NSURLJSONTransformer 2013-12-12 00:08:16 +01:00
Robert Böhnke
45229532cc WS 2013-12-12 00:06:29 +01:00
Robert Böhnke
62b4804a48 Re-capitalize like it's 1999 2013-12-12 00:06:29 +01:00
Robert Böhnke
6bc1d00d2c Fix capitalization 2013-12-11 20:49:02 +01:00
Robert Böhnke
7f817e6d7f Implicitly transform BOOLs to CFBooleanRef 2013-12-11 12:07:47 +01:00
Robert Böhnke
c60379122c Explicitly cast to less precise type 2013-12-11 01:16:18 +01:00
Robert Böhnke
e1cbe5334a Add implicit transformer hooks for primitives 2013-12-11 01:12:31 +01:00
Robert Böhnke
a7a769333a Fix README 2013-12-11 01:12:07 +01:00
Robert Böhnke
f55733e05c Merge branch 'master' into 2.0-development 2013-11-28 11:00:19 +01:00
Robert Böhnke
9708508807 Add implicit transformation of NSURLs to JSON 2013-11-16 18:15:59 +01:00
Robert Böhnke
6b8fad56a0 Merge branch 'master' into 2.0-development 2013-11-16 17:08:18 +01:00
Justin Spahr-Summers
b11a90f1a3 Merge pull request #174 from github/remove-1x-deprecations
Remove MTLJSONAdapter methods deprecated in 1.x
2013-11-08 00:11:33 -08:00
Robert Böhnke
34a0b741fd Merge remote-tracking branch 'origin/master' into 2.0-development
Conflicts:
	Mantle.xcodeproj/project.pbxproj
	MantleTests/MTLManagedObjectAdapterSpec.m
	MantleTests/TestModel.xcdatamodeld/TestModel.xcdatamodel/contents
2013-11-07 19:30:34 +01:00
Justin Spahr-Summers
2586cfc384 Merge pull request #170 from github/explicit-mapping
Explicit mapping
2013-11-05 16:22:46 -08:00
Robert Böhnke
22fce6512e Use mutable dictionary in MTLTestModel 2013-11-05 20:53:14 +01:00
Robert Böhnke
502175f690 Use arrays when removing dictionary entries 2013-11-05 20:49:39 +01:00
Robert Böhnke
fa4620138a Fix MTLJSONAdapter docs 2013-11-05 20:47:46 +01:00
Robert Böhnke
1eaafc5fa1 Improve README spelling 2013-11-05 20:46:55 +01:00
Robert Böhnke
c08877395c Remove old deprecated methods 2013-11-05 20:42:37 +01:00
Robert Böhnke
72c5455537 WS 2013-11-03 22:36:30 +01:00
Robert Böhnke
6da6e309c8 Remove MTLJSONAdapter methods deprecated in 1.x 2013-11-03 22:29:28 +01:00
Robert Böhnke
d2d4a51d53 Update README 2013-11-02 18:10:35 +01:00
Robert Böhnke
243ff42dc5 WS 2013-11-02 18:01:26 +01:00
Robert Böhnke
f7c83bef13 No longer allow mapping to NSNull 2013-11-02 18:01:19 +01:00
Robert Böhnke
bfc8ab3155 Fix Headers 2013-11-01 14:03:26 +01:00
Robert Böhnke
2c268ab2a2 Fix import statements
wtf appcode?
2013-10-31 22:35:09 +01:00
Robert Böhnke
21ae50296d Add MTLMappingAdditions to Mantle.h 2013-10-31 22:34:19 +01:00
Robert Böhnke
19cf0c2560 Move MTLIdentityMappingForClass to category method 2013-10-31 17:37:08 +01:00
Robert Böhnke
2e7860f6c5 Merge branch '2.0-development' into explicit-mapping
Conflicts:
	Mantle.xcodeproj/project.pbxproj
2013-10-31 17:09:02 +01:00
Justin Spahr-Summers
870fc7c475 Merge pull request #153 from github/error-transformer
Adding MTLTransformerErrorHandling
2013-10-31 08:52:00 -07:00
Justin Spahr-Summers
4fec757b93 Remove duplicate "Error" from input value userInfo key 2013-10-31 08:51:39 -07:00
Justin Spahr-Summers
aa6194c4d5 Put error constants at the top of the header 2013-10-31 08:49:27 -07:00
Robert Böhnke
7b5c965ac2 Add MTLTransformerErrorHandling.m to iOS target 2013-10-30 11:06:03 +01:00
Robert Böhnke
b403d42caf Move error domain etc. to the transformer protocol 2013-10-30 10:37:35 +01:00
Robert Böhnke
b3c5c46e3b Fix positional format string 2013-10-30 09:54:05 +01:00
Robert Böhnke
2f5f86f11d Remove userInfo from MTLCoreDataTestModel error 2013-10-30 09:50:47 +01:00
Robert Böhnke
71c9191a41 Make error handling specs more specific 2013-10-29 22:41:59 +01:00
Robert Böhnke
a0ebbe0c4a Expose and test MTLCoreDataTestModelsDomain 2013-10-29 22:34:07 +01:00
Robert Böhnke
858a162a18 WS 2013-10-29 22:28:07 +01:00
Robert Böhnke
ac469e7fb2 Return error if value mapping transformer fails 2013-10-29 22:26:08 +01:00
Robert Böhnke
c3370df403 Inline failureReason 2013-10-29 22:15:44 +01:00
Robert Böhnke
67279cb29d Expose MTLTransformerErrorHandling 2013-10-29 22:10:28 +01:00
Robert Böhnke
99b3c72c19 Tweak failure reason 2013-10-29 22:05:52 +01:00
Robert Böhnke
952db2ab7e WS 2013-10-29 22:05:10 +01:00
Robert Böhnke
b3298754e8 Add missing input value error 2013-10-29 21:46:54 +01:00
Robert Böhnke
e0ae9384b5 Rename error key 2013-10-29 21:39:55 +01:00
Robert Böhnke
7dd64d3891 Improve MTLTransformerErrorHandling docs 2013-10-29 21:35:51 +01:00
Robert Böhnke
d78160af02 Handle errors during object uniquing 2013-10-28 10:10:32 +01:00
Robert Böhnke
43103501a1 Fix reverse transformation documentation 2013-10-28 10:02:39 +01:00
Robert Böhnke
8fd199d897 Bubble up errors for malformed URLs 2013-10-28 09:35:55 +01:00
Robert Böhnke
2598eb86c1 Rename MTLValueTransformationBlock to old name 2013-10-28 09:25:30 +01:00
Robert Böhnke
66b138582f Inline old MTLValueTransformerBlock 2013-10-28 09:24:46 +01:00
Robert Böhnke
246cb8b309 Improve error handling interface, messages 2013-10-28 09:18:52 +01:00
Robert Böhnke
67dfe13d39 Improve documentation 2013-10-28 09:12:47 +01:00
Robert Böhnke
2cdc967f85 Fix style of blocks returning id 2013-10-28 09:07:44 +01:00
Robert Böhnke
c558644076 Add explicit required 2013-10-28 09:06:12 +01:00
Robert Böhnke
7539afe392 Use id<MTLTransformerErrorHandling> 2013-10-28 09:05:11 +01:00
Robert Böhnke
45ef45f00a Reduce nesting 2013-10-28 08:59:43 +01:00
Robert Böhnke
471b181b22 Flip encounteredError 2013-10-28 08:57:48 +01:00
Robert Böhnke
78b750a8cc Use explicit mapping in managed object serializing 2013-10-27 12:53:14 +01:00
Robert Böhnke
16c3e87901 Use explicit mapping in MTLJSONSerializing 2013-10-26 17:55:41 +02:00
Robert Böhnke
ad7743d446 Add MTLIdentityMappingForClass 2013-10-26 17:55:41 +02:00
Robert Böhnke
91643209f1 WS 2013-10-26 01:54:33 +02:00
Robert Böhnke
ea63804d9c Revert "Improve upgrade path for managed objects"
This reverts commit 16734e1fb7.
2013-10-26 01:52:50 +02:00
Robert Böhnke
ecd90e21da WS 2013-10-23 18:19:24 +02:00
Robert Böhnke
16734e1fb7 Improve upgrade path for managed objects 2013-10-23 00:34:00 +02:00
Robert Böhnke
8df8f6b8fe Use beIdenticalTo for CFBooleanRefs 2013-10-21 11:33:50 +02:00
Robert Böhnke
6f2f896401 Improve transformer error examples 2013-10-21 11:29:12 +02:00
Robert Böhnke
3a063d53f2 Style fixes 2013-10-21 11:25:44 +02:00
Robert Böhnke
221474aca4 Use block-based enumeration 2013-10-21 11:24:08 +02:00
Robert Böhnke
0763e6cfd6 Simplify transformer casts 2013-10-21 11:06:06 +02:00
Robert Böhnke
cb77448cf4 Rename JSONDictionary: 2013-10-21 11:00:21 +02:00
Robert Böhnke
503a2ca40b Use error handling when serializing to Core Data 2013-10-19 19:09:10 +02:00
Robert Böhnke
3054d9f9df Use error handling when serializing to JSON 2013-10-19 19:02:42 +02:00
Robert Böhnke
a19b6f9b75 Use error handling when deserializing from Core Data 2013-10-19 18:55:23 +02:00
Robert Böhnke
297631c4df WS 2013-10-19 18:41:22 +02:00
Robert Böhnke
d0638371d3 Reverse transformer direction for managed objects
The old direction did not match the one in MTLJSONAdapter where the
forward direction is invoked when deserializing from a JSON dictionary.
This meant that for instance built-in NSURL transformer could not be used.
2013-10-19 18:17:00 +02:00
Robert Böhnke
4e6485461d Use error handling functionality when deserializing JSON 2013-10-19 17:51:32 +02:00
Robert Böhnke
8695996ad4 Remove dictionary and array specific error codes 2013-10-19 16:26:04 +02:00
Robert Böhnke
6f919235e9 Improve error handling in built-in transformers
The URL and number transformer map to nil for the time being
to prevent the MTLPredefinedTransformerErrorInvalidInputKey from
mapping to nil
2013-10-10 12:31:10 +02:00
Robert Böhnke
20e14e4d73 Remove MTLUppercasingValueTransformer 2013-10-10 12:10:03 +02:00
Robert Böhnke
96d2ab95b3 Fix style 2013-10-10 12:09:30 +02:00
Robert Böhnke
9852a8b920 Remove unnecessary cast 2013-10-10 12:07:07 +02:00
Robert Böhnke
2cebc434fd Improve compiler checking 2013-10-10 12:05:22 +02:00
Robert Böhnke
4913f6d817 Improve upgrade path for deprecated methods 2013-10-10 11:55:36 +02:00
Robert Böhnke
f522bd0105 Refactor MTLValueTransformer internals 2013-10-10 11:53:36 +02:00
Robert Böhnke
7a45248206 Rename MTLValueTransformer class methods 2013-10-10 11:51:19 +02:00
Robert Böhnke
aea453c81c Improve documentation of MTLValueTransformer 2013-10-10 11:47:40 +02:00
Robert Böhnke
9d9d9b536b Improve documentation of MTLTransformerErrorHandling 2013-10-10 11:39:57 +02:00
Robert Böhnke
cd24d09722 Make MTLTransformerErrorHandling.h public 2013-10-10 11:23:02 +02:00
Robert Böhnke
48f46d7dc8 Rename MTLValueTransformer methods 2013-10-09 23:11:01 +02:00
Robert Böhnke
74444a6aa6 Fix spelling 2013-10-09 22:52:49 +02:00
Robert Böhnke
f3d15d800b Add MTLTransformerErrorExamples to Manle iOS Tests 2013-10-09 22:25:49 +02:00
Robert Böhnke
c6ee880f37 Add error handling tests for built-in transformers 2013-10-09 22:12:55 +02:00
Robert Böhnke
4a3ed6d193 Adding error handling to built-in transformers 2013-10-09 12:44:29 +02:00
Robert Böhnke
0ce245c251 Adding success paramter to MTLValueTransformer methods 2013-10-09 12:27:42 +02:00
Robert Böhnke
796b8eae8f Adding success parameter to MTLValueTransformationBlock 2013-10-07 23:20:11 +02:00
Robert Böhnke
2550434089 Move transformer error handling into a protocol 2013-10-06 15:16:03 +02:00
Robert Böhnke
5e915245d8 WS 2013-10-04 11:29:40 +02:00
Robert Böhnke
8c90017634 Add error handling to boolean and URL transformers 2013-10-04 00:01:00 +02:00
Robert Böhnke
9578541d62 WS 2013-10-03 01:22:22 +02:00
Robert Böhnke
75ecc76925 Suppress -Wdeprecated-implementations 2013-10-03 01:21:55 +02:00
Robert Böhnke
9218025916 Adding error handling support to MTLValueTransformer
as well as existing transformers
2013-10-03 01:14:03 +02:00
Robert Böhnke
45705984f7 Fixing spec description 2013-10-01 00:22:32 +02:00
Robert Böhnke
4f69bcc943 Adding NSValueTransformer+MTLErrorHandling to iOS target 2013-10-01 00:22:00 +02:00
Robert Böhnke
762c382cc6 Adding NSValueTransformer+MTLErrorHandling 2013-10-01 00:15:39 +02:00
73 changed files with 4706 additions and 2983 deletions

2
.gitignore vendored
View File

@ -18,4 +18,4 @@ profile
# Desktop Servies
.DS_Store
Carthage.build
Carthage/Build

View File

@ -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
View 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 projects 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
Mantles 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.

View File

@ -1,3 +1,3 @@
github "jspahrsummers/xcconfigs" >= 0.8
github "Quick/Quick" ~> 0.2
github "Quick/Nimble" ~> 0.2
github "jspahrsummers/xcconfigs" "1ef9763"
github "Quick/Quick" ~> 1.1.0
github "Quick/Nimble" ~> 7.0.1

View File

@ -1,3 +1,3 @@
github "Quick/Nimble" "v0.2.0"
github "Quick/Quick" "v0.2.3"
github "jspahrsummers/xcconfigs" "0.8.1"
github "Quick/Nimble" "v7.0.1"
github "Quick/Quick" "v1.1.0"
github "jspahrsummers/xcconfigs" "1ef97639ffbe041da0b1392b2114fa19b922a7a1"

@ -1 +1 @@
Subproject commit 6f787eeb75b2fa71464981f28b3dc8d7bd532e69
Subproject commit 39b67002306fda9de4c9fd1290a6295f97edd09e

@ -1 +1 @@
Subproject commit ce729620fa9db8d3c76b69bbfc429cece97dbe36
Subproject commit e4fa1e85c0305ba4e0866f25812d3fa398f3a048

@ -1 +1 @@
Subproject commit 99624a6af366c015b678a1135e4c558776a59be6
Subproject commit 1ef97639ffbe041da0b1392b2114fa19b922a7a1

View File

@ -1,4 +1,4 @@
**Copyright (c) 2012 - 2014, 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

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0620"
LastUpgradeVersion = "0800"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
parallelizeBuildables = "NO"
buildImplicitDependencies = "NO">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
@ -20,6 +20,34 @@
ReferencedContainer = "container:Mantle.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
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"
@ -37,10 +65,10 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Release">
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
@ -62,16 +90,19 @@
ReferencedContainer = "container:Mantle.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
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
@ -86,10 +117,10 @@
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
buildConfiguration = "Profile"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0620"
LastUpgradeVersion = "0800"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
parallelizeBuildables = "NO"
buildImplicitDependencies = "NO">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
@ -20,6 +20,34 @@
ReferencedContainer = "container:Mantle.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
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"
@ -37,10 +65,10 @@
</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">
@ -62,15 +90,18 @@
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
@ -85,10 +116,10 @@
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
buildConfiguration = "Release"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference

View 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>

View File

@ -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>

View File

@ -7,7 +7,7 @@
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>org.mantle.$(PRODUCT_NAME:rfc1034identifier)</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>

View File

@ -8,165 +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;
// The provided JSONDictionary is not valid.
/// 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.
/// The model's implementation of +JSONKeyPathsByPropertyKey included a key which
/// does not actually exist in +propertyKeys.
extern const NSInteger MTLJSONAdapterErrorInvalidJSONMapping;
// Converts a MTLModel object to and from a JSON dictionary.
/// 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;
// 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.
/// 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;
// 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;
/// 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;
// 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.
//
// Returns a JSON array, or nil if a serialization error occurred for any
// model.
+ (NSArray *)JSONArrayFromModels:(NSArray *)models;
/// 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;
// 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 and an error with code
// MTLJSONAdapterErrorInvalidJSONDictionary.
// 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;
/// 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;
// 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;
/// 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 the receiver's `model` into JSON.
//
// Returns a JSON dictionary, or nil if a serialization error occurred.
- (NSDictionary *)JSONDictionary;
/// 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;
// Looks up the JSON key path in the model's +propertyKeys.
//
// Subclasses may override this method to customize the adapter's seralizing
// behavior. You should not call this method directly.
//
// key - The property key to retrieve the corresponding JSON key path for. This
// argument must not be nil.
//
// Returns a key path to use, or nil to omit the property from JSON.
- (NSString *)JSONKeyPathForPropertyKey:(NSString *)key;
/// 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

View File

@ -6,9 +6,18 @@
// 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;
@ -19,7 +28,7 @@ const NSInteger MTLJSONAdapterErrorInvalidJSONMapping = 4;
const NSInteger MTLJSONAdapterErrorExceptionThrown = 1;
// Associated with the NSException that was caught.
static NSString * const MTLJSONAdapterThrownExceptionErrorKey = @"MTLJSONAdapterThrownException";
NSString * const MTLJSONAdapterThrownExceptionErrorKey = @"MTLJSONAdapterThrownException";
@interface MTLJSONAdapter ()
@ -30,12 +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.
// A cached copy of the return value of -valueTransformersForModelClass:
@property (nonatomic, copy, readonly) NSDictionary *valueTransformersByPropertyKey;
// 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 transform from or to. 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 transformer to use, or nil to not transform the property.
- (NSValueTransformer *)JSONTransformerForKey:(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
@ -44,8 +74,9 @@ 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];
}
+ (NSArray *)modelsOfClass:(Class)modelClass fromJSONArray:(NSArray *)JSONArray error:(NSError **)error {
@ -65,25 +96,26 @@ static NSString * const MTLJSONAdapterThrownExceptionErrorKey = @"MTLJSONAdapter
MTLModel *model = [self modelOfClass:modelClass fromJSONDictionary:JSONDictionary error:error];
if (model == nil) return nil;
[models addObject:model];
}
return models;
}
+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel<MTLJSONSerializing> *)model {
MTLJSONAdapter *adapter = [[self alloc] initWithModel:model];
return adapter.JSONDictionary;
+ (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 {
+ (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];
NSDictionary *JSONDictionary = [self JSONDictionaryFromModel:model error:error];
if (JSONDictionary == nil) return nil;
[JSONArray addObject:JSONDictionary];
@ -95,29 +127,139 @@ static NSString * const MTLJSONAdapterThrownExceptionErrorKey = @"MTLJSONAdapter
#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 || ![JSONDictionary isKindOfClass:NSDictionary.class]) {
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Missing JSON dictionary", @""),
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"%@ could not be created because an invalid JSON dictionary was provided: %@", @""), NSStringFromClass(modelClass), JSONDictionary.class],
};
*error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorInvalidJSONDictionary userInfo:userInfo];
self = [super init];
if (self == nil) return 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;
}
return nil;
}
if ([modelClass respondsToSelector:@selector(classForParsingJSONDictionary:)]) {
modelClass = [modelClass classForParsingJSONDictionary:JSONDictionary];
if (modelClass == 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", @""),
@ -130,69 +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];
NSSet *propertyKeys = [self.modelClass propertyKeys];
for (NSString *propertyKey in [self.modelClass propertyKeys]) {
id JSONKeyPaths = self.JSONKeyPathsByPropertyKey[propertyKey];
for (NSString *mappedPropertyKey in self.JSONKeyPathsByPropertyKey) {
if (![propertyKeys containsObject:mappedPropertyKey]) {
NSAssert(NO, @"%@ is not a property of %@.", mappedPropertyKey, modelClass);
return nil;
}
id value = self.JSONKeyPathsByPropertyKey[mappedPropertyKey];
if (![value isKindOfClass:NSString.class] && value != NSNull.null) {
NSAssert(NO, @"%@ must either map to a JSON key path or NSNull, got: %@.",mappedPropertyKey, value);
return nil;
}
}
for (NSString *propertyKey in propertyKeys) {
NSString *JSONKeyPath = [self JSONKeyPathForPropertyKey:propertyKey];
if (JSONKeyPath == nil) continue;
if (JSONKeyPaths == nil) continue;
id value;
@try {
value = [JSONDictionary valueForKeyPath:JSONKeyPath];
} @catch (NSException *ex) {
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Invalid JSON dictionary", nil),
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"%1$@ could not be parsed because an invalid JSON dictionary was provided for key path \"%2$@\"", nil), modelClass, JSONKeyPath],
MTLJSONAdapterThrownExceptionErrorKey: ex
};
*error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorInvalidJSONDictionary userInfo:userInfo];
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;
}
return nil;
value = dictionary;
} else {
BOOL success = NO;
value = [JSONDictionary mtl_valueForJSONKeyPath:JSONKeyPaths success:&success error:error];
if (!success) return nil;
}
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
@ -200,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
};
@ -213,92 +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 JSONKeyPathForPropertyKey: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]) {
IMP imp = [self.modelClass methodForSelector:selector];
NSValueTransformer * (*function)(id, SEL) = (__typeof__(function))imp;
NSValueTransformer *transformer = function(self.modelClass, selector);
return transformer;
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 *)JSONKeyPathForPropertyKey:(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
@ -308,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

View File

@ -1,215 +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 a set of property keys used by the adapter to check for an already
// existing managed object when converting the MTLModel to its related
// NSManagedObject.
//
// The adapter will first map any keys provided by this method to the correct
// keys in managedObjectKeysByPropertyKey.
//
// The adapter will then perform a fetch request in the provided context for a
// managed object that matches the MTLModel's managedObjectEntityName and has
// equal values set for the property keys on the MTLModel.
//
// The managed object returned by the fetch request will then be set with all
// values from the MTLModel that the managed object is being converted from.
//
// If a property value of our MTLModel is yet another MTLModel which needs to be
// converted to a managed object, the class for that MTLModel can also implement
// this method to perform its own uniqing.
//
// For example:
// 1. An MTLModel subclass has id_number = 10.
// 2. An NSManagedObject accessible to the adapter's context has idnumber = 10.
// 3. managedObjectKeysByPropertyKey returns @{@"id_number" : @"idnumber"}
// 4. propertyKeysForManagedObjectUniquing returns
// [NSSet setWithObject:@"id_number"];
// 5. Then our fetch request may return this managed object (or another managed
// object with idnumber = 10).
//
// NOTE: If multiple managed objects follow the same uniquing criteria only one
// of them will be set with our MTLModel's values.
+ (NSSet *)propertyKeysForManagedObjectUniquing;
// 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;
// Overriden when merging the value of the given key on the receiver with the
// value of the same key from the given `NSManagedObject` requires custom
// handling.
//
// By default, this method is not implemented, and precedence will be given to
// the value of the receiving model implicitly.
//
// When implemented, this method is called when an existing `NSManagedObject`
// is found for the receiving model, before updating the `NSManagedObject`'s
// properties.
//
// When implementing, you should use `+managedObjectKeysByPropertyKey` to map
// the given `key` to the appropriate `NSManagedObject` property.
- (void)mergeValueForKey:(NSString *)key fromManagedObject:(NSManagedObject *)managedObject;
// Overriden when merging values on the receiver with the given
// `NSManagedObject` requires custom handling.
//
// By default, this method is not implemented, and precedence will be given to
// the values of the receiving model implicitly.
//
// When implemented, this method is called when an existing `NSManagedObject`
// is found for the receiving model, before updating the `NSManagedObject`'s
// properties.
//
// When implementing, you should use `+managedObjectKeysByPropertyKey` to map
// the given `key` to the appropriate `NSManagedObject` property.
//
// If you have also implemented `mergeValueForKey:fromManagedObject:` you have
// to make sure to call `mergeValueForKey:fromManagedObject:` from this method
// when appropriate.
- (void)mergeValuesForKeysFromManagedObject:(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;
// The fetch request to find an existing managed object based on
// `+propertyKeysForManagedObjectUniquing` failed.
extern const NSInteger MTLManagedObjectAdapterErrorUniqueFetchRequestFailed;
// 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;
// The model's implementation of +managedObjectKeysByPropertyKey included a key
// which does not actually exist in +propertyKeys.
extern const NSInteger MTLManagedObjectAdapterErrorInvalidManagedObjectMapping;
// 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.
+ (id)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model insertingIntoContext:(NSManagedObjectContext *)context error:(NSError **)error;
@end

View File

@ -1,641 +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 "MTLEXTScope.h"
#import "MTLModel.h"
#import "MTLReflection.h"
#import "NSArray+MTLManipulationAdditions.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;
const NSInteger MTLManagedObjectAdapterErrorUniqueFetchRequestFailed = 7;
const NSInteger MTLManagedObjectAdapterErrorInvalidManagedObjectMapping = 8;
// 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;
}
@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.
- (id)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.
+ (id)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;
// Looks at propertyKeysForManagedObjectUniquing and forms an NSPredicate
// using the uniquing keys and the provided model.
- (NSPredicate *)uniquingPredicateForModel:(MTLModel<MTLManagedObjectSerializing> *)model;
@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 {
NSSet *propertyKeys = [modelClass propertyKeys];
for (NSString *mappedPropertyKey in [modelClass managedObjectKeysByPropertyKey]) {
if ([propertyKeys containsObject:mappedPropertyKey]) continue;
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Invalid entity attribute mapping", nil),
NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"%1$@ could not be parsed because its entity attribute mapping contains illegal property keys.", nil), modelClass]
};
*error = [NSError errorWithDomain:MTLManagedObjectAdapterErrorDomain code:MTLManagedObjectAdapterErrorInvalidManagedObjectMapping userInfo:userInfo];
}
return nil;
}
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];
}
- (id)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");
Class fetchRequestClass = NSClassFromString(@"NSFetchRequest");
NSAssert(fetchRequestClass != nil, @"CoreData.framework must be linked to use MTLManagedObjectAdapter");
// If a uniquing predicate is provided, perform a fetch request to guarantee a unique managed object.
__block NSManagedObject *managedObject = nil;
NSPredicate *uniquingPredicate = [self uniquingPredicateForModel:model];
if (uniquingPredicate != nil) {
__block NSError *fetchRequestError = nil;
__block BOOL encountedError = NO;
managedObject = performInContext(context, ^ id {
NSFetchRequest *fetchRequest = [[fetchRequestClass alloc] init];
fetchRequest.entity = [entityDescriptionClass entityForName:entityName inManagedObjectContext:context];
fetchRequest.predicate = uniquingPredicate;
fetchRequest.returnsObjectsAsFaults = NO;
fetchRequest.fetchLimit = 1;
NSArray *results = [context executeFetchRequest:fetchRequest error:&fetchRequestError];
if (results == nil) {
encountedError = YES;
if (error != NULL) {
NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"Failed to fetch a managed object for uniqing predicate \"%@\".", @""), uniquingPredicate];
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not serialize managed object", @""),
NSLocalizedFailureReasonErrorKey: failureReason,
};
fetchRequestError = [NSError errorWithDomain:MTLManagedObjectAdapterErrorDomain code:MTLManagedObjectAdapterErrorUniqueFetchRequestFailed userInfo:userInfo];
}
return nil;
}
return results.mtl_firstObject;
});
if (encountedError && error != NULL) {
*error = fetchRequestError;
return nil;
}
}
if (managedObject == nil) {
managedObject = [entityDescriptionClass insertNewObjectForEntityForName:entityName inManagedObjectContext:context];
} else {
// Our CoreData store already has data for this model, we need to merge
[self mergeValuesOfModel:model forKeysFromManagedObject:managedObject];
}
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;
}
// Assign all errors to this variable to work around a memory problem.
//
// See https://github.com/github/Mantle/pull/120 for more context.
__block NSError *tmpError;
// 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:&tmpError]) return NO;
[managedObject setValue:transformedValue forKey:managedObjectKey];
return YES;
};
NSManagedObject * (^objectForRelationshipFromModel)(id) = ^ id (id model) {
if (![model isKindOfClass:MTLModel.class] || ![model conformsToProtocol:@protocol(MTLManagedObjectSerializing)]) {
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
};
tmpError = [NSError errorWithDomain:MTLManagedObjectAdapterErrorDomain code:MTLManagedObjectAdapterErrorUnsupportedRelationshipClass userInfo:userInfo];
return nil;
}
return [self.class managedObjectFromModel:model insertingIntoContext:context processedObjects:processedObjects error:&tmpError];
};
BOOL (^serializeRelationship)(NSRelationshipDescription *) = ^(NSRelationshipDescription *relationshipDescription) {
if (value == nil) return YES;
if ([relationshipDescription isToMany]) {
if (![value conformsToProtocol:@protocol(NSFastEnumeration)]) {
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
};
tmpError = [NSError errorWithDomain:MTLManagedObjectAdapterErrorDomain code:MTLManagedObjectAdapterErrorUnsupportedRelationshipClass userInfo:userInfo];
return NO;
}
id relationshipCollection;
if ([relationshipDescription isOrdered]) {
relationshipCollection = [NSMutableOrderedSet orderedSet];
} else {
relationshipCollection = [NSMutableSet set];
}
for (MTLModel *model in value) {
NSManagedObject *nestedObject = objectForRelationshipFromModel(model);
if (nestedObject == nil) return NO;
[relationshipCollection addObject:nestedObject];
}
[managedObject setValue:relationshipCollection forKey:managedObjectKey];
} 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) {
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
};
tmpError = [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 {
NSString *failureReason = [NSString stringWithFormat:NSLocalizedString(@"Property descriptions of class %@ are unsupported.", @""), propertyClassName];
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not serialize managed object", @""),
NSLocalizedFailureReasonErrorKey: failureReason
};
tmpError = [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 != nil && ![managedObject validateForInsert:&tmpError]) {
managedObject = performInContext(context, ^ id {
[context deleteObject:managedObject];
return nil;
});
}
if (error != NULL) {
*error = tmpError;
}
return managedObject;
}
+ (id)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];
}
+ (id)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]) {
IMP imp = [self.modelClass methodForSelector:selector];
NSValueTransformer * (*function)(id, SEL) = (__typeof__(function))imp;
NSValueTransformer *transformer = function(self.modelClass, selector);
return transformer;
}
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;
}
}
- (void)mergeValueOfModel:(MTLModel<MTLManagedObjectSerializing> *)model forKey:(NSString *)key fromManagedObject:(NSManagedObject *)managedObject {
[model mergeValueForKey:key fromManagedObject:managedObject];
}
- (void)mergeValuesOfModel:(MTLModel<MTLManagedObjectSerializing> *)model forKeysFromManagedObject:(NSManagedObject *)managedObject {
if ([model respondsToSelector:@selector(mergeValuesForKeysFromManagedObject:)]) {
[model mergeValuesForKeysFromManagedObject:managedObject];
} else if ([model respondsToSelector:@selector(mergeValueForKey:fromManagedObject:)]) {
[[model.class managedObjectKeysByPropertyKey] enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *managedObjectKey, BOOL *stop) {
[self mergeValueOfModel:model forKey:key fromManagedObject:managedObject];
}];
}
}
- (NSPredicate *)uniquingPredicateForModel:(MTLModel<MTLManagedObjectSerializing> *)model {
if (![self.modelClass respondsToSelector:@selector(propertyKeysForManagedObjectUniquing)]) return nil;
NSSet *propertyKeys = [self.modelClass propertyKeysForManagedObjectUniquing];
if (propertyKeys == nil) return nil;
NSAssert(propertyKeys.count > 0, @"+propertyKeysForManagedObjectUniquing must not be empty.");
NSMutableArray *subpredicates = [NSMutableArray array];
for (NSString *propertyKey in propertyKeys) {
NSString *managedObjectKey = [self managedObjectKeyForKey:propertyKey];
NSAssert(managedObjectKey != nil, @"%@ must map to a managed object key.", propertyKey);
id transformedValue = [model valueForKeyPath:propertyKey];
NSValueTransformer *attributeTransformer = [self entityAttributeTransformerForKey:propertyKey];
if (attributeTransformer != nil) transformedValue = [attributeTransformer transformedValue:transformedValue];
NSPredicate *subpredicate = [NSPredicate predicateWithFormat:@"%K == %@", managedObjectKey, transformedValue];
[subpredicates addObject:subpredicate];
}
return [NSCompoundPredicate andPredicateWithSubpredicates:subpredicates];
}
@end

View File

@ -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

View File

@ -7,10 +7,9 @@
//
#import "MTLModel+NSCoding.h"
#import "MTLEXTRuntimeExtensions.h"
#import "MTLEXTScope.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";

View File

@ -8,118 +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.
//
// 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;
// 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;
// Compares the receiver with another object for equality.
//
// The default implementation is equivalent to comparing both models'
// -dictionaryValue.
//
// Note that this may lead to infinite loops if the receiver holds a circular
// reference to another MTLModel and both use the default behavior.
// It is recommended to override -isEqual: in this scenario.
- (BOOL)isEqual:(id)object;
/// Returns the keys for all @property declarations, except for `readonly`
/// properties without ivars, or properties on MTLModel itself.
+ (NSSet *)propertyKeys;
// A string that describes the contents of the receiver.
//
// The default implementation is based on the receiver's class and its
// -dictionaryValue.
//
// Note that this may lead to infinite loops if the receiver holds a circular
// reference to another MTLModel and both use the default behavior.
// It is recommended to override -description in this scenario.
- (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.
/// 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

View File

@ -8,19 +8,22 @@
#import "NSError+MTLModelException.h"
#import "MTLModel.h"
#import "MTLEXTRuntimeExtensions.h"
#import "MTLEXTScope.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:");
@ -176,12 +263,13 @@ static BOOL MTLValidateAndSetValue(id obj, NSString *key, id value, BOOL forceUp
}
IMP imp = [self methodForSelector:selector];
void (*function)(id, SEL, MTLModel *) = (__typeof__(function))imp;
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;
@ -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];

View File

@ -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)));

View 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

View 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";

View File

@ -8,22 +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
@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

View File

@ -26,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];
}
@ -57,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
@ -82,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

View File

@ -15,12 +15,13 @@ FOUNDATION_EXPORT double MantleVersionNumber;
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>

View File

@ -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

View 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

View 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

View File

@ -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

View File

@ -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

View 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

View 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

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -8,77 +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;
/// 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;
// 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 that will map from keys to values in dictionary
// for forward transformation, and from values to keys for reverse
// transformations. If no matching key or value can be found, the respective
// default value is returned.
+ (NSValueTransformer *)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;
// 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 *)mtl_valueMappingTransformerWithDictionary:(NSDictionary *)dictionary;
@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 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

View File

@ -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,98 +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 (id 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 (id 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];
+ (NSValueTransformer *)mtl_JSONArrayTransformerWithModelClass:(Class)modelClass {
NSValueTransformer *dictionaryTransformer = [self mtl_JSONDictionaryTransformerWithModelClass:modelClass];
return [MTLValueTransformer
reversibleTransformerWithForwardBlock:^ id (NSArray *dictionaries) {
if (dictionaries == nil) return nil;
NSAssert([dictionaries isKindOfClass:NSArray.class], @"Expected an array of dictionaries, got: %@", dictionaries);
NSMutableArray *models = [NSMutableArray arrayWithCapacity:dictionaries.count];
for (id JSONDictionary in dictionaries) {
if (JSONDictionary == NSNull.null) {
[models addObject:NSNull.null];
continue;
}
NSAssert([JSONDictionary isKindOfClass:NSDictionary.class], @"Expected a dictionary or an NSNull, got: %@", JSONDictionary);
id model = [dictionaryTransformer transformedValue:JSONDictionary];
if (model == nil) continue;
[models addObject:model];
}
return models;
}
reverseBlock:^ id (NSArray *models) {
if (models == nil) return nil;
NSAssert([models isKindOfClass:NSArray.class], @"Expected an array of MTLModels, got: %@", models);
NSMutableArray *dictionaries = [NSMutableArray arrayWithCapacity:models.count];
for (id model in models) {
if (model == NSNull.null) {
[dictionaries 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];
}
NSAssert([model isKindOfClass:MTLModel.class], @"Expected an MTLModel or an NSNull, got: %@", model);
NSDictionary *dict = [dictionaryTransformer reverseTransformedValue:model];
if (dict == nil) continue;
[dictionaries addObject:dict];
if (transformedValue == nil) continue;
[transformedValues addObject:transformedValue];
}
return dictionaries;
}];
return transformedValues;
};
}
if (reverseBlock != nil) {
return [MTLValueTransformer transformerUsingForwardBlock:forwardBlock reverseBlock:reverseBlock];
} else {
return [MTLValueTransformer transformerUsingForwardBlock:forwardBlock];
}
}
+ (NSValueTransformer *)mtl_valueMappingTransformerWithDictionary:(NSDictionary *)dictionary {
return [self mtl_valueMappingTransformerWithDictionary:dictionary defaultValue:nil reverseDefaultValue:nil];
+ (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<NSCopying> key) {
return dictionary[key ?: NSNull.null] ?: defaultValue;
} reverseBlock:^(id object) {
__block id result = nil;
[dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id anObject, BOOL *stop) {
if ([object isEqual:anObject]) {
result = key;
*stop = YES;
return [MTLValueTransformer
transformerUsingForwardBlock:^ id (id <NSCopying> key, BOOL *success, NSError **error) {
return dictionary[key ?: NSNull.null] ?: defaultValue;
}
}];
return result ?: reverseDefaultValue;
}];
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 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

View File

@ -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

View File

@ -7,7 +7,7 @@
// Released under the MIT license.
//
#import "MTLEXTRuntimeExtensions.h"
#import "EXTRuntimeExtensions.h"
#import <Foundation/Foundation.h>

View File

@ -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);

View File

@ -7,7 +7,7 @@
// Released under the MIT license.
//
#import "MTLEXTScope.h"
#import "EXTScope.h"
void mtl_executeCleanupBlock (__strong mtl_cleanupBlock_t *block) {
(*block)();

View File

@ -7,7 +7,7 @@
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>org.mantle.$(PRODUCT_NAME:rfc1034identifier)</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>

View File

@ -1,52 +0,0 @@
//
// MTLCoreDataObjects.h
// Mantle
//
// Created by Robert Böhnke on 9/4/13.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <CoreData/CoreData.h>
@class MTLParent;
@interface MTLChild : NSManagedObject
+ (instancetype)insertInManagedObjectContext:(NSManagedObjectContext*)moc;
@property (readwrite, nonatomic, strong) NSNumber *childID;
@property (readwrite, nonatomic, strong) MTLParent *parent1;
@property (readwrite, nonatomic, strong) MTLParent *parent2;
@end
@interface MTLParent : NSManagedObject
+ (instancetype)insertInManagedObjectContext:(NSManagedObjectContext*)moc;
@property (readwrite, nonatomic, strong) NSDate *date;
@property (readwrite, nonatomic, strong) NSNumber* number;
@property (readwrite, nonatomic, copy) NSString *string;
@property (readwrite, nonatomic, copy) NSOrderedSet *orderedChildren;
@property (readwrite, nonatomic, copy) NSSet *unorderedChildren;
@end
@interface MTLParent (CoreDataGeneratedAccesssors)
- (void)addOrderedChildren:(NSOrderedSet*)orderedChildren;
- (void)removeOrderedChildren:(NSOrderedSet*)orderedChildren;
- (void)addOrderedChildrenObject:(MTLChild*)child;
- (void)removeOrderedChildrenObject:(MTLChild*)child;
- (void)addUnorderedChildren:(NSSet*)unorderedChildren;
- (void)removeUnorderedChildren:(NSSet*)unorderedChildren;
- (void)addUnorderedChildrenObject:(MTLChild*)child;
- (void)removeUnorderedChildrenObject:(MTLChild*)child;
@end

View File

@ -1,56 +0,0 @@
//
// MTLCoreDataObjects.m
// Mantle
//
// Created by Robert Böhnke on 9/4/13.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "MTLCoreDataObjects.h"
@implementation MTLChild
@dynamic childID;
@dynamic parent1;
@dynamic parent2;
+ (id)insertInManagedObjectContext:(NSManagedObjectContext*)moc {
NSParameterAssert(moc != nil);
return [NSEntityDescription insertNewObjectForEntityForName:@"Child" inManagedObjectContext:moc];
}
@end
@implementation MTLParent
@dynamic date;
@dynamic number;
@dynamic string;
@dynamic unorderedChildren;
@dynamic orderedChildren;
+ (id)insertInManagedObjectContext:(NSManagedObjectContext*)moc {
NSParameterAssert(moc != nil);
return [NSEntityDescription insertNewObjectForEntityForName:@"Parent" inManagedObjectContext:moc];
}
// Working around http://openradar.appspot.com/10114310
- (void)addOrderedChildrenObject:(MTLChild*)child {
NSMutableOrderedSet *mutableCopy = [self.orderedChildren mutableCopy];
[mutableCopy addObject:child];
self.orderedChildren = [mutableCopy copy];
}
- (void)removeOrderedChildrenObject:(MTLChild*)child {
NSMutableOrderedSet *mutableCopy = [self.orderedChildren mutableCopy];
[mutableCopy removeObject:child];
self.orderedChildren = [mutableCopy copy];
}
@end

View File

@ -1,64 +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
// Model for Parent that has custom merging behaviour for CoreData
@interface MTLParentMergingTestModel : MTLParentTestModel
@end
// Model for Parent entity which doesn't serialize required properties
@interface MTLParentIncorrectTestModel : MTLModel <MTLManagedObjectSerializing>
@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
@interface MTLBadChildTestModel : MTLModel <MTLManagedObjectSerializing>
@property (nonatomic, assign) NSUInteger childID;
@end
// Claims to correspond to the `Empty` entity which lacks the `notSupported`
// property.
@interface MTLFailureModel : MTLModel <MTLManagedObjectSerializing>
// Not present in the `Empty` entity.
@property (nonatomic, assign) NSString *notSupported;
@end
// Maps a non-existant property "name" to the "string" attribute.
@interface MTLIllegalManagedObjectMappingModel : MTLModel <MTLManagedObjectSerializing>
@end

View File

@ -1,154 +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"
};
}
+ (NSSet *)propertyKeysForManagedObjectUniquing {
return [NSSet setWithObject:@"numberString"];
}
+ (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,
};
}
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p>", self.class, self];
}
@end
@implementation MTLParentMergingTestModel
- (void)mergeValueForKey:(NSString *)key fromManagedObject:(NSManagedObject *)managedObject {
if ([key isEqualToString:@"requiredString"]) {
self.requiredString = @"merged";
}
}
@end
@implementation MTLParentIncorrectTestModel
+ (NSString *)managedObjectEntityName {
return @"Parent";
}
+ (NSDictionary *)managedObjectKeysByPropertyKey {
return @{};
}
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p>", self.class, self];
}
@end
@implementation MTLChildTestModel
+ (NSString *)managedObjectEntityName {
return @"Child";
}
+ (NSDictionary *)managedObjectKeysByPropertyKey {
return @{};
}
+ (NSSet *)propertyKeysForManagedObjectUniquing {
return [NSSet setWithObjects:@"childID", nil];
}
+ (NSDictionary *)relationshipModelClassesByPropertyKey {
return @{
@"parent1": MTLParentTestModel.class,
@"parent2": MTLParentTestModel.class,
};
}
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p>", self.class, self];
}
@end
@implementation MTLBadChildTestModel
+ (NSString *)managedObjectEntityName {
return @"BadChild";
}
+ (NSDictionary *)managedObjectKeysByPropertyKey {
return @{};
}
+ (NSSet *)propertyKeysForManagedObjectUniquing {
return [NSSet setWithObjects:@"childID", nil];
}
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p>", self.class, self];
}
@end
@implementation MTLFailureModel
+ (NSDictionary *)managedObjectKeysByPropertyKey {
return @{};
}
+ (NSString *)managedObjectEntityName {
return @"Empty";
}
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p>", self.class, self];
}
@end
@implementation MTLIllegalManagedObjectMappingModel
+ (NSString *)managedObjectEntityName {
return @"Parent";
}
+ (NSDictionary *)managedObjectKeysByPropertyKey {
return @{
@"name": @"username"
};
}
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p>", self.class, self];
}
@end

View File

@ -38,27 +38,27 @@ describe(@"-mtl_dictionaryByAddingEntriesFromDictionary:", ^{
});
});
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"]];
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];
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));
});
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));
});

View 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

View File

@ -10,29 +10,40 @@
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import "MTLTestJSONAdapter.h"
#import "MTLTestModel.h"
#import "MTLTransformerErrorExamples.h"
@interface MTLJSONAdapter (SpecExtensions)
// Used for testing transformer lifetimes.
- (NSValueTransformer *)JSONTransformerForKey:(NSString *)key;
+ (NSValueTransformer *)NSDateJSONTransformer;
@end
@implementation MTLJSONAdapter (SpecExtensions)
+ (NSValueTransformer *)NSDateJSONTransformer {
return [[NSValueTransformer alloc] init];
}
@end
QuickSpecBegin(MTLJSONAdapterSpec)
it(@"should initialize from JSON", ^{
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];
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());
MTLTestModel *model = (id)adapter.model;
expect(model).notTo(beNil());
expect(model.name).to(beNil());
expect(@(model.count)).to(equal(@5));
@ -43,26 +54,9 @@ it(@"should initialize from JSON", ^{
@"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", ^{
@ -81,7 +75,35 @@ it(@"should initialize nested key paths from JSON", ^{
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(@"it should initialize properties with multiple key paths from JSON", ^{
NSDictionary *values = @{
@"location": @20,
@"length": @12,
@"nested": @{
@"location": @12,
@"length": @34
}
};
NSError *error = nil;
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",^{
@ -116,10 +138,11 @@ it(@"should support key paths across arrays", ^{
NSError *error = nil;
MTLArrayTestModel *model = [MTLJSONAdapter modelOfClass:MTLArrayTestModel.class fromJSONDictionary:values error:&error];
expect(model).notTo(beNil());
expect(error).to(beNil());
expect(model).to(beNil());
expect(error).notTo(beNil());
expect(model.names).to(equal((@[ @"foo", @"bar", @"baz" ])));
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",^{
@ -139,25 +162,6 @@ it(@"should initialize without returning any error when using a JSON dictionary
expect(model.nestedName).to(beNil());
});
it(@"should return nil and an error with a nil JSON dictionary", ^{
NSError *error = nil;
MTLJSONAdapter *adapter = [[MTLJSONAdapter alloc] initWithJSONDictionary:nil modelClass:MTLTestModel.class error:&error];
expect(adapter).to(beNil());
expect(error).notTo(beNil());
expect(error.domain).to(equal(MTLJSONAdapterErrorDomain));
expect(@(error.code)).to(equal(@(MTLJSONAdapterErrorInvalidJSONDictionary)));
});
it(@"should return nil and an error with a wrong data type as dictionary", ^{
NSError *error = nil;
id wrongDictionary = @"";
MTLJSONAdapter *adapter = [[MTLJSONAdapter alloc] initWithJSONDictionary:wrongDictionary modelClass:MTLTestModel.class error:&error];
expect(adapter).to(beNil());
expect(error).notTo(beNil());
expect(error.domain).to(equal(MTLJSONAdapterErrorDomain));
expect(@(error.code)).to(equal(@(MTLJSONAdapterErrorInvalidJSONDictionary)));
});
it(@"should ignore unrecognized JSON keys", ^{
NSDictionary *values = @{
@"foobar": @"foo",
@ -189,6 +193,172 @@ it(@"should fail to initialize if JSON dictionary validation fails", ^{
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", ^{
NSDictionary *values = @{
@"username": @"foo",
@ -205,7 +375,51 @@ it(@"should parse a different model class", ^{
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", ^{
@ -218,6 +432,119 @@ it(@"should return an error when no suitable model class is found", ^{
expect(@(error.code)).to(equal(@(MTLJSONAdapterErrorNoClassFound)));
});
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"
@ -277,7 +604,10 @@ it(@"should return an array of dictionaries from models", ^{
MTLTestModel *model2 = [[MTLTestModel alloc] init];
model2.name = @"bar";
NSArray *JSONArray = [MTLJSONAdapter JSONArrayFromModels:@[ model1, model2 ]];
NSError *error;
NSArray *JSONArray = [MTLJSONAdapter JSONArrayFromModels:@[ model1, model2 ] error:&error];
expect(error).to(beNil());
expect(JSONArray).notTo(beNil());
expect(@(JSONArray.count)).to(equal(@2));
@ -286,13 +616,10 @@ it(@"should return an array of dictionaries from models", ^{
});
it(@"should not leak transformers", ^{
MTLTestModel *model = [[MTLTestModel alloc] init];
MTLJSONAdapter *adapter = [[MTLJSONAdapter alloc] initWithModel:model];
__weak id weakTransformer;
@autoreleasepool {
id transformer = [adapter JSONTransformerForKey:@"count"];
id transformer = [MTLJSONAdapter transformerForModelPropertiesOfClass:NSDate.class];
weakTransformer = transformer;
expect(transformer).notTo(beNil());
@ -301,4 +628,65 @@ it(@"should not leak transformers", ^{
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

View File

@ -1,410 +0,0 @@
//
// MTLManagedObjectAdapterSpec.m
// Mantle
//
// Created by Justin Spahr-Summers on 2013-05-17.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Mantle/Mantle.h>
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import "MTLCoreDataObjects.h"
#import "MTLCoreDataTestModels.h"
QuickSpecBegin(MTLManagedObjectAdapterSpec)
__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 MTLParent *parent;
__block NSDate *date;
__block NSString *numberString;
__block NSString *requiredString;
beforeEach(^{
date = [NSDate date];
numberString = @"123";
requiredString = @"foobar";
parent = [MTLParent insertInManagedObjectContext:context];
expect(parent).notTo(beNil());
for (NSUInteger i = 0; i < 3; i++) {
MTLChild *child = [MTLChild insertInManagedObjectContext:context];
expect(child).notTo(beNil());
child.childID = @(i);
[parent addOrderedChildrenObject:child];
}
for (NSUInteger i = 3; i < 6; i++) {
MTLChild *child = [MTLChild insertInManagedObjectContext:context];
expect(child).notTo(beNil());
child.childID = @(i);
[parent addUnorderedChildrenObject:child];
}
parent.string = requiredString;
__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(beAnInstanceOf(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(beAnInstanceOf(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(beAnInstanceOf(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;
MTLParent *parent = [MTLManagedObjectAdapter managedObjectFromModel:parentModel insertingIntoContext:context error:&error];
expect(parent).notTo(beNil());
expect(parent).to(beAnInstanceOf(MTLParent.class));
expect(error).to(beNil());
expect(parent.entity).to(equal(parentEntity));
expect(context.insertedObjects).to(contain(parent));
expect(parent.date).to(equal(parentModel.date));
expect(parent.number.stringValue).to(equal(parentModel.numberString));
expect(parent.string).to(equal(parentModel.requiredString));
expect(@(parent.orderedChildren.count)).to(equal(@3));
expect(@(parent.unorderedChildren.count)).to(equal(@3));
for (NSUInteger i = 0; i < 3; i++) {
MTLChild *child = parent.orderedChildren[i];
expect(child).to(beAnInstanceOf(MTLChild.class));
expect(child.entity).to(equal(childEntity));
expect(context.insertedObjects).to(contain(child));
expect(child.childID).to(equal(@(i)));
expect(child.parent1).to(beNil());
expect(child.parent2).to(equal(parent));
}
for (MTLChild *child in parent.unorderedChildren) {
expect(child).to(beAnInstanceOf(MTLChild.class));
expect(child.entity).to(equal(childEntity));
expect(context.insertedObjects).to(contain(child));
expect(child.childID).to(beGreaterThanOrEqualTo(@3));
expect(child.childID).to(beLessThan(@6));
expect(child.parent1).to(equal(parent));
expect(child.parent2).to(beNil());
}
expect(@([context save:&error])).to(beTruthy());
expect(error).to(beNil());
});
it(@"should return an error if a model object could not be inserted", ^{
MTLFailureModel *failureModel = [MTLFailureModel modelWithDictionary:@{
@"notSupported": @"foobar"
} error:NULL];
__block NSError *error = nil;
NSManagedObject *failure =[MTLManagedObjectAdapter managedObjectFromModel:failureModel insertingIntoContext:context error:&error];
expect(failure).to(beNil());
expect(error).notTo(beNil());
});
it(@"should return an error if model doesn't validate for attribute description", ^{
MTLParentTestModel *parentModel = [MTLParentTestModel modelWithDictionary:@{} error:NULL];
NSError *error;
NSManagedObject *managedObject = [MTLManagedObjectAdapter managedObjectFromModel:parentModel insertingIntoContext:context error:&error];
expect(managedObject).to(beNil());
expect(error).notTo(beNil());
});
it(@"should return nil and error with an illegal JSON mapping", ^{
MTLParent *parent = [MTLParent insertInManagedObjectContext:context];
expect(parent).notTo(beNil());
parent.string = @"foobar";
NSError *error = nil;
MTLIllegalManagedObjectMappingModel *model = [MTLManagedObjectAdapter modelOfClass:MTLIllegalManagedObjectMappingModel.class fromManagedObject:parent error:&error];
expect(model).to(beNil());
expect(error).notTo(beNil());
expect(error.domain).to(equal(MTLManagedObjectAdapterErrorDomain));
expect(@(error.code)).to(equal(@(MTLManagedObjectAdapterErrorInvalidManagedObjectMapping)));
});
it(@"should return an error if model doesn't validate for insert", ^{
MTLParentIncorrectTestModel *parentModel = [MTLParentIncorrectTestModel modelWithDictionary:@{} error:NULL];
NSError *error;
NSManagedObject *managedObject = [MTLManagedObjectAdapter managedObjectFromModel:parentModel insertingIntoContext:context error:&error];
expect(managedObject).to(beNil());
expect(error).notTo(beNil());
});
it(@"should respect the uniqueness constraint", ^{
NSError *errorOne;
MTLParent *parentOne = [MTLManagedObjectAdapter managedObjectFromModel:parentModel insertingIntoContext:context error:&errorOne];
expect(parentOne).notTo(beNil());
expect(errorOne).to(beNil());
NSError *errorTwo;
MTLParent *parentTwo = [MTLManagedObjectAdapter managedObjectFromModel:parentModel insertingIntoContext:context error:&errorTwo];
expect(parentTwo).notTo(beNil());
expect(errorTwo).to(beNil());
expect(parentOne.objectID).to(equal(parentTwo.objectID));
});
it(@"should update relationships for an existing object", ^{
NSError *error;
MTLParent *parentOne = [MTLManagedObjectAdapter managedObjectFromModel:parentModel insertingIntoContext:context error:&error];
expect(parentOne).notTo(beNil());
expect(error).to(beNil());
expect(@(parentOne.orderedChildren.count)).to(equal(@3));
expect(@(parentOne.unorderedChildren.count)).to(equal(@3));
MTLChild *child1Parent1 = parentOne.orderedChildren[0];
MTLChild *child2Parent1 = parentOne.orderedChildren[1];
MTLChild *child3Parent1 = parentOne.orderedChildren[2];
MTLParentTestModel *parentModelCopy = [parentModel copy];
[[parentModelCopy mutableOrderedSetValueForKey:@"orderedChildren"] removeObjectAtIndex:1];
MTLChildTestModel *childToDeleteModel = [parentModelCopy.unorderedChildren anyObject];
[[parentModelCopy mutableSetValueForKey:@"unorderedChildren"] removeObject:childToDeleteModel];
MTLParent *parentTwo = [MTLManagedObjectAdapter managedObjectFromModel:parentModelCopy insertingIntoContext:context error:&error];
expect(parentTwo).notTo(beNil());
expect(error).to(beNil());
expect(@(parentTwo.orderedChildren.count)).to(equal(@2));
expect(@(parentTwo.unorderedChildren.count)).to(equal(@2));
for (MTLChild *child in parentTwo.orderedChildren) {
expect(child.childID).notTo(equal(child2Parent1.childID));
}
for (MTLChild *child in parentTwo.unorderedChildren) {
expect(child.childID).notTo(equal(@(childToDeleteModel.childID)));
}
MTLChild *child1Parent2 = parentTwo.orderedChildren[0];
MTLChild *child2Parent2 = parentTwo.orderedChildren[1];
expect(child1Parent2).to(equal(child1Parent1));
expect(child2Parent2).to(equal(child3Parent1));
});
it(@"should try to merge existing values before overwriting data", ^{
NSError *error;
MTLParent *parentOne = [MTLManagedObjectAdapter managedObjectFromModel:parentModel insertingIntoContext:context error:&error];
expect(parentOne).notTo(beNil());
expect(error).to(beNil());
NSDictionary *updates = @{
@"date": [NSDate date],
@"numberString": @"1234",
@"requiredString": @"We expect this string to be 'merged' after insertion"
};
MTLParentMergingTestModel *updatedParentModel = [MTLParentMergingTestModel modelWithDictionary:updates error:NULL];
expect(parentModel).notTo(beNil());
BOOL saveSuccessful = [context save:nil];
expect(@(saveSuccessful)).to(beTruthy());
NSString *initialValueOfRequiredString = updatedParentModel.requiredString;
MTLParent *updatedParentOne = [MTLManagedObjectAdapter managedObjectFromModel:updatedParentModel insertingIntoContext:context error:&error];
expect(updatedParentOne).notTo(beNil());
expect(updatedParentOne.string).notTo(equal(initialValueOfRequiredString));
expect(updatedParentOne.string).to(equal(@"merged"));
});
});
});
describe(@"with a main queue context", ^{
__block NSManagedObjectContext *context;
beforeEach(^{
context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
expect(context).notTo(beNil());
context.undoManager = nil;
context.persistentStoreCoordinator = persistentStoreCoordinator;
});
it(@"should not deadlock on the main thread", ^{
MTLParent *parent = [MTLParent insertInManagedObjectContext:context];
expect(parent).notTo(beNil());
parent.string = @"foobar";
NSError *error = nil;
MTLParentTestModel *parentModel = [MTLManagedObjectAdapter modelOfClass:MTLParentTestModel.class fromManagedObject:parent error:&error];
expect(parentModel).to(beAnInstanceOf(MTLParentTestModel.class));
expect(error).to(beNil());
});
});
describe(@"with a child that fails serialization", ^{
__block NSManagedObjectContext *context;
__block NSEntityDescription *parentEntity;
__block NSEntityDescription *childEntity;
__block MTLParentTestModel *parentModel;
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:@"BadChild" inManagedObjectContext:context];
expect(childEntity).notTo(beNil());
parentModel = [MTLParentTestModel modelWithDictionary:@{
@"date": [NSDate date],
@"numberString": @"1234",
@"requiredString": @"foobar"
} error:NULL];
expect(parentModel).notTo(beNil());
NSMutableArray *orderedChildren = [NSMutableArray array];
for (NSUInteger i = 3; i < 6; i++) {
MTLBadChildTestModel *child = [MTLBadChildTestModel modelWithDictionary:@{
@"childID": @(i)
} error:NULL];
expect(child).notTo(beNil());
[orderedChildren addObject:child];
}
parentModel.orderedChildren = orderedChildren;
});
it(@"should insert a managed object with children", ^{
__block NSError *error = nil;
MTLParent *parent = [MTLManagedObjectAdapter managedObjectFromModel:parentModel insertingIntoContext:context error:&error];
expect(parent).to(beNil());
expect(error).notTo(beNil());
expect(@([context save:&error])).to(beTruthy());
});
});
QuickSpecEnd

View File

@ -103,6 +103,13 @@ describe(@"with a dictionary of values", ^{
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", ^{
@ -128,6 +135,32 @@ it(@"should merge two models together", ^{
expect(@(target.count)).to(equal(@8));
});
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;
@ -167,4 +200,5 @@ describe(@"merging with model subclasses", ^{
});
});
QuickSpecEnd

View File

@ -9,6 +9,7 @@
#import <Mantle/Mantle.h>
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import "MTLTransformerErrorExamples.h"
#import "MTLTestModel.h"
@ -21,111 +22,161 @@ enum : NSInteger {
QuickSpecBegin(MTLPredefinedTransformerAdditions)
it(@"should define a URL value transformer", ^{
NSValueTransformer *transformer = [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
expect(transformer).notTo(beNil());
expect(@([transformer.class allowsReverseTransformation])).to(beTruthy());
describe(@"The URL transformer", ^{
__block NSValueTransformer *transformer;
NSString *URLString = @"http://www.github.com/";
expect([transformer transformedValue:URLString]).to(equal([NSURL URLWithString:URLString]));
expect([transformer reverseTransformedValue:[NSURL URLWithString:URLString]]).to(equal(URLString));
beforeEach(^{
transformer = [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
expect([transformer transformedValue:nil]).to(beNil());
expect([transformer reverseTransformedValue:nil]).to(beNil());
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(@"The UUID transformer", ^{
__block NSValueTransformer *transformer;
beforeEach(^{
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
};
});
});
describe(@"JSON transformers", ^{
describe(@"dictionary transformer", ^{
__block NSValueTransformer *transformer;
describe(@"The number transformer", ^{
__block NSValueTransformer *transformer;
__block MTLTestModel *model;
__block NSDictionary *JSONDictionary;
beforeEach(^{
transformer = [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName];
expect(transformer).notTo(beNil());
expect(@([transformer.class allowsReverseTransformation])).to(beTruthy());
});
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);
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(^{
model = [[MTLTestModel alloc] init];
JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:model];
transformer = [NSValueTransformer mtl_JSONDictionaryTransformerWithModelClass:MTLTestModel.class];
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:JSONDictionary]).to(equal(model));
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:model]).to(equal(JSONDictionary));
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;
__block NSArray *models;
__block NSArray *JSONDictionaries;
describe(@"when called with a non-reversible transformer", ^{
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];
expect(dict).notTo(beNil());
[mutableDictionaries addObject:dict];
}
uniqueModels[2] = NSNull.null;
mutableDictionaries[2] = NSNull.null;
models = [uniqueModels copy];
JSONDictionaries = [mutableDictionaries copy];
transformer = [NSValueTransformer mtl_JSONArrayTransformerWithModelClass:MTLTestModel.class];
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", ^{
@ -170,4 +221,84 @@ describe(@"value mapping transformer", ^{
});
});
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

View 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

View 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

View File

@ -12,6 +12,8 @@ extern NSString * const MTLTestModelErrorDomain;
extern const NSInteger MTLTestModelNameTooLong;
extern const NSInteger MTLTestModelNameMissing;
@interface MTLEmptyTestModel : MTLModel
@end
@ -38,7 +40,7 @@ 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
@ -62,7 +64,7 @@ extern const NSInteger MTLTestModelNameMissing;
@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;
@ -73,6 +75,155 @@ extern const NSInteger MTLTestModelNameMissing;
@interface MTLSelfValidatingModel : MTLValidationModel
@end
// Maps a non-existant property "name" to the "username" key in JSON.
@interface MTLIllegalJSONMappingModel : MTLModel <MTLJSONSerializing>
@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

View File

@ -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 {
@ -136,7 +153,7 @@ static NSUInteger modelVersion = 1;
@implementation MTLSubstitutingTestModel
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{};
return [NSDictionary mtl_identityPropertyMapWithModel:self];
}
+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary {
@ -153,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) {
@ -176,11 +199,375 @@ static NSUInteger modelVersion = 1;
@end
@implementation MTLIllegalJSONMappingModel
@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 @{
@"name": @"username"
@"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"
};
}

View 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;

View 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

View File

@ -13,7 +13,7 @@
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"];
}];
@ -25,7 +25,7 @@ it(@"should return a forward transformer with a block", ^{
});
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"];
}];
@ -38,10 +38,10 @@ it(@"should return a reversible transformer with a block", ^{
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];
}];

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="3396" systemVersion="13A598" minimumToolsVersion="Xcode 4.3" macOSVersion="Automatic" iOSVersion="Automatic">
<entity name="BadChild" representedClassName="MTLChild" syncable="YES">
<attribute name="childID_BROKEN" attributeType="Integer 16" defaultValueString="0" indexed="YES" syncable="YES"/>
</entity>
<entity name="Child" representedClassName="MTLChild" syncable="YES">
<attribute name="childID" 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="Empty" syncable="YES"/>
<entity name="Parent" representedClassName="MTLParent" 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="0" positionY="0" width="0" height="0"/>
<element name="Empty" positionX="0" positionY="0" width="0" height="0"/>
<element name="Parent" positionX="0" positionY="0" width="0" height="0"/>
<element name="BadChild" positionX="0" positionY="0" width="0" height="0"/>
</elements>
</model>

View File

@ -1,4 +1,4 @@
# Mantle
# Mantle [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
Mantle makes it easy to write a simple model layer for your Cocoa or Cocoa Touch
application.
@ -209,6 +209,8 @@ typedef enum : NSUInteger {
return @{
@"URL": @"url",
@"HTMLURL": @"html_url",
@"number": @"number",
@"state": @"state",
@"reporterLogin": @"user.login",
@"assignee": @"assignee",
@"updatedAt": @"updated_at"
@ -231,13 +233,13 @@ typedef enum : NSUInteger {
}
+ (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];
}];
}
@ -270,9 +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
JSONDictionaryFromModel:error:]` can transform any model object conforming to
`<MTLJSONSerializing>` back into a JSON dictionary. `+[MTLJSONAdapter
JSONArrayForModels:]` is the same but turns an array of model objects into an JSON array of dictionaries.
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.
@ -292,14 +294,14 @@ XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSON
```
```objc
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:user];
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. Properties that map to
`NSNull` will not be present in the JSON representation, for example:
properties map to the keys in the JSON representation, for example:
```objc
@ -317,8 +319,8 @@ properties map to the keys in the JSON representation. Properties that map to
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"createdAt": @"created_at",
@"meUser": NSNull.null
@"name": @"name",
@"createdAt": @"created_at"
};
}
@ -337,8 +339,7 @@ properties map to the keys in the JSON representation. Properties that map to
In this example, the `XYUser` class declares four properties that Mantle
handles in different ways:
- `name` is implicitly mapped to a key of the same name in the JSON
representation.
- `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.
@ -346,6 +347,9 @@ handles in different ways:
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:
@ -378,16 +382,17 @@ deserializing from JSON.
}
```
`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
+ (NSValueTransformer *)createdAtJSONTransformer {
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];
}];
}
@ -467,32 +472,24 @@ in memory at once, Core Data may be a better choice.
## System Requirements
Mantle supports OS X 10.7+ and iOS 5.0+.
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 `script/bootstrap` from within the Mantle folder.
1. Drag and drop `Mantle.xcodeproj` into your application's Xcode project or
workspace.
1. On the "Build Phases" tab of your application target, add Mantle to the
"Link Binary With Libraries" phase.
* **On iOS**, add `libMantle.a`.
* **On OS X**, add `Mantle.framework`. Mantle must also be added to any
"Copy Frameworks" build phase. If you don't already have one, simply add a
"Copy Files" build phase and target the "Frameworks" destination.
1. Add `"$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/include" $(inherited)`
to the "Header Search Paths" build setting (this is only
necessary for archive builds, but it has no negative effect otherwise).
1. **For iOS targets**, add `-ObjC` to the "Other Linker Flags" build setting.
1. **If you added Mantle to a project (not a workspace)**, you will also need
to add the appropriate Mantle target to the "Target Dependencies" of your
application.
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/Mantle) that
[Mantle podspecs](https://github.com/CocoaPods/Specs/tree/master/Specs/5/d/c/Mantle) that
have been generously contributed by third parties.
If youre instead developing Mantle on its own, use the `Mantle.xcworkspace` file.

View File

@ -1,18 +0,0 @@
**Copyright (c) 2013 Justin Spahr-Summers**
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,82 +0,0 @@
# objc-build-scripts
This project is a collection of scripts created with two goals:
1. To standardize how Objective-C projects are bootstrapped after cloning
1. To easily build Objective-C projects on continuous integration servers
## Scripts
Right now, there are two important scripts: [`bootstrap`](#bootstrap) and
[`cibuild`](#cibuild). Both are Bash scripts, to maximize compatibility and
eliminate pesky system configuration issues (like setting up a working Ruby
environment).
The structure of the scripts on disk is meant to follow that of a typical Ruby
project:
```
script/
bootstrap
cibuild
```
### bootstrap
This script is responsible for bootstrapping (initializing) your project after
it's been checked out. Here, you should install or clone any dependencies that
are required for a working build and development environment.
By default, the script will verify that [xctool][] is installed, then initialize
and update submodules recursively. If any submodules contain `script/bootstrap`,
that will be run as well.
To check that other tools are installed, you can set the `REQUIRED_TOOLS`
environment variable before running `script/bootstrap`, or edit it within the
script directly. Note that no installation is performed automatically, though
this can always be added within your specific project.
### cibuild
This script is responsible for building the project, as you would want it built
for continuous integration. This is preferable to putting the logic on the CI
server itself, since it ensures that any changes are versioned along with the
source.
By default, the script will run [`bootstrap`](#bootstrap), look for any Xcode
workspace or project in the working directory, then build all targets/schemes
(as found by `xcodebuild -list`) using [xctool][].
You can also specify the schemes to build by passing them into the script:
```sh
script/cibuild ReactiveCocoa-Mac ReactiveCocoa-iOS
```
As with the `bootstrap` script, there are several environment variables that can
be used to customize behavior. They can be set on the command line before
invoking the script, or the defaults changed within the script directly.
## Getting Started
To add the scripts to your project, read the contents of this repository into
a `script` folder:
```
$ git remote add objc-build-scripts https://github.com/jspahrsummers/objc-build-scripts.git
$ git fetch objc-build-scripts
$ git read-tree --prefix=script/ -u objc-build-scripts/master
```
Then commit the changes, to incorporate the scripts into your own repository's
history. You can also freely tweak the scripts for your specific project's
needs.
To merge in upstream changes later:
```
$ git fetch -p objc-build-scripts
$ git merge --ff --squash -Xsubtree=script objc-build-scripts/master
```
[xctool]: https://github.com/facebook/xctool

View File

@ -1,80 +0,0 @@
#!/bin/bash
export SCRIPT_DIR=$(dirname "$0")
##
## Configuration Variables
##
config ()
{
# A whitespace-separated list of executables that must be present and locatable.
: ${REQUIRED_TOOLS="xctool"}
export REQUIRED_TOOLS
}
##
## Bootstrap Process
##
main ()
{
config
if [ -n "$REQUIRED_TOOLS" ]
then
echo "*** Checking dependencies..."
check_deps
fi
local submodules=$(git submodule status)
local result=$?
if [ "$result" -ne "0" ]
then
exit $result
fi
if [ -n "$submodules" ]
then
echo "*** Updating submodules..."
update_submodules
fi
}
check_deps ()
{
for tool in $REQUIRED_TOOLS
do
which -s "$tool"
if [ "$?" -ne "0" ]
then
echo "*** Error: $tool not found. Please install it and bootstrap again."
exit 1
fi
done
}
bootstrap_submodule ()
{
local bootstrap="script/bootstrap"
if [ -e "$bootstrap" ]
then
echo "*** Bootstrapping $name..."
"$bootstrap" >/dev/null
else
update_submodules
fi
}
update_submodules ()
{
git submodule sync --quiet && git submodule update --init && git submodule foreach --quiet bootstrap_submodule
}
export -f bootstrap_submodule
export -f update_submodules
main

View File

@ -1,153 +0,0 @@
#!/bin/bash
export SCRIPT_DIR=$(dirname "$0")
##
## Configuration Variables
##
SCHEMES="$@"
config ()
{
# The workspace to build.
#
# If not set and no workspace is found, the -workspace flag will not be passed
# to `xctool`.
#
# Only one of `XCWORKSPACE` and `XCODEPROJ` needs to be set. The former will
# take precedence.
: ${XCWORKSPACE=$(find_pattern "*.xcworkspace")}
# The project to build.
#
# If not set and no project is found, the -project flag will not be passed
# to `xctool`.
#
# Only one of `XCWORKSPACE` and `XCODEPROJ` needs to be set. The former will
# take precedence.
: ${XCODEPROJ=$(find_pattern "*.xcodeproj")}
# A bootstrap script to run before building.
#
# If this file does not exist, it is not considered an error.
: ${BOOTSTRAP="$SCRIPT_DIR/bootstrap"}
# Extra options to pass to xctool.
: ${XCTOOL_OPTIONS="RUN_CLANG_STATIC_ANALYZER=NO"}
# A whitespace-separated list of default schemes to build.
#
# Individual names can be quoted to avoid word splitting.
: ${SCHEMES:=$(xcodebuild -list -project "$XCODEPROJ" 2>/dev/null | awk -f "$SCRIPT_DIR/schemes.awk")}
export XCWORKSPACE
export XCODEPROJ
export BOOTSTRAP
export XCTOOL_OPTIONS
export SCHEMES
}
##
## Build Process
##
main ()
{
config
if [ -f "$BOOTSTRAP" ]
then
echo "*** Bootstrapping..."
"$BOOTSTRAP" || exit $?
fi
echo "*** The following schemes will be built:"
echo "$SCHEMES" | xargs -n 1 echo " "
echo
echo "$SCHEMES" | xargs -n 1 | (
local status=0
while read scheme
do
build_scheme "$scheme" || status=1
done
exit $status
)
}
find_pattern ()
{
ls -d $1 2>/dev/null | head -n 1
}
run_xctool ()
{
if [ -n "$XCWORKSPACE" ]
then
xctool -workspace "$XCWORKSPACE" $XCTOOL_OPTIONS "$@" 2>&1
elif [ -n "$XCODEPROJ" ]
then
xctool -project "$XCODEPROJ" $XCTOOL_OPTIONS "$@" 2>&1
else
echo "*** No workspace or project file found."
exit 1
fi
}
parse_build ()
{
awk -f "$SCRIPT_DIR/xctool.awk" 2>&1 >/dev/null
}
build_scheme ()
{
local scheme=$1
echo "*** Cleaning $scheme..."
run_xctool -scheme "$scheme" clean >/dev/null || exit $?
echo "*** Building and testing $scheme..."
echo
local sdkflag=
local action=test
# Determine whether we can run unit tests for this target.
run_xctool -scheme "$scheme" run-tests | parse_build
local awkstatus=$?
if [ "$awkstatus" -eq "1" ]
then
# SDK not found, try for iphonesimulator.
sdkflag="-sdk iphonesimulator"
# Determine whether the unit tests will run with iphonesimulator
run_xctool $sdkflag -scheme "$scheme" run-tests | parse_build
awkstatus=$?
if [ "$awkstatus" -ne "0" ]
then
# Unit tests will not run on iphonesimulator.
sdkflag=""
fi
fi
if [ "$awkstatus" -ne "0" ]
then
# Unit tests aren't supported.
action=build
fi
run_xctool $sdkflag -scheme "$scheme" $action
}
export -f build_scheme
export -f run_xctool
export -f parse_build
main

View File

@ -1,10 +0,0 @@
BEGIN {
FS = "\n";
}
/Schemes:/ {
while (getline && $0 != "") {
sub(/^ +/, "");
print "'" $0 "'";
}
}

View File

@ -1,25 +0,0 @@
# Exit statuses:
#
# 0 - No errors found.
# 1 - Wrong SDK. Retry with SDK `iphonesimulator`.
# 2 - Missing target.
BEGIN {
status = 0;
}
{
print;
}
/Testing with the '(.+)' SDK is not yet supported/ {
status = 1;
}
/does not contain a target named/ {
status = 2;
}
END {
exit status;
}