Add outbound source capabilities model

This commit is contained in:
John Burwell 2026-05-29 16:29:11 -05:00
parent e6f529e0fc
commit 58ed0ca7ca
8 changed files with 86 additions and 0 deletions

View File

@ -13,6 +13,7 @@ struct MinecraftSource: Identifiable, Hashable, Sendable {
var origin: MinecraftSourceOrigin var origin: MinecraftSourceOrigin
var accessDescriptor: SourceAccessDescriptor var accessDescriptor: SourceAccessDescriptor
var availability: SourceAvailability var availability: SourceAvailability
var capabilities: SourceCapabilities
var bookmarkData: Data? var bookmarkData: Data?
var displayName: String var displayName: String
var displayItems: [MinecraftContentItem] var displayItems: [MinecraftContentItem]
@ -56,6 +57,7 @@ struct MinecraftSource: Identifiable, Hashable, Sendable {
refreshStrategy: resolvedOrigin.defaultRefreshStrategy refreshStrategy: resolvedOrigin.defaultRefreshStrategy
) )
self.availability = availability self.availability = availability
self.capabilities = resolvedOrigin.defaultCapabilities
self.bookmarkData = bookmarkData self.bookmarkData = bookmarkData
self.displayName = normalizedFolderURL.lastPathComponent self.displayName = normalizedFolderURL.lastPathComponent
self.displayItems = [] self.displayItems = []

View File

@ -0,0 +1,26 @@
//
// SourceCapabilities.swift
// World Manager for Minecraft
//
// Created by OpenAI on 2026-05-29.
//
import Foundation
struct SourceCapabilities: Hashable, Sendable, Codable {
var canScan: Bool = true
var canMaterializeItems: Bool = true
var canExportPortablePackages: Bool = true
static let localFolder = SourceCapabilities(
canScan: true,
canMaterializeItems: true,
canExportPortablePackages: true
)
static let connectedDevice = SourceCapabilities(
canScan: true,
canMaterializeItems: true,
canExportPortablePackages: true
)
}

View File

@ -77,6 +77,15 @@ nonisolated enum MinecraftSourceOrigin: Hashable, Sendable, Codable {
return .staged return .staged
} }
} }
nonisolated var defaultCapabilities: SourceCapabilities {
switch self {
case .localFolder:
return .localFolder
case .connectedDevice:
return .connectedDevice
}
}
} }
nonisolated enum MinecraftSourceKind: String, Hashable, Sendable, Codable { nonisolated enum MinecraftSourceKind: String, Hashable, Sendable, Codable {

View File

@ -131,6 +131,7 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
source.bookmarkData = bookmarkData source.bookmarkData = bookmarkData
} }
source.accessDescriptor = sourceAccessMethod.accessDescriptor(for: source) source.accessDescriptor = sourceAccessMethod.accessDescriptor(for: source)
source.capabilities = source.origin.defaultCapabilities
} }
startScan(for: normalizedURL, mode: .fullScan) startScan(for: normalizedURL, mode: .fullScan)
return normalizedURL return normalizedURL
@ -155,6 +156,7 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
existingSource.origin = source.origin existingSource.origin = source.origin
existingSource.accessDescriptor = source.accessDescriptor existingSource.accessDescriptor = source.accessDescriptor
existingSource.availability = source.availability existingSource.availability = source.availability
existingSource.capabilities = source.capabilities
if existingSource.bookmarkData == nil { if existingSource.bookmarkData == nil {
existingSource.bookmarkData = source.bookmarkData existingSource.bookmarkData = source.bookmarkData
} }
@ -165,6 +167,7 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer
} else { } else {
var resolvedSource = source var resolvedSource = source
resolvedSource.accessDescriptor = sourceAccessMethod.accessDescriptor(for: resolvedSource) resolvedSource.accessDescriptor = sourceAccessMethod.accessDescriptor(for: resolvedSource)
resolvedSource.capabilities = resolvedSource.origin.defaultCapabilities
sources.append(resolvedSource) sources.append(resolvedSource)
sources.sort { $0.displayName.localizedStandardCompare($1.displayName) == .orderedAscending } sources.sort { $0.displayName.localizedStandardCompare($1.displayName) == .orderedAscending }
} }

View File

@ -45,6 +45,11 @@ struct AppleMobileDeviceSourceAccess: ConnectedDeviceSourceAccessMethod {
} }
} }
nonisolated func capabilities(for source: MinecraftSource) async -> SourceCapabilities {
_ = source
return .connectedDevice
}
nonisolated func listConnectedDevices() async throws -> [ConnectedDevice] { nonisolated func listConnectedDevices() async throws -> [ConnectedDevice] {
let devices = try await AppleMobileDeviceAccess.connectedDevices() let devices = try await AppleMobileDeviceAccess.connectedDevices()
return devices.compactMap { device in return devices.compactMap { device in

View File

@ -16,6 +16,7 @@ protocol SourceAccessMethod: Sendable {
nonisolated var accessorIdentifier: SourceAccessorIdentifier { get } nonisolated var accessorIdentifier: SourceAccessorIdentifier { get }
nonisolated func accessDescriptor(for source: MinecraftSource) -> SourceAccessDescriptor nonisolated func accessDescriptor(for source: MinecraftSource) -> SourceAccessDescriptor
nonisolated func availability(for source: MinecraftSource) async -> SourceAvailability nonisolated func availability(for source: MinecraftSource) async -> SourceAvailability
nonisolated func capabilities(for source: MinecraftSource) async -> SourceCapabilities
nonisolated func discoverItems( nonisolated func discoverItems(
for source: MinecraftSource, for source: MinecraftSource,
mode: SourceDiscoveryMode, mode: SourceDiscoveryMode,
@ -49,6 +50,10 @@ extension SourceAccessMethod {
return .unknown return .unknown
} }
nonisolated func capabilities(for source: MinecraftSource) async -> SourceCapabilities {
source.origin.defaultCapabilities
}
nonisolated func discoverItems( nonisolated func discoverItems(
for source: MinecraftSource, for source: MinecraftSource,
mode: SourceDiscoveryMode, mode: SourceDiscoveryMode,
@ -171,6 +176,10 @@ struct SourceAccessCoordinator: SourceAccessMethod {
return await accessMethod(for: source).availability(for: source) return await accessMethod(for: source).availability(for: source)
} }
nonisolated func capabilities(for source: MinecraftSource) async -> SourceCapabilities {
return await accessMethod(for: source).capabilities(for: source)
}
nonisolated func enrich(_ item: MinecraftContentItem, for source: MinecraftSource) async -> MinecraftContentItem { nonisolated func enrich(_ item: MinecraftContentItem, for source: MinecraftSource) async -> MinecraftContentItem {
return await accessMethod(for: source).enrich(item, for: source) return await accessMethod(for: source).enrich(item, for: source)
} }

View File

@ -43,6 +43,11 @@ struct LocalFolderSourceAccess: SourceAccessMethod {
return FileManager.default.fileExists(atPath: candidateURL.path) ? .available : .unavailable return FileManager.default.fileExists(atPath: candidateURL.path) ? .available : .unavailable
} }
nonisolated func capabilities(for source: MinecraftSource) async -> SourceCapabilities {
_ = source
return .localFolder
}
nonisolated func discoverItems( nonisolated func discoverItems(
for source: MinecraftSource, for source: MinecraftSource,
mode: SourceDiscoveryMode, mode: SourceDiscoveryMode,

View File

@ -12,6 +12,33 @@ import Testing
@MainActor @MainActor
struct World_Manager_for_MinecraftTests { struct World_Manager_for_MinecraftTests {
@Test func sourceOriginsExposeOutboundCapabilities() async throws {
let localSource = MinecraftSource(folderURL: URL(fileURLWithPath: "/tmp/local"))
#expect(localSource.capabilities == .localFolder)
let device = ConnectedDevice(
udid: "device",
name: "Device",
productType: nil,
osVersion: nil,
connection: .usb,
trustState: .trusted
)
let container = DeviceAppContainer(
deviceUDID: device.udid,
appID: "com.mojang.minecraftpe",
appName: "Minecraft",
accessMode: .documents,
minecraftFolderRelativePath: "Documents/games/com.mojang"
)
let deviceSource = MinecraftSource(
folderURL: URL(fileURLWithPath: "/tmp/device"),
origin: .connectedDevice(device: device, container: container)
)
#expect(deviceSource.capabilities == .connectedDevice)
}
@Test func packIdentityUsesUUIDAndVersion() async throws { @Test func packIdentityUsesUUIDAndVersion() async throws {
let first = PackIdentity( let first = PackIdentity(
type: .behaviorPack, type: .behaviorPack,