Files
CoreStore/Sources/DynamicSchema+Convenience.swift

386 lines
18 KiB
Swift

//
// 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<String> = []
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 = "/* <required> */"
}
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 = " = /* <required> */"
}
case let defaultValue:
defaultString = " = /* \"\(defaultValue)\" */"
}
}
else if attribute.isOptional {
defaultString = " = nil"
}
else {
defaultString = " = /* <required> */"
}
if attribute.isOptional {
valueTypeString += "?"
}
case .undefinedAttributeType where attribute.isTransient:
coderString = ", customGetter: \\* <required> *\\"
if let attributeValueClassName = attribute.attributeValueClassName {
valueTypeString = String(describing: NSClassFromString(attributeValueClassName)!)
}
else {
valueTypeString = " = /* <required> */"
}
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
}
}