From 02dea1cce01ada8f87313510a9d3bc9716147269 Mon Sep 17 00:00:00 2001 From: Fumiya Tanaka Date: Wed, 11 Feb 2026 17:30:01 +0900 Subject: [PATCH] =?UTF-8?q?=E6=A9=9F=E8=83=BD=E8=BF=BD=E5=8A=A0:=20Firesto?= =?UTF-8?q?reClientType=20=E3=83=97=E3=83=AD=E3=83=88=E3=82=B3=E3=83=AB?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FirestoreClient (actor) のモック可能な抽象化レイヤーとして FirestoreClientType プロトコルを導入。 - 全パブリックメソッド(21メソッド)をプロトコル要件として定義 - デフォルト引数を提供する convenience extension を追加 - FirestoreClient の既存APIに変更なし(後方互換性100%) - writeTransaction は複雑なシグネチャのため除外 これにより、依存先プロジェクトで Mockolo 等を使用した ユニットテスト用モックの生成が可能になる。 --- Sources/Firestore/FirestoreClient.swift | 3 + Sources/Firestore/FirestoreClientType.swift | 392 ++++++++++++++++++++ 2 files changed, 395 insertions(+) create mode 100644 Sources/Firestore/FirestoreClientType.swift 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 + ) + } +}