Skip to content

Commit 389dcdb

Browse files
committed
feat: introduce LevelKeyValueStore protocol to enable database abstraction and dependency injection
1 parent 8981206 commit 389dcdb

File tree

16 files changed

+354
-123
lines changed

16 files changed

+354
-123
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
//
2+
// Created by yechentide on 2025/11/12
3+
//
4+
5+
import Foundation
6+
import LvDBWrapper
7+
8+
/// Protocol abstraction for a key-value store used to persist Minecraft Bedrock world data.
9+
///
10+
/// This protocol defines the interface for accessing world data through a LevelDB-like store.
11+
/// Implementations can provide real LevelDB access or mock stores for testing and SwiftUI Previews.
12+
///
13+
/// ## Topics
14+
///
15+
/// ### Database State
16+
/// - ``isClosed``
17+
/// - ``close()``
18+
///
19+
/// ### Key Operations
20+
/// - ``containsKey(_:)``
21+
/// - ``data(forKey:)``
22+
/// - ``putData(_:forKey:)``
23+
/// - ``removeValue(forKey:)``
24+
///
25+
/// ### Advanced Operations
26+
/// - ``makeIterator()``
27+
/// - ``writeBatch(_:)``
28+
/// - ``compactRange(from:to:)``
29+
///
30+
/// ## Usage
31+
///
32+
/// The default implementation is `LvDB` from `LvDBWrapper`. To inject a custom store:
33+
///
34+
/// ```swift
35+
/// let customStore: LevelKeyValueStore = MyCustomStore()
36+
/// let world = try MCWorld(from: worldURL, database: customStore)
37+
/// ```
38+
///
39+
/// This is especially useful for:
40+
/// - **Testing**: Inject in-memory stores without file I/O
41+
/// - **SwiftUI Previews**: Use mock data without real database files
42+
/// - **Remote storage**: Implement cloud-backed world storage
43+
public protocol LevelKeyValueStore: AnyObject {
44+
/// Returns `true` if the database has been closed.
45+
var isClosed: Bool { get }
46+
47+
/// Closes the database and releases associated resources.
48+
///
49+
/// After calling this method, all operations on this store will fail.
50+
/// All active iterators created by this store will also be destroyed.
51+
func close()
52+
53+
/// Checks whether the specified key exists in the database.
54+
///
55+
/// - Parameter key: The key to check for existence.
56+
/// - Returns: `true` if the key exists; otherwise, `false`.
57+
func containsKey(_ key: Data) -> Bool
58+
59+
/// Retrieves the value associated with the specified key.
60+
///
61+
/// - Parameter key: The key to retrieve the value for.
62+
/// - Returns: The data associated with the key.
63+
/// - Throws: An error if the operation fails or the key does not exist.
64+
func data(forKey key: Data) throws -> Data
65+
66+
/// Stores a key-value pair in the database.
67+
///
68+
/// If the key already exists, its value will be updated.
69+
///
70+
/// - Parameters:
71+
/// - key: The key to store.
72+
/// - value: The data value to associate with the key.
73+
/// - Throws: An error if the operation fails.
74+
func putData(_ data: Data, forKey key: Data) throws
75+
76+
/// Removes the key-value pair associated with the specified key.
77+
///
78+
/// - Parameter key: The key to remove.
79+
/// - Throws: An error if the operation fails.
80+
func removeValue(forKey key: Data) throws
81+
82+
/// Creates an iterator for traversing the database entries.
83+
///
84+
/// - Returns: An `LvDBIterator` instance for iterating over the database.
85+
/// - Throws: An error if iterator creation fails.
86+
func makeIterator() throws -> LvDBIterator
87+
88+
/// Applies a batch of write operations atomically.
89+
///
90+
/// - Parameter batch: The write batch containing operations to apply.
91+
/// - Throws: An error if the batch write fails.
92+
func writeBatch(_ batch: LvDBWriteBatch) throws
93+
94+
/// Compacts the database in the specified key range.
95+
///
96+
/// This operation optimizes storage by removing deleted entries and reorganizing data.
97+
///
98+
/// - Parameters:
99+
/// - begin: The beginning of the range to compact, or `nil` for the start of the database.
100+
/// - end: The end of the range to compact, or `nil` for the end of the database.
101+
/// - Throws: An error if the compaction fails.
102+
func compactRange(from begin: Data?, to end: Data?) throws
103+
}
104+
105+
// MARK: - LvDB Conformance
106+
107+
/// Extends `LvDB` to conform to `LevelKeyValueStore`.
108+
///
109+
/// This extension allows `LvDB` from `LvDBWrapper` to be used as a `LevelKeyValueStore`
110+
/// without requiring any changes to the `LvDBWrapper` package itself.
111+
///
112+
/// The Objective-C methods (`has:`, `get:error:`, `put::error:`, `remove:error:`,
113+
/// `newIterator:`, `write:error:`, and `compactRange:end:error:`) are bridged to Swift
114+
/// and called by the protocol methods.
115+
extension LvDB: LevelKeyValueStore {
116+
/// Checks whether the specified key exists in the database.
117+
public func containsKey(_ key: Data) -> Bool {
118+
// Call the Objective-C has: method (bridged to Swift as has(_:))
119+
return self.has(key)
120+
}
121+
122+
/// Retrieves the value for the specified key.
123+
public func data(forKey key: Data) throws -> Data {
124+
// Call the Objective-C get:error: method (bridged to Swift as get(_:))
125+
return try self.get(key)
126+
}
127+
128+
/// Stores a key-value pair in the database.
129+
public func putData(_ data: Data, forKey key: Data) throws {
130+
// Call the Objective-C put::error: method (bridged to Swift as put(_:_:))
131+
try self.put(key, data)
132+
}
133+
134+
/// Removes the key-value pair for the specified key.
135+
public func removeValue(forKey key: Data) throws {
136+
// Call the Objective-C remove:error: method (bridged to Swift as remove(_:))
137+
try self.remove(key)
138+
}
139+
140+
/// Creates an iterator for traversing the database.
141+
public func makeIterator() throws -> LvDBIterator {
142+
// Call the Objective-C newIterator: method (bridged to Swift as newIterator())
143+
return try self.newIterator()
144+
}
145+
146+
/// Applies a write batch atomically.
147+
public func writeBatch(_ batch: LvDBWriteBatch) throws {
148+
// Call the Objective-C write:error: method (bridged to Swift as write(_:))
149+
try self.write(batch)
150+
}
151+
152+
/// Compacts the database in the specified range.
153+
public func compactRange(from begin: Data?, to end: Data?) throws {
154+
// Call the Objective-C compactRange:end:error: method (bridged to Swift as compactRange(_:_:))
155+
try self.compactRange(begin, end)
156+
}
157+
}

Sources/CoreBedrock/LvDBKey/LvDB+Enumerate.swift renamed to Sources/CoreBedrock/LvDB/LvDB+Enumerate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import LvDBWrapper
66

7-
public extension LvDB {
7+
public extension LevelKeyValueStore {
88
func enumerateActorKeys(
99
digpData: Data,
1010
handler: @escaping (Int, Data) -> Void

Sources/CoreBedrock/LvDBKey/LvDB+Extract.swift renamed to Sources/CoreBedrock/LvDB/LvDB+Extract.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
import LvDBWrapper
66

7-
public extension LvDB {
7+
public extension LevelKeyValueStore {
88
func getStringKey(type: LvDBStringKeyType) -> Data? {
99
let keyData = type.rawValue.data(using: .utf8)!
10-
guard contains(keyData) else {
10+
guard containsKey(keyData) else {
1111
return nil
1212
}
1313

@@ -21,7 +21,7 @@ public extension LvDB {
2121
for strKeyType in LvDBStringKeyType.allCases {
2222
guard !excludeTypes.contains(strKeyType),
2323
let keyData = strKeyType.rawValue.data(using: .utf8),
24-
contains(keyData)
24+
containsKey(keyData)
2525
else {
2626
continue
2727
}
@@ -36,8 +36,8 @@ public extension LvDB {
3636
var keys = [Data]()
3737

3838
let digpKey = Data("digp".utf8) + keyPrefix
39-
guard contains(digpKey),
40-
let digpData = try? get(digpKey),
39+
guard containsKey(digpKey),
40+
let digpData = try? data(forKey: digpKey),
4141
!digpData.isEmpty,
4242
digpData.count % 8 == 0
4343
else {
@@ -48,7 +48,7 @@ public extension LvDB {
4848

4949
for i in 0..<digpData.count / 8 {
5050
let actorKey = Data("actorprefix".utf8) + digpData[i * 8...i * 8 + 7]
51-
guard contains(actorKey) else {
51+
guard containsKey(actorKey) else {
5252
continue
5353
}
5454

@@ -63,7 +63,7 @@ public extension LvDB {
6363

6464
for yIndex in Int8(-4)...Int8(20) {
6565
let key = keyPrefix + LvDBChunkKeyType.subChunkPrefix.rawValue.data + yIndex.data
66-
if contains(key) {
66+
if containsKey(key) {
6767
keys.append(key)
6868
}
6969
}
@@ -74,7 +74,7 @@ public extension LvDB {
7474
}
7575

7676
let key = keyPrefix + type.rawValue.data
77-
if contains(key) {
77+
if containsKey(key) {
7878
keys.append(key)
7979
}
8080
}
@@ -100,7 +100,7 @@ public extension LvDB {
100100
break
101101
}
102102

103-
if contains(keyData) {
103+
if containsKey(keyData) {
104104
keys.append(keyData)
105105
}
106106
}

Sources/CoreBedrock/LvDBKey/LvDB+Remove.swift renamed to Sources/CoreBedrock/LvDB/LvDB+Remove.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import LvDBWrapper
66

7-
public extension LvDB {
7+
public extension LevelKeyValueStore {
88
func deleteAllChunks(in dimension: MCDimension) throws {
99
try autoreleasepool {
1010
let iter = try self.makeIterator()
@@ -166,7 +166,7 @@ public extension LvDB {
166166
private func removeActorAndDigpKeys(keyPrefix: Data, batch: LvDBWriteBatch) {
167167
let digpKey = Data("digp".utf8) + keyPrefix
168168

169-
guard let digpData = try? get(digpKey), !digpData.isEmpty, digpData.count % 8 == 0 else {
169+
guard let digpData = try? data(forKey: digpKey), !digpData.isEmpty, digpData.count % 8 == 0 else {
170170
return
171171
}
172172

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@
44

55
import LvDBWrapper
66

7-
public extension LvDB {
7+
public extension LevelKeyValueStore {
88
func chunkExists(chunkX: Int, chunkZ: Int, dimension: MCDimension) -> Bool {
99
let chunkX = Int32(truncatingIfNeeded: chunkX)
1010
let chunkZ = Int32(truncatingIfNeeded: chunkZ)
1111
let versionKey = LvDBKeyFactory.makeChunkKey(x: chunkX, z: chunkZ, dimension: dimension, type: .chunkVersion)
12-
if let versionData = try? self.get(versionKey), versionData.count == 1 {
12+
if let versionData = try? self.data(forKey: versionKey), versionData.count == 1 {
1313
return true
1414
}
1515
let legacyVersionKey = LvDBKeyFactory.makeChunkKey(
1616
x: chunkX, z: chunkZ, dimension: dimension, type: .legacyChunkVersion
1717
)
18-
if let legacyVersionData = try? self.get(legacyVersionKey), legacyVersionData.count == 1 {
18+
if let legacyVersionData = try? self.data(forKey: legacyVersionKey), legacyVersionData.count == 1 {
1919
return true
2020
}
2121
return false

Sources/CoreBedrock/World/MCWorld.swift

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,67 @@
55
import Foundation
66
import LvDBWrapper
77

8+
/// Represents a Minecraft Bedrock Edition world with its directory, database, and metadata.
9+
///
10+
/// `MCWorld` provides access to world data stored in LevelDB format. The database can be
11+
/// injected for testing or alternative storage implementations.
12+
///
13+
/// ## Topics
14+
///
15+
/// ### Creating a World
16+
/// - ``init(from:meta:)``
17+
/// - ``init(from:database:meta:)``
18+
///
19+
/// ### Accessing World Data
20+
/// - ``dirURL``
21+
/// - ``database``
22+
/// - ``worldName``
23+
/// - ``meta``
24+
///
25+
/// ### Managing the Database
26+
/// - ``closeDB()``
27+
/// - ``reloadMetaFile()``
28+
///
29+
/// ## Usage
30+
///
31+
/// ### Opening a World
32+
/// ```swift
33+
/// let worldURL = URL(fileURLWithPath: "/path/to/world")
34+
/// let world = try MCWorld(from: worldURL)
35+
/// defer { world.closeDB() }
36+
/// ```
37+
///
38+
/// ### Injecting a Custom Database
39+
/// Useful for testing, SwiftUI Previews, or custom storage backends:
40+
/// ```swift
41+
/// let mockDB: LevelKeyValueStore = MockDatabase()
42+
/// let world = try MCWorld(from: worldURL, database: mockDB)
43+
/// ```
844
public class MCWorld {
45+
/// The directory URL containing the world files.
946
public let dirURL: URL
10-
public let db: LvDB
1147

48+
/// The key-value store used to access world data.
49+
///
50+
/// This property allows injecting custom database implementations for testing
51+
/// or alternative storage backends. By default, it uses `LvDB` from `LvDBWrapper`.
52+
public let database: LevelKeyValueStore
53+
54+
/// The display name of the world, extracted from metadata.
1255
public var worldName = "???"
56+
57+
/// The world's metadata, typically loaded from `level.dat`.
1358
public var meta: MCWorldMeta
1459

15-
public init(from dirURL: URL, meta: MCWorldMeta? = nil) throws {
60+
/// Creates a new `MCWorld` instance by opening the database at the specified directory.
61+
///
62+
/// This convenience initializer opens an `LvDB` instance and forwards to the designated initializer.
63+
///
64+
/// - Parameters:
65+
/// - dirURL: The directory URL containing the world files.
66+
/// - meta: Optional pre-loaded metadata. If `nil`, metadata will be loaded from `level.dat`.
67+
/// - Throws: An error if the database cannot be opened or metadata cannot be loaded.
68+
public convenience init(from dirURL: URL, meta: MCWorldMeta? = nil) throws {
1669
let dbPath = MCDir.generatePath(for: .db, in: dirURL)
1770
let db: LvDB
1871
do {
@@ -21,8 +74,24 @@ public class MCWorld {
2174
throw LvDBError(nsError: nsError)
2275
}
2376

77+
try self.init(from: dirURL, database: db, meta: meta)
78+
}
79+
80+
/// Creates a new `MCWorld` instance with an injected database.
81+
///
82+
/// This designated initializer allows dependency injection of the database, enabling:
83+
/// - **Testing**: Use in-memory or mock databases
84+
/// - **SwiftUI Previews**: Provide sample data without file I/O
85+
/// - **Custom Storage**: Implement cloud-backed or alternative storage backends
86+
///
87+
/// - Parameters:
88+
/// - dirURL: The directory URL containing the world files.
89+
/// - database: The key-value store to use for world data access.
90+
/// - meta: Optional pre-loaded metadata. If `nil`, metadata will be loaded from `level.dat`.
91+
/// - Throws: An error if metadata cannot be loaded.
92+
public init(from dirURL: URL, database: LevelKeyValueStore, meta: MCWorldMeta? = nil) throws {
2493
self.dirURL = dirURL
25-
self.db = db
94+
self.database = database
2695

2796
if let metaArg = meta {
2897
self.meta = metaArg
@@ -35,10 +104,17 @@ public class MCWorld {
35104
}
36105
}
37106

107+
/// Closes the database and releases associated resources.
108+
///
109+
/// After calling this method, no further database operations should be performed on this world.
110+
/// All active iterators will also be destroyed.
38111
public func closeDB() {
39-
self.db.close()
112+
self.database.close()
40113
}
41114

115+
/// Reloads the world metadata from the `level.dat` file.
116+
///
117+
/// - Throws: An error if the metadata file cannot be read or parsed.
42118
public func reloadMetaFile() throws {
43119
let levelDatURL = MCDir.generateURL(for: .levelDat, in: self.dirURL)
44120
self.meta = try MCWorldMeta(from: levelDatURL)

0 commit comments

Comments
 (0)