A protocol-oriented, batteries-included foundation for persistence in Swift.
This library has ambitious goals:
- Provide a protocol-oriented foundation for all the critical aspects of a typical, modern Swift application's persistence layer.
- Provide standard, high performance primitives for the most common data formats (
JSON,CSV,XMLetc.). - Unf***
Codable.
- An opinionated, protocol-oriented encapsulated of persistent identifiers (both type identifiers and instance identifiers).
- A modular plugin system for
Codable(achieved by custom encoders & decoders that can wrap existing ones, macros, and a suite of protocols). - Better diagnostics for
Codableerrors (EncodingErrorandDecodingErrorare subpar). - Essential data storage primitives (see
@FileStorageand@FolderStorage– similar to SwiftUI's@AppStoragebut for the application's persistence layer.) - A high performance
JSONprimitive. - A high performance
CSVprimitive. - A high performance
XMLprimitive (backed by the excellentXMLCoderlibrary for now).
Examples:
The JSONSchema product provides a language-independent representation of the
commonly used JSON Schema Draft 2020-12 core, applicator, validation, and
metadata vocabulary. It preserves Boolean schemas, $schema, $id, $defs,
single and union type declarations, object/array/string/number constraints,
and schema composition and conditionals during Codable round trips.
import JSONSchemaJSONSchema.validate(_:) validates Codable values or CorePersistence's
JSON representation. It resolves local #/$defs/... references. Schema
retrieval for external references and assertion of annotation-only keywords
such as format, readOnly, and writeOnly are deliberately left to callers.
let restaurantBookingSchema = JSONSchema(
type: .object,
description: "Information required to make a restaurant booking",
properties: [
"name": JSONSchema(
type: .string,
description: "The name of the restaurant"
),
"date" : JSONSchema(
type: .string,
description: "The date of the restaurant booking in yyyy-MM-dd format. Should be a date with a year, month, day."
),
"time" : JSONSchema(
type: .string,
description: "The time of the reservation in HH:mm format. Should include hours and minutes."
),
"number_of_people" : JSONSchema(
type: .integer,
description: "The total number of people the reservation is for"
)
],
// the required parameter specifies whether all properties listed are required or not
// note that you can also pass in an array of strings specifying the properties that are required as follows:
// required: ["name", "date", "time"] - "number_of_people" not required
required: false
)You can also create a JSONSchema based on your object as follows:
struct RestaurantBooking: Codable, Hashable, Sendable {
let name: String?
let date: String?
let time: String?
let numberOfPeople: Int?
}
func createRestaurantBookingSchema() -> JSONSchema? {
do {
let restaurantBookingSchema: JSONSchema = try JSONSchema(
type: RestaurantBooking.self,
description: "Information required to make a restaurant booking",
propertyDescriptions: [
"name": "The name of the restaurant",
"date": "The date of the restaurant booking in yyyy-MM-dd format. Should be a date with a year, month, day.",
"time": "The time of the reservation in HH:mm format. Should include hours and minutes.",
"number_of_people": "The total number of people the reservation is for"
],
required: false
)
return restaurantBookingSchema
} catch {
print(error)
return nil
}
}The @FileStorage property wrapper is a tool designed to simplify data persistence by automatically handling the reading and writing of data to files. Features include:
- Automatic Data Handling:
@FileStorageautomates the process of storing and retrieving data. Data is automatically written to a file whenever it changes, and read from the file when needed. - Configurable Storage Location: You can specify where the data should be stored, such as in the application's documents directory (
.appDocuments) or other specific locations (e.g..desktop,.downloads,.musicDirectory) that can be configured based on permissions and user settings. This flexibility ensures that data storage can be adapted to different application needs and environments. - Customizable Serialization: It supports different data coders like JSON or property list, allowing for easy serialization and deserialization of complex data types. This is useful for storing custom objects, as long as they conform to the
Codableprotocol. - Error Handling Strategies:
@FileStorageoffers customizable error handling strategies, such as discarding corrupted data and resetting to default values, or even halting the application on errors. This is critical for maintaining data integrity and application stability.
Using @FileStorage can be used as an alternative to SwiftData for simpler, smaller-scale applications because it offers a more straightforward and lightweight approach to data persistence. It seamlessly integrates with SwiftUI, providing an easy-to-use, declarative syntax that minimizes boilerplate code and automatically handles serialization of Codable objects. This eliminates the need for complex database setup, schema management, and migrations, making it ideal for applications that don't require the advanced features and overhead of a full-fledged database system. Additionally, @FileStorage allows for customizable error handling strategies, ensuring data integrity without the complexity of managing a relational database.
// Making DataStore an ObservableObject allows to receive notifications when values change
// In View: @StateObject var dataStore: DataStore = .shared
public final class DataStore: ObservableObject {
@MainActor
// the DataStore should be a singleton
public static let shared = DataStore()
@FileStorage(
// directory of the app docuemnts set to .appDocuments means that the file will be stored in the app sandbox. User permissions dialog will not show up
// if you use other options (e.g. .documents, .desktop, .downloads, .musicDirectory, etc), make sure to enable app permissions to access those folders. The user will have to grant permissions.
.appDocuments,
path: "path.json",
coder: .json, // .propertyList is also supported
// In case of read error, discard existing data (if any) and reset with the initial value.
options: .init(readErrorRecoveryStrategy: .discardAndReset)
)
// the file will be auto-updated as objects are changed
var objects: IdentifierIndexingArrayOf<MyIdentifiableObject> = []
@MainActor
private init() {
if objects.isEmpty {
objects = [.init(someText: "Hello World")]
}
}
}
// must conform to Identifiable, Hashable, and Codable
public struct MyIdentifiableObject: Identifiable, Hashable, Codable {
public var id = UUID()
public var someText: String?
init(someText: String?) {
self.someText = someText
}
}Other custom options for initializing @FileStorage:
// For Application Groups
@FileStorage(
location: {
return try! URL(
directory: .securityApplicationGroup("group.com.yourgroupname.Shared")
)
.appending(path: "DirectoryPath", directoryHint: .isDirectory)
.appending(path: "data.json")
},
coder: JSONCoder(), // TOMLCoder() also supported
options: .init(readErrorRecoveryStrategy: .fatalError)
)
// Storing in Home Directory using URL
// This will require to updated settings to allow Read/Write access to the directory
@FileStorage(
url: URL.homeDirectory.appending(path: "data.json"),
coder: JSONCoder()
)
// Specifying Path & Filename
@FileStorage(
directory: .appDocuments,
path: "ProjectName",
filename: UUID.self,
coder: HadeanTopLevelCoder(coder: JSONCoder()),
options: .init(readErrorRecoveryStrategy: .discardAndReset)
)@HadeanIdentifier is a general purpose persistent identifier to identify distinct objects.
Usage:
@HadeanIdentifier("guvol-haboz-motiz-povag")
struct MyObject {
// your object code here
}It is recommended to use a Proquint - Identifiers that are not real words but are as Readable, Spellable, and Pronounceable as words.
Python scrip to generate a proquint:
from proquint import uint2quint
import random
# Generate a unique string of 4 proquint words
unique_string = '-'.join(uint2quint(random.getrandbits(32)) for _ in range(4))
print(unique_string)Sample JSON Data:
let jsonData = """
{
"id": 1,
"name": "John Doe",
"email": "john@example.com"
}
""".data(using: .utf8)!Using regular JSONDecoder():
do {
// using regular JSONDecoder()
let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: jsonData)
} catch {
print(error)
}Printed Error when using JSONDecoder():
keyNotFound(CodingKeys(stringValue: "wrongKey", intValue: nil),
Swift.DecodingError.Context(codingPath: [],
debugDescription: "No value associated with key CodingKeys(stringValue: \"wrongKey\", intValue: nil) (\"wrongKey\").",
underlyingError: nil))
Using JSONDecoder()._modular():
do {
// using regular JSONDecoder()._modular()
let decoder = JSONDecoder()._modular()
let user = try decoder.decode(User.self, from: jsonData)
} catch {
print(error)
}Printed Error when using JSONDecoder()._modular() (actual JSON Data printed out):
keyNotFound("wrongKey",
context for User: (coding path: []),
Optional(["id": 1.0, "email": "john@example.com", "name": "John Doe"]))
CorePersistence is licensed under the MIT License.
XMLCoder
- Link: https://github.com/CoreOffice/XMLCoder
- License: MIT License
- Authors: Shawn Moore and XMLCoder contributors