A package used for serializing and restoring Swift objects.
Any object that conforms to Storable
can easily be written to and read from the database.
// Lets define a database first
// Realistically this would be managed at the application level
let database = SerializationDatabase()
// Here's the object we want to read/write
let person = Person(name: "Andre", height: 188.0)
Writing to the database. Returns true
on success and false
on failure.
// Writes our person object to the database
// Generates a random record ID
database.write(Record(data: person))
// You can also provide your own record ID
// (Overrides any record with the same ID)
database.write(Record(id: "myID", data: person))
There's three ways to read from the database.
// Reads a specific Person object by their record ID
let readPerson: Person? = database.read(id: "myID")
// Reads all saved Person objects
let readPeople: [Person] = database.read()
// Reads all saved Person record IDs
let readPeopleIDs: [String] = database.readIDs(Person.self)
We can count records. If we wish we can specify a specific object type.
// Counts all records in the database
let count = database.count()
// Counts the number of People records
let peopleCount = database.count(Person.self)
We can delete records, or even clear the database.
// Delete a record with specific record ID
// (Returns true if a record was deleted)
database.delete(id: "myID")
// Delete all Person records
// (Returns the number of records deleted)
database.delete(Person.self)
// Delete everything
// (Returns the number of records deleted)
database.clearDatabase()
We can also use transactions if we wish. So long as we haven't committed yet, all changes made during a transaction can be rolled back.
database.startTransaction()
let person1Saved = database.write(Record(data: person1))
let person2Saved = database.write(Record(data: person2))
if person1Saved && person2Saved {
// We can commit the transaction to finalise the changes
database.commitTransaction()
} else {
// Or we can rollback to undo all changes made during the transaction
database.rollbackTransaction()
}
Everything works asynchronously - even from multiple concurrent threads.
DispatchQueue.global().async {
let readPerson: Person? = database.read(id: "myID")
DispatchQueue.main.async {
// Update UI when done
}
}
Class definitions, and how you would conform them to Storable
to be serialized.
class Person: Storable {
private(set) var firstName: String
private(set) var lastName: String
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
// MARK: - Serialization
private enum Field: String {
case firstName
case lastName
}
required init(dataObject: DataObject) {
self.firstName = dataObject.get(Field.firstName.rawValue)
self.lastName = dataObject.get(Field.lastName.rawValue)
// NOTE: You can also override the default return value if the value can't be found
// self.lastName = dataObject.get(Field.lastName.rawValue, onFail: "MISSING")
}
func toDataObject() -> DataObject {
return DataObject(self)
.add(key: Field.firstName.rawValue, value: self.firstName)
.add(key: Field.lastName.rawValue, value: self.lastName)
}
}
class Student: Person {
private(set) var homework = [Homework]()
private(set) var debt: Double
private(set) var teacher: Teacher
private(set) var subjectNames: [String]
init(firstName: String, lastName: String, debt: Double, teacher: Teacher, subjectNames: [String]) {
self.debt = debt
self.teacher = teacher
self.subjectNames = subjectNames
super.init(firstName: firstName, lastName: lastName)
}
func giveHomework(_ homework: Homework) {
self.homework.append(homework)
}
// MARK: - Serialization
private enum Field: String {
case debt
case homework
case teacher
case subjectNames
}
required init(dataObject: DataObject) {
self.debt = dataObject.get(Field.debt.rawValue)
self.homework = dataObject.getObjectArray(Field.homework.rawValue, type: Homework.self)
self.teacher = dataObject.getObject(Field.teacher.rawValue, type: Teacher.self)
self.subjectNames = dataObject.get(Field.subjectNames.rawValue)
super.init(dataObject: dataObject)
}
override func toDataObject() -> DataObject {
return super.toDataObject()
.add(key: Field.debt.rawValue, value: self.debt)
.add(key: Field.homework.rawValue, value: self.homework)
.add(key: Field.teacher.rawValue, value: self.teacher)
.add(key: Field.subjectNames.rawValue, value: self.subjectNames)
}
}
class Teacher: Person {
private(set) var salary: Double
init(firstName: String, lastName: String, salary: Double) {
self.salary = salary
super.init(firstName: firstName, lastName: lastName)
}
// MARK: - Serialization
private enum Field: String {
case salary
}
required init(dataObject: DataObject) {
self.salary = dataObject.get(Field.salary.rawValue)
super.init(dataObject: dataObject)
}
override func toDataObject() -> DataObject {
return super.toDataObject()
.add(key: Field.salary.rawValue, value: self.salary)
}
}
class Homework: Storable {
public let answers: String
private(set) var grade: Int?
init(answers: String, grade: Int?) {
self.answers = answers
self.grade = grade
}
// MARK: - Serialization
private enum Field: String {
case answers
case grade
}
required init(dataObject: DataObject) {
self.answers = dataObject.get(Field.answers.rawValue)
self.grade = dataObject.get(Field.grade.rawValue)
}
func toDataObject() -> DataObject {
return DataObject(self)
.add(key: Field.answers.rawValue, value: self.answers)
.add(key: Field.grade.rawValue, value: self.grade)
}
}
Serialization works with typealias
definitions, but the syntax is a little different.
class Person // ...
protocol HasDebt // ...
typealias Student = Person & HasDebt
// ...
// In init(dataObject: DataObject)
self.students = dataObject.getObjectArray(Field.students.rawValue, type: Person.self) as! [any Student]
// In toDataObject() -> DataObject
.add(key: Field.students.rawValue, value: self.students as [Person])
Your classes will change over time.
If you remove properties from your class but had saved it previously, just don't read it from the DataObject
.
If you had previously saved your objects then later added new properties to their class definition, you define within the class initialiser how the class handles the missing data.
private var firstName: String
// ...
// By default, self.firstName will be set to "" if no value is returned
self.firstName = dataObject.get(Field.firstName.rawValue)
// self.firstName will be set to "MISSING" if no value is returned
self.firstName = dataObject.get(Field.firstName.rawValue, onFail: "MISSING")
If the property is optional and no value is returned, it will be set to nil
.
private var firstName: String?
// ...
// self.firstName will be set to nil if no value is returned
self.firstName = dataObject.get(Field.firstName.rawValue)
You may change the names of your classes and properties. You have to account for these refactors.
Here's how your class may look beforehand:
class Person: Storable {
private(set) var name: String
init(name: String) {
self.name = name
}
// MARK: - Serialization
private enum Field: String {
case name
}
required init(dataObject: DataObject) {
self.name = dataObject.get(Field.name.rawValue)
}
func toDataObject() -> DataObject {
return DataObject(self)
.add(key: Field.name.rawValue, value: self.name)
}
}
The rules to follow are basically:
- For class name refactors, call
Legacy.addClassRefactor
with the new name. - For property name refactors, assuming you change the
Field
case, include thelegacyKeys
parameter in the.get
method call for that property.
If you refactor Person
to Human
, and name
to firstName
, the result should look as such:
// On application startup
Legacy.addClassRefactor(old: "Person", new: "Human")
// ...
class Human: Storable {
private(set) var firstName: String
init(firstName: String) {
self.firstName = firstName
}
// MARK: - Serialization
private enum Field: String {
case firstName
}
required init(dataObject: DataObject) {
self.firstName = dataObject.get(Field.firstName.rawValue, legacyKeys: ["name"])
}
func toDataObject() -> DataObject {
return DataObject(self)
.add(key: Field.firstName.rawValue, value: self.firstName)
}
}
init()
/// True if a transaction is ongoing
var transactionActive: Bool
/// Write a record to the database. If the id already exists, replace it.
/// - Parameters:
/// - record: The record to be written
/// - Returns: If the write was successful
func write<T: Storable>(_ record: Record<T>) -> Bool
/// Retrieve all storable objects of a specified type.
/// - Returns: All saved objects of the specified type
func read<T: Storable>() -> [T]
/// Retrieve the storable object with the matching id.
/// - Parameters:
/// - id: The id of the stored record
/// - Returns: The storable object with the matching id
func read<T: Storable>(id: String) -> T?
/// Retrieve all the record IDs of all objects of a specific type.
/// - Parameters:
/// - allOf: The type to retrieve the ids from
/// - Returns: All stored record ids of the provided type
func readIDs<T: Storable>(_ allOf: T.Type) -> [String]
/// Delete all instances of an object
/// - Parameters:
/// - allOf: The type to delete
/// - Returns: The number of records deleted
func delete<T: Storable>(_ allOf: T.Type) -> Int
/// Delete the record with the matching id.
/// - Parameters:
/// - id: The id of the stored record to delete
/// - Returns: If any record was successfully deleted
func delete(id: String) -> Bool
/// Clear the entire database.
/// - Returns: The number of records deleted
func clearDatabase() -> Int
/// Count the number of records saved.
/// - Returns: The number of records
func count() -> Int
/// Count the number of records of a certain type saved.
/// - Parameters:
/// - allOf: The type to count
/// - Returns: The number of records of the provided type currently saved
func count<T: Storable>(_ allOf: T.Type) -> Int
/// Begin a database transaction.
/// Changes are still made immediately, however to finalise the transaction, `commitTransaction` should be executed.
/// All changes made during the transaction are cancelled if `rollbackTransaction` is executed.
/// If a new transaction is started before this one is committed, this transaction's changes are rolled back.
/// - Parameters:
/// - override: Override (roll back) the current transaction if one is currently active already - true by default
/// - Returns: True if the transaction was successfully started
func startTransaction(override: Bool) -> Bool
/// Commit the current transaction. All changes made during the transaction are finalised.
/// - Returns: True if there was an active transaction and it was committed
func commitTransaction() -> Bool
/// Rollback the current transaction. All changes made during the transaction are undone.
/// - Returns: True if there was an active transaction and it was rolled back
func rollbackTransaction() -> Bool
/// Constructor.
/// - Parameters:
/// - object: The Storable object this will represent
init(_ object: Storable)
/// Constructor.
/// - Parameters:
/// - rawString: The raw JSON string to populate this with, generated from another DataObject
init(rawString: String)
/// This DataObject's raw data
var rawData: Data
func add(key: String, value: String) -> Self
func add(key: String, value: String?) -> Self
func add(key: String, value: [String]) -> Self
func add(key: String, value: [String?]) -> Self
func add(key: String, value: Int) -> Self
func add(key: String, value: Int?) -> Self
func add(key: String, value: [Int]) -> Self
func add(key: String, value: [Int?]) -> Self
func add(key: String, value: Double) -> Self
func add(key: String, value: Double?) -> Self
func add(key: String, value: [Double]) -> Self
func add(key: String, value: [Double?]) -> Self
func add(key: String, value: Bool) -> Self
func add(key: String, value: Bool?) -> Self
func add(key: String, value: [Bool]) -> Self
func add(key: String, value: [Bool?]) -> Self
func add(key: String, value: Date) -> Self
func add(key: String, value: Date?) -> Self
func add(key: String, value: [Date]) -> Self
func add(key: String, value: [Date?]) -> Self
func add<T: Storable>(key: String, value: T) -> Self
func add<T: Storable>(key: String, value: T?) -> Self
func add<T: Storable>(key: String, value: [T]) -> Self
func add<T: Storable>(key: String, value: [T?]) -> Self
func get(_ key: String, onFail: String = "", legacyKeys: [String] = []) -> String
func get(_ key: String, legacyKeys: [String] = []) -> String?
func get(_ key: String, legacyKeys: [String] = []) -> [String]
func get(_ key: String, legacyKeys: [String] = []) -> [String?]
func get(_ key: String, onFail: Int = 0, legacyKeys: [String] = []) -> Int
func get(_ key: String, legacyKeys: [String] = []) -> Int?
func get(_ key: String, legacyKeys: [String] = []) -> [Int]
func get(_ key: String, legacyKeys: [String] = []) -> [Int?]
func get(_ key: String, onFail: Double = 0.0, legacyKeys: [String] = []) -> Double
func get(_ key: String, legacyKeys: [String] = []) -> Double?
func get(_ key: String, legacyKeys: [String] = []) -> [Double]
func get(_ key: String, legacyKeys: [String] = []) -> [Double?]
func get(_ key: String, onFail: Bool, legacyKeys: [String] = []) -> Bool
func get(_ key: String, legacyKeys: [String] = []) -> Bool?
func get(_ key: String, legacyKeys: [String] = []) -> [Bool]
func get(_ key: String, legacyKeys: [String] = []) -> [Bool?]
func get(_ key: String, onFail: Date = Date(), legacyKeys: [String] = []) -> Date
func get(_ key: String, legacyKeys: [String] = []) -> Date?
func get(_ key: String, legacyKeys: [String] = []) -> [Date]
func get(_ key: String, legacyKeys: [String] = []) -> [Date?]
func getObject<T>(_ key: String, type: T.Type, legacyKeys: [String] = []) -> T where T: Storable
func getObjectOptional<T>(_ key: String, type: T.Type, legacyKeys: [String] = []) -> T? where T: Storable
func getObjectArray<T>(_ key: String, type: T.Type, legacyKeys: [String] = []) -> [T] where T: Storable
func toRawString() -> String?