//////////////////////////////////////////////////////////////////////////// // // Copyright 2021 Realm Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //////////////////////////////////////////////////////////////////////////// import Foundation import Realm.Private // A type which we can get the runtime schema information from public protocol _RealmSchemaDiscoverable { // The Realm property type associated with this type static var _rlmType: PropertyType { get } static var _rlmOptional: Bool { get } // Does this type require @objc for legacy declarations? Not used for modern // declarations as no types use @objc. static var _rlmRequireObjc: Bool { get } // Set any fields of the property applicable to this type other than type/optional. // There are both static and non-static versions of this function because // some times need data from an instance (e.g. LinkingObjects, where the // source property name is runtime data and not part of the type), while // wrappers like Optional need to be able to recur to the wrapped type // without creating an instance of that. func _rlmPopulateProperty(_ prop: RLMProperty) static func _rlmPopulateProperty(_ prop: RLMProperty) // Iterating over collections requires mapping NSNull to nil, but we can't // just do `nil as T` because of non-nullable collections. RealmProperty also // relies on this for the same reason. static func _nilValue() -> Self } extension _RealmSchemaDiscoverable { public static func _nilValue() -> Self { fatalError("Should never have nil value") } } internal protocol SchemaDiscoverable: _RealmSchemaDiscoverable {} extension SchemaDiscoverable { public static var _rlmOptional: Bool { false } public static var _rlmRequireObjc: Bool { true } public func _rlmPopulateProperty(_ prop: RLMProperty) { } public static func _rlmPopulateProperty(_ prop: RLMProperty) { } } internal extension RLMProperty { convenience init(name: String, value: _RealmSchemaDiscoverable) { let valueType = Swift.type(of: value) self.init() self.name = name self.type = valueType._rlmType self.optional = valueType._rlmOptional value._rlmPopulateProperty(self) valueType._rlmPopulateProperty(self) if valueType._rlmRequireObjc { self.updateAccessors() } } } private func getModernProperties(_ object: ObjectBase) -> [RLMProperty] { return Mirror(reflecting: object).children.compactMap { prop in guard let label = prop.label else { return nil } guard let value = prop.value as? DiscoverablePersistedProperty else { return nil } let property = RLMProperty(name: label, value: value) property.swiftIvar = ivar_getOffset(class_getInstanceVariable(type(of: object), label)!) return property } } // If the property is a storage property for a lazy Swift property, return // the base property name (e.g. `foo.storage` becomes `foo`). Otherwise, nil. private func baseName(forLazySwiftProperty name: String) -> String? { // A Swift lazy var shows up as two separate children on the reflection tree: // one named 'x', and another that is optional and is named "$__lazy_storage_$_propName" if let storageRange = name.range(of: "$__lazy_storage_$_", options: [.anchored]) { return String(name[storageRange.upperBound...]) } return nil } private func getLegacyProperties(_ object: ObjectBase, _ cls: ObjectBase.Type) -> [RLMProperty] { let indexedProperties: Set<String> let ignoredPropNames: Set<String> let columnNames = cls._realmColumnNames() // FIXME: ignored properties on EmbeddedObject appear to not be supported? if let realmObject = object as? Object { indexedProperties = Set(type(of: realmObject).indexedProperties()) ignoredPropNames = Set(type(of: realmObject).ignoredProperties()) } else { indexedProperties = Set() ignoredPropNames = Set() } return Mirror(reflecting: object).children.filter { (prop: Mirror.Child) -> Bool in guard let label = prop.label else { return false } if ignoredPropNames.contains(label) { return false } if let lazyBaseName = baseName(forLazySwiftProperty: label) { if ignoredPropNames.contains(lazyBaseName) { return false } throwRealmException("Lazy managed property '\(lazyBaseName)' is not allowed on a Realm Swift object" + " class. Either add the property to the ignored properties list or make it non-lazy.") } return true }.compactMap { prop in guard let label = prop.label else { return nil } var rawValue = prop.value if let value = rawValue as? RealmEnum { rawValue = type(of: value)._rlmToRawValue(value) } guard let value = rawValue as? _RealmSchemaDiscoverable else { if class_getProperty(cls, label) != nil { throwRealmException("Property \(cls).\(label) is declared as \(type(of: prop.value)), which is not a supported managed Object property type. If it is not supposed to be a managed property, either add it to `ignoredProperties()` or do not declare it as `@objc dynamic`. See https://realm.io/docs/swift/latest/api/Classes/Object.html for more information.") } if prop.value as? RealmOptionalProtocol != nil { throwRealmException("Property \(cls).\(label) has unsupported RealmOptional type \(type(of: prop.value)). Extending RealmOptionalType with custom types is not currently supported. ") } return nil } RLMValidateSwiftPropertyName(label) let valueType = type(of: value) let property = RLMProperty(name: label, value: value) property.indexed = indexedProperties.contains(property.name) property.columnName = columnNames?[property.name] if let objcProp = class_getProperty(cls, label) { var count: UInt32 = 0 let attrs = property_copyAttributeList(objcProp, &count)! defer { free(attrs) } var computed = true for i in 0..<Int(count) { let attr = attrs[i] switch attr.name[0] { case Int8(UInt8(ascii: "R")): // Read only return nil case Int8(UInt8(ascii: "V")): // Ivar name computed = false case Int8(UInt8(ascii: "G")): // Getter name property.getterName = String(cString: attr.value) case Int8(UInt8(ascii: "S")): // Setter name property.setterName = String(cString: attr.value) default: break } } // If there's no ivar name and no ivar with the same name as // the property then this is a computed property and we should // implicitly ignore it if computed && class_getInstanceVariable(cls, label) == nil { return nil } } else if valueType._rlmRequireObjc { // Implicitly ignore non-@objc dynamic properties return nil } else { property.swiftIvar = ivar_getOffset(class_getInstanceVariable(cls, label)!) } property.isLegacy = true property.updateAccessors() return property } } private func getProperties(_ cls: RLMObjectBase.Type) -> [RLMProperty] { // Check for any modern properties and only scan for legacy properties if // none are found. let object = cls.init() let props = getModernProperties(object) if props.count > 0 { return props } return getLegacyProperties(object, cls) } internal class ObjectUtil { private static let runOnce: Void = { RLMSwiftAsFastEnumeration = { (obj: Any) -> Any? in // Intermediate cast to AnyObject due to https://bugs.swift.org/browse/SR-8651 if let collection = obj as AnyObject as? UntypedCollection { return collection.asNSFastEnumerator() } return nil } RLMSwiftBridgeValue = { (value: Any) -> Any? in if let value = value as? CustomObjectiveCBridgeable { return value.objCValue } if let value = value as? RealmEnum { return type(of: value)._rlmToRawValue(value) } return nil } }() internal class func getSwiftProperties(_ cls: RLMObjectBase.Type) -> [RLMProperty] { _ = ObjectUtil.runOnce return getProperties(cls) } }