Files
luma/Sources/LumaCore/Models/NotebookEntry.swift
Ole André Vadla Ravnås f8d25015ae Drop the comments that only restate the code
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.
2026-08-13 13:09:18 +02:00

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
)
}
}