File: SimplePersistence.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (181 lines) | stat: -rw-r--r-- 6,207 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
/*
 This source file is part of the Swift.org open source project

 Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
 Licensed under Apache License v2.0 with Runtime Library Exception

 See http://swift.org/LICENSE.txt for license information
 See http://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import protocol Foundation.CustomNSError
import var Foundation.NSLocalizedDescriptionKey
import TSCBasic

/// A protocol which needs to be implemented by the objects which can be
/// persisted using SimplePersistence
public protocol SimplePersistanceProtocol: AnyObject, JSONSerializable {
    /// Restores state from the given json object.
    func restore(from json: JSON) throws

    /// Restores state from the given json object and supported schema version.
    func restore(from json: JSON, supportedSchemaVersion: Int) throws
}

public extension SimplePersistanceProtocol {
    func restore(from json: JSON, supportedSchemaVersion: Int) throws {}
}

extension SimplePersistence.Error: CustomStringConvertible {
    public var description: String {
        switch self {
        case let .invalidSchemaVersion(version):
            return "unsupported schema version \(version)"

        case let .restoreFailure(stateFile, error):
            return "unable to restore state from \(stateFile); \(error)"
        }
    }
}

extension SimplePersistence.Error: CustomNSError {
    public var errorUserInfo: [String : Any] {
        return [NSLocalizedDescriptionKey: self.description]
    }
}

/// A simple persistence helper.
///
/// This class can be used to save and restore state of objects in simple JSON
/// format. Note: This class is not thread safe.
public final class SimplePersistence {
    /// Describes a SimplePersistence errors.
    public enum Error: Swift.Error {
        case invalidSchemaVersion(Int)

        case restoreFailure(stateFile: AbsolutePath, error: Swift.Error)
    }

    /// The fileSystem to operate on.
    private let fileSystem: FileSystem

    /// The schema of the state file.
    private let schemaVersion: Int

    /// The schema versions, besides the current schema, that are supported for restoring.
    private let supportedSchemaVersions: Set<Int>

    /// The path at which we persist the state.
    private let statePath: AbsolutePath

    /// The list of paths to search for restore if no state was found at statePath.
    private let otherStatePaths: [AbsolutePath]

    /// Writes the state files with pretty print JSON.
    private let prettyPrint: Bool

    public init(
        fileSystem: FileSystem,
        schemaVersion: Int,
        supportedSchemaVersions: Set<Int> = [],
        statePath: AbsolutePath,
        otherStatePaths: [AbsolutePath] = [],
        prettyPrint: Bool = false
    ) {
        assert(!supportedSchemaVersions.contains(schemaVersion), "Supported schema versions should not include the current schema")
        self.fileSystem = fileSystem
        self.schemaVersion = schemaVersion
        self.supportedSchemaVersions = supportedSchemaVersions
        self.statePath = statePath
        self.otherStatePaths = otherStatePaths
        self.prettyPrint = prettyPrint
    }

    @discardableResult
    public func restoreState(_ object: SimplePersistanceProtocol) throws -> Bool {
        do {
            return try _restoreState(object)
        } catch {
            throw Error.restoreFailure(stateFile: statePath, error: error)
        }
    }

    private func _restoreState(_ object: SimplePersistanceProtocol) throws -> Bool {
        guard let path = findStatePath() else {
            return false
        }
        // Load the state.
        let json = try JSON(bytes: try fileSystem.readFileContents(path))
        // Get the schema version.
        let version: Int = try json.get("version")

        // Restore the state based on the provided schema version.
        switch version {
        case schemaVersion:
            try object.restore(from: json.get("object"))

        case _ where supportedSchemaVersions.contains(version):
            try object.restore(from: json.get("object"), supportedSchemaVersion: version)

        default:
            throw Error.invalidSchemaVersion(version)
        }

        // If we loaded an old file path, migrate to the new one.
        if path != statePath {
            try fileSystem.move(from: path, to: statePath)
        }

        return true
    }

    /// Merges the two given json if they both are dictionaries.
    ///
    /// In case of collisions, keep the value from new dictionary.
    private func merge(old: JSON?, new: JSON) -> JSON {
        guard case let .dictionary(oldDict)? = old,
              case var .dictionary(newDict) = new else {
            return new
        }

        // Merge the dictionaries, keeping new values in case of collisions.
        for (key, value) in oldDict where newDict[key] == nil {
            newDict[key] = value
        }

        return JSON(newDict)
    }

    public func saveState(_ object: SimplePersistanceProtocol) throws {
        var json = [String: JSON]()

        // Load the current data.
        let jsonData = try? JSON(bytes: fileSystem.readFileContents(statePath))
        if case let .dictionary(dict)? = jsonData {
            json = dict
        }

        // Set the schema version.
        json["version"] = self.schemaVersion.toJSON()

        // Set the object, keeping any keys in object which we don't know about.
        json["object"] = merge(old: json["object"], new: object.toJSON())

        try fileSystem.createDirectory(statePath.parentDirectory, recursive: true)
        // FIXME: This should write atomically.
        try fileSystem.writeFileContents(
            statePath, bytes: JSON(json).toBytes(prettyPrint: self.prettyPrint))
    }

    /// Returns true if the state file exists on the filesystem.
    public func stateFileExists() -> Bool {
        return findStatePath() != nil
    }

    private func findStatePath() -> AbsolutePath? {
        // Return the first path that exists.
        let allPaths = [statePath] + otherStatePaths
        let path = allPaths.first(where: { fileSystem.exists($0) })
        return path
    }
}