diff --git a/Sources/Firestore/FirestoreClient.swift b/Sources/Firestore/FirestoreClient.swift index 991718b..e8832d8 100644 --- a/Sources/Firestore/FirestoreClient.swift +++ b/Sources/Firestore/FirestoreClient.swift @@ -930,3 +930,6 @@ extension FirestoreClient { } } } + +// MARK: - FirestoreClientType Conformance +extension FirestoreClient: FirestoreClientType {} diff --git a/Sources/Firestore/FirestoreClientType.swift b/Sources/Firestore/FirestoreClientType.swift new file mode 100644 index 0000000..2dca368 --- /dev/null +++ b/Sources/Firestore/FirestoreClientType.swift @@ -0,0 +1,392 @@ +// +// FirestoreClientType.swift +// EasyFirebaseFirestore +// +// Created by Fumiya Tanaka on 2025/02/11. +// + +import FirebaseFirestore + +/// A protocol abstracting FirestoreClient for dependency injection and testing. +/// +/// All methods are marked `async` so that both actor-isolated implementations +/// (where actor isolation satisfies the async requirement per SE-0306) +/// and regular class-based mocks can conform. +/// +/// `writeTransaction` is intentionally excluded due to its complex signature +/// (`WritableKeyPath` + tuple closure). It can be added later if needed. +/// +/// @mockable +public protocol FirestoreClientType { + + // MARK: - FirestoreModel - Write + + @discardableResult + func write( + _ model: Model, + newDocumentIdIfNotExists: String? + ) async throws -> DocumentReference + + // MARK: - FirestoreModel - Get + + func get( + documentId: String, + includeCache: Bool + ) async throws -> Model + + func get( + filter: [FirestoreQueryFilter], + includeCache: Bool, + order: [FirestoreQueryOrder], + limit: Int? + ) async throws -> [Model] + + // MARK: - FirestoreModel - Listen + + func listen( + documentId: String, + includeCache: Bool + ) async -> AsyncThrowingStream + + func listen( + filter: [FirestoreQueryFilter], + includeCache: Bool, + order: [FirestoreQueryOrder], + limit: Int? + ) async -> AsyncThrowingStream<[Model], Error> + + func listenChanges( + filter: [FirestoreQueryFilter], + includeCache: Bool, + order: [FirestoreQueryOrder], + limit: Int? + ) async -> AsyncThrowingStream<[FirestoreDocumentChange], Error> + + // MARK: - FirestoreModel - Delete + + func delete(_ model: Model) async throws + + // MARK: - SubCollectionModel - Create/Update + + func create(_ model: Model) async throws + + func update( + _ model: Model + ) async throws + + // MARK: - SubCollectionModel - Get + + func get( + parent parentDocumentId: String, + filter: [FirestoreQueryFilter], + includeCache: Bool, + order: [FirestoreQueryOrder], + limit: Int? + ) async throws -> [Model] + + func get( + documentId: String, + parent parentUid: String, + includeCache: Bool + ) async throws -> Model + + // MARK: - SubCollectionModel - Listen + + func listen( + parentDocumentId: String, + documentId: String, + filter: [FirestoreQueryFilter], + includeCache: Bool, + order: [FirestoreQueryOrder], + limit: Int? + ) async -> AsyncThrowingStream + + func listen( + parent: String, + superParent: String?, + filter: [FirestoreQueryFilter], + includeCache: Bool, + order: [FirestoreQueryOrder], + limit: Int? + ) async -> AsyncThrowingStream<[Model], Error> + + func listenChanges( + parent: String, + superParent: String?, + filter: [FirestoreQueryFilter], + includeCache: Bool, + order: [FirestoreQueryOrder], + limit: Int? + ) async -> AsyncThrowingStream<[FirestoreDocumentChange], Error> + + // MARK: - CollectionGroup + + func getCollectionGroup( + filter: [FirestoreQueryFilter], + includeCache: Bool, + order: [FirestoreQueryOrder], + limit: Int? + ) async throws -> [Model] + + func listenCollectionGroup( + collectionName: String, + filter: [FirestoreQueryFilter], + includeCache: Bool, + order: [FirestoreQueryOrder], + limit: Int? + ) async -> AsyncThrowingStream<[Model], Error> + + func listenCollectionGroupChanges( + collectionName: String, + filter: [FirestoreQueryFilter], + includeCache: Bool, + order: [FirestoreQueryOrder], + limit: Int? + ) async -> AsyncThrowingStream<[FirestoreDocumentChange], Error> + + // MARK: - Listener Management + + func stopListening(type: Model.Type) async + func stopListening( + type: Model.Type, + documentID: String + ) async + func stopListening(ref: DocumentReference) async + func stopListeningAll() async +} + +// MARK: - Convenience Methods (Default Parameters) + +extension FirestoreClientType { + + // MARK: FirestoreModel - Write + + @discardableResult + public func write( + _ model: Model + ) async throws -> DocumentReference { + try await write(model, newDocumentIdIfNotExists: nil) + } + + // MARK: FirestoreModel - Get + + public func get( + documentId: String + ) async throws -> Model { + try await get(documentId: documentId, includeCache: true) + } + + public func get( + filter: [FirestoreQueryFilter] = [], + order: [FirestoreQueryOrder] = [], + limit: Int? = nil + ) async throws -> [Model] { + try await get( + filter: filter, + includeCache: true, + order: order, + limit: limit + ) + } + + public func get( + filter: [FirestoreQueryFilter] = [], + includeCache: Bool, + order: [FirestoreQueryOrder] = [], + limit: Int? = nil + ) async throws -> [Model] { + try await get( + filter: filter, + includeCache: includeCache, + order: order, + limit: limit + ) + } + + // MARK: FirestoreModel - Listen + + public func listen( + documentId: String + ) async -> AsyncThrowingStream { + await listen(documentId: documentId, includeCache: true) + } + + public func listen( + filter: [FirestoreQueryFilter] = [], + order: [FirestoreQueryOrder] = [], + limit: Int? = nil + ) async -> AsyncThrowingStream<[Model], Error> { + await listen( + filter: filter, + includeCache: true, + order: order, + limit: limit + ) + } + + public func listen( + filter: [FirestoreQueryFilter] = [], + includeCache: Bool, + order: [FirestoreQueryOrder] = [], + limit: Int? = nil + ) async -> AsyncThrowingStream<[Model], Error> { + await listen( + filter: filter, + includeCache: includeCache, + order: order, + limit: limit + ) + } + + public func listenChanges( + filter: [FirestoreQueryFilter] = [], + order: [FirestoreQueryOrder] = [], + limit: Int? = nil + ) async -> AsyncThrowingStream<[FirestoreDocumentChange], Error> { + await listenChanges( + filter: filter, + includeCache: true, + order: order, + limit: limit + ) + } + + public func listenChanges( + filter: [FirestoreQueryFilter] = [], + includeCache: Bool, + order: [FirestoreQueryOrder] = [], + limit: Int? = nil + ) async -> AsyncThrowingStream<[FirestoreDocumentChange], Error> { + await listenChanges( + filter: filter, + includeCache: includeCache, + order: order, + limit: limit + ) + } + + // MARK: SubCollectionModel - Get + + public func get( + parent parentDocumentId: String, + filter: [FirestoreQueryFilter] = [], + order: [FirestoreQueryOrder] = [], + limit: Int? = nil + ) async throws -> [Model] { + try await get( + parent: parentDocumentId, + filter: filter, + includeCache: true, + order: order, + limit: limit + ) + } + + public func get( + parent parentDocumentId: String, + filter: [FirestoreQueryFilter] = [], + includeCache: Bool, + order: [FirestoreQueryOrder] = [], + limit: Int? = nil + ) async throws -> [Model] { + try await get( + parent: parentDocumentId, + filter: filter, + includeCache: includeCache, + order: order, + limit: limit + ) + } + + public func get( + documentId: String, + parent parentUid: String + ) async throws -> Model { + try await get( + documentId: documentId, + parent: parentUid, + includeCache: true + ) + } + + // MARK: SubCollectionModel - Listen + + public func listen( + parent: String, + superParent: String? = nil, + filter: [FirestoreQueryFilter], + order: [FirestoreQueryOrder], + limit: Int? + ) async -> AsyncThrowingStream<[Model], Error> { + await listen( + parent: parent, + superParent: superParent, + filter: filter, + includeCache: true, + order: order, + limit: limit + ) + } + + public func listenChanges( + parent: String, + superParent: String? = nil, + filter: [FirestoreQueryFilter], + order: [FirestoreQueryOrder], + limit: Int? + ) async -> AsyncThrowingStream<[FirestoreDocumentChange], Error> { + await listenChanges( + parent: parent, + superParent: superParent, + filter: filter, + includeCache: true, + order: order, + limit: limit + ) + } + + // MARK: CollectionGroup + + public func getCollectionGroup( + filter: [FirestoreQueryFilter] = [], + order: [FirestoreQueryOrder] = [], + limit: Int? = nil + ) async throws -> [Model] { + try await getCollectionGroup( + filter: filter, + includeCache: true, + order: order, + limit: limit + ) + } + + public func listenCollectionGroup( + collectionName: String, + filter: [FirestoreQueryFilter] = [], + order: [FirestoreQueryOrder] = [], + limit: Int? = nil + ) async -> AsyncThrowingStream<[Model], Error> { + await listenCollectionGroup( + collectionName: collectionName, + filter: filter, + includeCache: true, + order: order, + limit: limit + ) + } + + public func listenCollectionGroupChanges( + collectionName: String, + filter: [FirestoreQueryFilter] = [], + order: [FirestoreQueryOrder] = [], + limit: Int? = nil + ) async -> AsyncThrowingStream<[FirestoreDocumentChange], Error> { + await listenCollectionGroupChanges( + collectionName: collectionName, + filter: filter, + includeCache: true, + order: order, + limit: limit + ) + } +}