// // DynamicSchema+Convenience.swift // CoreStore // // Copyright © 2018 John Rommel Estropia // // 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. // import CoreData import Foundation // MARK: - DynamicSchema extension DynamicSchema { /** Prints the `DynamicSchema` as their corresponding `CoreStoreObject` Swift declarations. This is useful for converting current `XcodeDataModelSchema`-based models into the new `CoreStoreSchema` framework. Additional adjustments may need to be done to the generated source code; for example: `Transformable` concrete types need to be provided, as well as `default` values. - Important: After transitioning to the new `CoreStoreSchema` framework, it is recommended to add the new schema as a new version that the existing versions' `XcodeDataModelSchema` can migrate to. It is discouraged to load existing SQLite files created with `XcodeDataModelSchema` directly into a `CoreStoreSchema`. - returns: a string that represents the source code for the `DynamicSchema` as their corresponding `CoreStoreObject` Swift declarations. */ public func printCoreStoreSchema() -> String { let model = self.rawModel() let entitiesByName = model.entitiesByName var output = "/// Generated by CoreStore on \(DateFormatter.localizedString(from: Date(), dateStyle: .short, timeStyle: .short))\n" var addedInverse: Set = [] for (entityName, entity) in entitiesByName { let superName: String if let superEntity = entity.superentity { superName = superEntity.name! } else { superName = String(describing: CoreStoreObject.self) } output.append("class \(entityName): \(superName) {\n") defer { output.append("}\n") } let attributesByName = entity.attributesByName if !attributesByName.isEmpty { output.append(" \n") for (attributeName, attribute) in attributesByName { let containerType: String if attribute.isTransient || attribute.attributeType == .undefinedAttributeType { containerType = "Field.Virtual" } else if attribute.attributeType == .transformableAttributeType { containerType = "Field.Coded" } else { containerType = "Field.Stored" } var valueTypeString: String var defaultString = "" var coderString = "" switch attribute.attributeType { case .integer16AttributeType: valueTypeString = String(describing: Int16.self) if let defaultValue = (attribute.defaultValue as! Int16.QueryableNativeType?).flatMap(Int16.cs_fromQueryableNativeType) { defaultString = " = \(defaultValue)" } else if attribute.isOptional { valueTypeString += "?" defaultString = " = nil" } case .integer32AttributeType: valueTypeString = String(describing: Int32.self) if let defaultValue = (attribute.defaultValue as! Int32.QueryableNativeType?).flatMap(Int32.cs_fromQueryableNativeType) { defaultString = " = \(defaultValue)" } else if attribute.isOptional { valueTypeString += "?" defaultString = " = nil" } case .integer64AttributeType: valueTypeString = String(describing: Int64.self) if let defaultValue = (attribute.defaultValue as! Int64.QueryableNativeType?).flatMap(Int64.cs_fromQueryableNativeType) { defaultString = " = \(defaultValue)" } else if attribute.isOptional { valueTypeString += "?" defaultString = " = nil" } case .decimalAttributeType: valueTypeString = String(describing: NSDecimalNumber.self) if let defaultValue = (attribute.defaultValue as! NSDecimalNumber?) { defaultString = " = NSDecimalNumber(string: \"\(defaultValue.description(withLocale: nil))\")" } else if attribute.isOptional { valueTypeString += "?" defaultString = " = nil" } case .doubleAttributeType: valueTypeString = String(describing: Double.self) if let defaultValue = (attribute.defaultValue as! Double.QueryableNativeType?).flatMap(Double.cs_fromQueryableNativeType) { defaultString = " = \(defaultValue)" } else if attribute.isOptional { valueTypeString += "?" defaultString = " = nil" } case .floatAttributeType: valueTypeString = String(describing: Float.self) if let defaultValue = (attribute.defaultValue as! Float.QueryableNativeType?).flatMap(Float.cs_fromQueryableNativeType) { defaultString = " = \(defaultValue)" } else if attribute.isOptional { valueTypeString += "?" defaultString = " = nil" } case .stringAttributeType: valueTypeString = String(describing: String.self) if let defaultValue = (attribute.defaultValue as! String.QueryableNativeType?).flatMap(String.cs_fromQueryableNativeType) { defaultString = " = \"\(defaultValue.replacingOccurrences(of: "\\", with: "\\\\"))\"" } else if attribute.isOptional { valueTypeString += "?" defaultString = " = nil" } case .booleanAttributeType: valueTypeString = String(describing: Bool.self) if let defaultValue = (attribute.defaultValue as! Bool.QueryableNativeType?).flatMap(Bool.cs_fromQueryableNativeType) { defaultString = " = \(defaultValue ? "true" : "false")" } else if attribute.isOptional { valueTypeString += "?" defaultString = " = nil" } case .dateAttributeType: valueTypeString = String(describing: Date.self) if let defaultValue = (attribute.defaultValue as! Date.QueryableNativeType?).flatMap(Date.cs_fromQueryableNativeType) { defaultString = " = Date(timeIntervalSinceReferenceDate: \(defaultValue.timeIntervalSinceReferenceDate))" } else if attribute.isOptional { valueTypeString += "?" defaultString = " = nil" } case .binaryDataAttributeType: valueTypeString = String(describing: Data.self) if let defaultValue = (attribute.defaultValue as! Data.QueryableNativeType?).flatMap(Data.cs_fromQueryableNativeType) { let bytes = defaultValue.withUnsafeBytes { (pointer) in return pointer .bindMemory(to: UInt64.self) .map({ "\("0x\(String($0, radix: 16, uppercase: false))")" }) } defaultString = " = Data(bytes: [\(bytes.joined(separator: ", "))])" } else if attribute.isOptional { valueTypeString += "?" defaultString = " = nil" } case .transformableAttributeType: if let valueTransformerName = attribute.valueTransformerName { coderString = ", coder: /* Required compatible FieldCoderType implementation for ValueTransformer named \"\(valueTransformerName)\" */" } else { coderString = ", coder: FieldCoders.NSCoding.self" } if let attributeValueClassName = attribute.attributeValueClassName { valueTypeString = String(describing: NSClassFromString(attributeValueClassName)!) } else { valueTypeString = "/* */" } if let defaultValue = attribute.defaultValue { switch defaultValue { case let defaultValueBox as Internals.AnyFieldCoder.TransformableDefaultValueCodingBox: if let defaultValue = defaultValueBox.value { defaultString = " = /* \"\(defaultValue)\" */" } else if attribute.isOptional { defaultString = " = nil" } else { defaultString = " = /* */" } case let defaultValue: defaultString = " = /* \"\(defaultValue)\" */" } } else if attribute.isOptional { defaultString = " = nil" } else { defaultString = " = /* */" } if attribute.isOptional { valueTypeString += "?" } case .undefinedAttributeType where attribute.isTransient: coderString = ", customGetter: \\* *\\" if let attributeValueClassName = attribute.attributeValueClassName { valueTypeString = String(describing: NSClassFromString(attributeValueClassName)!) } else { valueTypeString = " = /* */" } if attribute.isOptional { valueTypeString += "?" defaultString = " = nil" } default: fatalError("Unsupported attribute type: \(attribute.attributeType.rawValue)") } let versionHashModifierString = attribute.versionHashModifier .map({ ", versionHashModifier: \"\($0)\"" }) ?? "" let renamingIdentifierString = attribute.renamingIdentifier .map({ ($0 == attributeName ? "" : ", previousVersionKeyPath: \"\($0)\"") }) ?? "" if attributeName.hasPrefix("_") { output.append(" #warning(\"Field variable names cannot start with underscores)") } output.append(" @\(containerType)(\"\(attributeName)\"\(versionHashModifierString)\(renamingIdentifierString)\(coderString))\n") output.append(" var \(attributeName): \(valueTypeString)\(defaultString)\n\n") } } let relationshipsByName = entity.relationshipsByName if !relationshipsByName.isEmpty { output.append(" \n") for (relationshipName, relationship) in relationshipsByName { let containerType: String let destinationEntityName = relationship.destinationEntity!.name! var minCountString = "" var maxCountString = "" if relationship.isToMany { let minCount = relationship.minCount let maxCount = relationship.maxCount if relationship.isOrdered { containerType = "[\(destinationEntityName)]" } else { containerType = "Set<\(destinationEntityName)>" } if minCount > 0 { minCountString = ", minCount: \(minCount)" } if maxCount > 0 { maxCountString = ", maxCount: \(maxCount)" } } else { containerType = "\(destinationEntityName)?" } var inverseString = "" let relationshipQualifier = "\(entityName).\(relationshipName)" if !addedInverse.contains(relationshipQualifier), let inverseRelationship = relationship.inverseRelationship { inverseString = ", inverse: \\.$\(inverseRelationship.name)" addedInverse.insert("\(destinationEntityName).\(inverseRelationship.name)") } var deleteRuleString = "" if relationship.deleteRule != .nullifyDeleteRule { switch relationship.deleteRule { case .cascadeDeleteRule: deleteRuleString = ", deleteRule: .cascade" case .denyDeleteRule: deleteRuleString = ", deleteRule: .deny" case .nullifyDeleteRule: deleteRuleString = ", deleteRule: .nullify" default: fatalError("Unsupported delete rule \((relationship.deleteRule)) for relationship \"\(relationshipQualifier)\"") } } let versionHashModifierString = relationship.versionHashModifier .map({ ", versionHashModifier: \"\($0)\"" }) ?? "" let renamingIdentifierString = relationship.renamingIdentifier .map({ ($0 == relationshipName ? "" : ", previousVersionKeyPath: \"\($0)\"") }) ?? "" if relationshipName.hasPrefix("_") { output.append(" #error(\"Field variable names cannot start with underscores)\n") } output.append(" @Field.Relationship(\"\(relationshipName)\"\(minCountString)\(maxCountString)\(inverseString)\(deleteRuleString)\(versionHashModifierString)\(renamingIdentifierString))\n") output.append(" var \(relationshipName): \(containerType)\n\n") } } } output.append("\n\n\n") output.append("CoreStoreSchema(\n") output.append(" modelVersion: \"\(self.modelVersion)\",\n") output.append(" entities: [\n") for (entityName, entity) in entitiesByName { var abstractString = "" if entity.isAbstract { abstractString = ", isAbstract: true" } var versionHashModifierString = "" if let versionHashModifier = entity.versionHashModifier { versionHashModifierString = ", versionHashModifier: \"\(versionHashModifier)\"" } output.append(" Entity<\(entityName)>(\"\(entityName)\"\(abstractString)\(versionHashModifierString)),\n") } output.append(" ],\n") output.append(" versionLock: \(VersionLock(entityVersionHashesByName: model.entityVersionHashesByName).description.components(separatedBy: "\n").joined(separator: "\n "))\n") output.append(")\n\n") return output } }