1 ////////////////////////////////////////////////////////////////////////////
3 // Copyright 2014 Realm Inc.
5 // Licensed under the Apache License, Version 2.0 (the "License");
6 // you may not use this file except in compliance with the License.
7 // You may obtain a copy of the License at
9 // http://www.apache.org/licenses/LICENSE-2.0
11 // Unless required by applicable law or agreed to in writing, software
12 // distributed under the License is distributed on an "AS IS" BASIS,
13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 // See the License for the specific language governing permissions and
15 // limitations under the License.
17 ////////////////////////////////////////////////////////////////////////////
19 #import "RLMProperty_Private.hpp"
21 #import "RLMArray_Private.hpp"
22 #import "RLMListBase.h"
24 #import "RLMObject_Private.h"
25 #import "RLMOptionalBase.h"
26 #import "RLMSchema_Private.h"
27 #import "RLMSwiftSupport.h"
30 static_assert((int)RLMPropertyTypeInt == (int)realm::PropertyType::Int, "");
31 static_assert((int)RLMPropertyTypeBool == (int)realm::PropertyType::Bool, "");
32 static_assert((int)RLMPropertyTypeFloat == (int)realm::PropertyType::Float, "");
33 static_assert((int)RLMPropertyTypeDouble == (int)realm::PropertyType::Double, "");
34 static_assert((int)RLMPropertyTypeString == (int)realm::PropertyType::String, "");
35 static_assert((int)RLMPropertyTypeData == (int)realm::PropertyType::Data, "");
36 static_assert((int)RLMPropertyTypeDate == (int)realm::PropertyType::Date, "");
37 static_assert((int)RLMPropertyTypeObject == (int)realm::PropertyType::Object, "");
39 BOOL RLMPropertyTypeIsComputed(RLMPropertyType propertyType) {
40 return propertyType == RLMPropertyTypeLinkingObjects;
43 // Swift obeys the ARC naming conventions for method families (except for init)
44 // but the end result doesn't really work (using KVC on a method returning a
45 // retained value results in a leak, but not returning a retained value results
46 // in crashes). Objective-C makes properties with naming fitting the method
47 // families a compile error, so we just disallow them in Swift as well.
48 // http://clang.llvm.org/docs/AutomaticReferenceCounting.html#arc-method-families
49 void RLMValidateSwiftPropertyName(NSString *name) {
50 // To belong to a method family, the property name must begin with the family
51 // name followed by a non-lowercase letter (or nothing), with an optional
53 const char *str = name.UTF8String;
56 auto nameSize = strlen(str);
58 // Note that "init" is deliberately not in this list because Swift does not
59 // infer family membership for it.
60 for (auto family : {"alloc", "new", "copy", "mutableCopy"}) {
61 auto familySize = strlen(family);
62 if (nameSize < familySize || !std::equal(str, str + familySize, family)) {
65 if (familySize == nameSize || !islower(str[familySize])) {
66 @throw RLMException(@"Property names beginning with '%s' are not "
67 "supported. Swift follows ARC's ownership "
68 "rules for methods based on their name, which "
69 "results in memory leaks when accessing "
70 "properties which return retained values via KVC.",
77 static bool rawTypeShouldBeTreatedAsComputedProperty(NSString *rawType) {
78 return [rawType isEqualToString:@"@\"RLMLinkingObjects\""] || [rawType hasPrefix:@"@\"RLMLinkingObjects<"];
81 @implementation RLMProperty
83 + (instancetype)propertyForObjectStoreProperty:(const realm::Property &)prop {
84 auto ret = [[RLMProperty alloc] initWithName:@(prop.name.c_str())
85 type:static_cast<RLMPropertyType>(prop.type & ~realm::PropertyType::Flags)
86 objectClassName:prop.object_type.length() ? @(prop.object_type.c_str()) : nil
87 linkOriginPropertyName:prop.link_origin_property_name.length() ? @(prop.link_origin_property_name.c_str()) : nil
88 indexed:prop.is_indexed
89 optional:is_nullable(prop.type)];
90 if (is_array(prop.type)) {
96 - (instancetype)initWithName:(NSString *)name
97 type:(RLMPropertyType)type
98 objectClassName:(NSString *)objectClassName
99 linkOriginPropertyName:(NSString *)linkOriginPropertyName
100 indexed:(BOOL)indexed
101 optional:(BOOL)optional {
106 _objectClassName = objectClassName;
107 _linkOriginPropertyName = linkOriginPropertyName;
109 _optional = optional;
110 [self updateAccessors];
116 - (void)setName:(NSString *)name {
118 [self updateAccessors];
121 - (void)updateAccessors {
122 // populate getter/setter names if generic
127 // Objective-C setters only capitalize the first letter of the property name if it falls between 'a' and 'z'
128 int asciiCode = [_name characterAtIndex:0];
129 BOOL shouldUppercase = asciiCode >= 'a' && asciiCode <= 'z';
130 NSString *firstChar = [_name substringToIndex:1];
131 firstChar = shouldUppercase ? firstChar.uppercaseString : firstChar;
132 _setterName = [NSString stringWithFormat:@"set%@%@:", firstChar, [_name substringFromIndex:1]];
135 _getterSel = NSSelectorFromString(_getterName);
136 _setterSel = NSSelectorFromString(_setterName);
139 static realm::util::Optional<RLMPropertyType> typeFromProtocolString(const char *type) {
140 if (strncmp(type, "RLM", 3)) {
144 if (strcmp(type, "Int>\"") == 0) {
145 return RLMPropertyTypeInt;
147 if (strcmp(type, "Float>\"") == 0) {
148 return RLMPropertyTypeFloat;
150 if (strcmp(type, "Double>\"") == 0) {
151 return RLMPropertyTypeDouble;
153 if (strcmp(type, "Bool>\"") == 0) {
154 return RLMPropertyTypeBool;
156 if (strcmp(type, "String>\"") == 0) {
157 return RLMPropertyTypeString;
159 if (strcmp(type, "Data>\"") == 0) {
160 return RLMPropertyTypeData;
162 if (strcmp(type, "Date>\"") == 0) {
163 return RLMPropertyTypeDate;
168 // determine RLMPropertyType from objc code - returns true if valid type was found/set
169 - (BOOL)setTypeFromRawType:(NSString *)rawType {
170 const char *code = rawType.UTF8String;
175 case 'q': // long long
176 _type = RLMPropertyTypeInt;
179 _type = RLMPropertyTypeFloat;
182 _type = RLMPropertyTypeDouble;
184 case 'c': // BOOL is stored as char - since rlm has no char type this is ok
186 _type = RLMPropertyTypeBool;
195 static const char arrayPrefix[] = "@\"RLMArray<";
196 static const int arrayPrefixLen = sizeof(arrayPrefix) - 1;
198 static const char numberPrefix[] = "@\"NSNumber<";
199 static const int numberPrefixLen = sizeof(numberPrefix) - 1;
201 static const char linkingObjectsPrefix[] = "@\"RLMLinkingObjects";
202 static const int linkingObjectsPrefixLen = sizeof(linkingObjectsPrefix) - 1;
204 if (strcmp(code, "@\"NSString\"") == 0) {
205 _type = RLMPropertyTypeString;
207 else if (strcmp(code, "@\"NSDate\"") == 0) {
208 _type = RLMPropertyTypeDate;
210 else if (strcmp(code, "@\"NSData\"") == 0) {
211 _type = RLMPropertyTypeData;
213 else if (strncmp(code, arrayPrefix, arrayPrefixLen) == 0) {
215 if (auto type = typeFromProtocolString(code + arrayPrefixLen)) {
220 // get object class from type string - @"RLMArray<objectClassName>"
221 _objectClassName = [[NSString alloc] initWithBytes:code + arrayPrefixLen
222 length:strlen(code + arrayPrefixLen) - 2 // drop trailing >"
223 encoding:NSUTF8StringEncoding];
225 if ([RLMSchema classForString:_objectClassName]) {
227 _type = RLMPropertyTypeObject;
230 @throw RLMException(@"Property '%@' is of type 'RLMArray<%@>' which is not a supported RLMArray object type. "
231 @"RLMArrays can only contain instances of RLMObject subclasses. "
232 @"See https://realm.io/docs/objc/latest/#to-many for more information.", _name, _objectClassName);
234 else if (strncmp(code, numberPrefix, numberPrefixLen) == 0) {
235 auto type = typeFromProtocolString(code + numberPrefixLen);
236 if (type && (*type == RLMPropertyTypeInt || *type == RLMPropertyTypeFloat || *type == RLMPropertyTypeDouble || *type == RLMPropertyTypeBool)) {
240 @throw RLMException(@"Property '%@' is of type %s which is not a supported NSNumber object type. "
241 @"NSNumbers can only be RLMInt, RLMFloat, RLMDouble, and RLMBool at the moment. "
242 @"See https://realm.io/docs/objc/latest for more information.", _name, code + 1);
244 else if (strncmp(code, linkingObjectsPrefix, linkingObjectsPrefixLen) == 0 &&
245 (code[linkingObjectsPrefixLen] == '"' || code[linkingObjectsPrefixLen] == '<')) {
246 _type = RLMPropertyTypeLinkingObjects;
250 if (!_objectClassName || !_linkOriginPropertyName) {
251 @throw RLMException(@"Property '%@' is of type RLMLinkingObjects but +linkingObjectsProperties did not specify the class "
252 "or property that is the origin of the link.", _name);
255 // If the property was declared with a protocol indicating the contained type, validate that it matches
256 // the class from the dictionary returned by +linkingObjectsProperties.
257 if (code[linkingObjectsPrefixLen] == '<') {
258 NSString *classNameFromProtocol = [[NSString alloc] initWithBytes:code + linkingObjectsPrefixLen + 1
259 length:strlen(code + linkingObjectsPrefixLen) - 3 // drop trailing >"
260 encoding:NSUTF8StringEncoding];
261 if (![_objectClassName isEqualToString:classNameFromProtocol]) {
262 @throw RLMException(@"Property '%@' was declared with type RLMLinkingObjects<%@>, but a conflicting "
263 "class name of '%@' was returned by +linkingObjectsProperties.", _name,
264 classNameFromProtocol, _objectClassName);
268 else if (strcmp(code, "@\"NSNumber\"") == 0) {
269 @throw RLMException(@"Property '%@' requires a protocol defining the contained type - example: NSNumber<RLMInt>.", _name);
271 else if (strcmp(code, "@\"RLMArray\"") == 0) {
272 @throw RLMException(@"Property '%@' requires a protocol defining the contained type - example: RLMArray<Person>.", _name);
277 if (code[1] == '\0') {
281 // for objects strip the quotes and @
282 className = [rawType substringWithRange:NSMakeRange(2, rawType.length-3)];
283 cls = [RLMSchema classForString:className];
287 @throw RLMException(@"Property '%@' is declared as '%@', which is not a supported RLMObject property type. "
288 @"All properties must be primitives, NSString, NSDate, NSData, NSNumber, RLMArray, RLMLinkingObjects, or subclasses of RLMObject. "
289 @"See https://realm.io/docs/objc/latest/api/Classes/RLMObject.html for more information.", _name, className);
292 _type = RLMPropertyTypeObject;
294 _objectClassName = [cls className] ?: className;
299 - (void)parseObjcProperty:(objc_property_t)property
300 readOnly:(bool *)readOnly
301 computed:(bool *)computed
302 rawType:(NSString **)rawType {
304 objc_property_attribute_t *attrs = property_copyAttributeList(property, &count);
307 for (size_t i = 0; i < count; ++i) {
308 switch (*attrs[i].name) {
310 *rawType = @(attrs[i].value);
322 _getterName = @(attrs[i].value);
325 _setterName = @(attrs[i].value);
327 case 'V': // backing ivar name
337 - (instancetype)initSwiftPropertyWithName:(NSString *)name
338 indexed:(BOOL)indexed
339 linkPropertyDescriptor:(RLMPropertyDescriptor *)linkPropertyDescriptor
340 property:(objc_property_t)property
341 instance:(RLMObject *)obj {
347 RLMValidateSwiftPropertyName(name);
352 if (linkPropertyDescriptor) {
353 _objectClassName = [linkPropertyDescriptor.objectClass className];
354 _linkOriginPropertyName = linkPropertyDescriptor.propertyName;
358 bool readOnly = false;
359 bool isComputed = false;
360 [self parseObjcProperty:property readOnly:&readOnly computed:&isComputed rawType:&rawType];
361 if (!readOnly && isComputed) {
362 // Check for lazy property.
363 NSString *backingPropertyName = [NSString stringWithFormat:@"%@.storage", name];
364 if (class_getInstanceVariable([obj class], backingPropertyName.UTF8String)) {
369 if (readOnly || isComputed) {
373 id propertyValue = [obj valueForKey:_name];
375 // FIXME: temporarily workaround added since Objective-C generics used in Swift show up as `@`
376 // * broken starting in Swift 3.0 Xcode 8 b1
377 // * tested to still be broken in Swift 3.0 Xcode 8 b6
378 // * if the Realm Objective-C Swift tests pass with this removed, it's been fixed
379 // * once it has been fixed, remove this entire conditional block (contents included) entirely
380 // * Bug Report: SR-2031 https://bugs.swift.org/browse/SR-2031
381 if ([rawType isEqualToString:@"@"]) {
383 rawType = [NSString stringWithFormat:@"@\"%@\"", [propertyValue class]];
384 } else if (linkPropertyDescriptor) {
385 // we're going to naively assume that the user used the correct type since we can't check it
386 rawType = @"@\"RLMLinkingObjects\"";
390 // convert array types to objc variant
391 if ([rawType isEqualToString:@"@\"RLMArray\""]) {
392 RLMArray *value = propertyValue;
394 _optional = value.optional;
396 _objectClassName = value.objectClassName;
397 if (_type == RLMPropertyTypeObject && ![RLMSchema classForString:_objectClassName]) {
398 @throw RLMException(@"Property '%@' is of type 'RLMArray<%@>' which is not a supported RLMArray object type. "
399 @"RLMArrays can only contain instances of RLMObject subclasses. "
400 @"See https://realm.io/docs/objc/latest/#to-many for more information.", _name, _objectClassName);
403 else if ([rawType isEqualToString:@"@\"NSNumber\""]) {
404 const char *numberType = [propertyValue objCType];
406 @throw RLMException(@"Can't persist NSNumber without default value: use a Swift-native number type or provide a default value.");
409 switch (*numberType) {
410 case 'i': case 'l': case 'q':
411 _type = RLMPropertyTypeInt;
414 _type = RLMPropertyTypeFloat;
417 _type = RLMPropertyTypeDouble;
420 _type = RLMPropertyTypeBool;
423 @throw RLMException(@"Can't persist NSNumber of type '%s': only integers, floats, doubles, and bools are currently supported.", numberType);
426 else if (![self setTypeFromRawType:rawType]) {
427 @throw RLMException(@"Can't persist property '%@' with incompatible type. "
428 "Add to Object.ignoredProperties() class method to ignore.",
432 if ([rawType isEqualToString:@"c"]) {
433 // Check if it's a BOOL or Int8 by trying to set it to 2 and seeing if
434 // it actually sets it to 1.
435 [obj setValue:@2 forKey:name];
436 NSNumber *value = [obj valueForKey:name];
437 _type = value.intValue == 2 ? RLMPropertyTypeInt : RLMPropertyTypeBool;
440 // update getter/setter names
441 [self updateAccessors];
446 - (instancetype)initWithName:(NSString *)name
447 indexed:(BOOL)indexed
448 linkPropertyDescriptor:(RLMPropertyDescriptor *)linkPropertyDescriptor
449 property:(objc_property_t)property
459 if (linkPropertyDescriptor) {
460 _objectClassName = [linkPropertyDescriptor.objectClass className];
461 _linkOriginPropertyName = linkPropertyDescriptor.propertyName;
465 bool isReadOnly = false;
466 bool isComputed = false;
467 [self parseObjcProperty:property readOnly:&isReadOnly computed:&isComputed rawType:&rawType];
468 bool shouldBeTreatedAsComputedProperty = rawTypeShouldBeTreatedAsComputedProperty(rawType);
469 if ((isReadOnly || isComputed) && !shouldBeTreatedAsComputedProperty) {
473 if (![self setTypeFromRawType:rawType]) {
474 @throw RLMException(@"Can't persist property '%@' with incompatible type. "
475 "Add to ignoredPropertyNames: method to ignore.", self.name);
478 if (!isReadOnly && shouldBeTreatedAsComputedProperty) {
479 @throw RLMException(@"Property '%@' must be declared as readonly as %@ properties cannot be written to.",
480 self.name, RLMTypeToString(_type));
483 // update getter/setter names
484 [self updateAccessors];
489 - (instancetype)initSwiftListPropertyWithName:(NSString *)name
490 instance:(id)object {
497 _swiftIvar = class_getInstanceVariable([object class], name.UTF8String);
499 RLMArray *array = [object_getIvar(object, _swiftIvar) _rlmArray];
501 _optional = array.optional;
502 _objectClassName = array.objectClassName;
504 // no obj-c property for generic lists, and thus no getter/setter names
509 - (instancetype)initSwiftOptionalPropertyWithName:(NSString *)name
510 indexed:(BOOL)indexed
512 propertyType:(RLMPropertyType)propertyType {
519 _type = propertyType;
524 // no obj-c property for generic optionals, and thus no getter/setter names
529 - (instancetype)initSwiftLinkingObjectsPropertyWithName:(NSString *)name
531 objectClassName:(NSString *)objectClassName
532 linkOriginPropertyName:(NSString *)linkOriginPropertyName {
539 _type = RLMPropertyTypeLinkingObjects;
541 _objectClassName = objectClassName;
542 _linkOriginPropertyName = linkOriginPropertyName;
545 // no obj-c property for generic linking objects properties, and thus no getter/setter names
550 - (id)copyWithZone:(NSZone *)zone {
551 RLMProperty *prop = [[RLMProperty allocWithZone:zone] init];
554 prop->_objectClassName = _objectClassName;
555 prop->_array = _array;
556 prop->_indexed = _indexed;
557 prop->_getterName = _getterName;
558 prop->_setterName = _setterName;
559 prop->_getterSel = _getterSel;
560 prop->_setterSel = _setterSel;
561 prop->_isPrimary = _isPrimary;
562 prop->_swiftIvar = _swiftIvar;
563 prop->_optional = _optional;
564 prop->_linkOriginPropertyName = _linkOriginPropertyName;
569 - (RLMProperty *)copyWithNewName:(NSString *)name {
570 RLMProperty *prop = [self copy];
575 - (BOOL)isEqual:(id)object {
576 if (![object isKindOfClass:[RLMProperty class]]) {
580 return [self isEqualToProperty:object];
583 - (BOOL)isEqualToProperty:(RLMProperty *)property {
584 return _type == property->_type
585 && _indexed == property->_indexed
586 && _isPrimary == property->_isPrimary
587 && _optional == property->_optional
588 && [_name isEqualToString:property->_name]
589 && (_objectClassName == property->_objectClassName || [_objectClassName isEqualToString:property->_objectClassName])
590 && (_linkOriginPropertyName == property->_linkOriginPropertyName ||
591 [_linkOriginPropertyName isEqualToString:property->_linkOriginPropertyName]);
594 - (NSString *)description {
595 return [NSString stringWithFormat:
598 "\tobjectClassName = %@;\n"
599 "\tlinkOriginPropertyName = %@;\n"
601 "\tisPrimary = %@;\n"
605 self.name, RLMTypeToString(self.type), self.objectClassName,
606 self.linkOriginPropertyName,
607 self.indexed ? @"YES" : @"NO",
608 self.isPrimary ? @"YES" : @"NO",
609 self.array ? @"YES" : @"NO",
610 self.optional ? @"YES" : @"NO"];
613 - (realm::Property)objectStoreCopy {
615 p.name = _name.UTF8String;
616 p.object_type = _objectClassName ? _objectClassName.UTF8String : "";
617 p.is_indexed = (bool)_indexed;
618 p.link_origin_property_name = _linkOriginPropertyName ? _linkOriginPropertyName.UTF8String : "";
619 p.type = static_cast<realm::PropertyType>(_type);
621 p.type |= realm::PropertyType::Array;
624 p.type |= realm::PropertyType::Nullable;
631 @implementation RLMPropertyDescriptor
633 + (instancetype)descriptorWithClass:(Class)objectClass propertyName:(NSString *)propertyName
635 RLMPropertyDescriptor *descriptor = [[RLMPropertyDescriptor alloc] init];
636 descriptor->_objectClass = objectClass;
637 descriptor->_propertyName = propertyName;