diff --git a/Package.swift b/Package.swift index 8d3f75b..ae9e2e6 100644 --- a/Package.swift +++ b/Package.swift @@ -1,30 +1,32 @@ -// swift-tools-version:5.3 -// The swift-tools-version declares the minimum version of Swift required to build this package. - +// swift-tools-version:6.0 import PackageDescription let package = Package( - name: "soto-elasticsearch-nio-client", + name: "soto-elasticsearch", platforms: [ - .macOS(.v10_15) + .macOS(.v13) ], products: [ - .library(name: "SotoElasticsearchNIOClient", targets: ["SotoElasticsearchNIOClient"]) + .library( + name: "SotoElasticsearch", + targets: ["SotoElasticsearch"] + ) ], dependencies: [ - .package(url: "https://github.com/brokenhandsio/elasticsearch-nio-client.git", from: "0.4.0"), - .package(url: "https://github.com/soto-project/soto.git", from: "5.0.0"), + .package( + url: "https://github.com/brokenhandsio/elasticsearch-nio-client.git", branch: "swift-6"), + .package(url: "https://github.com/soto-project/soto.git", from: "7.0.0"), ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( - name: "SotoElasticsearchNIOClient", dependencies: [ - .product(name: "ElasticsearchNIOClient", package: "elasticsearch-nio-client"), + name: "SotoElasticsearch", + dependencies: [ + .product(name: "Elasticsearch", package: "elasticsearch-nio-client"), .product(name: "SotoElasticsearchService", package: "soto"), - ]), + ]), .testTarget( - name: "SotoElasticsearchNIOClientTests", - dependencies: ["SotoElasticsearchNIOClient"]), + name: "SotoElasticsearchTests", + dependencies: ["SotoElasticsearch"] + ), ] ) diff --git a/Sources/SotoElasticsearchNIOClient/.swift-format b/Sources/SotoElasticsearchNIOClient/.swift-format new file mode 100644 index 0000000..d24dda1 --- /dev/null +++ b/Sources/SotoElasticsearchNIOClient/.swift-format @@ -0,0 +1,70 @@ +{ + "fileScopedDeclarationPrivacy": { + "accessLevel": "private" + }, + "indentation": { + "spaces": 4 + }, + "indentConditionalCompilationBlocks": true, + "indentSwitchCaseLabels": false, + "lineBreakAroundMultilineExpressionChainComponents": false, + "lineBreakBeforeControlFlowKeywords": false, + "lineBreakBeforeEachArgument": false, + "lineBreakBeforeEachGenericRequirement": false, + "lineLength": 140, + "maximumBlankLines": 1, + "multiElementCollectionTrailingCommas": true, + "noAssignmentInExpressions": { + "allowedFunctions": [ + "XCTAssertNoThrow" + ] + }, + "prioritizeKeepingFunctionOutputTogether": false, + "respectsExistingLineBreaks": true, + "rules": { + "AllPublicDeclarationsHaveDocumentation": false, + "AlwaysUseLiteralForEmptyCollectionInit": false, + "AlwaysUseLowerCamelCase": true, + "AmbiguousTrailingClosureOverload": true, + "BeginDocumentationCommentWithOneLineSummary": false, + "DoNotUseSemicolons": true, + "DontRepeatTypeInStaticProperties": true, + "FileScopedDeclarationPrivacy": true, + "FullyIndirectEnum": true, + "GroupNumericLiterals": true, + "IdentifiersMustBeASCII": true, + "NeverForceUnwrap": false, + "NeverUseForceTry": false, + "NeverUseImplicitlyUnwrappedOptionals": false, + "NoAccessLevelOnExtensionDeclaration": true, + "NoAssignmentInExpressions": true, + "NoBlockComments": true, + "NoCasesWithOnlyFallthrough": true, + "NoEmptyTrailingClosureParentheses": true, + "NoLabelsInCasePatterns": true, + "NoLeadingUnderscores": false, + "NoParensAroundConditions": true, + "NoPlaygroundLiterals": true, + "NoVoidReturnOnFunctionSignature": true, + "OmitExplicitReturns": false, + "OneCasePerLine": true, + "OneVariableDeclarationPerLine": true, + "OnlyOneTrailingClosureArgument": true, + "OrderedImports": true, + "ReplaceForEachWithForLoop": true, + "ReturnVoidInsteadOfEmptyTuple": true, + "TypeNamesShouldBeCapitalized": true, + "UseEarlyExits": false, + "UseExplicitNilCheckInConditions": true, + "UseLetInEveryBoundCaseVariable": true, + "UseShorthandTypeNames": true, + "UseSingleLinePropertyGetter": true, + "UseSynthesizedInitializer": true, + "UseTripleSlashForDocumentationComments": true, + "UseWhereClausesInForLoops": false, + "ValidateDocumentationComments": false + }, + "spacesAroundRangeFormationOperators": false, + "tabWidth": 4, + "version": 1 +} diff --git a/Sources/SotoElasticsearchNIOClient/SotoElasticsearchClient.swift b/Sources/SotoElasticsearchNIOClient/SotoElasticsearchClient.swift new file mode 100644 index 0000000..e5117cd --- /dev/null +++ b/Sources/SotoElasticsearchNIOClient/SotoElasticsearchClient.swift @@ -0,0 +1,126 @@ +import AsyncHTTPClient +@_exported import Elasticsearch +import Foundation +import SotoElasticsearchService + +public struct SotoElasticsearchClient { + public let elasticSearchClient: ElasticsearchClient + + public init( + awsClient: AWSClient, + region: Region? = nil, + logger: Logger, + httpClient: HTTPClient, + scheme: String = "http", + host: String, + port: Int? = 9200, + username: String? = nil, + password: String? = nil, + jsonEncoder: JSONEncoder = JSONEncoder(), + jsonDecoder: JSONDecoder = JSONDecoder() + ) throws { + let requester = SotoElasticsearchRequester( + awsClient: awsClient, + region: region, + logger: logger, + client: httpClient + ) + self.elasticSearchClient = try ElasticsearchClient( + requester: requester, + logger: logger, + scheme: scheme, + host: host, + port: port, + username: username, + password: password, + jsonEncoder: jsonEncoder, + jsonDecoder: jsonDecoder + ) + } + + public func get(id: String, from indexName: String) async throws -> ESGetSingleDocumentResponse { + try await self.elasticSearchClient.get(id: id, from: indexName) + } + + public func bulk(_ operations: [ESBulkOperation]) async throws -> ESBulkResponse { + try await self.elasticSearchClient.bulk(operations) + } + + public func createDocument( + _ document: Document, in indexName: String + ) async throws -> ESCreateDocumentResponse { + try await self.elasticSearchClient.createDocument(document, in: indexName) + } + + public func createDocumentWithID( + _ document: Document, in indexName: String + ) async throws -> ESCreateDocumentResponse { + try await self.elasticSearchClient.createDocumentWithID(document, in: indexName) + } + + public func updateDocument( + _ document: Document, id: ID, in indexName: String + ) async throws -> ESUpdateDocumentResponse { + try await self.elasticSearchClient.updateDocument(document, id: id, in: indexName) + } + + public func updateDocument( + _ document: Document, in indexName: String + ) async throws -> ESUpdateDocumentResponse { + try await self.elasticSearchClient.updateDocument(document, in: indexName) + } + + public func updateDocumentWithScript( + _ script: Script, id: ID, in indexName: String + ) async throws -> ESUpdateDocumentResponse { + try await self.elasticSearchClient.updateDocumentWithScript(script, id: id, in: indexName) + } + + public func deleteDocument(id: ID, from indexName: String) async throws -> ESDeleteDocumentResponse { + try await self.elasticSearchClient.deleteDocument(id: id, from: indexName) + } + + public func searchDocuments( + from indexName: String, searchTerm: String, type: Document.Type = Document.self + ) async throws -> ESGetMultipleDocumentsResponse { + try await self.elasticSearchClient.searchDocuments(from: indexName, searchTerm: searchTerm, type: type) + } + + public func searchDocumentsCount(from indexName: String, searchTerm: String?) async throws -> ESCountResponse { + try await self.elasticSearchClient.searchDocumentsCount(from: indexName, searchTerm: searchTerm) + } + + public func searchDocumentsPaginated( + from indexName: String, searchTerm: String, size: Int = 10, offset: Int = 0, + type: Document.Type = Document.self + ) async throws -> ESGetMultipleDocumentsResponse { + try await self.elasticSearchClient.searchDocumentsPaginated( + from: indexName, searchTerm: searchTerm, size: size, offset: offset, type: type) + } + + public func searchDocumentsCount(from indexName: String, query: Query) async throws -> ESCountResponse { + try await self.elasticSearchClient.searchDocumentsCount(from: indexName, query: query) + } + + public func searchDocumentsPaginated( + from indexName: String, queryBody: QueryBody, size: Int = 10, offset: Int = 0, + type: Document.Type = Document.self + ) async throws -> ESGetMultipleDocumentsResponse { + try await self.elasticSearchClient.searchDocumentsPaginated( + from: indexName, queryBody: queryBody, size: size, offset: offset, type: type) + } + + public func customSearch( + from indexName: String, query: Query, type: Document.Type = Document.self + ) async throws -> ESGetMultipleDocumentsResponse { + try await self.elasticSearchClient.customSearch(from: indexName, query: query, type: type) + } + + public func deleteIndex(_ name: String) async throws -> ESAcknowledgedResponse { + try await self.elasticSearchClient.deleteIndex(name) + } + + public func checkIndexExists(_ name: String) async throws -> Bool { + try await self.elasticSearchClient.checkIndexExists(name) + } +} diff --git a/Sources/SotoElasticsearchNIOClient/SotoElasticsearchNIOClient.swift b/Sources/SotoElasticsearchNIOClient/SotoElasticsearchNIOClient.swift deleted file mode 100644 index d1a1dd7..0000000 --- a/Sources/SotoElasticsearchNIOClient/SotoElasticsearchNIOClient.swift +++ /dev/null @@ -1,74 +0,0 @@ -@_exported import ElasticsearchNIOClient -import SotoElasticsearchService -import AsyncHTTPClient -import Foundation - -public struct SotoElasticsearchClient { - public let elasticSearchClient: ElasticsearchClient - - public init(awsClient: AWSClient, region: Region? = nil, eventLoop: EventLoop, logger: Logger, httpClient: HTTPClient, scheme: String = "http", host: String, port: Int? = 9200, username: String? = nil, password: String? = nil, jsonEncoder: JSONEncoder = JSONEncoder(), jsonDecoder: JSONDecoder = JSONDecoder()) { - let requester = SotoElasticsearchRequester(awsClient: awsClient, region: region, eventLoop: eventLoop, logger: logger, client: httpClient) - self.elasticSearchClient = ElasticsearchClient(requester: requester, eventLoop: eventLoop, logger: logger, scheme: scheme, host: host, port: port, username: username, password: password, jsonEncoder: jsonEncoder, jsonDecoder: jsonDecoder) - } - - public func get(id: String, from indexName: String) -> EventLoopFuture> { - self.elasticSearchClient.get(id: id, from: indexName) - } - - public func bulk(_ operations: [ESBulkOperation]) -> EventLoopFuture { - self.elasticSearchClient.bulk(operations) - } - - public func createDocument(_ document: Document, in indexName: String) -> EventLoopFuture { - self.elasticSearchClient.createDocument(document, in: indexName) - } - - public func createDocumentWithID(_ document: Document, in indexName: String) -> EventLoopFuture { - self.elasticSearchClient.createDocumentWithID(document, in: indexName) - } - - public func updateDocument(_ document: Document, id: String, in indexName: String) -> EventLoopFuture { - self.elasticSearchClient.updateDocument(document, id: id, in: indexName) - } - - public func updateDocumentWithScript(_ script: Script, id: String, in indexName: String) -> EventLoopFuture { - self.elasticSearchClient.updateDocumentWithScript(script, id: id, in: indexName) - } - - public func deleteDocument(id: String, from indexName: String) -> EventLoopFuture { - self.elasticSearchClient.deleteDocument(id: id, from: indexName) - } - - public func searchDocuments(from indexName: String, searchTerm: String, type: Document.Type = Document.self) -> EventLoopFuture> { - self.elasticSearchClient.searchDocuments(from: indexName, searchTerm: searchTerm, type: type) - } - - public func searchDocumentsCount(from indexName: String, searchTerm: String?) -> EventLoopFuture { - self.elasticSearchClient.searchDocumentsCount(from: indexName, searchTerm: searchTerm) - } - - public func searchDocumentsPaginated(from indexName: String, searchTerm: String, size: Int = 10, offset: Int = 0, type: Document.Type = Document.self) -> EventLoopFuture> { - self.elasticSearchClient.searchDocumentsPaginated(from: indexName, searchTerm: searchTerm, size: size, offset: offset, type: type) - } - - public func searchDocumentsCount(from indexName: String, query: Query) -> EventLoopFuture { - self.elasticSearchClient.searchDocumentsCount(from: indexName, query: query) - } - - - public func searchDocumentsPaginated(from indexName: String, queryBody: QueryBody, size: Int = 10, offset: Int = 0, type: Document.Type = Document.self) -> EventLoopFuture> { - self.elasticSearchClient.searchDocumentsPaginated(from: indexName, queryBody: queryBody, size: size, offset: offset, type: type) - } - - public func customSearch(from indexName: String, query: Query, type: Document.Type = Document.self) -> EventLoopFuture> { - self.elasticSearchClient.customSearch(from: indexName, query: query, type: type) - } - - public func deleteIndex(_ name: String) -> EventLoopFuture { - self.elasticSearchClient.deleteIndex(name) - } - - public func checkIndexExists(_ name: String) -> EventLoopFuture { - self.elasticSearchClient.checkIndexExists(name) - } -} diff --git a/Sources/SotoElasticsearchNIOClient/SotoElasticsearchRequester.swift b/Sources/SotoElasticsearchNIOClient/SotoElasticsearchRequester.swift index 7ddffb6..f72d548 100644 --- a/Sources/SotoElasticsearchNIOClient/SotoElasticsearchRequester.swift +++ b/Sources/SotoElasticsearchNIOClient/SotoElasticsearchRequester.swift @@ -1,41 +1,51 @@ -import ElasticsearchNIOClient -import SotoElasticsearchService import AsyncHTTPClient +import Elasticsearch import Foundation +import HTTPTypes import Logging +import SotoCore +import SotoElasticsearchService struct SotoElasticsearchRequester: ElasticsearchRequester { let awsClient: AWSClient let region: Region? - let eventLoop: EventLoop let logger: Logger let client: HTTPClient - func executeRequest(url urlString: String, method: HTTPMethod, headers: HTTPHeaders, body: ByteBuffer?) -> EventLoopFuture { + func executeRequest( + url urlString: String, + method: HTTPRequest.Method, + headers: HTTPFields, + body: HTTPClientRequest.Body? + ) async throws -> HTTPClientResponse { let es = ElasticsearchService(client: awsClient, region: self.region) guard let url = URL(string: urlString) else { - return self.eventLoop.makeFailedFuture(ElasticSearchClientError(message: "Failed to convert \(urlString) to a URL", status: nil)) - } - let awsBody: AWSPayload - if let body = body { - awsBody = AWSPayload.byteBuffer(body) - } else { - awsBody = .empty + throw ElasticsearchClientError(message: "Failed to convert \(urlString) to a URL", status: nil) } - return es.signHeaders(url: url, httpMethod: method, headers: headers, body: awsBody).flatMap { headers in - let request: HTTPClient.Request - do { - request = try HTTPClient.Request(url: url, method: method, headers: headers, body: awsBody.asByteBuffer().map { .byteBuffer($0) } - ) - } catch { - return self.eventLoop.makeFailedFuture(error) - } - self.logger.trace("Request: \(request)") - if let requestBody = body { - let bodyString = String(buffer: requestBody) - self.logger.trace("Request body: \(bodyString)") + + let awsBody = + if let body = body { + AWSHTTPBody(asyncSequence: body, length: nil) + } else { + AWSHTTPBody() } - return self.client.execute(request: request, eventLoop: HTTPClient.EventLoopPreference.delegateAndChannel(on: self.eventLoop), logger: self.logger) + + let httpMethod = HTTPMethod(rawValue: method.rawValue) + + var httpHeaders = HTTPHeaders() + for header in headers { + httpHeaders.add(name: header.name.canonicalName, value: header.value) } + + let headers = try await es.signHeaders(url: url, httpMethod: httpMethod, headers: httpHeaders, body: awsBody) + + var request = HTTPClientRequest(url: urlString) + request.method = httpMethod + request.headers = headers + request.body = body + + self.logger.trace("Request: \(request)") + + return try await self.client.execute(request, timeout: .seconds(30)) } } diff --git a/Tests/SotoElasticsearchNIOClientTests/ElasticsearchNIOClientTests.swift b/Tests/SotoElasticsearchNIOClientTests/ElasticsearchNIOClientTests.swift deleted file mode 100644 index aad657f..0000000 --- a/Tests/SotoElasticsearchNIOClientTests/ElasticsearchNIOClientTests.swift +++ /dev/null @@ -1,219 +0,0 @@ -import XCTest -import SotoElasticsearchNIOClient -import NIO -import AsyncHTTPClient -import Logging -import SotoElasticsearchService - -class ElasticSearchIntegrationTests: XCTestCase { - - // MARK: - Properties - var eventLoopGroup: MultiThreadedEventLoopGroup! - var client: SotoElasticsearchClient! - var httpClient: HTTPClient! - var awsClient: AWSClient! - let indexName = "some-index" - - // MARK: - Overrides - override func setUpWithError() throws { - eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - let logger = Logger(label: "io.brokenhands.swift-soto-elasticsearch.test") - httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup)) - awsClient = AWSClient(credentialProvider: .static(accessKeyId: "SOMETHING", secretAccessKey: "SOMETHINGLESE"), httpClientProvider: .shared(httpClient), logger: logger) - client = SotoElasticsearchClient(awsClient: awsClient, eventLoop: eventLoopGroup.next(), logger: logger, httpClient: httpClient, scheme: "http", host: "localhost", port: 9200) - _ = try client.deleteIndex("_all").wait() - } - - override func tearDownWithError() throws { - try awsClient.syncShutdown() - try httpClient.syncShutdown() - try eventLoopGroup.syncShutdownGracefully() - } - - // MARK: - Tests - func testSearchingItems() throws { - try setupItems() - - let results: ESGetMultipleDocumentsResponse = try client.searchDocuments(from: indexName, searchTerm: "Apples").wait() - XCTAssertEqual(results.hits.hits.count, 5) - } - - func testSearchingItemsWithTypeProvided() throws { - try setupItems() - - let results = try client.searchDocuments(from: indexName, searchTerm: "Apples", type: SomeItem.self).wait() - XCTAssertEqual(results.hits.hits.count, 5) - } - - func testSearchItemsCount() throws { - try setupItems() - - let results = try client.searchDocumentsCount(from: indexName, searchTerm: "Apples").wait() - XCTAssertEqual(results.count, 5) - } - - func testCreateDocument() throws { - let item = SomeItem(id: UUID(), name: "Banana") - let response = try client.createDocument(item, in: self.indexName).wait() - XCTAssertNotEqual(item.id.uuidString, response.id) - XCTAssertEqual(response.index, self.indexName) - XCTAssertEqual(response.result, "created") - } - - func testCreateDocumentWithID() throws { - let item = SomeItem(id: UUID(), name: "Banana") - let response = try client.createDocumentWithID(item, in: self.indexName).wait() - XCTAssertEqual(item.id.uuidString, response.id) - XCTAssertEqual(response.index, self.indexName) - XCTAssertEqual(response.result, "created") - } - - func testUpdatingDocument() throws { - let item = SomeItem(id: UUID(), name: "Banana") - _ = try client.createDocumentWithID(item, in: self.indexName).wait() - Thread.sleep(forTimeInterval: 0.5) - let updatedItem = SomeItem(id: item.id, name: "Bananas") - let response = try client.updateDocument(updatedItem, id: item.id.uuidString, in: self.indexName).wait() - XCTAssertEqual(response.result, "updated") - } - - func testDeletingDocument() throws { - try setupItems() - let item = SomeItem(id: UUID(), name: "Banana") - _ = try client.createDocumentWithID(item, in: self.indexName).wait() - Thread.sleep(forTimeInterval: 1.0) - - let results = try client.searchDocumentsCount(from: indexName, searchTerm: "Banana").wait() - XCTAssertEqual(results.count, 1) - Thread.sleep(forTimeInterval: 0.5) - - let response = try client.deleteDocument(id: item.id.uuidString, from: self.indexName).wait() - XCTAssertEqual(response.result, "deleted") - Thread.sleep(forTimeInterval: 0.5) - - let updatedResults = try client.searchDocumentsCount(from: indexName, searchTerm: "Banana").wait() - XCTAssertEqual(updatedResults.count, 0) - } - - func testIndexExists() throws { - let item = SomeItem(id: UUID(), name: "Banana") - let response = try client.createDocument(item, in: self.indexName).wait() - XCTAssertEqual(response.index, self.indexName) - XCTAssertEqual(response.result, "created") - Thread.sleep(forTimeInterval: 0.5) - - let exists = try client.checkIndexExists(self.indexName).wait() - XCTAssertTrue(exists) - - let notExists = try client.checkIndexExists("some-random-index").wait() - XCTAssertFalse(notExists) - } - - func testDeleteIndex() throws { - let item = SomeItem(id: UUID(), name: "Banana") - _ = try client.createDocument(item, in: self.indexName).wait() - Thread.sleep(forTimeInterval: 0.5) - - let exists = try client.checkIndexExists(self.indexName).wait() - XCTAssertTrue(exists) - - let response = try client.deleteIndex(self.indexName).wait() - XCTAssertEqual(response.acknowledged, true) - - let notExists = try client.checkIndexExists(self.indexName).wait() - XCTAssertFalse(notExists) - } - - func testBulkCreate() throws { - var items = [SomeItem]() - for index in 1...10 { - let name: String - if index % 2 == 0 { - name = "Some \(index) Apples" - } else { - name = "Some \(index) Bananas" - } - let item = SomeItem(id: UUID(), name: name) - items.append(item) - } - - let itemsWithIndex = items.map { ESBulkOperation(operationType: .create, index: self.indexName, id: $0.id.uuidString, document: $0) } - let response = try client.bulk(itemsWithIndex).wait() - XCTAssertEqual(response.errors, false) - XCTAssertEqual(response.items.count, 10) - XCTAssertEqual(response.items.first?.create?.result, "created") - Thread.sleep(forTimeInterval: 1.0) - - let results = try client.searchDocumentsCount(from: indexName, searchTerm: nil).wait() - XCTAssertEqual(results.count, 10) - } - - func testBulkCreateUpdateDeleteIndex() throws { - let item1 = SomeItem(id: UUID(), name: "Item 1") - let item2 = SomeItem(id: UUID(), name: "Item 2") - let item3 = SomeItem(id: UUID(), name: "Item 3") - let item4 = SomeItem(id: UUID(), name: "Item 4") - let bulkOperation = [ - ESBulkOperation(operationType: .create, index: self.indexName, id: item1.id.uuidString, document: item1), - ESBulkOperation(operationType: .index, index: self.indexName, id: item2.id.uuidString, document: item2), - ESBulkOperation(operationType: .update, index: self.indexName, id: item3.id.uuidString, document: item3), - ESBulkOperation(operationType: .delete, index: self.indexName, id: item4.id.uuidString, document: item4), - ] - - let response = try client.bulk(bulkOperation).wait() - XCTAssertEqual(response.items.count, 4) - XCTAssertNotNil(response.items[0].create) - XCTAssertNotNil(response.items[1].index) - XCTAssertNotNil(response.items[2].update) - XCTAssertNotNil(response.items[3].delete) - } - - func testSearchingItemsPaginated() throws { - for index in 1...100 { - let name = "Some \(index) Apples" - let item = SomeItem(id: UUID(), name: name) - _ = try client.createDocument(item, in: self.indexName).wait() - } - - // This is required for ES to settle and load the indexes to return the right results - Thread.sleep(forTimeInterval: 1.0) - - let results: ESGetMultipleDocumentsResponse = try client.searchDocumentsPaginated(from: indexName, searchTerm: "Apples", size: 20, offset: 10).wait() - XCTAssertEqual(results.hits.hits.count, 20) - XCTAssertTrue(results.hits.hits.contains(where: { $0.source.name == "Some 11 Apples" })) - XCTAssertTrue(results.hits.hits.contains(where: { $0.source.name == "Some 29 Apples" })) - } - - func testSearchingItemsWithTypeProvidedPaginated() throws { - for index in 1...100 { - let name = "Some \(index) Apples" - let item = SomeItem(id: UUID(), name: name) - _ = try client.createDocument(item, in: self.indexName).wait() - } - - // This is required for ES to settle and load the indexes to return the right results - Thread.sleep(forTimeInterval: 1.0) - - let results = try client.searchDocumentsPaginated(from: indexName, searchTerm: "Apples", size: 20, offset: 10, type: SomeItem.self).wait() - XCTAssertEqual(results.hits.hits.count, 20) - XCTAssertTrue(results.hits.hits.contains(where: { $0.source.name == "Some 11 Apples" })) - XCTAssertTrue(results.hits.hits.contains(where: { $0.source.name == "Some 29 Apples" })) - } - - // MARK: - Private - private func setupItems() throws { - for index in 1...10 { - let name: String - if index % 2 == 0 { - name = "Some \(index) Apples" - } else { - name = "Some \(index) Bananas" - } - let item = SomeItem(id: UUID(), name: name) - _ = try client.createDocument(item, in: self.indexName).wait() - } - - // This is required for ES to settle and load the indexes to return the right results - Thread.sleep(forTimeInterval: 1.0) - } -} diff --git a/Tests/SotoElasticsearchNIOClientTests/SomeItem.swift b/Tests/SotoElasticsearchTests/SomeItem.swift similarity index 100% rename from Tests/SotoElasticsearchNIOClientTests/SomeItem.swift rename to Tests/SotoElasticsearchTests/SomeItem.swift diff --git a/Tests/SotoElasticsearchTests/SotoElasticsearchTests.swift b/Tests/SotoElasticsearchTests/SotoElasticsearchTests.swift new file mode 100644 index 0000000..50ef42e --- /dev/null +++ b/Tests/SotoElasticsearchTests/SotoElasticsearchTests.swift @@ -0,0 +1,254 @@ +import AsyncHTTPClient +import Foundation +import Logging +import SotoElasticsearch +import SotoElasticsearchService +import Testing + +@Suite(.serialized) +class SotoElasticSearchIntegrationTests { + var client: SotoElasticsearchClient! + var awsClient: AWSClient! + let indexName = "some-index" + let logger = Logger(label: "io.brokenhands.swift-soto-elasticsearch.test") + + init() async throws { + var logger = Logger(label: "io.brokenhands.swift-soto-elasticsearch.test") + logger.logLevel = .trace + awsClient = AWSClient( + credentialProvider: .static(accessKeyId: "SOMETHING", secretAccessKey: "SOMETHINGELSE")) + client = try SotoElasticsearchClient( + awsClient: awsClient, logger: logger, httpClient: .shared, host: "localhost") + + if try await client.checkIndexExists(indexName) { + _ = try await client.deleteIndex(indexName) + } + } + + deinit { + try! awsClient.syncShutdown() + } + + @Test("Search Items") + func testSearchingItems() async throws { + try await setupItems() + + let results: ESGetMultipleDocumentsResponse = try await client.searchDocuments( + from: indexName, searchTerm: "Apples" + ) + #expect(results.hits.hits.count == 5) + } + + @Test("Search Items with Type Provided") + func testSearchingItemsWithTypeProvided() async throws { + try await setupItems() + + let results = try await client.searchDocuments( + from: indexName, searchTerm: "Apples", type: SomeItem.self + ) + #expect(results.hits.hits.count == 5) + } + + @Test("Search Items Count") + func testSearchItemsCount() async throws { + try await setupItems() + + let results = try await client.searchDocumentsCount(from: indexName, searchTerm: "Apples") + #expect(results.count == 5) + } + + @Test("Create Document") + func testCreateDocument() async throws { + let item = SomeItem(id: UUID(), name: "Banana") + let response = try await client.createDocument(item, in: self.indexName) + #expect(item.id.uuidString != response.id) + #expect(response.index == self.indexName) + #expect(response.result == "created") + } + + @Test("Create Document With ID") + func testCreateDocumentWithID() async throws { + let item = SomeItem(id: UUID(), name: "Banana") + let response = try await client.createDocumentWithID(item, in: self.indexName) + #expect(item.id == response.id) + #expect(response.index == self.indexName) + #expect(response.result == "created") + } + + @Test("Update Document") + func testUpdatingDocument() async throws { + let item = SomeItem(id: UUID(), name: "Banana") + _ = try await client.createDocumentWithID(item, in: self.indexName) + try await Task.sleep(for: .seconds(0.5)) + let updatedItem = SomeItem(id: item.id, name: "Bananas") + let response = try await client.updateDocument( + updatedItem, id: item.id.uuidString, in: self.indexName + ) + #expect(response.result == "updated") + } + + @Test("Delete Document") + func testDeletingDocument() async throws { + try await setupItems() + let item = SomeItem(id: UUID(), name: "Banana") + _ = try await client.createDocumentWithID(item, in: self.indexName) + try await Task.sleep(for: .seconds(1)) + + let results = try await client.searchDocumentsCount(from: indexName, searchTerm: "Banana") + #expect(results.count == 1) + try await Task.sleep(for: .seconds(0.5)) + + let response = try await client.deleteDocument(id: item.id.uuidString, from: self.indexName) + + #expect(response.result == "deleted") + try await Task.sleep(for: .seconds(0.5)) + + let updatedResults = try await client.searchDocumentsCount( + from: indexName, searchTerm: "Banana") + + #expect(updatedResults.count == 0) + } + + @Test("Index Exists") + func testIndexExists() async throws { + let item = SomeItem(id: UUID(), name: "Banana") + let response = try await client.createDocument(item, in: self.indexName) + #expect(response.index == self.indexName) + #expect(response.result == "created") + try await Task.sleep(for: .seconds(0.5)) + + let exists = try await client.checkIndexExists(self.indexName) + #expect(exists) + + let notExists = try await client.checkIndexExists("some-random-index") + #expect(notExists == false) + } + + @Test("Delete Index") + func testDeleteIndex() async throws { + let item = SomeItem(id: UUID(), name: "Banana") + _ = try await client.createDocument(item, in: self.indexName) + try await Task.sleep(for: .seconds(0.5)) + + let exists = try await client.checkIndexExists(self.indexName) + #expect(exists) + + let response = try await client.deleteIndex(self.indexName) + #expect(response.acknowledged == true) + + let notExists = try await client.checkIndexExists(self.indexName) + #expect(notExists == false) + } + + @Test("Bulk Create") + func testBulkCreate() async throws { + var items = [SomeItem]() + for index in 1...10 { + let name: String + if index % 2 == 0 { + name = "Some \(index) Apples" + } else { + name = "Some \(index) Bananas" + } + let item = SomeItem(id: UUID(), name: name) + items.append(item) + } + + let itemsWithIndex = items.map { + ESBulkOperation( + operationType: .create, index: self.indexName, id: $0.id.uuidString, document: $0) + } + let response = try await client.bulk(itemsWithIndex) + #expect(response.errors == false) + #expect(response.items.count == 10) + #expect(response.items.first?.create?.result == "created") + try await Task.sleep(for: .seconds(1)) + + let results = try await client.searchDocumentsCount(from: indexName, searchTerm: nil) + #expect(results.count == 10) + } + + @Test("Bulk Create/Update/Delete Index") + func testBulkCreateUpdateDeleteIndex() async throws { + let item1 = SomeItem(id: UUID(), name: "Item 1") + let item2 = SomeItem(id: UUID(), name: "Item 2") + let item3 = SomeItem(id: UUID(), name: "Item 3") + let item4 = SomeItem(id: UUID(), name: "Item 4") + let bulkOperation = [ + ESBulkOperation( + operationType: .create, index: self.indexName, id: item1.id.uuidString, + document: item1), + ESBulkOperation( + operationType: .index, index: self.indexName, id: item2.id.uuidString, + document: item2), + ESBulkOperation( + operationType: .update, index: self.indexName, id: item3.id.uuidString, + document: item3), + ESBulkOperation( + operationType: .delete, index: self.indexName, id: item4.id.uuidString, + document: item4), + ] + + let response = try await client.bulk(bulkOperation) + #expect(response.items.count == 4) + #expect(response.items[0].create != nil) + #expect(response.items[1].index != nil) + #expect(response.items[2].update != nil) + #expect(response.items[3].delete != nil) + } + + @Test("Search Items Paginated") + func testSearchingItemsPaginated() async throws { + for index in 1...100 { + let name = "Some \(index) Apples" + let item = SomeItem(id: UUID(), name: name) + _ = try await client.createDocument(item, in: self.indexName) + } + + // This is required for ES to settle and load the indexes to return the right results + try await Task.sleep(for: .seconds(1)) + + let results: ESGetMultipleDocumentsResponse = + try await client.searchDocumentsPaginated( + from: indexName, searchTerm: "Apples", size: 20, offset: 10 + ) + #expect(results.hits.hits.count == 20) + #expect(results.hits.hits.contains(where: { $0.source.name == "Some 11 Apples" })) + #expect(results.hits.hits.contains(where: { $0.source.name == "Some 29 Apples" })) + } + + @Test("Search Items With Type Provided Paginated") + func testSearchingItemsWithTypeProvidedPaginated() async throws { + for index in 1...100 { + let name = "Some \(index) Apples" + let item = SomeItem(id: UUID(), name: name) + _ = try await client.createDocument(item, in: self.indexName) + } + + // This is required for ES to settle and load the indexes to return the right results + try await Task.sleep(for: .seconds(1)) + + let results = try await client.searchDocumentsPaginated( + from: indexName, searchTerm: "Apples", size: 20, offset: 10, type: SomeItem.self + ) + #expect(results.hits.hits.count == 20) + #expect(results.hits.hits.contains(where: { $0.source.name == "Some 11 Apples" })) + #expect(results.hits.hits.contains(where: { $0.source.name == "Some 29 Apples" })) + } + + private func setupItems() async throws { + for index in 1...10 { + let name: String + if index % 2 == 0 { + name = "Some \(index) Apples" + } else { + name = "Some \(index) Bananas" + } + let item = SomeItem(id: UUID(), name: name) + _ = try await client.createDocument(item, in: self.indexName) + } + + // This is required for ES to settle and load the indexes to return the right results + try await Task.sleep(for: .seconds(1)) + } +} diff --git a/scripts/startLocalDockerESTest.swift b/scripts/startLocalDockerESTest.swift index e209dd7..4d15ed3 100755 --- a/scripts/startLocalDockerESTest.swift +++ b/scripts/startLocalDockerESTest.swift @@ -11,13 +11,13 @@ func shell(_ args: String..., returnStdOut: Bool = false) -> (Int32, Pipe) { let task = Process() task.launchPath = "/usr/bin/env" task.arguments = args - let pipe = Pipe() - if returnStdOut { - task.standardOutput = pipe - } - task.launch() - task.waitUntilExit() - return (task.terminationStatus, pipe) + let pipe = Pipe() + if returnStdOut { + task.standardOutput = pipe + } + task.launch() + task.waitUntilExit() + return (task.terminationStatus, pipe) } extension Pipe { @@ -33,7 +33,10 @@ extension Pipe { } } -let (dockerResult, _) = shell("docker", "run", "--name", containerName, "-p", "\(port):9200", "-e", "discovery.type=single-node", "-e", "ES_JAVA_OPTS=-Xms256m -Xmx256m", "-d", "docker.elastic.co/elasticsearch/elasticsearch:7.6.2") +let (dockerResult, _) = shell( + "docker", "run", "--name", containerName, "-p", "\(port):9200", "-e", + "discovery.type=single-node", "-e", "ES_JAVA_OPTS=-Xms256m -Xmx256m", "-d", + "docker.elastic.co/elasticsearch/elasticsearch:7.6.2") guard dockerResult == 0 else { print("❌ ERROR: Failed to create the Elasticsearch instance")