diff --git a/Package.swift b/Package.swift index 95360f9..a35549f 100644 --- a/Package.swift +++ b/Package.swift @@ -1,12 +1,23 @@ -// Generated automatically by Perfect Assistant Application -// Date: 2018-03-02 16:12:45 +0000 +// swift-tools-version:4.0 import PackageDescription let package = Package( - name: "StORM", - targets: [], - dependencies: [ - .Package(url: "https://github.com/PerfectlySoft/PerfectLib.git", majorVersion: 3), - .Package(url: "https://github.com/iamjono/SwiftMoment.git", majorVersion: 1), - .Package(url: "https://github.com/iamjono/SwiftString.git", majorVersion: 2), - ] + name: "StORM", + products: [ + // Products define the executables and libraries produced by a package, and make them visible to other packages. + .library( + name: "StORM", + targets: ["StORM"]), + ], + dependencies: [ + .package(url: "https://github.com/ryancoyne/PerfectLib.git", from: "4.0.0"), + .package(url: "https://github.com/ryancoyne/SwiftMoment.git", from: "1.0.0"), +// .package(url: "https://github.com/ryancoyne/SwiftString.git", from: "2.0.0"), + ], + targets: [ + .target( + name: "StORM", + dependencies: ["PerfectLib", "SwiftMoment"], + path: "Sources/StORM" + ), + ] ) diff --git a/README.md b/README.md index 8f51ec9..54eff62 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,116 @@ StORM is a modular ORM for Swift, layered on top of Perfect. It aims to be easy to use, but flexible, and maintain consistency between datasource implementations for the user: you, the developer. It tries to allow you to write great code without worrying about the details of how to interact with the database. Please see the full documentation at: [https://www.perfect.org/docs/StORM.html](https://www.perfect.org/docs/StORM.html) + +# Latest Updates: + +- Increased flexibility with determining the primary key, without having to have it your first declared variable in your model. +- Includes subclassing support. +- Includes optional variable support. + +## Usage: +### Example -> Conforming to StORMirroring: +Say we have to models using PostgresStORM: + +``` +class AuditFields: PostgresStORM { + + /// This is when the table row has been created. + var created : Int? = nil + /// This is the id of the user that has created the row. + var createdby : String? = nil + /// This is when the table row has been modified. + var modified : Int? = nil + /// This is the id of the user that has modified the row. + var modifiedby : String? = nil + + // This is needed when created a subclass containing other fields to re-use for other models. + override init() { + super.init() + self.didInitializeSuperclass() + } +} + +// The outer most class does not need to override init & call didInitializeSuperclass. This helps with identifying the id in the model. +class TestUser2: AuditFields { + // In this example we have id at the top but that is not mandatory now if you implement the primaryKeyLabel overrided function. + var id : Int? = nil + var firstname : String? = nil { + didSet { + if oldValue != nil && firstname == nil { + self.nullColumns.insert("firstname") + } else if firstname != nil { + self.nullColumns.remove("firstname") + } + } + } + var lastname : String? = nil { + didSet { + if oldValue != nil && lastname == nil { + self.nullColumns.insert("lastname") + } else if firstname != nil { + self.nullColumns.remove("lastname") + } + } + } + var phonenumber : String? = nil { + didSet { + if oldValue != nil && phonenumber == nil { + self.nullColumns.insert("phonenumber") + } else if firstname != nil { + self.nullColumns.remove("phonenumber") + } + } + } + + override open func table() -> String { + return "testuser2" + } + + override func to(_ this: StORMRow) { + + // Audit fields: + id = this.data["id"] as? Int + created = this.data["created"] as? Int + createdby = this.data["createdby"] as? String + modified = this.data["modified"] as? Int + modifiedby = this.data["modifiedby"] as? String + + firstname = this.data["firstname"] as? String + lastname = this.data["lastname"] as? String + phonenumber = this.data["phonenumber"] as? String + + } + + func rows() -> [TestUser2] { + var rows = [TestUser2]() + for i in 0.. Getting all the children: +``` +// Getting all the children (including nil values or NOT) +// Returns an array of children, making sure the overrided function primaryKeyLabel takes the child for that key and places it in the first index of the array: +self.allChildren(includingNilValues: true, primaryKey: self.primaryKeyLabel()) +``` + +This provides the capability for PostgresStORM and other database supported StORM modules to be given the ability to subclass PostgresStORM objects deeper than just the single mirror of the class. + +StORMMirroring also gives you the flexibility to place your primary key in a superclass model. All you would need to do is override the primaryKeyLabel function that StORMMirroring offers. This ensures that when we create the array of children from all the mirrors, it places the primary key as the first child in the array. + +### Supporting Optionals in other database-StORM Modules: +- Make sure to keep track of going from an optional Non-nil value to nil, to update NULL/DEFAULT or NULL. See TestUser2 model. + +### Other -> Automatic Auditing For other database-StORM Modules: +- StORMMirroring also supports automatic created & modified values to come back if they exist in the model. They are intended to be used as integers to store as epoch timestamps. Make sure to skip over modified or created depending on if you are creating a new record or modifying a record. + diff --git a/Sources/StORM/Extract.swift b/Sources/StORM/Extract.swift index 65ef8c2..5544e27 100644 --- a/Sources/StORM/Extract.swift +++ b/Sources/StORM/Extract.swift @@ -7,7 +7,6 @@ import Foundation import SwiftMoment -import SwiftString extension StORM { @@ -78,14 +77,14 @@ extension StORM { // Array Of Strings // ======================================================================================= public static func arrayOfStrings(_ data: [String: Any], _ name: String, _ def: [String]? = [String]()) -> [String]? { - return (data[name] as? String ?? "").split(",").map{ $0.trimmed() } // note default ignored right now + return (data[name] as? String ?? "").split(separator: ",").map{ $0.trimmingCharacters(in: .whitespacesAndNewlines) } // note default ignored right now } // ======================================================================================= // Array Of Integers // ======================================================================================= public static func arrayOfIntegers(_ data: [String: Any], _ name: String, _ def: [Int]? = [Int]()) -> [Int]? { - return (data[name] as? String ?? "").split(",").map{ Int($0.trimmed()) ?? 0 } // note default ignored right now + return (data[name] as? String ?? "").split(separator: ",").map{ Int($0.trimmingCharacters(in: .whitespacesAndNewlines)) ?? 0 } // note default ignored right now } // ======================================================================================= diff --git a/Sources/StORM/StORM.swift b/Sources/StORM/StORM.swift index 6903f37..be253b7 100644 --- a/Sources/StORM/StORM.swift +++ b/Sources/StORM/StORM.swift @@ -11,124 +11,130 @@ /// When true, certain methods will generate a debug message under certain conditions. public var StORMdebug = false - /// Base StORM superclass from which all Database-Connector StORM classes inherit. /// Provides base functionality and rules. -open class StORM { - - /// Results container of type StORMResultSet. - open var results = StORMResultSet() - - /// connection error status of type StORMError. - open var error = StORMError() - - /// Contain last error message as string. - open var errorMsg = "" - - /// Base empty init function. - public init() {} - - /// Provides structure introspection to client methods. - public func cols(_ offset: Int = 0) -> [(String, Any)] { - var c = [(String, Any)]() - var count = 0 - let mirror = Mirror(reflecting: self) - for child in mirror.children { - guard let key = child.label else { - continue - } - if count >= offset && !key.hasPrefix("internal_") && !key.hasPrefix("_") { - c.append((key, type(of:child.value))) - //c[key] = type(of:child.value) - } - count += 1 - } - return c - } - - open func modifyValue(_ v: Any, forKey k: String) -> Any { return v } - - /// Returns a [(String,Any)] object representation of the current object. - /// If any object property begins with an underscore, or with "internal_" it is omitted from the response. - open func asData(_ offset: Int = 0) -> [(String, Any)] { - var c = [(String, Any)]() - var count = 0 - let mirror = Mirror(reflecting: self) - for case let (label?, value) in mirror.children { - if count >= offset && !label.hasPrefix("internal_") && !label.hasPrefix("_") { - if value is [String:Any] { - c.append((label, modifyValue(try! (value as! [String:Any]).jsonEncodedString(), forKey: label))) - } else if value is [String] { - c.append((label, modifyValue((value as! [String]).joined(separator: ","), forKey: label))) - } else { - c.append((label, modifyValue(value, forKey: label))) - } - } - count += 1 - } - return c - } - - /// Returns a [String:Any] object representation of the current object. - /// If any object property begins with an underscore, or with "internal_" it is omitted from the response. - open func asDataDict(_ offset: Int = 0) -> [String: Any] { - var c = [String: Any]() - var count = 0 - let mirror = Mirror(reflecting: self) - for case let (label?, value) in mirror.children { - if count >= offset && !label.hasPrefix("internal_") && !label.hasPrefix("_") { - if value is [String:Any] { - c[label] = modifyValue(try! (value as! [String:Any]).jsonEncodedString(), forKey: label) - } else if value is [String] { - c[label] = modifyValue((value as! [String]).joined(separator: ","), forKey: label) - } else { - c[label] = modifyValue(value, forKey: label) - } - } - count += 1 - } - return c - } - - /// Returns a tuple of name & value of the object's key - /// The key is determined to be it's first property, which is assumed to be the object key. - public func firstAsKey() -> (String, Any) { - let mirror = Mirror(reflecting: self) - for case let (label?, value) in mirror.children { - return (label, modifyValue(value, forKey: label)) - } - return ("id", "unknown") - } - - /// Returns a boolean that is true if the first property in the class contains a value. - public func keyIsEmpty() -> Bool { - let (_, val) = firstAsKey() +open class StORM : StORMMirror { + + /// Results container of type StORMResultSet. + open var results = StORMResultSet() + + /// connection error status of type StORMError. + open var error = StORMError() + + /// Contain last error message as string. + open var errorMsg = "" + + /// Base empty init function. + public override init() {} + + /// Provides structure introspection to client methods. + public func cols(_ offset : Int = 0) -> [(String, Any)] { + + var c = [(String, Any)]() + var count = 0 + // If the StORM primary key is nil, we should assume the first will be the primary key. + for child in self.allChildren(includingNilValues: true, primaryKey: self.primaryKeyLabel()) { + guard let key = child.label else { + continue + } + if !key.hasPrefix("internal_") && !key.hasPrefix("_") { + c.append((child.label!, child.value)) + } + count += 1 + } + + return c + } + + /// This modifyValue function now supports optional variables and forces the value as the type it is, so that the String(describing) doesn't include the optional string wrapping the actual value. + open func modifyValue(_ v: Any, forKey k: String) -> Any { + + guard String(describing: v) != "nil" else { return v } + switch type(of: v) { + case is Int?.Type: + return v as! Int + case is String?.Type: + return v as! String + case is Double?.Type: + return v as! Double + case is Float?.Type: + return v as! Float + case is [String]?.Type: + return v as! [String] + case is [String:Any]?.Type: + return try! (v as! [String:Any]).jsonEncodedString() + default: + // Here we will support new database types by returning what we need conforming to CustomStringConvertable. + return v + } + + } + + /// Returns a [(String,Any)] object representation of the current object. + /// If any object property begins with an underscore, or with "internal_" it is omitted from the response. + open func asData(_ offset : Int = 0) -> [(String, Any)] { + var c = [(String, Any)]() + var count = 0 + for case let (label?, value) in self.allChildren(primaryKey: self.primaryKeyLabel()) { + if count >= offset && !label.hasPrefix("internal_") && !label.hasPrefix("_") { + c.append((label, modifyValue(value, forKey: label))) + } + count += 1 + } + return c + } + + /// Returns a [String:Any] object representation of the current object. + /// If any object property begins with an underscore, or with "internal_" it is omitted from the response. + open func asDataDict(_ offset : Int = 0) -> [String: Any] { + var c = [String: Any]() + var count = 0 + for case let (label?, value) in self.allChildren(primaryKey: self.primaryKeyLabel()) { + if count >= offset && !label.hasPrefix("internal_") && !label.hasPrefix("_") { + c[label] = modifyValue(value, forKey: label) + } + count += 1 + } + return c + } + + /// Returns a tuple of name & value of the object's key + /// The key is determined to be it's first property, which is assumed to be the object key. + public func firstAsKey() -> (String, Any) { + for case let (label, value) in self.allChildren(includingNilValues: true, primaryKey: self.primaryKeyLabel()) { + return (label!, modifyValue(value, forKey: label!)) + } + return ("id", "unknown") + } + + /// Returns a boolean that is true if the first property in the class contains a value. + public func keyIsEmpty() -> Bool { + let (_, val) = firstAsKey() // Grab the type of value: - let type = type(of: val) + let thetype = type(of: val) // Check if we are nil, we would then of course have an empty primary key. guard String(describing: val) != "nil" else { return true } // For now we will be expecting String & Integer key types: - switch type { + switch thetype { case is Int.Type, is Int?.Type: return (val as! Int == 0) case is String.Type, is String?.Type: return (val as! String).isEmpty default: - print("[StORM] WARNING: [\(#function)] Unexpected \(type) for PRIMARY KEY.") + print("[StORM] WARNING: [\(#function)] Unexpected \(thetype) for PRIMARY KEY.") return false } - } - - /// The create method is designed to be overridden - /// If not set in the chile class it will return an error of the enum value .notImplemented - open func create() throws { - throw StORMError.notImplemented - } - + } + + /// The create method is designed to be overridden + /// If not set in the chile class it will return an error of the enum value .notImplemented + open func create() throws { + throw StORMError.notImplemented + } + } - diff --git a/Sources/StORM/StORMExtensions.swift b/Sources/StORM/StORMExtensions.swift new file mode 100644 index 0000000..19fd3f4 --- /dev/null +++ b/Sources/StORM/StORMExtensions.swift @@ -0,0 +1,139 @@ +// +// StORMExtensions.swift +// StORM +// +// Created by Ryan Coyne on 11/22/17. +// + +import SwiftMoment +import Foundation + +//MARK: - Optionals +extension Optional { + /// This returns whether or not the optional is equal to nil. + var isNil : Bool { + return self == nil + } + /// This returns whether or not the optional is not equal to nil. + var isNotNil : Bool { + return self != nil + } + /// This returns the optional unwrapped into a boolean value. + var boolValue : Bool? { + if self.isNil { + return nil + } + switch self { + case is String, is String?: + return Bool(self as! String) + case is Int, is Int?: + return Bool(self as! Int) + default: + return nil + } + } + /// This returns the optionally wrapped object as a string value. + var stringValue : String? { + return self as? String + } + /// This returns the optionally wrapped object as a dictionary value. + var dicValue : [String:Any]! { + return self as? [String:Any] ?? [:] + } + /// This returns the optionally wrapped object as an array of dictionaries...value. + var arrayDicValue : [[String:Any]]! { + return self as? [[String:Any]] ?? [] + } + /// This returns the optionally wrapped object as an integer value. + var intValue : Int? { + return self as? Int + } + /// This returns the optionally wrapped object as a double value. + var doubleValue : Double? { + return self as? Double + } + /// This returns the optionally wrapped object as a float value. + var floatValue : Float? { + return self as? Float + } + /// This returns the optionally wrapped object as a URL value. + var urlValue : URL? { + if self.isNil { + return nil + } + switch self { + case is String, is Optional: + return URL(string: self.stringValue!) + case is URL, is URL?: + return self as? URL + default: + return nil + } + } +} +//MARK: - Boolean +extension Bool { + init(_ integerValue : Int) { + if integerValue == 0 { + self.init(false) + } else { + self.init(true) + } + } +} +//MARK: - Mirror Array: +extension Array where Iterator.Element == Mirror { + /// This function will automatically return the created/modified values IF your model has a variable with that name. Created/modified are intended to be integer values of UTC time since 1970. + /// Createdby/Modifiedby fields are automatically supported at the database level in PostgresStORM. + func allChildren(includingNilValues: Bool = false, primaryKey: String? = nil) -> [Mirror.Child] { + var allChild : [Mirror.Child] = [] + for mirror in self { + mirror.children.forEach({ (child) in + // Make sure our child has a label & the string describing the value is not nil. (Making optionals supported) + if !includingNilValues { + if child.label.isNotNil, String(describing: child.value) != "nil" { + // If we default a created/modified integer to zero we need to overwrite it here: + if child.label! == "created" || child.label! == "modified" { + var mutableChild = child + mutableChild.value = Int(utc().epoch()) + allChild.append(mutableChild) + } else { + allChild.append(child) + } + // Automatic created & modified fields: + } else if child.label.isNotNil, child.label == "created" || child.label == "modified" { + var mutableChild = child + mutableChild.value = Int(utc().epoch()) + allChild.append(mutableChild) + } + } else { + if child.label.isNotNil { + if child.label! == "created" || child.label! == "modified" { + var mutableChild = child + mutableChild.value = Int(utc().epoch()) + allChild.append(mutableChild) + } else { + allChild.append(child) + } + } + } + }) + } + // Lets make sure if the primaryKey is set, it is the first object returned for asData/asDataDic & firstAsKey functions: + if let keyLabel = primaryKey, allChild.first?.label != keyLabel { + if let index = allChild.index(where: { (child) -> Bool in + return child.label == keyLabel + }) { + allChild.move(at: index, to: 0) + } + } + return allChild + } +} + +extension Array { + /// This removes & inserts the object at the old index, to the new specified index. + mutating func move(at oldIndex: Int, to newIndex: Int) { + self.insert(self.remove(at: oldIndex), at: newIndex) + } +} diff --git a/Sources/StORM/StORMMirroring.swift b/Sources/StORM/StORMMirroring.swift new file mode 100644 index 0000000..ca04788 --- /dev/null +++ b/Sources/StORM/StORMMirroring.swift @@ -0,0 +1,53 @@ +// +// StORMMirroring.swift +// StORMMirror.swift +// +// Created by Ryan Coyne on 11/22/17. +// + +public protocol StORMMirroring { + func didInitializeSuperclass() + func allChildren(includingNilValues : Bool, primaryKey: String?) -> [Mirror.Child] + func primaryKeyLabel() -> String? +} + +open class StORMMirror: StORMMirroring { + // The superclass count will include CCXMirror, StORM, & PostgresStORM by the time we get to the subclasses we need to process. + private var superclassCount = 0 + public func didInitializeSuperclass() { + self.superclassCount += 1 + } + + /// This is intended to make it easier to specify your primary key, rather than having the id specifically in this model. + /// + /// - Returns: This returns the label for the primary key for this model. + open func primaryKeyLabel() -> String? { + return nil + } + /// This function goes through all the superclass mirrors. This is dependent on the CCXMirroring protocol. + private func superclassMirrors() -> [Mirror] { + var mirrors : [Mirror] = [] + let mir = Mirror(reflecting: self) + mirrors.append(mir) + var currentContext : Mirror? + for _ in 0...self.superclassCount { + if currentContext.isNil { + currentContext = mir.superclassMirror + } else { + currentContext = currentContext?.superclassMirror + } + if currentContext.isNotNil { + // we only want to bring in the variables from the superclasses that are beyond PostgresStORM: + mirrors.append(currentContext!) + } + } + return mirrors + } + /// This returns all the children, even all the superclass mirrored children. Use allChildren().asData() to return an array of key/values. + /// The includingNilValues optional parameter is used to return the mirror children even if the value is nil. + public func allChildren(includingNilValues : Bool = false, primaryKey: String? = nil) -> [Mirror.Child] { + // Remove out the superclass count which is private: + let children = self.superclassMirrors().allChildren(includingNilValues: includingNilValues, primaryKey: primaryKey) + return children + } +}