125 lines
4.0 KiB
Swift
125 lines
4.0 KiB
Swift
import Foundation
|
|
|
|
public protocol ProjectStore: Sendable {
|
|
func create(_ project: SongProject) async throws
|
|
func save(_ project: SongProject) async throws
|
|
func open(id: String) async throws -> SongProject
|
|
func loadProjectIDs() async throws -> [String]
|
|
func loadProjectList() async throws -> [SongProjectListItem]
|
|
}
|
|
|
|
public struct SongProjectListItem: Codable, Equatable, Identifiable, Sendable {
|
|
public let id: String
|
|
public var title: String
|
|
public var idea: String
|
|
public var createdAt: Date
|
|
public var updatedAt: Date
|
|
|
|
public init(id: String, title: String, idea: String, createdAt: Date, updatedAt: Date) {
|
|
self.id = id
|
|
self.title = title
|
|
self.idea = idea
|
|
self.createdAt = createdAt
|
|
self.updatedAt = updatedAt
|
|
}
|
|
|
|
public init(project: SongProject) {
|
|
self.init(
|
|
id: project.id,
|
|
title: project.title,
|
|
idea: project.idea,
|
|
createdAt: project.createdAt,
|
|
updatedAt: project.updatedAt
|
|
)
|
|
}
|
|
}
|
|
|
|
public actor LocalSongProjectStore: ProjectStore {
|
|
private let directoryURL: URL
|
|
private let encoder: JSONEncoder
|
|
private let decoder: JSONDecoder
|
|
|
|
public init(directoryURL: URL) {
|
|
self.directoryURL = directoryURL
|
|
self.encoder = JSONEncoder()
|
|
self.decoder = JSONDecoder()
|
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
}
|
|
|
|
public func create(_ project: SongProject) async throws {
|
|
try ensureDirectoryExists()
|
|
let url = projectURL(for: project.id)
|
|
guard !FileManager.default.fileExists(atPath: url.path) else {
|
|
throw LocalSongProjectStoreError.projectAlreadyExists(project.id)
|
|
}
|
|
try saveEncoded(project, to: url)
|
|
}
|
|
|
|
public func save(_ project: SongProject) async throws {
|
|
try ensureDirectoryExists()
|
|
try saveEncoded(project, to: projectURL(for: project.id))
|
|
}
|
|
|
|
public func open(id: String) async throws -> SongProject {
|
|
let url = projectURL(for: id)
|
|
guard FileManager.default.fileExists(atPath: url.path) else {
|
|
throw LocalSongProjectStoreError.projectNotFound(id)
|
|
}
|
|
let data = try Data(contentsOf: url)
|
|
return try decoder.decode(SongProject.self, from: data)
|
|
}
|
|
|
|
public func loadProjectIDs() async throws -> [String] {
|
|
try await loadProjectList().map(\.id)
|
|
}
|
|
|
|
public func loadProjectList() async throws -> [SongProjectListItem] {
|
|
try ensureDirectoryExists()
|
|
return try projectFileURLs()
|
|
.map { url in
|
|
let data = try Data(contentsOf: url)
|
|
let project = try decoder.decode(SongProject.self, from: data)
|
|
return SongProjectListItem(project: project)
|
|
}
|
|
.sorted { left, right in
|
|
if left.updatedAt == right.updatedAt {
|
|
return left.title.localizedStandardCompare(right.title) == .orderedAscending
|
|
}
|
|
return left.updatedAt > right.updatedAt
|
|
}
|
|
}
|
|
|
|
private func ensureDirectoryExists() throws {
|
|
try FileManager.default.createDirectory(
|
|
at: directoryURL,
|
|
withIntermediateDirectories: true
|
|
)
|
|
}
|
|
|
|
private func saveEncoded(_ project: SongProject, to url: URL) throws {
|
|
let data = try encoder.encode(project)
|
|
try data.write(to: url, options: [.atomic])
|
|
}
|
|
|
|
private func projectFileURLs() throws -> [URL] {
|
|
try FileManager.default.contentsOfDirectory(
|
|
at: directoryURL,
|
|
includingPropertiesForKeys: nil
|
|
)
|
|
.filter { $0.pathExtension == Self.fileExtension }
|
|
}
|
|
|
|
private func projectURL(for id: String) -> URL {
|
|
directoryURL
|
|
.appendingPathComponent(id)
|
|
.appendingPathExtension(Self.fileExtension)
|
|
}
|
|
|
|
private static let fileExtension = "json"
|
|
}
|
|
|
|
public enum LocalSongProjectStoreError: Error, Equatable, Sendable {
|
|
case projectAlreadyExists(String)
|
|
case projectNotFound(String)
|
|
}
|