A comment is a last resort, and these were not one: section banners, doc headers naming what the declaration below already names, and notes saying in prose what the next line says in code. What is left is what the code cannot show -- an ordering that matters, a platform quirk, why the obvious way fails.
216 lines
7.1 KiB
Swift
216 lines
7.1 KiB
Swift
import Foundation
|
|
import GRDB
|
|
|
|
public struct NotebookEntry: Codable, Identifiable, Sendable, FetchableRecord, PersistableRecord {
|
|
public static let databaseTableName = "notebook_entry"
|
|
|
|
public enum Kind: String, Codable, Sendable {
|
|
case note
|
|
case capture
|
|
/// Smalltalk the reader can edit and run, its source held in `details`.
|
|
case pharo
|
|
}
|
|
|
|
public var id: UUID
|
|
public var kind: Kind
|
|
/// Original creator at index 0, followed by everyone who's edited the
|
|
/// entry (deduped, preserving first-edit order). The server appends on
|
|
/// each `update` op it applies.
|
|
public var editors: [Author]
|
|
public var timestamp: Date
|
|
/// Server-authoritative sort key. New entries get `(current max + 1000)`;
|
|
/// drag operations slot in at the midpoint of two neighbors. Clients
|
|
/// sort by `(position, id)` so ties break deterministically.
|
|
public var position: Double
|
|
public var title: String
|
|
public var details: String
|
|
public var styledDetails: StyledText?
|
|
public var jsValue: JSInspectValue?
|
|
public var pharoSnapshot: PharoSnapshot?
|
|
public var pharoResultFuel: Data?
|
|
public var binaryData: Data?
|
|
public var sessionID: UUID?
|
|
public var processName: String?
|
|
|
|
public var author: Author? { editors.first }
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id
|
|
case kind
|
|
case editors
|
|
case timestamp
|
|
case position
|
|
case title
|
|
case details
|
|
case styledDetails = "styled_details"
|
|
case jsValue = "js_value"
|
|
case pharoSnapshot = "pharo_snapshot"
|
|
case pharoResultFuel = "pharo_result_fuel"
|
|
case binaryData = "binary_data"
|
|
case sessionID = "session_id"
|
|
case processName = "process_name"
|
|
}
|
|
|
|
public init(
|
|
id: UUID = UUID(),
|
|
kind: Kind = .capture,
|
|
editors: [Author] = [],
|
|
timestamp: Date = .now,
|
|
position: Double = 0,
|
|
title: String,
|
|
details: String,
|
|
styledDetails: StyledText? = nil,
|
|
jsValue: JSInspectValue? = nil,
|
|
pharoSnapshot: PharoSnapshot? = nil,
|
|
pharoResultFuel: Data? = nil,
|
|
binaryData: Data? = nil,
|
|
sessionID: UUID? = nil,
|
|
processName: String? = nil
|
|
) {
|
|
self.id = id
|
|
self.kind = kind
|
|
self.editors = editors
|
|
self.timestamp = timestamp
|
|
self.position = position
|
|
self.title = title
|
|
self.details = details
|
|
self.styledDetails = styledDetails
|
|
self.jsValue = jsValue
|
|
self.pharoSnapshot = pharoSnapshot
|
|
self.pharoResultFuel = pharoResultFuel
|
|
self.binaryData = binaryData
|
|
self.sessionID = sessionID
|
|
self.processName = processName
|
|
}
|
|
|
|
public func toJSON() -> [String: Any] {
|
|
let formatter = ISO8601DateFormatter()
|
|
formatter.timeZone = TimeZone(secondsFromGMT: 0)
|
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
|
|
var obj: [String: Any] = [
|
|
"id": id.uuidString,
|
|
"kind": kind.rawValue,
|
|
"editors": editors.map { author in
|
|
[
|
|
"id": author.id,
|
|
"name": author.name,
|
|
"avatar": author.avatarURL,
|
|
]
|
|
},
|
|
"timestamp": formatter.string(from: timestamp),
|
|
"position": position,
|
|
"title": title,
|
|
"details": details,
|
|
]
|
|
|
|
if let styled = styledDetails,
|
|
let data = try? JSONEncoder().encode(styled),
|
|
let jsonObject = try? JSONSerialization.jsonObject(with: data)
|
|
{
|
|
obj["styled_details"] = jsonObject
|
|
}
|
|
|
|
if let val = jsValue,
|
|
let data = try? JSONEncoder().encode(val),
|
|
let jsonObject = try? JSONSerialization.jsonObject(with: data)
|
|
{
|
|
obj["js_value"] = jsonObject
|
|
}
|
|
|
|
if let snapshot = pharoSnapshot,
|
|
let data = try? JSONEncoder().encode(snapshot),
|
|
let jsonObject = try? JSONSerialization.jsonObject(with: data)
|
|
{
|
|
obj["pharo_snapshot"] = jsonObject
|
|
}
|
|
|
|
if let pharoResultFuel {
|
|
obj["pharo_result_fuel"] = pharoResultFuel.base64EncodedString()
|
|
}
|
|
|
|
if let processName {
|
|
obj["process_name"] = processName
|
|
}
|
|
|
|
return obj
|
|
}
|
|
|
|
public static func fromJSON(_ obj: [String: Any], binaryData data: [UInt8]?) -> NotebookEntry? {
|
|
let formatter = ISO8601DateFormatter()
|
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
|
|
guard
|
|
let idString = obj["id"] as? String,
|
|
let uuid = UUID(uuidString: idString),
|
|
let kindRaw = obj["kind"] as? String,
|
|
let kind = Kind(rawValue: kindRaw),
|
|
let timestampString = obj["timestamp"] as? String,
|
|
let ts = formatter.date(from: timestampString),
|
|
let title = obj["title"] as? String,
|
|
let details = obj["details"] as? String
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
var editors: [Author] = []
|
|
if let arr = obj["editors"] as? [[String: Any]] {
|
|
for item in arr {
|
|
guard let id = item["id"] as? String,
|
|
let name = item["name"] as? String else { continue }
|
|
let avatar = (item["avatar"] as? String) ?? ""
|
|
editors.append(Author(id: id, name: name, avatarURL: avatar))
|
|
}
|
|
}
|
|
|
|
let position = (obj["position"] as? Double)
|
|
?? (obj["position"] as? NSNumber)?.doubleValue
|
|
?? 0
|
|
|
|
var styledDetails: StyledText? = nil
|
|
if let raw = obj["styled_details"],
|
|
let data = try? JSONSerialization.data(withJSONObject: raw),
|
|
let decoded = try? JSONDecoder().decode(StyledText.self, from: data)
|
|
{
|
|
styledDetails = decoded
|
|
}
|
|
|
|
var jsValue: JSInspectValue? = nil
|
|
if let raw = obj["js_value"] {
|
|
if let data = try? JSONSerialization.data(withJSONObject: raw),
|
|
let decoded = try? JSONDecoder().decode(JSInspectValue.self, from: data)
|
|
{
|
|
jsValue = decoded
|
|
}
|
|
}
|
|
|
|
var pharoSnapshot: PharoSnapshot? = nil
|
|
if let raw = obj["pharo_snapshot"],
|
|
let data = try? JSONSerialization.data(withJSONObject: raw),
|
|
let decoded = try? JSONDecoder().decode(PharoSnapshot.self, from: data)
|
|
{
|
|
pharoSnapshot = decoded
|
|
}
|
|
|
|
let pharoResultFuel = (obj["pharo_result_fuel"] as? String).flatMap { Data(base64Encoded: $0) }
|
|
|
|
let processName = obj["process_name"] as? String
|
|
|
|
return NotebookEntry(
|
|
id: uuid,
|
|
kind: kind,
|
|
editors: editors,
|
|
timestamp: ts,
|
|
position: position,
|
|
title: title,
|
|
details: details,
|
|
styledDetails: styledDetails,
|
|
jsValue: jsValue,
|
|
pharoSnapshot: pharoSnapshot,
|
|
pharoResultFuel: pharoResultFuel,
|
|
binaryData: data.map { Data($0) },
|
|
processName: processName
|
|
)
|
|
}
|
|
}
|