From 68fd468450948834bceb65e296492aeb72d1b1b7 Mon Sep 17 00:00:00 2001 From: Sergey Date: Mon, 8 Apr 2024 00:13:48 +0300 Subject: [PATCH] 1.4.0 (#17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * swift-docc-plugin 1.3.0 * async-http-client 1.19.0 * replaced deprecated .createNew with the new .singleton * Adding _find capability. (#13) * docs * tabs * renames * added test for find method with body * docs added * findError added * tests renamed * added test for find method with generic type * docs * README updated * updated dependencies * docs updated * try Swift 5.7.1 * Update build-ubuntu.yml * Swift 5.7.3 * Revert "Swift 5.7.3" This reverts commit ab8f67a89f5b83ed20fe30a9965a3f2f58f6d2c1. * updated dependencies * Docs updated * Delete .github/workflows/CodeQL.yml * dependencies updated * fixed when update method didn’t use passed dateEncodingStrategy * noData error added * migrating to new HTTPClientRequest from HTTPClient.Request wip * migrating to new HTTPClientRequest from HTTPClient.Request wip * migrating to new HTTPClientRequest from HTTPClient.Request wip * migrating to new HTTPClientRequest from HTTPClient.Request wip * migrating to new HTTPClientRequest from HTTPClient.Request wip * migrating to new HTTPClientRequest from HTTPClient.Request wip * … * migrating to new HTTPClientRequest from HTTPClient.Request wip * rename * tests updated * auth fixed * collect body bytes before returning response * param renamed to make keep backward compatibility with old methods * marking old find as deprecated * private * deprecated message * renames * moved deprecations * docs * docs * added Codable to CouchDBRepresentable protocol * added RowsResponse model * docs * Vapor tutorial updated * docs * docs and refactoring * docs and refactoring * Tutorials updated * docs * platforms list updated * minimum swift version 5.8 * workflow updated * Update build-ubuntu.yml * Update build-macos.yml * import NIOFoundationCompat to fix building on Ubuntu --------- Co-authored-by: Gregório Gevartosky Torrezan --- .github/workflows/build-macos.yml | 2 +- .github/workflows/build-ubuntu.yml | 2 +- Package.swift | 4 +- README.md | 12 +- .../CouchDBClient/CouchDB+Deprecated.swift | 164 +++++++ .../CouchDBClient.docc/CouchDBClient.md | 4 +- .../CouchDBClient.docc/Extensions/Client.md | 14 +- .../ErrorsHandlingTutorial.tutorial | 6 +- .../Tutorials/macOS/macOSTutorial-4.swift | 2 +- .../Tutorials/macOS/macOSTutorial-5.swift | 2 +- .../Tutorials/macOS/macOSTutorial-6.swift | 2 +- .../Tutorials/macOS/macOSTutorial-7.swift | 2 +- .../Tutorials/macOS/macOSTutorial-8.swift | 2 +- .../Tutorials/macOS/macOSTutorial.tutorial | 52 +-- .../Tutorials/vapor/VaporTutorial-0.swift | 14 +- .../Tutorials/vapor/VaporTutorial-3.swift | 12 - .../Tutorials/vapor/VaporTutorial-4.swift | 12 - .../Tutorials/vapor/VaporTutorial-5.swift | 14 +- .../Tutorials/vapor/VaporTutorial-6.swift | 14 +- .../Tutorials/vapor/VaporTutorial.tutorial | 48 +-- Sources/CouchDBClient/CouchDBClient.swift | 408 ++++++++++-------- .../Models/CouchDBFindResponse.swift | 2 +- .../Models/CouchDBRepresentable.swift | 6 +- .../CouchDBClient/Models/RowsResponse.swift | 23 + .../CouchDBClientTests.swift | 81 ++-- 25 files changed, 559 insertions(+), 345 deletions(-) create mode 100644 Sources/CouchDBClient/CouchDB+Deprecated.swift create mode 100644 Sources/CouchDBClient/Models/RowsResponse.swift diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 6528098..1b848ce 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -12,7 +12,7 @@ on: jobs: macOS: name: Build on macOS - runs-on: macOS-latest + runs-on: macOS-14 steps: - name: Print Swift version run: swift --version diff --git a/.github/workflows/build-ubuntu.yml b/.github/workflows/build-ubuntu.yml index bb991a2..5eeb04c 100644 --- a/.github/workflows/build-ubuntu.yml +++ b/.github/workflows/build-ubuntu.yml @@ -11,7 +11,7 @@ jobs: name: Build on Ubuntu with Swift ${{matrix.swift}} strategy: matrix: - swift: [5.9, 5.8.1, 5.7.3] + swift: ["5.10", "5.9", "5.8"] runs-on: ubuntu-latest container: image: swift:${{matrix.swift}} diff --git a/Package.swift b/Package.swift index 3c96474..27c9a46 100644 --- a/Package.swift +++ b/Package.swift @@ -1,11 +1,11 @@ -// swift-tools-version:5.7.1 +// swift-tools-version:5.8 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "couchdb-vapor", - platforms: [.macOS(.v10_15), .iOS(.v13)], + platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library(name: "CouchDBClient", targets: ["CouchDBClient"]), diff --git a/README.md b/README.md index 4b867f9..b09e7c1 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ This is a simple lib to work with CouchDB in Swift. -- Latest version is based on async/await and requires Swift 5.6 and newer. Works with Vapor 4.50 and newer. +- Latest version is based on async/await and requires Swift 5.8 or newer. Works with Vapor 4.50 and newer. - Version 1.0.0 can be used with Vapor 4 without async/await. Swift 5.3 is required - You can use the old version for Vapor 3 from vapor3 branch or using version < 1.0.0. @@ -55,7 +55,7 @@ let couchDBClient = CouchDBClient( ) ``` -If you don’t want to have your password in the code you can pass COUCHDB_PASS param in your command line. For example you can run your Server Side Swift project: +If you don’t want to have your password in the code you can pass `COUCHDB_PASS` param in your command line. For example you can run your Server Side Swift project: ```bash COUCHDB_PASS=myPassword /path/.build/x86_64-unknown-linux-gnu/release/Run ``` @@ -76,7 +76,7 @@ Define your document model: ```swift // Example struct -struct ExpectedDoc: CouchDBRepresentable, Codable { +struct ExpectedDoc: CouchDBRepresentable { var name: String var _id: String? var _rev: String? @@ -98,7 +98,7 @@ print(testDoc) // testDoc has _id and _rev values now ### Update data ```swift -// get data from DB by document ID +// get data from a database by document ID var doc: ExpectedDoc = try await couchDBClient.get(dbName: "databaseName", uri: "documentId") print(doc) @@ -121,7 +121,7 @@ let response = try await couchDBClient.delete(fromDb: "databaseName", doc: doc) let response = try await couchDBClient.delete(fromDb: "databaseName", uri: doc._id,rev: doc._rev) ``` -Get all DBs example: +Get all databases example: ```swift let dbs = try await couchDBClient.getAllDBs() @@ -129,7 +129,7 @@ print(dbs) // prints: ["_global_changes", "_replicator", "_users", "yourDBname"] ``` -Find documents in DB by selector: +Find documents in a database by selector: ```swift let selector = ["selector": ["name": "Sam"]] let docs: [ExpectedDoc] = try await couchDBClient.find(in: "databaseName", selector: selector) diff --git a/Sources/CouchDBClient/CouchDB+Deprecated.swift b/Sources/CouchDBClient/CouchDB+Deprecated.swift new file mode 100644 index 0000000..3ef03e5 --- /dev/null +++ b/Sources/CouchDBClient/CouchDB+Deprecated.swift @@ -0,0 +1,164 @@ +// +// CouchDB+Deprecated.swift +// +// +// Created by Sergei Armodin on 02.04.2024. +// + +import Foundation +import AsyncHTTPClient +import NIO +import NIOHTTP1 + +extension CouchDBClient { + /// Get data from DB. + /// + /// Examples: + /// + /// Define your document model: + /// ```swift + /// // Example struct + /// struct ExpectedDoc: CouchDBRepresentable, Codable { + /// var name: String + /// var _id: String? + /// var _rev: String? + /// } + /// ``` + /// + /// Get document by ID: + /// ```swift + /// // get data from DB by document ID + /// var response = try await couchDBClient.get(dbName: "databaseName", uri: "documentId") + /// + /// // parse JSON + /// let bytes = response.body!.readBytes(length: response.body!.readableBytes)! + /// let doc = try JSONDecoder().decode(ExpectedDoc.self, from: Data(bytes)) + /// ``` + /// + /// You can also provide CouchDB view document as uri and key in query. + /// + /// Get data and parse `RowsResponse`: + /// ```swift + /// let response = try await couchDBClient.get( + /// dbName: "databaseName", + /// uri: "_design/all/_view/by_url", + /// query: ["key": "\"\(url)\""] + /// ) + /// let bytes = response.body!.readBytes(length: response.body!.readableBytes)! + /// let decodedResponse = try JSONDecoder().decode(RowsResponse.self, from: data) + /// print(decodedResponse.rows) + /// print(decodedResponse.rows.first?.value) + /// ``` + /// + /// - Parameters: + /// - dbName: DB name. + /// - uri: URI (view or document id). + /// - query: Request query items. + /// - eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. + /// - Returns: Request response. + @available(*, deprecated, message: "Use the new `get(fromDB:uri:queryItems:eventLoopGroup)` method.") + public func get(dbName: String, uri: String, queryItems: [URLQueryItem]? = nil, eventLoopGroup: EventLoopGroup? = nil) async throws -> HTTPClient.Response { + try await authIfNeed(eventLoopGroup: eventLoopGroup) + + let httpClient: HTTPClient + if let eventLoopGroup = eventLoopGroup { + httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup)) + } else { + httpClient = HTTPClient(eventLoopGroupProvider: .singleton) + } + + defer { + DispatchQueue.main.async { + try? httpClient.syncShutdown() + } + } + + let url = buildUrl(path: "/" + dbName + "/" + uri, query: queryItems ?? []) + let request = try buildRequestOld(fromUrl: url, withMethod: .GET) + let response = try await httpClient + .execute(request: request, deadline: .now() + .seconds(requestsTimeout)) + .get() + + if response.status == .unauthorized { + throw CouchDBClientError.unauthorized + } + + return response + } + + /// Find data in DB by selector. + /// + /// Example: + /// ```swift + /// let selector = ["selector": ["name": "Greg"]] + /// let bodyData = try JSONEncoder().encode(selector) + /// var findResponse = try await couchDBClient.find(in: testsDB, body: .data(bodyData)) + /// + /// let bytes = findResponse.body!.readBytes(length: findResponse.body!.readableBytes)! + /// let docs = try JSONDecoder().decode(CouchDBFindResponse.self, from: Data(bytes)).docs + /// ``` + /// - Parameters: + /// - dbName: DB name. + /// - body: Request body data. + /// - eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. + /// - Returns: Request response. + @available(*, deprecated, message: "Use the new 'find(inDB:body:eventLoopGroup)' method.") + public func find(in dbName: String, body: HTTPClient.Body, eventLoopGroup: EventLoopGroup? = nil) async throws -> HTTPClient.Response { + try await authIfNeed(eventLoopGroup: eventLoopGroup) + + let httpClient: HTTPClient + if let eventLoopGroup = eventLoopGroup { + httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup)) + } else { + httpClient = HTTPClient(eventLoopGroupProvider: .singleton) + } + + defer { + DispatchQueue.main.async { + try? httpClient.syncShutdown() + } + } + + let url = buildUrl(path: "/" + dbName + "/_find", query: []) + var request = try buildRequestOld(fromUrl: url, withMethod: .POST) + request.body = body + let response = try await httpClient + .execute(request: request, deadline: .now() + .seconds(requestsTimeout)) + .get() + + if response.status == .unauthorized { + throw CouchDBClientError.unauthorized + } + + return response + } + + /// Build HTTP request from url string. + /// - Parameters: + /// - url: URL string. + /// - method: HTTP method. + /// - Returns: HTTP Request. + private func buildRequestOld(fromUrl url: String, withMethod method: HTTPMethod) throws -> HTTPClient.Request { + var headers = HTTPHeaders() + headers.add(name: "Content-Type", value: "application/json") + if let cookie = sessionCookie { + headers.add(name: "Cookie", value: cookie) + } + return try HTTPClient.Request( + url: url, + method: method, + headers: headers, + body: nil + ) + } + + @available(*, deprecated, renamed: "get", message: "Renamed to: get(fromDB:uri:queryItems:dateDecodingStrategy:eventLoopGroup)") + public func get (dbName: String, uri: String, queryItems: [URLQueryItem]? = nil, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .secondsSince1970, eventLoopGroup: EventLoopGroup? = nil) async throws -> T { + return try await get(fromDB: dbName, uri: uri, queryItems: queryItems, dateDecodingStrategy: dateDecodingStrategy, eventLoopGroup: eventLoopGroup) + } + + @available(*, deprecated, renamed: "find", message: "Renamed to: find(inDB:selector:dateDecodingStrategy:eventLoopGroup)") + public func find(in dbName: String, selector: Codable, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .secondsSince1970, eventLoopGroup: EventLoopGroup? = nil) async throws -> [T] { + return try await find(inDB: dbName, selector: selector, dateDecodingStrategy: dateDecodingStrategy, eventLoopGroup: eventLoopGroup) + } +} diff --git a/Sources/CouchDBClient/CouchDBClient.docc/CouchDBClient.md b/Sources/CouchDBClient/CouchDBClient.docc/CouchDBClient.md index 0a95d58..b5bc23f 100644 --- a/Sources/CouchDBClient/CouchDBClient.docc/CouchDBClient.md +++ b/Sources/CouchDBClient/CouchDBClient.docc/CouchDBClient.md @@ -6,7 +6,7 @@ A simple CouchDB client written in Swift. Source code is available on [GitHub](https://github.com/makoni/couchdb-vapor). -CouchDBClient allows you to make simple requests to CouchDB. It's using Swift Concurrency (async/await) and supports Linux, iOS 13+ and macOS 10.15+. +CouchDBClient allows you to make simple requests to CouchDB. It's using Swift Concurrency (async/await) and supports Linux, iOS 13+, iPadOS 13+, tvOS 13+, watchOS 6+, visionOS 1.0+ and macOS 10.15+. It's using [AsyncHTTPClient](https://github.com/swift-server/async-http-client) which makes it easy to use CouchDBClient for server-side development with Vapor 4. But it's easy to use it with any iOS or macOS app. Check the Essentials section for examples. @@ -15,7 +15,7 @@ Currently CouchDBClient supports: - Create DB. - Delete DB. - Get databases list. -- Get document by id or documents using view. +- Get a document by id or documents using a view. - Insert/update documents. - Find documents by selector. - Delete documents. diff --git a/Sources/CouchDBClient/CouchDBClient.docc/Extensions/Client.md b/Sources/CouchDBClient/CouchDBClient.docc/Extensions/Client.md index 65efbf7..35cd017 100644 --- a/Sources/CouchDBClient/CouchDBClient.docc/Extensions/Client.md +++ b/Sources/CouchDBClient/CouchDBClient.docc/Extensions/Client.md @@ -21,13 +21,19 @@ A CouchDB client class with methods using Swift Concurrency. - ``createDB(_:eventLoopGroup:)`` - ``deleteDB(_:eventLoopGroup:)`` - ``dbExists(_:eventLoopGroup:)`` -- ``get(dbName:uri:queryItems:eventLoopGroup:)`` -- ``get(dbName:uri:queryItems:dateDecodingStrategy:eventLoopGroup:)`` +- ``get(fromDB:uri:queryItems:eventLoopGroup:)`` +- ``get(fromDB:uri:queryItems:dateDecodingStrategy:eventLoopGroup:)`` - ``insert(dbName:body:eventLoopGroup:)`` - ``insert(dbName:doc:dateEncodingStrategy:eventLoopGroup:)`` - ``update(dbName:doc:dateEncodingStrategy:eventLoopGroup:)`` - ``update(dbName:uri:body:eventLoopGroup:)`` -- ``find(in:body:eventLoopGroup:)`` -- ``find(in:selector:dateDecodingStrategy:eventLoopGroup:)`` +- ``find(inDB:body:eventLoopGroup:)`` +- ``find(inDB:selector:dateDecodingStrategy:eventLoopGroup:)`` - ``delete(fromDb:doc:eventLoopGroup:)`` - ``delete(fromDb:uri:rev:eventLoopGroup:)`` + +### Deprecated methods +- ``get(dbName:uri:queryItems:eventLoopGroup:)`` +- ``get(dbName:uri:queryItems:dateDecodingStrategy:eventLoopGroup:)`` +- ``find(in:body:eventLoopGroup:)`` +- ``find(in:selector:dateDecodingStrategy:eventLoopGroup:)`` diff --git a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/ErrorsHandling/ErrorsHandlingTutorial.tutorial b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/ErrorsHandling/ErrorsHandlingTutorial.tutorial index 313adfa..6af9e6f 100644 --- a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/ErrorsHandling/ErrorsHandlingTutorial.tutorial +++ b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/ErrorsHandling/ErrorsHandlingTutorial.tutorial @@ -1,13 +1,13 @@ @Tutorial(time: 3) { @Intro(title: "Handling CouchDB errors") { - Use CouchDBClient in macOS app + Use CouchDBClient in a macOS app @Image(source: chapter1.png, alt: "Application icon") } - @Section(title: "Use CouchDBClient in macOS app") { + @Section(title: "macOS app example") { @ContentAndMedia { - Use CouchDBClient in macOS app + Use CouchDBClient in a macOS app @Image(source: chapter1.png, alt: "Application icon") } diff --git a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-4.swift b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-4.swift index 409aa4b..dfe5228 100644 --- a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-4.swift +++ b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-4.swift @@ -11,7 +11,7 @@ let couchDBClient = CouchDBClient( let dbName = "fortests" -struct MyDoc: CouchDBRepresentable, Codable { +struct MyDoc: CouchDBRepresentable { var _id: String? var _rev: String? var title: String diff --git a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-5.swift b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-5.swift index 53b56fd..54a647c 100644 --- a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-5.swift +++ b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-5.swift @@ -11,7 +11,7 @@ let couchDBClient = CouchDBClient( let dbName = "fortests" -struct MyDoc: CouchDBRepresentable, Codable { +struct MyDoc: CouchDBRepresentable { var _id: String? var _rev: String? var title: String diff --git a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-6.swift b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-6.swift index e8e4c81..1307d3b 100644 --- a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-6.swift +++ b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-6.swift @@ -11,7 +11,7 @@ let couchDBClient = CouchDBClient( let dbName = "fortests" -struct MyDoc: CouchDBRepresentable, Codable { +struct MyDoc: CouchDBRepresentable { var _id: String? var _rev: String? var title: String diff --git a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-7.swift b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-7.swift index a0ab1b3..533ca10 100644 --- a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-7.swift +++ b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-7.swift @@ -11,7 +11,7 @@ let couchDBClient = CouchDBClient( let dbName = "fortests" -struct MyDoc: CouchDBRepresentable, Codable { +struct MyDoc: CouchDBRepresentable { var _id: String? var _rev: String? var title: String diff --git a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-8.swift b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-8.swift index a0d6a01..aa1672e 100644 --- a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-8.swift +++ b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial-8.swift @@ -11,7 +11,7 @@ let couchDBClient = CouchDBClient( let dbName = "fortests" -struct MyDoc: CouchDBRepresentable, Codable { +struct MyDoc: CouchDBRepresentable { var _id: String? var _rev: String? var title: String diff --git a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial.tutorial b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial.tutorial index 6cd9d2d..f894b6d 100644 --- a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial.tutorial +++ b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/macOS/macOSTutorial.tutorial @@ -1,29 +1,29 @@ @Tutorial(time: 10) { - @Intro(title: "Using in macOS app") { + @Intro(title: "Using in macOS app") { Learn how to use CouchDBClient in your macOS app - + @Image(source: chapter1.png, alt: "Application icon") - } - - @Section(title: "Initializaton") { - @ContentAndMedia { + } + + @Section(title: "Initialization") { + @ContentAndMedia { Adding CouchDBClient to your project. - - @Image(source: chapter1.png, alt: "Application icon") - } - - @Steps { - @Step { + + @Image(source: chapter1.png, alt: "Application icon") + } + + @Steps { + @Step { Add `CouchDBClient` as a Swift Package dependency. - - @Image(source: macOSTutorial-1.png, alt: "Add CouchDBClient as a Swift Package dependency.") - } - - @Step { - Here's an example app. - - @Code(name: "main.swift", file: macOSTutorial-1.swift) - } + + @Image(source: macOSTutorial-1.png, alt: "Add CouchDBClient as a Swift Package dependency.") + } + + @Step { + Here's an example app. + + @Code(name: "main.swift", file: macOSTutorial-1.swift) + } @Step { Import `CouchDBClient`. @@ -38,7 +38,7 @@ } @Step { - Define a model for your CouchDB document. It should conform to `Codable` and `CouchDBRepresentable` protocols. + Define a model for your CouchDB document. It should conform to the `CouchDBRepresentable` protocol. @Code(name: "main.swift", file: macOSTutorial-4.swift) } @@ -56,13 +56,13 @@ } @Step { - Getting a document by its `_id` from DB with that method will parse JSON into your model if you provide it as a generic type. + Getting a document by its `_id` from a database with that method will parse JSON into your model if you provide it as a generic type. @Code(name: "main.swift", file: macOSTutorial-7.swift) } @Step { - Deleting a document from DB is also easy. + Deleting a document from a database is also easy. @Code(name: "main.swift", file: macOSTutorial-8.swift) } @@ -72,6 +72,6 @@ @Image(source: macOSTutorial-2.png, alt: "Add CouchDBClient as a Swift Package dependency.") } - } - } + } + } } diff --git a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-0.swift b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-0.swift index 6b3aaaa..9c467de 100644 --- a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-0.swift +++ b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-0.swift @@ -8,18 +8,6 @@ struct MyApp: Content, CouchDBRepresentable { let url: String let _id: String var _rev: String - - /// Row model for CouchDB - struct Row: Content { - let value: AppOnSiteModel - } - - /// Rows response - struct RowsResponse: Content { - let total_rows: Int - let offset: Int - let rows: [Row] - } } func routes(_ app: Application) throws { @@ -38,7 +26,7 @@ func routes(_ app: Application) throws { guard let bytes = response.body else { throw Abort(.notFound) } let data = Data(buffer: bytes) - let decodeResponse = try JSONDecoder().decode(RowsResponse.self, from: data) + let decodeResponse = try JSONDecoder().decode(RowsResponse.self, from: data) guard let myApp = decodeResponse.rows.first?.value else { throw Abort(.notFound) diff --git a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-3.swift b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-3.swift index c8983d0..bd1b9a1 100644 --- a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-3.swift +++ b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-3.swift @@ -8,18 +8,6 @@ struct MyApp: Content, CouchDBRepresentable { let url: String let _id: String var _rev: String - - /// Row model for CouchDB - struct Row: Content { - let value: MyApp - } - - /// Rows response - struct RowsResponse: Content { - let total_rows: Int - let offset: Int - let rows: [Row] - } } func routes(_ app: Application) throws { diff --git a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-4.swift b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-4.swift index 62853e2..fe6b2cf 100644 --- a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-4.swift +++ b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-4.swift @@ -8,18 +8,6 @@ struct MyApp: Content, CouchDBRepresentable { let url: String let _id: String var _rev: String - - /// Row model for CouchDB - struct Row: Content { - let value: MyApp - } - - /// Rows response - struct RowsResponse: Content { - let total_rows: Int - let offset: Int - let rows: [Row] - } } func routes(_ app: Application) throws { diff --git a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-5.swift b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-5.swift index 182c1f9..43ffbb9 100644 --- a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-5.swift +++ b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-5.swift @@ -8,18 +8,6 @@ struct MyApp: Content, CouchDBRepresentable { let url: String let _id: String var _rev: String - - /// Row model for CouchDB - struct Row: Content { - let value: MyApp - } - - /// Rows response - struct RowsResponse: Content { - let total_rows: Int - let offset: Int - let rows: [Row] - } } func routes(_ app: Application) throws { @@ -37,7 +25,7 @@ func routes(_ app: Application) throws { guard let bytes = response.body else { throw Abort(.notFound) } let data = Data(buffer: bytes) - let decodeResponse = try JSONDecoder().decode(RowsResponse.self, from: data) + let decodeResponse = try JSONDecoder().decode(RowsResponse.self, from: data) guard let myApp = decodeResponse.rows.first?.value else { throw Abort(.notFound) diff --git a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-6.swift b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-6.swift index 61917c2..8c3741d 100644 --- a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-6.swift +++ b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial-6.swift @@ -8,18 +8,6 @@ struct MyApp: Content, CouchDBRepresentable { let url: String let _id: String var _rev: String - - /// Row model for CouchDB - struct Row: Content { - let value: MyApp - } - - /// Rows response - struct RowsResponse: Content { - let total_rows: Int - let offset: Int - let rows: [Row] - } } func routes(_ app: Application) throws { @@ -37,7 +25,7 @@ func routes(_ app: Application) throws { guard let bytes = response.body else { throw Abort(.notFound) } let data = Data(buffer: bytes) - let decodeResponse = try JSONDecoder().decode(RowsResponse.self, from: data) + let decodeResponse = try JSONDecoder().decode(RowsResponse.self, from: data) guard let myApp = decodeResponse.rows.first?.value else { throw Abort(.notFound) diff --git a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial.tutorial b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial.tutorial index 14ba158..b4e2fed 100644 --- a/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial.tutorial +++ b/Sources/CouchDBClient/CouchDBClient.docc/Tutorials/vapor/VaporTutorial.tutorial @@ -1,29 +1,29 @@ @Tutorial(time: 15) { - @Intro(title: "Using with Vapor in server-side app") { + @Intro(title: "Using with Vapor in server-side app") { Use CouchDBClient for developing server-side apps built with Vapor. - + @Image(source: chapter1.png, alt: "Application icon") - } - - @Section(title: "Use CouchDBClient in macOS app") { - @ContentAndMedia { + } + + @Section(title: "Use CouchDBClient in macOS app") { + @ContentAndMedia { Vapor is built on top of Apple's [SwiftNIO](https://github.com/apple/swift-nio). `CouchDBClient` is compatible with SwiftNIO and can be used for server-side development. - - @Image(source: chapter1.png, alt: "Application icon") - } - - @Steps { - @Step { + + @Image(source: chapter1.png, alt: "Application icon") + } + + @Steps { + @Step { Add `CouchDBClient` as a Swift Package dependency. - - @Image(source: VaporTutorial-1.png, alt: "Add CouchDBClient as a Swift Package dependency.") - } - - @Step { + + @Image(source: VaporTutorial-1.png, alt: "Add CouchDBClient as a Swift Package dependency.") + } + + @Step { Open `routes.swift` in your Vapor project. - - @Code(name: "main.swift", file: VaporTutorial-1.swift) - } + + @Code(name: "main.swift", file: VaporTutorial-1.swift) + } @Step { Import `CouchDBClient` and create a client instance. @@ -32,13 +32,13 @@ } @Step { - Define your data model for CouchDB documents. Nested `Row` and `RowsResponse` models will be used to parse CouchDB responses. + Define your data model for CouchDB documents. `RowsResponse` model from the `CouchDBClient` library will be used to parse CouchDB responses. @Code(name: "main.swift", file: VaporTutorial-3.swift) } @Step { - Get your document from DB. That example is using `CouchDB View` to find the document by the url field. Its map function needs a `key` param which is `appUrl` in our case. + Get your document from a database. That example is using `CouchDB View` to find the document by the url field. Its map function needs a `key` param which is `appUrl` in our case. @Code(name: "main.swift", file: VaporTutorial-4.swift) } @@ -54,6 +54,6 @@ @Code(name: "main.swift", file: VaporTutorial-6.swift) } - } - } + } + } } diff --git a/Sources/CouchDBClient/CouchDBClient.swift b/Sources/CouchDBClient/CouchDBClient.swift index 1db97d4..a9146e2 100644 --- a/Sources/CouchDBClient/CouchDBClient.swift +++ b/Sources/CouchDBClient/CouchDBClient.swift @@ -8,47 +8,52 @@ import Foundation import NIO import NIOHTTP1 +import NIOFoundationCompat import AsyncHTTPClient /// CouchDB client errors. public enum CouchDBClientError: Error { - /// **id** property is empty or missing in provided document. + /// **id** property is empty or missing in the provided document. case idMissing - /// **\_rev** property is empty or missing in provided document. + /// **\_rev** property is empty or missing in the provided document. case revMissing - /// Get request wasn't successful. + /// The Get request wasn't successful. case getError(error: CouchDBError) - /// Insert request wasn't successful. + /// The Insert request wasn't successful. case insertError(error: CouchDBError) - /// Update request wasn't successful. + /// The Update request wasn't successful. case updateError(error: CouchDBError) - /// Find request wasn't successful. + /// The Find request wasn't successful. case findError(error: CouchDBError) - /// Uknown response from CouchDB. + /// Unknown response from CouchDB. case unknownResponse /// Wrong username or password. case unauthorized + /// Missing data in the response body. + case noData } extension CouchDBClientError: LocalizedError { public var errorDescription: String? { switch self { case .idMissing: - return "id property is empty or missing in provided document." + return "id property is empty or missing in the provided document." case .revMissing: - return "_rev property is empty or missing in provided document." + return "_rev property is empty or missing in the provided document." case .getError(let error): - return "Get request wasn't successful: \(error.localizedDescription)" + return "The Get request wasn't successful: \(error.localizedDescription)" case .insertError(let error): - return "Insert request wasn't successful: \(error.localizedDescription)" + return "The Insert request wasn't successful: \(error.localizedDescription)" case .updateError(let error): - return "Update request wasn't successful: \(error.localizedDescription)" + return "The Update request wasn't successful: \(error.localizedDescription)" case .findError(let error): - return "Find request wasn't successful: \(error.localizedDescription)" + return "The Find request wasn't successful: \(error.localizedDescription)" case .unknownResponse: - return "Uknown response from CouchDB." + return "Unknown response from CouchDB." case .unauthorized: return "Wrong username or password." + case .noData: + return "Missing data in the response body." } } } @@ -57,18 +62,18 @@ extension CouchDBClientError: LocalizedError { public class CouchDBClient { /// Protocol (URL scheme) that should be used to perform requests to CouchDB. public enum CouchDBProtocol: String { - /// Use HTTP protocol. + /// HTTP protocol. case http - /// Use HTTPS protocol. + /// HTTPS protocol. case https } // MARK: - Public properties - /// Flag if did authorize in CouchDB. + /// Flag if authorized in CouchDB. public var isAuthorized: Bool { authData?.ok ?? false } - /// You can set timeout for requests in seconds. Default value is 30. + /// You can set a timeout for requests in seconds. Default value is 30. public var requestsTimeout: Int64 = 30 // MARK: - Private properties @@ -80,9 +85,9 @@ public class CouchDBClient { private var couchPort: Int = 5984 /// Base URL. private var couchBaseURL: String = "" - /// Session cookie for requests that needs authorization. - private var sessionCookie: String? - /// Session cookie as Cookie struct + /// Session cookie for requests that need authorization. + internal var sessionCookie: String? + /// Session cookie as Cookie struct. internal var sessionCookieExpires: Date? /// CouchDB user name. private var userName: String = "" @@ -110,7 +115,7 @@ public class CouchDBClient { /// userPassword: "myPassword" /// ) /// ``` - /// If you don't want to have your password in the code you can pass `COUCHDB_PASS` param in you command line. + /// If you don't want to have your password in the code you can pass `COUCHDB_PASS` param in your command line. /// For example you can run your Server Side Swift project: /// ```bash /// COUCHDB_PASS=myPassword /path/.build/x86_64-unknown-linux-gnu/release/Run @@ -126,9 +131,9 @@ public class CouchDBClient { /// ``` /// /// - Parameters: - /// - couchProtocol: Protocol for requests (check ``CouchDBProtocol`` enum for avaiable values). - /// - couchHost: Host of CouchDB instance. - /// - couchPort: Port CouchDB works on. + /// - couchProtocol: Protocol for requests (check the ``CouchDBProtocol`` enum for available values). + /// - couchHost: Host of the CouchDB instance. + /// - couchPort: Port that CouchDB works on. /// - userName: Username. /// - userPassword: User password. public init(couchProtocol: CouchDBProtocol = .http, couchHost: String = "127.0.0.1", couchPort: Int = 5984, userName: String = "", userPassword: String = "") { @@ -145,7 +150,7 @@ public class CouchDBClient { // MARK: - Public methods - /// Get DBs list. + /// Get a database list. /// /// Example: /// ```swift @@ -153,7 +158,7 @@ public class CouchDBClient { /// ``` /// /// - Parameter eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. - /// - Returns: Array of strings containing DBs names. + /// - Returns: Array of strings containing database names. public func getAllDBs(eventLoopGroup: EventLoopGroup? = nil) async throws -> [String] { try await authIfNeed(eventLoopGroup: eventLoopGroup) @@ -174,22 +179,23 @@ public class CouchDBClient { let request = try buildRequest(fromUrl: url, withMethod: .GET) let response = try await httpClient - .execute(request: request) - .get() + .execute(request, timeout: .seconds(requestsTimeout)) if response.status == .unauthorized { throw CouchDBClientError.unauthorized } - guard var body = response.body, let bytes = body.readBytes(length: body.readableBytes) else { - throw CouchDBClientError.unknownResponse - } + let body = response.body + let expectedBytes = response.headers.first(name: "content-length").flatMap(Int.init) + var bytes = try await body.collect(upTo: expectedBytes ?? 1024 * 1024 * 10) - let data = Data(bytes) + guard let data = bytes.readData(length: bytes.readableBytes) else { + throw CouchDBClientError.noData + } return try JSONDecoder().decode([String].self, from: data) } - /// Check if DB exists + /// Check if database exists. /// /// Example: /// @@ -198,7 +204,7 @@ public class CouchDBClient { /// ``` /// /// - Parameters: - /// - dbName: DB name. + /// - dbName: Database name. /// - eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. /// - Returns: True or false. public func dbExists(_ dbName: String, eventLoopGroup: EventLoopGroup? = nil) async throws -> Bool { @@ -220,8 +226,7 @@ public class CouchDBClient { let url = buildUrl(path: "/" + dbName) let request = try buildRequest(fromUrl: url, withMethod: .HEAD) let response = try await httpClient - .execute(request: request) - .get() + .execute(request, timeout: .seconds(requestsTimeout)) if response.status == .unauthorized { throw CouchDBClientError.unauthorized @@ -230,7 +235,7 @@ public class CouchDBClient { return response.status == .ok } - /// Create DB. + /// Create a database. /// /// Example: /// ```swift @@ -238,9 +243,9 @@ public class CouchDBClient { /// ``` /// /// - Parameters: - /// - dbName: DB name. + /// - dbName: Database name. /// - eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. - /// - Returns: Request response. + /// - Returns: Creation response. @discardableResult public func createDB(_ dbName: String, eventLoopGroup: EventLoopGroup? = nil) async throws -> UpdateDBResponse { try await authIfNeed(eventLoopGroup: eventLoopGroup) @@ -262,18 +267,20 @@ public class CouchDBClient { let request = try self.buildRequest(fromUrl: url, withMethod: .PUT) let response = try await httpClient - .execute(request: request, deadline: .now() + .seconds(requestsTimeout)) - .get() + .execute(request, timeout: .seconds(requestsTimeout)) if response.status == .unauthorized { throw CouchDBClientError.unauthorized } - guard var body = response.body, let bytes = body.readBytes(length: body.readableBytes) else { - throw CouchDBClientError.unknownResponse + let body = response.body + let expectedBytes = response.headers.first(name: "content-length").flatMap(Int.init) + var bytes = try await body.collect(upTo: expectedBytes ?? 1024 * 1024 * 10) + + guard let data = bytes.readData(length: bytes.readableBytes) else { + throw CouchDBClientError.noData } - let data = Data(bytes) let decoder = JSONDecoder() do { @@ -287,7 +294,7 @@ public class CouchDBClient { } } - /// Delete DB. + /// Delete a database. /// /// Example: /// ```swift @@ -295,9 +302,9 @@ public class CouchDBClient { /// ``` /// /// - Parameters: - /// - dbName: DB name. + /// - dbName: Database name. /// - eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. - /// - Returns: Request response. + /// - Returns: Deletion response. @discardableResult public func deleteDB(_ dbName: String, eventLoopGroup: EventLoopGroup? = nil) async throws -> UpdateDBResponse { try await authIfNeed(eventLoopGroup: eventLoopGroup) @@ -319,18 +326,20 @@ public class CouchDBClient { let request = try self.buildRequest(fromUrl: url, withMethod: .DELETE) let response = try await httpClient - .execute(request: request, deadline: .now() + .seconds(requestsTimeout)) - .get() + .execute(request, timeout: .seconds(requestsTimeout)) if response.status == .unauthorized { throw CouchDBClientError.unauthorized } - guard var body = response.body, let bytes = body.readBytes(length: body.readableBytes) else { - throw CouchDBClientError.unknownResponse + let body = response.body + let expectedBytes = response.headers.first(name: "content-length").flatMap(Int.init) + var bytes = try await body.collect(upTo: expectedBytes ?? 1024 * 1024 * 10) + + guard let data = bytes.readData(length: bytes.readableBytes) else { + throw CouchDBClientError.noData } - let data = Data(bytes) let decoder = JSONDecoder() do { @@ -344,14 +353,14 @@ public class CouchDBClient { } } - /// Get data from DB. + /// Get data from a database. /// /// Examples: /// - /// Define your document model: + /// Define your document data model: /// ```swift /// // Example struct - /// struct ExpectedDoc: CouchDBRepresentable, Codable { + /// struct ExpectedDoc: CouchDBRepresentable { /// var name: String /// var _id: String? /// var _rev: String? @@ -361,47 +370,56 @@ public class CouchDBClient { /// Get document by ID: /// ```swift /// // get data from DB by document ID - /// var response = try await couchDBClient.get(dbName: "databaseName", uri: "documentId") + /// var response = try await couchDBClient.get( + /// dbName: "databaseName", + /// uri: "documentId" + /// ) /// /// // parse JSON - /// let bytes = response.body!.readBytes(length: response.body!.readableBytes)! - /// let doc = try JSONDecoder().decode(ExpectedDoc.self, from: Data(bytes)) - /// ``` + /// let expectedBytes = response.headers + /// .first(name: "content-length") + /// .flatMap(Int.init) ?? 1024 * 1024 * 10 + /// var bytes = try await response.body.collect(upTo: expectedBytes) + /// let data = bytes.readData(length: bytes.readableBytes) /// - /// You can also provide CouchDB view document as uri and key in query. - /// Define Row and RowsResponse models: - /// ```swift - /// struct Row: Codable { - /// let value: ExpectedDoc - /// } - /// - /// struct RowsResponse: Codable { - /// let total_rows: Int - /// let offset: Int - /// let rows: [Row] - /// } + /// let doc = try JSONDecoder().decode( + /// ExpectedDoc.self, + /// from: data! + /// ) /// ``` /// - /// Get data and parse RowsResponse: + /// You can also provide a CouchDB view document as uri and key in the query. + /// + /// Get data and parse `RowsResponse`: /// ```swift /// let response = try await couchDBClient.get( - /// dbName: "databaseName", + /// fromDB: "databaseName", /// uri: "_design/all/_view/by_url", /// query: ["key": "\"\(url)\""] /// ) - /// let bytes = response.body!.readBytes(length: response.body!.readableBytes)! - /// let decodedResponse = try JSONDecoder().decode(RowsResponse.self, from: data) + /// + /// let expectedBytes = response.headers + /// .first(name: "content-length") + /// .flatMap(Int.init) ?? 1024 * 1024 * 10 + /// var bytes = try await response.body.collect(upTo: expectedBytes) + /// let data = bytes.readData(length: bytes.readableBytes) + /// + /// let decodedResponse = try JSONDecoder().decode( + /// RowsResponse.self, + /// from: data! + /// ) + /// /// print(decodedResponse.rows) /// print(decodedResponse.rows.first?.value) /// ``` /// /// - Parameters: - /// - dbName: DB name. + /// - dbName: Database name. /// - uri: URI (view or document id). /// - query: Request query items. /// - eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. - /// - Returns: Request response. - public func get(dbName: String, uri: String, queryItems: [URLQueryItem]? = nil, eventLoopGroup: EventLoopGroup? = nil) async throws -> HTTPClient.Response { + /// - Returns: Response. + public func get(fromDB dbName: String, uri: String, queryItems: [URLQueryItem]? = nil, eventLoopGroup: EventLoopGroup? = nil) async throws -> HTTPClientResponse { try await authIfNeed(eventLoopGroup: eventLoopGroup) let httpClient: HTTPClient @@ -419,26 +437,31 @@ public class CouchDBClient { let url = buildUrl(path: "/" + dbName + "/" + uri, query: queryItems ?? []) let request = try buildRequest(fromUrl: url, withMethod: .GET) - let response = try await httpClient - .execute(request: request, deadline: .now() + .seconds(requestsTimeout)) - .get() + var response = try await httpClient + .execute(request, timeout: .seconds(requestsTimeout)) if response.status == .unauthorized { throw CouchDBClientError.unauthorized } + let body = response.body + let expectedBytes = response.headers.first(name: "content-length").flatMap(Int.init) ?? 1024 * 1024 * 10 + + response.body = .bytes( + try await body.collect(upTo: expectedBytes) + ) + return response } - - /// Get a document from DB. It will parse JSON using provided generic type. Check an example in Discussion. + /// Get a document from a database. It will parse JSON using the provided generic type. Check an example in Discussion. /// /// Example: /// /// Define your document model: /// ```swift /// // Example struct - /// struct ExpectedDoc: CouchDBRepresentable, Codable { + /// struct ExpectedDoc: CouchDBRepresentable { /// var name: String /// var _id: String? /// var _rev: String? @@ -447,28 +470,31 @@ public class CouchDBClient { /// /// Get document by ID: /// ```swift - /// // get data from DB by document ID - /// let doc: ExpectedDoc = try await couchDBClient.get(dbName: "databaseName", uri: "documentId") + /// // get data from the database by document ID + /// let doc: ExpectedDoc = try await couchDBClient.get(fromDB: "databaseName", uri: "documentId") /// ``` /// /// - Parameters: - /// - dbName: DB name. + /// - dbName: Database name. /// - uri: URI (view or document id). /// - queryItems: Request query items. /// - eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. /// - Returns: An object or a struct (of generic type) parsed from JSON. - public func get (dbName: String, uri: String, queryItems: [URLQueryItem]? = nil, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .secondsSince1970, eventLoopGroup: EventLoopGroup? = nil) async throws -> T { - let response = try await get(dbName: dbName, uri: uri, queryItems: queryItems, eventLoopGroup: eventLoopGroup) + public func get (fromDB dbName: String, uri: String, queryItems: [URLQueryItem]? = nil, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .secondsSince1970, eventLoopGroup: EventLoopGroup? = nil) async throws -> T { + let response: HTTPClientResponse = try await get(fromDB: dbName, uri: uri, queryItems: queryItems, eventLoopGroup: eventLoopGroup) if response.status == .unauthorized { throw CouchDBClientError.unauthorized } - guard var body = response.body, let bytes = body.readBytes(length: body.readableBytes) else { - throw CouchDBClientError.unknownResponse + let body = response.body + let expectedBytes = response.headers.first(name: "content-length").flatMap(Int.init) + var bytes = try await body.collect(upTo: expectedBytes ?? 1024 * 1024 * 10) + + guard let data = bytes.readData(length: bytes.readableBytes) else { + throw CouchDBClientError.noData } - let data = Data(bytes) let decoder = JSONDecoder() decoder.dateDecodingStrategy = dateDecodingStrategy @@ -482,37 +508,41 @@ public class CouchDBClient { throw parsingError } } - - /// Find data in DB by selector. + + /// Find data in a database by selector. /// /// Example: /// /// ```swift - /// // find documents in DB by selector + /// // find documents in the database by selector /// let selector = ["selector": ["name": "Sam"]] - /// let docs: [ExpectedDoc] = try await couchDBClient.find(in: testsDB, selector: selector) + /// let docs: [ExpectedDoc] = try await couchDBClient.find(inDB: testsDB, selector: selector) /// ``` /// /// - Parameters: - /// - in dbName: DB name. - /// - selector: Codable representation of json selector query. + /// - in dbName: Database name. + /// - selector: Codable representation of the JSON selector query. /// - eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. - /// - Returns: Array of documents [T]. - public func find(in dbName: String, selector: Codable, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .secondsSince1970, eventLoopGroup: EventLoopGroup? = nil) async throws -> [T] { + /// - Returns: Array of `[T]` documents. + public func find(inDB dbName: String, selector: Codable, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .secondsSince1970, eventLoopGroup: EventLoopGroup? = nil) async throws -> [T] { let encoder = JSONEncoder() let selectorData = try encoder.encode(selector) + let requestBody: HTTPClientRequest.Body = .bytes(ByteBuffer(data: selectorData)) let findResponse = try await find( - in: dbName, - body: .data(selectorData), + inDB: dbName, + body: requestBody, eventLoopGroup: eventLoopGroup ) - guard var body = findResponse.body, let bytes = body.readBytes(length: body.readableBytes) else { - throw CouchDBClientError.unknownResponse + let body = findResponse.body + let expectedBytes = findResponse.headers.first(name: "content-length").flatMap(Int.init) + var bytes = try await body.collect(upTo: expectedBytes ?? 1024 * 1024 * 10) + + guard let data = bytes.readData(length: bytes.readableBytes) else { + throw CouchDBClientError.noData } - let data = Data(bytes) let decoder = JSONDecoder() decoder.dateDecodingStrategy = dateDecodingStrategy @@ -527,23 +557,23 @@ public class CouchDBClient { } } - /// Find data in DB by selector. + /// Find data in a database by selector. /// /// Example: /// ```swift /// let selector = ["selector": ["name": "Greg"]] /// let bodyData = try JSONEncoder().encode(selector) - /// var findResponse = try await couchDBClient.find(in: testsDB, body: .data(bodyData)) + /// var findResponse = try await couchDBClient.find(inDB: testsDB, body: .data(bodyData)) /// /// let bytes = findResponse.body!.readBytes(length: findResponse.body!.readableBytes)! /// let docs = try JSONDecoder().decode(CouchDBFindResponse.self, from: Data(bytes)).docs /// ``` /// - Parameters: - /// - dbName: DB name. + /// - dbName: Database name. /// - body: Request body data. /// - eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. - /// - Returns: Request response. - public func find(in dbName: String, body: HTTPClient.Body, eventLoopGroup: EventLoopGroup? = nil) async throws -> HTTPClient.Response { + /// - Returns: Response. + public func find(inDB dbName: String, body: HTTPClientRequest.Body, eventLoopGroup: EventLoopGroup? = nil) async throws -> HTTPClientResponse { try await authIfNeed(eventLoopGroup: eventLoopGroup) let httpClient: HTTPClient @@ -562,25 +592,31 @@ public class CouchDBClient { let url = buildUrl(path: "/" + dbName + "/_find", query: []) var request = try buildRequest(fromUrl: url, withMethod: .POST) request.body = body - let response = try await httpClient - .execute(request: request, deadline: .now() + .seconds(requestsTimeout)) - .get() + var response = try await httpClient + .execute(request, timeout: .seconds(requestsTimeout)) if response.status == .unauthorized { throw CouchDBClientError.unauthorized } + let body = response.body + let expectedBytes = response.headers.first(name: "content-length").flatMap(Int.init) ?? 1024 * 1024 * 10 + + response.body = .bytes( + try await body.collect(upTo: expectedBytes) + ) + return response } - /// Update data in DB. + /// Update data in a database. /// /// Examples: /// /// Define your document model: /// ```swift /// // Example struct - /// struct ExpectedDoc: CouchDBRepresentable, Codable { + /// struct ExpectedDoc: CouchDBRepresentable { /// var name: String /// var _id: String? /// var _rev: String? @@ -588,23 +624,24 @@ public class CouchDBClient { /// ``` /// Get document by ID and update it: /// ```swift - /// // get data from DB by document ID + /// // get data from the database by document ID /// var response = try await couchDBClient.get(dbName: "databaseName", uri: "documentId") /// /// // parse JSON /// let bytes = response.body!.readBytes(length: response.body!.readableBytes)! /// var doc = try JSONDecoder().decode(ExpectedDoc.self, from: Data(bytes)) /// - /// // Update value + /// // update some value /// doc.name = "Updated name" /// - /// // encode document into JSON string + /// // encode document into a JSON string /// let data = try encoder.encode(updatedData) + /// let body: HTTPClientRequest.Body = .bytes(ByteBuffer(data: data)) /// /// let response = try await couchDBClient.update( /// dbName: testsDB, /// uri: doc._id!, - /// body: .data(data) + /// body: body /// ) /// /// print(response) @@ -612,12 +649,12 @@ public class CouchDBClient { /// /// /// - Parameters: - /// - dbName: DB name. + /// - dbName: Database name. /// - uri: URI (view or document id). - /// - body: Request body data. New will be created if nil value provided. + /// - body: Request body data. /// - eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. /// - Returns: Update response. - public func update(dbName: String, uri: String, body: HTTPClient.Body, eventLoopGroup: EventLoopGroup? = nil) async throws -> CouchUpdateResponse { + public func update(dbName: String, uri: String, body: HTTPClientRequest.Body, eventLoopGroup: EventLoopGroup? = nil) async throws -> CouchUpdateResponse { try await authIfNeed(eventLoopGroup: eventLoopGroup) let httpClient: HTTPClient @@ -638,18 +675,20 @@ public class CouchDBClient { request.body = body let response = try await httpClient - .execute(request: request, deadline: .now() + .seconds(requestsTimeout)) - .get() + .execute(request, timeout: .seconds(requestsTimeout)) if response.status == .unauthorized { throw CouchDBClientError.unauthorized } - guard var body = response.body, let bytes = body.readBytes(length: body.readableBytes) else { - throw CouchDBClientError.unknownResponse + let body = response.body + let expectedBytes = response.headers.first(name: "content-length").flatMap(Int.init) + var bytes = try await body.collect(upTo: expectedBytes ?? 1024 * 1024 * 10) + + guard let data = bytes.readData(length: bytes.readableBytes) else { + throw CouchDBClientError.noData } - let data = Data(bytes) let decoder = JSONDecoder() do { @@ -663,22 +702,22 @@ public class CouchDBClient { } } - /// Update document in DB. That method will mutate `doc` to update it's `_rev` with the value from CouchDB response. + /// Update document in a database. That method will mutate `doc` to update its `_rev` with the value from CouchDB response. /// /// Examples: /// /// Define your document model: /// ```swift /// // Example struct - /// struct ExpectedDoc: CouchDBRepresentable, Codable { + /// struct ExpectedDoc: CouchDBRepresentable { /// var name: String /// var _id: String? /// var _rev: String? /// } /// ``` - /// Get document by ID and update it: + /// Get a document by ID and update it: /// ```swift - /// // get data from DB by document ID + /// // get data from the database by document ID /// var doc: ExpectedDoc = try await couchDBClient.get(dbName: "databaseName", uri: "documentId") /// print(doc) /// @@ -694,11 +733,11 @@ public class CouchDBClient { /// ``` /// /// - Parameters: - /// - dbName: DB name. That method will mutate `doc` to update it's `_id` and `_rev` properties from insert request. - /// - doc: Document object/struct. Should confirm to ``CouchDBRepresentable`` and Codable protocols. + /// - dbName: Database name. That method will mutate `doc` to update its `_id` and `_rev` properties from insert request. + /// - doc: Document object/struct. Should conform to the ``CouchDBRepresentable`` protocol. /// - eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. /// - Returns: Update response. - public func update (dbName: String, doc: inout T, dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .secondsSince1970, eventLoopGroup: EventLoopGroup? = nil ) async throws { + public func update (dbName: String, doc: inout T, dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .secondsSince1970, eventLoopGroup: EventLoopGroup? = nil ) async throws { guard let id = doc._id else { throw CouchDBClientError.idMissing } guard doc._rev?.isEmpty == false else { throw CouchDBClientError.revMissing } @@ -706,10 +745,12 @@ public class CouchDBClient { encoder.dateEncodingStrategy = dateEncodingStrategy let encodedData = try encoder.encode(doc) + let body: HTTPClientRequest.Body = .bytes(ByteBuffer(data: encodedData)) + let updateResponse = try await update( dbName: dbName, uri: id, - body: .data(encodedData), + body: body, eventLoopGroup: eventLoopGroup ) @@ -721,14 +762,14 @@ public class CouchDBClient { doc._id = updateResponse.id } - /// Insert data in DB. + /// Insert data in a database. Accepts `HTTPClientRequest.Body` as body parameter. /// /// Examples: /// /// Define your document model: /// ```swift /// // Example struct - /// struct ExpectedDoc: CouchDBRepresentable, Codable { + /// struct ExpectedDoc: CouchDBRepresentable { /// var name: String /// var _id: String? /// var _rev: String? @@ -740,20 +781,22 @@ public class CouchDBClient { /// let testDoc = ExpectedDoc(name: "My name") /// let data = try JSONEncoder().encode(testData) /// + /// let body: HTTPClientRequest.Body = .bytes(ByteBuffer(data: insertEncodeData)) + /// /// let response = try await couchDBClient.insert( /// dbName: "databaseName", - /// body: .data(data) + /// body: body /// ) /// /// print(response) /// ``` /// /// - Parameters: - /// - dbName: DB name. + /// - dbName: Database name. /// - body: Request body data. /// - eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. - /// - Returns: Insert request response. - public func insert(dbName: String, body: HTTPClient.Body, eventLoopGroup: EventLoopGroup? = nil) async throws -> CouchUpdateResponse { + /// - Returns: Insertion response. + public func insert(dbName: String, body: HTTPClientRequest.Body, eventLoopGroup: EventLoopGroup? = nil) async throws -> CouchUpdateResponse { try await authIfNeed(eventLoopGroup: eventLoopGroup) let httpClient: HTTPClient @@ -775,18 +818,20 @@ public class CouchDBClient { request.body = body let response = try await httpClient - .execute(request: request, deadline: .now() + .seconds(requestsTimeout)) - .get() + .execute(request, timeout: .seconds(requestsTimeout)) if response.status == .unauthorized { throw CouchDBClientError.unauthorized } - guard var body = response.body, let bytes = body.readBytes(length: body.readableBytes) else { - throw CouchDBClientError.unknownResponse + let body = response.body + let expectedBytes = response.headers.first(name: "content-length").flatMap(Int.init) + var bytes = try await body.collect(upTo: expectedBytes ?? 1024 * 1024 * 10) + + guard let data = bytes.readData(length: bytes.readableBytes) else { + throw CouchDBClientError.noData } - let data = Data(bytes) let decoder = JSONDecoder() do { @@ -800,14 +845,14 @@ public class CouchDBClient { } } - /// Insert document in DB. That method will mutate `doc` to update it's `_id` and `_rev` with the values from CouchDB response. + /// Insert document in a database. That method will mutate `doc` to update its `_id` and `_rev` with the values from CouchDB response. /// /// Examples: /// /// Define your document model: /// ```swift /// // Example struct - /// struct ExpectedDoc: CouchDBRepresentable, Codable { + /// struct ExpectedDoc: CouchDBRepresentable { /// var name: String /// var _id: String? /// var _rev: String? @@ -827,17 +872,19 @@ public class CouchDBClient { /// ``` /// /// - Parameters: - /// - dbName: DB name. - /// - doc: Document object/struct. Should confirm to ``CouchDBRepresentable`` protocol. + /// - dbName: Database name. + /// - doc: Document object/struct. Should conform to the ``CouchDBRepresentable`` protocol. /// - eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. - public func insert (dbName: String, doc: inout T, dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .secondsSince1970, eventLoopGroup: EventLoopGroup? = nil ) async throws { + public func insert (dbName: String, doc: inout T, dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .secondsSince1970, eventLoopGroup: EventLoopGroup? = nil ) async throws { let encoder = JSONEncoder() encoder.dateEncodingStrategy = dateEncodingStrategy let insertEncodeData = try encoder.encode(doc) + let body: HTTPClientRequest.Body = .bytes(ByteBuffer(data: insertEncodeData)) + let insertResponse = try await insert( dbName: dbName, - body: .data(insertEncodeData), + body: body, eventLoopGroup: eventLoopGroup ) @@ -849,7 +896,7 @@ public class CouchDBClient { doc._id = insertResponse.id } - /// Delete document from DB by URI. + /// Delete a document from a database by URI. /// /// Examples: /// @@ -858,7 +905,7 @@ public class CouchDBClient { /// ``` /// /// - Parameters: - /// - dbName: DB name. + /// - dbName: Database name. /// - uri: document uri (usually _id). /// - rev: document revision. /// - eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. @@ -883,22 +930,24 @@ public class CouchDBClient { let request = try self.buildRequest(fromUrl: url, withMethod: .DELETE) let response = try await httpClient - .execute(request: request, deadline: .now() + .seconds(requestsTimeout)) - .get() + .execute(request, timeout: .seconds(requestsTimeout)) if response.status == .unauthorized { throw CouchDBClientError.unauthorized } - guard var body = response.body, let bytes = body.readBytes(length: body.readableBytes) else { + let body = response.body + let expectedBytes = response.headers.first(name: "content-length").flatMap(Int.init) + var bytes = try await body.collect(upTo: expectedBytes ?? 1024 * 1024 * 10) + + guard let data = bytes.readData(length: bytes.readableBytes) else { return CouchUpdateResponse(ok: false, id: "", rev: "") } - let data = Data(bytes) return try JSONDecoder().decode(CouchUpdateResponse.self, from: data) } - /// Delete document from DB. + /// Delete a document from a database. /// /// Examples: /// @@ -907,8 +956,8 @@ public class CouchDBClient { /// ``` /// /// - Parameters: - /// - dbName: DB name. - /// - doc: Document object/struct. Should confirm to ``CouchDBRepresentable`` protocol. + /// - dbName: Database name. + /// - doc: Document object/struct. Should conform to the ``CouchDBRepresentable`` protocol. /// - eventLoopGroup: NIO's EventLoopGroup object. New will be created if nil value provided. /// - Returns: Delete request response. public func delete(fromDb dbName: String, doc: CouchDBRepresentable, eventLoopGroup: EventLoopGroup? = nil) async throws -> CouchUpdateResponse { @@ -968,14 +1017,14 @@ internal extension CouchDBClient { let url = buildUrl(path: "/_session") - var request = try HTTPClient.Request(url:url, method: .POST) + var request = HTTPClientRequest(url: url) + request.method = .POST request.headers.add(name: "Content-Type", value: "application/x-www-form-urlencoded") let dataString = "name=\(userName)&password=\(userPassword)" - request.body = HTTPClient.Body.string(dataString) + request.body = .bytes(ByteBuffer(string: dataString)) let response = try await httpClient - .execute(request: request, deadline: .now() + .seconds(requestsTimeout)) - .get() + .execute(request, timeout: .seconds(requestsTimeout)) if response.status == .unauthorized { throw CouchDBClientError.unauthorized @@ -1009,29 +1058,28 @@ internal extension CouchDBClient { sessionCookie = cookie - guard var body = response.body, let bytes = body.readBytes(length: body.readableBytes) else { return nil } + let body = response.body + let expectedBytes = response.headers.first(name: "content-length").flatMap(Int.init) + var bytes = try await body.collect(upTo: expectedBytes ?? 1024 * 1024 * 10) + + guard let data = bytes.readData(length: bytes.readableBytes) else { + throw CouchDBClientError.noData + } - let data = Data(bytes) authData = try JSONDecoder().decode(CreateSessionResponse.self, from: data) return authData } - /// Build HTTP request from url string. - /// - Parameters: - /// - url: URL string. - /// - method: HTTP method. - /// - Returns: HTTP Request. - func buildRequest(fromUrl url: String, withMethod method: HTTPMethod) throws -> HTTPClient.Request { + func buildRequest(fromUrl url: String, withMethod method: HTTPMethod) throws -> HTTPClientRequest { var headers = HTTPHeaders() headers.add(name: "Content-Type", value: "application/json") if let cookie = sessionCookie { headers.add(name: "Cookie", value: cookie) } - return try HTTPClient.Request( - url: url, - method: method, - headers: headers, - body: nil - ) + + var request = HTTPClientRequest(url: url) + request.method = method + request.headers = headers + return request } } diff --git a/Sources/CouchDBClient/Models/CouchDBFindResponse.swift b/Sources/CouchDBClient/Models/CouchDBFindResponse.swift index cd40015..dc5b045 100644 --- a/Sources/CouchDBClient/Models/CouchDBFindResponse.swift +++ b/Sources/CouchDBClient/Models/CouchDBFindResponse.swift @@ -7,7 +7,7 @@ import Foundation -public struct CouchDBFindResponse: Codable { +public struct CouchDBFindResponse: Codable { var docs: [T] var bookmark: String? } diff --git a/Sources/CouchDBClient/Models/CouchDBRepresentable.swift b/Sources/CouchDBClient/Models/CouchDBRepresentable.swift index 637e8a7..74409ee 100644 --- a/Sources/CouchDBClient/Models/CouchDBRepresentable.swift +++ b/Sources/CouchDBClient/Models/CouchDBRepresentable.swift @@ -7,18 +7,18 @@ import Foundation -/// Every CouchDB document should have **\_id** and **\_rev** properties. Both should be optional **String?** type. Unfortunatelly DocC ignores properties starting with `_` symbol so check the example in Overview section. +/// Every CouchDB document should have **\_id** and **\_rev** properties. Both should be optional **String?** type. Unfortunately DocC ignores properties starting with `_` symbol so check the example in the Overview section. /// /// Example: /// ```swift /// // Example struct -/// struct ExpectedDoc: CouchDBRepresentable, Codable { +/// struct ExpectedDoc: CouchDBRepresentable { /// var name: String /// var _id: String? /// var _rev: String? /// } /// ``` -public protocol CouchDBRepresentable { +public protocol CouchDBRepresentable: Codable { /// Document ID. var _id: String? { get set } /// Revision MVCC token. diff --git a/Sources/CouchDBClient/Models/RowsResponse.swift b/Sources/CouchDBClient/Models/RowsResponse.swift new file mode 100644 index 0000000..328609b --- /dev/null +++ b/Sources/CouchDBClient/Models/RowsResponse.swift @@ -0,0 +1,23 @@ +// +// RowsResponse.swift +// +// +// Created by Sergei Armodin on 07.04.2024. +// + +import Foundation + +/// Rows response model. +public struct RowsResponse: Codable { + public struct Row: Codable { + /// A CouchDB document. + public let value: T + } + + /// Total documents in a response. + public let total_rows: Int + /// Results offset. + public let offset: Int + /// CouchDB documents. + public let rows: [Row] +} diff --git a/Tests/CouchDBClientTests/CouchDBClientTests.swift b/Tests/CouchDBClientTests/CouchDBClientTests.swift index 63b4c45..7e04c4a 100644 --- a/Tests/CouchDBClientTests/CouchDBClientTests.swift +++ b/Tests/CouchDBClientTests/CouchDBClientTests.swift @@ -6,7 +6,7 @@ import AsyncHTTPClient final class CouchDBClientTests: XCTestCase { - struct ExpectedDoc: CouchDBRepresentable, Codable { + struct ExpectedDoc: CouchDBRepresentable { var name: String var _id: String? var _rev: String? @@ -85,7 +85,7 @@ final class CouchDBClientTests: XCTestCase { // get inserted doc do { - testDoc = try await couchDBClient.get(dbName: testsDB, uri: expectedInsertId) + testDoc = try await couchDBClient.get(fromDB: testsDB, uri: expectedInsertId) } catch CouchDBClientError.getError(let error) { XCTFail(error.reason) return @@ -115,14 +115,20 @@ final class CouchDBClientTests: XCTestCase { XCTAssertEqual(testDoc._id, expectedInsertId) // get updated doc - var getResponse2 = try await couchDBClient.get( - dbName: testsDB, + let getResponse2 = try await couchDBClient.get( + fromDB: testsDB, uri: expectedInsertId ) XCTAssertNotNil(getResponse2.body) - let bytes2 = getResponse2.body!.readBytes(length: getResponse2.body!.readableBytes)! - testDoc = try JSONDecoder().decode(ExpectedDoc.self, from: Data(bytes2)) + let expectedBytes2 = getResponse2.headers.first(name: "content-length").flatMap(Int.init) ?? 1024 * 1024 * 10 + var bytes2 = try await getResponse2.body.collect(upTo: expectedBytes2) + let data2 = bytes2.readData(length: bytes2.readableBytes) + + testDoc = try JSONDecoder().decode( + ExpectedDoc.self, + from: data2! + ) XCTAssertEqual(expectedName, testDoc.name) @@ -151,7 +157,7 @@ final class CouchDBClientTests: XCTestCase { let insertEncodeData = try JSONEncoder().encode(testDoc) let response = try await couchDBClient.insert( dbName: testsDB, - body: .data(insertEncodeData) + body: .bytes(ByteBuffer(data: insertEncodeData)) ) XCTAssertEqual(response.ok, true) @@ -167,11 +173,17 @@ final class CouchDBClientTests: XCTestCase { // Test Get var expectedName = testDoc.name do { - var response = try await couchDBClient.get(dbName: testsDB, uri: expectedInsertId) + let response = try await couchDBClient.get(fromDB: testsDB, uri: expectedInsertId) XCTAssertNotNil(response.body) - let bytes = response.body!.readBytes(length: response.body!.readableBytes)! - testDoc = try JSONDecoder().decode(ExpectedDoc.self, from: Data(bytes)) + let expectedBytes = response.headers.first(name: "content-length").flatMap(Int.init) ?? 1024 * 1024 * 10 + var bytes = try await response.body.collect(upTo: expectedBytes) + let data = bytes.readData(length: bytes.readableBytes) + + testDoc = try JSONDecoder().decode( + ExpectedDoc.self, + from: data! + ) XCTAssertEqual(expectedName, testDoc.name) XCTAssertEqual(testDoc._rev, expectedInsertRev) @@ -186,10 +198,12 @@ final class CouchDBClientTests: XCTestCase { do { let updateEncodedData = try JSONEncoder().encode(testDoc) + let body: HTTPClientRequest.Body = .bytes(ByteBuffer(data: updateEncodedData)) + let updateResponse = try await couchDBClient.update( dbName: testsDB, uri: expectedInsertId, - body: .data(updateEncodedData) + body: body ) XCTAssertFalse(updateResponse.rev.isEmpty) @@ -197,14 +211,20 @@ final class CouchDBClientTests: XCTestCase { XCTAssertNotEqual(updateResponse.rev, expectedInsertRev) XCTAssertEqual(updateResponse.id, expectedInsertId) - var getResponse = try await couchDBClient.get( - dbName: testsDB, + let getResponse = try await couchDBClient.get( + fromDB: testsDB, uri: expectedInsertId ) XCTAssertNotNil(getResponse.body) - let bytes = getResponse.body!.readBytes(length: getResponse.body!.readableBytes)! - testDoc = try JSONDecoder().decode(ExpectedDoc.self, from: Data(bytes)) + let expectedBytes = getResponse.headers.first(name: "content-length").flatMap(Int.init) ?? 1024 * 1024 * 10 + var bytes = try await getResponse.body.collect(upTo: expectedBytes) + let data = bytes.readData(length: bytes.readableBytes) + + testDoc = try JSONDecoder().decode( + ExpectedDoc.self, + from: data! + ) XCTAssertEqual(expectedName, testDoc.name) } catch let error { @@ -249,23 +269,36 @@ final class CouchDBClientTests: XCTestCase { let insertEncodedData = try JSONEncoder().encode(testDoc) let insertResponse = try await couchDBClient.insert( dbName: testsDB, - body: .data(insertEncodedData) + body: .bytes(ByteBuffer(data: insertEncodedData)) ) + let selector = ["selector": ["name": "Greg"]] let bodyData = try JSONEncoder().encode(selector) - var findResponse = try await couchDBClient.find(in: testsDB, body: .data(bodyData)) + let requestBody: HTTPClientRequest.Body = .bytes(ByteBuffer(data: bodyData)) + + let findResponse = try await couchDBClient.find( + inDB: testsDB, + body: requestBody + ) + + let body = findResponse.body + let expectedBytes = findResponse.headers.first(name: "content-length").flatMap(Int.init) + var bytes = try await body.collect(upTo: expectedBytes ?? 1024 * 1024 * 10) + + guard let data = bytes.readData(length: bytes.readableBytes) else { + throw CouchDBClientError.noData + } - let bytes = findResponse.body!.readBytes(length: findResponse.body!.readableBytes)! - let decodedResponse = try JSONDecoder().decode(CouchDBFindResponse.self, from: Data(bytes)) + let decodedResponse = try JSONDecoder().decode(CouchDBFindResponse.self, from: data) XCTAssertTrue(decodedResponse.docs.count > 0) - XCTAssertEqual(decodedResponse.docs.first!._id, insertResponse.id) + XCTAssertTrue(decodedResponse.docs.contains(where: { $0._id == insertResponse.id })) _ = try await couchDBClient.delete( fromDb: testsDB, - uri: decodedResponse.docs.first!._id!, - rev: decodedResponse.docs.first!._rev! + uri: insertResponse.id, + rev: insertResponse.rev ) } catch { XCTFail(error.localizedDescription) @@ -278,11 +311,11 @@ final class CouchDBClientTests: XCTestCase { let insertEncodedData = try JSONEncoder().encode(testDoc) let insertResponse = try await couchDBClient.insert( dbName: testsDB, - body: .data(insertEncodedData) + body: .bytes(ByteBuffer(data: insertEncodedData)) ) let selector = ["selector": ["name": "Sam"]] - let docs: [ExpectedDoc] = try await couchDBClient.find(in: testsDB, selector: selector) + let docs: [ExpectedDoc] = try await couchDBClient.find(inDB: testsDB, selector: selector) XCTAssertTrue(docs.count > 0) XCTAssertEqual(docs.first!._id, insertResponse.id)