From 1018ea42e35a9eb6fbf9e7e662c3b3312c8a3133 Mon Sep 17 00:00:00 2001 From: Hayden Date: Tue, 6 Aug 2024 21:01:31 +0700 Subject: [PATCH 1/5] feat: presets --- Sources/Typesense/Client.swift | 24 ++++-- .../Typesense/Models/PresetDeleteSchema.swift | 21 +++++ Sources/Typesense/Models/PresetSchema.swift | 15 ++++ .../Typesense/Models/PresetUpsertSchema.swift | 14 ++++ Sources/Typesense/Models/PresetValue.swift | 28 +++++++ .../Models/PresetsRetrieveSchema.swift | 14 ++++ Sources/Typesense/Override.swift | 4 +- Sources/Typesense/Preset.swift | 39 +++++++++ Sources/Typesense/Presets.swift | 44 ++++++++++ Tests/TypesenseTests/PresetTests.swift | 49 +++++++++++ Tests/TypesenseTests/PresetsTests.swift | 83 +++++++++++++++++++ Tests/TypesenseTests/UtilsTests.swift | 28 +++++++ 12 files changed, 353 insertions(+), 10 deletions(-) create mode 100644 Sources/Typesense/Models/PresetDeleteSchema.swift create mode 100644 Sources/Typesense/Models/PresetSchema.swift create mode 100644 Sources/Typesense/Models/PresetUpsertSchema.swift create mode 100644 Sources/Typesense/Models/PresetValue.swift create mode 100644 Sources/Typesense/Models/PresetsRetrieveSchema.swift create mode 100644 Sources/Typesense/Preset.swift create mode 100644 Sources/Typesense/Presets.swift create mode 100644 Tests/TypesenseTests/PresetTests.swift create mode 100644 Tests/TypesenseTests/PresetsTests.swift diff --git a/Sources/Typesense/Client.swift b/Sources/Typesense/Client.swift index 39d9334..a00b7b7 100644 --- a/Sources/Typesense/Client.swift +++ b/Sources/Typesense/Client.swift @@ -1,38 +1,46 @@ import Foundation public struct Client { - + var configuration: Configuration var apiCall: ApiCall public var collections: Collections - + public init(config: Configuration) { self.configuration = config self.apiCall = ApiCall(config: config) self.collections = Collections(config: config) } - + public func collection(name: String) -> Collection { return Collection(config: self.configuration, collectionName: name) } - + public func keys() -> ApiKeys { return ApiKeys(config: self.configuration) } - + public func aliases() -> Alias { return Alias(config: self.configuration) } - + public func operations() -> Operations { return Operations(config: self.configuration) } - + public func multiSearch() -> MultiSearch { return MultiSearch(config: self.configuration) } - + public func analytics() -> Analytics { return Analytics(config: self.configuration) } + + public func presets() -> Presets { + return Presets(apiCall: apiCall) + } + + public func preset(_ presetName: String) -> Preset { + return Preset(apiCall: apiCall, presetName: presetName) + } } diff --git a/Sources/Typesense/Models/PresetDeleteSchema.swift b/Sources/Typesense/Models/PresetDeleteSchema.swift new file mode 100644 index 0000000..377af2c --- /dev/null +++ b/Sources/Typesense/Models/PresetDeleteSchema.swift @@ -0,0 +1,21 @@ +// +// PresetDeleteSchema.swift +// +// Generated by swagger-codegen +// https://github.com/swagger-api/swagger-codegen +// + +import Foundation + + + +public struct PresetDeleteSchema: Codable { + + public var name: String + + public init(name: String) { + self.name = name + } + + +} diff --git a/Sources/Typesense/Models/PresetSchema.swift b/Sources/Typesense/Models/PresetSchema.swift new file mode 100644 index 0000000..2fbfe00 --- /dev/null +++ b/Sources/Typesense/Models/PresetSchema.swift @@ -0,0 +1,15 @@ +import Foundation + + + +public struct PresetSchema: Codable { + public var name: String + public var value: PresetValue + + public init(name: String, value: PresetValue) { + self.name = name + self.value = value + } + + +} diff --git a/Sources/Typesense/Models/PresetUpsertSchema.swift b/Sources/Typesense/Models/PresetUpsertSchema.swift new file mode 100644 index 0000000..02c9e98 --- /dev/null +++ b/Sources/Typesense/Models/PresetUpsertSchema.swift @@ -0,0 +1,14 @@ +import Foundation + + + +public struct PresetUpsertSchema: Codable { + + public var value: PresetValue + + public init(value: PresetValue) { + self.value = value + } + + +} diff --git a/Sources/Typesense/Models/PresetValue.swift b/Sources/Typesense/Models/PresetValue.swift new file mode 100644 index 0000000..96f6dff --- /dev/null +++ b/Sources/Typesense/Models/PresetValue.swift @@ -0,0 +1,28 @@ +public enum PresetValue: Codable { + case multiSearch(MultiSearchSearchesParameter) + case singleCollectionSearch(SearchParameters) + + public init (from decoder: Decoder) throws { + if let multiSearch = try? MultiSearchSearchesParameter(from: decoder) { + self = .multiSearch(multiSearch) + } + else if let singleCollectionSearch = try? SearchParameters(from: decoder) { + self = .singleCollectionSearch(singleCollectionSearch) + } else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Unable to decode value for preset `value`" + ) + ) + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .multiSearch(let multiSearch): + try multiSearch.encode(to: encoder) + case .singleCollectionSearch(let singleCollectionSearch): + try singleCollectionSearch.encode(to: encoder) + } + } +} \ No newline at end of file diff --git a/Sources/Typesense/Models/PresetsRetrieveSchema.swift b/Sources/Typesense/Models/PresetsRetrieveSchema.swift new file mode 100644 index 0000000..e36db85 --- /dev/null +++ b/Sources/Typesense/Models/PresetsRetrieveSchema.swift @@ -0,0 +1,14 @@ +import Foundation + + + +public struct PresetsRetrieveSchema: Codable { + + public var presets: [PresetSchema] + + public init(presets: [PresetSchema]) { + self.presets = presets + } + + +} diff --git a/Sources/Typesense/Override.swift b/Sources/Typesense/Override.swift index ac0a6bf..2d9114c 100644 --- a/Sources/Typesense/Override.swift +++ b/Sources/Typesense/Override.swift @@ -28,8 +28,8 @@ public struct Override { public func delete() async throws -> (SearchOverrideDeleteResponse?, URLResponse?) { let (data, response) = try await apiCall.delete(endPoint: endpointPath()) if let result = data { - let overrides = try decoder.decode(SearchOverrideDeleteResponse.self, from: result) - return (overrides, response) + let decodedData = try decoder.decode(SearchOverrideDeleteResponse.self, from: result) + return (decodedData, response) } return (nil, response) } diff --git a/Sources/Typesense/Preset.swift b/Sources/Typesense/Preset.swift new file mode 100644 index 0000000..9cd107a --- /dev/null +++ b/Sources/Typesense/Preset.swift @@ -0,0 +1,39 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct Preset { + private var apiCall: ApiCall + private var presetName: String + + + init(apiCall: ApiCall, presetName: String) { + self.apiCall = apiCall + self.presetName = presetName + } + + public func retrieve() async throws -> (PresetSchema?, URLResponse?) { + let (data, response) = try await apiCall.get(endPoint: endpointPath()) + if let result = data { + let preset = try decoder.decode(PresetSchema.self, from: result) + return (preset, response) + } + return (nil, response) + } + + public func delete() async throws -> (PresetDeleteSchema?, URLResponse?) { + let (data, response) = try await apiCall.delete(endPoint: endpointPath()) + if let result = data { + let decodedData = try decoder.decode(PresetDeleteSchema.self, from: result) + return (decodedData, response) + } + return (nil, response) + } + + private func endpointPath() -> String { + return "\(Presets.RESOURCEPATH)/\(presetName)" + } + + +} diff --git a/Sources/Typesense/Presets.swift b/Sources/Typesense/Presets.swift new file mode 100644 index 0000000..e4399f2 --- /dev/null +++ b/Sources/Typesense/Presets.swift @@ -0,0 +1,44 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct Presets { + static let RESOURCEPATH = "presets" + private var apiCall: ApiCall + + + init(apiCall: ApiCall) { + self.apiCall = apiCall + } + + public func upsert(presetName: String, params: PresetUpsertSchema) async throws -> (PresetSchema?, URLResponse?) { + let schemaData = try encoder.encode(params) + let (data, response) = try await self.apiCall.put(endPoint: endpointPath(presetName), body: schemaData) + if let result = data { + let decodedData = try decoder.decode(PresetSchema.self, from: result) + return (decodedData, response) + } + return (nil, response) + } + + public func retrieve() async throws -> (PresetsRetrieveSchema?, URLResponse?) { + let (data, response) = try await self.apiCall.get(endPoint: endpointPath()) + if let result = data { + let decodedData = try decoder.decode(PresetsRetrieveSchema.self, from: result) + return (decodedData, response) + } + return (nil, response) + } + + private func endpointPath(_ operation: String? = nil) -> String { + let baseEndpoint = "\(Presets.RESOURCEPATH)" + if let operation = operation { + return "\(baseEndpoint)/\(operation)" + } else { + return baseEndpoint + } + } + + +} diff --git a/Tests/TypesenseTests/PresetTests.swift b/Tests/TypesenseTests/PresetTests.swift new file mode 100644 index 0000000..b6f2c5f --- /dev/null +++ b/Tests/TypesenseTests/PresetTests.swift @@ -0,0 +1,49 @@ +import XCTest +@testable import Typesense + +final class PresetTests: XCTestCase { + override func setUp() async throws { + try? await createSingleCollectionSearchPreset() + } + override func tearDown() async throws { + try! await tearDownPresets() + } + + func testPresetRetrieve() async { + do { + let (result, _) = try await client.preset("test-id").retrieve() + XCTAssertNotNil(result) + guard let validResult = result else { + throw DataError.dataNotFound + } + print(validResult) + XCTAssertEqual("test-id", validResult.name) + switch validResult.value { + case .singleCollectionSearch(let value): + XCTAssertEqual("apple", value.q) + default: + XCTAssertTrue(false) + } + } catch (let error) { + print(error.localizedDescription) + XCTAssertTrue(false) + } + } + + func testPresetDelete() async { + do { + let (result, _) = try await client.preset("test-id").delete() + XCTAssertNotNil(result) + guard let validResult = result else { + throw DataError.dataNotFound + } + print(validResult) + XCTAssertEqual("test-id", validResult.name) + } catch (let error) { + print(error.localizedDescription) + XCTAssertTrue(false) + } + + } + +} diff --git a/Tests/TypesenseTests/PresetsTests.swift b/Tests/TypesenseTests/PresetsTests.swift new file mode 100644 index 0000000..7a79ff8 --- /dev/null +++ b/Tests/TypesenseTests/PresetsTests.swift @@ -0,0 +1,83 @@ +import XCTest +@testable import Typesense + +final class PresetsTests: XCTestCase { + override func tearDown() async throws { + try! await tearDownPresets() + } + + func testPresetsUpsertSearchParameters() async { + let schema = PresetUpsertSchema( + value: PresetValue.singleCollectionSearch(SearchParameters(q: "apple")) + ) + do { + let (result, _) = try await client.presets().upsert(presetName: "test-id", params: schema) + XCTAssertNotNil(result) + guard let validResult = result else { + throw DataError.dataNotFound + } + print(validResult) + XCTAssertEqual("test-id", validResult.name) + switch validResult.value { + case .singleCollectionSearch(let value): + XCTAssertEqual("apple", value.q) + default: + XCTAssertTrue(false) + } + } catch (let error) { + print(error.localizedDescription) + XCTAssertTrue(false) + } + } + + func testPresetsUpsertMultiSearchSearchesParameter() async { + let schema = PresetUpsertSchema( + value: PresetValue.multiSearch(MultiSearchSearchesParameter(searches: [MultiSearchCollectionParameters(q: "apple")])) + ) + do { + let (result, _) = try await client.presets().upsert(presetName: "test-id", params: schema) + XCTAssertNotNil(result) + guard let validResult = result else { + throw DataError.dataNotFound + } + print(validResult) + XCTAssertEqual("test-id", validResult.name) + switch validResult.value { + case .multiSearch(let value): + XCTAssertEqual("apple", value.searches[0].q) + default: + XCTAssertTrue(false) + } + } catch (let error) { + print(error.localizedDescription) + XCTAssertTrue(false) + } + } + + func testPresetsRetrieveAll() async { + try! await createSingleCollectionSearchPreset(); + try! await createMultiSearchPreset(); + do { + let (result, _) = try await client.presets().retrieve() + guard let validResult = result else { + throw DataError.dataNotFound + } + print(validResult) + XCTAssertEqual(2, validResult.presets.count) + for preset in validResult.presets{ + switch preset.value { + case .singleCollectionSearch(let value): + XCTAssertEqual("test-id", preset.name) + XCTAssertEqual("apple", value.q) + case .multiSearch(let value): + XCTAssertEqual("test-id-preset-multi-search", preset.name) + XCTAssertEqual("banana", value.searches[0].q) + } + } + } catch (let error) { + print(error) + XCTAssertTrue(false) + } + } + +} diff --git a/Tests/TypesenseTests/UtilsTests.swift b/Tests/TypesenseTests/UtilsTests.swift index 52b8716..29b2375 100644 --- a/Tests/TypesenseTests/UtilsTests.swift +++ b/Tests/TypesenseTests/UtilsTests.swift @@ -13,6 +13,16 @@ func tearDownCollections() async throws { } } +func tearDownPresets() async throws { + let (presets, _) = try await client.presets().retrieve() + guard let validData = presets else { + throw DataError.dataNotFound + } + for item in validData.presets { + let _ = try! await client.preset(item.name).delete() + } +} + func setUpCollection() async throws{ let schema = CollectionSchema(name: "test-utils-collection", fields: [Field(name: "company_name", type: "string"), Field(name: "num_employees", type: "int32"), Field(name: "country", type: "string", facet: true)], defaultSortingField: "num_employees") let (collResp, _) = try! await client.collections.create(schema: schema) @@ -26,4 +36,22 @@ func createAnOverride() async throws { overrideId: "test-id", params: SearchOverrideSchema(rule: SearchOverrideRule(filterBy: "test"), filterBy: "test:=true", metadata: SearchOverrideExclude(_id: "exclude-id")) ) +} + +func createSingleCollectionSearchPreset() async throws { + let _ = try! await client.presets().upsert( + presetName: "test-id", + params: PresetUpsertSchema( + value: .singleCollectionSearch(SearchParameters(q: "apple")) + ) + ) +} + +func createMultiSearchPreset() async throws { + let _ = try! await client.presets().upsert( + presetName: "test-id-preset-multi-search", + params: PresetUpsertSchema( + value: .multiSearch(MultiSearchSearchesParameter(searches: [MultiSearchCollectionParameters(q: "banana")])) + ) + ) } \ No newline at end of file From 86c04907eefdaf81679b5e9a337da5f01a6b56ac Mon Sep 17 00:00:00 2001 From: Hayden Date: Tue, 6 Aug 2024 21:15:17 +0700 Subject: [PATCH 2/5] update README: add documentation for presets --- README.md | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6fbb503..a552467 100644 --- a/README.md +++ b/README.md @@ -109,13 +109,13 @@ let (data, response) = try await client.collection(name: "books").overrides().up ### Retrieve all overrides ```swift -let (data, response) = try await client.collection(name: "books").overrides().retrieve(metadataType: Never.self ) +let (data, response) = try await client.collection(name: "books").overrides().retrieve(metadataType: Never.self) ``` ### Retrieve an override ```swift -let (data, response) = try await client.collection(name: "books").override("test-id").retrieve(metadataType: MetadataType.self ) +let (data, response) = try await client.collection(name: "books").override("test-id").retrieve(metadataType: MetadataType.self) ``` ### Delete an override @@ -124,6 +124,41 @@ let (data, response) = try await client.collection(name: "books").override("test let (data, response) = try await client.collection(name: "books").override("test-id").delete() ``` +### Create or update a preset + +```swift +let schema = PresetUpsertSchema( + value: PresetValue.singleCollectionSearch(SearchParameters(q: "apple")) + // or: value: PresetValue.multiSearch(MultiSearchSearchesParameter(searches: [MultiSearchCollectionParameters(q: "apple")])) +) +let (data, response) = try await client.presets().upsert(presetName: "listing_view", params: schema) +``` + +### Retrieve all presets + +```swift +let (data, response) = try await client.presets().retrieve() +``` + +### Retrieve a preset + +```swift +let (data, response) = try await client.preset("listing_view").retrieve() + +switch data?.value { + case .singleCollectionSearch(let value): + print(value) + case .multiSearch(let value): + print(value) +} +``` + +### Delete a preset + +```swift +let (data, response) = try await client.preset("listing_view").delete() +``` + ## Contributing Issues and pull requests are welcome on GitHub at [Typesense Swift](https://github.com/typesense/typesense-swift). Do note that the Models used in the Swift client are generated by [Swagger-Codegen](https://github.com/swagger-api/swagger-codegen) and are automated to be modified in order to prevent major errors. So please do use the shell script that is provided in the repo to generate the models: From 4fc57b75a0ce78d188e4dac303663f229548fa74 Mon Sep 17 00:00:00 2001 From: Hayden Date: Wed, 7 Aug 2024 09:50:23 +0700 Subject: [PATCH 3/5] test: search with preset --- Tests/TypesenseTests/DocumentTests.swift | 155 +++++++++++++------- Tests/TypesenseTests/MultiSearchTests.swift | 135 ++++++++++++----- Tests/TypesenseTests/UtilsTests.swift | 15 ++ 3 files changed, 219 insertions(+), 86 deletions(-) diff --git a/Tests/TypesenseTests/DocumentTests.swift b/Tests/TypesenseTests/DocumentTests.swift index b2035b2..b8ba1fe 100644 --- a/Tests/TypesenseTests/DocumentTests.swift +++ b/Tests/TypesenseTests/DocumentTests.swift @@ -2,7 +2,9 @@ import XCTest @testable import Typesense final class DocumentTests: XCTestCase { - + override func tearDown() async throws { + try! await tearDownCollections() + } //Example Struct to match the Companies Collection struct Company: Codable { var id: String @@ -10,7 +12,7 @@ final class DocumentTests: XCTestCase { var num_employees: Int var country: String } - + //Partial data format to be used in update() method struct PartialCompany: Codable { var id: String? @@ -18,12 +20,12 @@ final class DocumentTests: XCTestCase { var num_employees: Int? var country: String? } - + func testDocumentCreate() async { let config = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) - + let client = Client(config: config) - + let document = Company(id: "125", company_name: "Stark Industries", num_employees: 5215, country: "USA") do { @@ -39,7 +41,7 @@ final class DocumentTests: XCTestCase { XCTAssertEqual(docuResp.id, "125") XCTAssertEqual(docuResp.country, "USA") print(docuResp) - + } catch ResponseError.documentAlreadyExists(let desc), ResponseError.invalidCollection(let desc) { print(desc) XCTAssertTrue(true) @@ -52,10 +54,10 @@ final class DocumentTests: XCTestCase { XCTAssertTrue(false) } } - + func testDocumentUpsert() async { let config = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) - + let client = Client(config: config) let document = Company(id: "124", company_name: "Stark Industries", num_employees: 5215, country: "USA") @@ -86,19 +88,19 @@ final class DocumentTests: XCTestCase { XCTAssertTrue(false) } } - + func testDocumentDelete() async { let config = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) - + let client = Client(config: config) - + do { let (data, _) = try await client.collection(name: "companies").document(id: "125").delete() XCTAssertNotNil(data) guard let validResp = data else { throw DataError.dataNotFound } - + let docuResp = try decoder.decode(Company.self, from: validResp) XCTAssertEqual(docuResp.company_name, "Stark Industries") let emps = [5215, 5500] @@ -106,7 +108,7 @@ final class DocumentTests: XCTestCase { XCTAssertEqual(docuResp.id, "125") XCTAssertEqual(docuResp.country, "USA") print(docuResp) - + } catch ResponseError.documentDoesNotExist(let desc), ResponseError.invalidCollection(let desc) { print(desc) XCTAssertTrue(true) @@ -119,19 +121,19 @@ final class DocumentTests: XCTestCase { XCTAssertTrue(false) } } - + func testDocumentRetrieve() async { let config = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) - + let client = Client(config: config) - + do { let (data, _) = try await client.collection(name: "companies").document(id: "125").retrieve() XCTAssertNotNil(data) guard let validResp = data else { throw DataError.dataNotFound } - + let docuResp = try decoder.decode(Company.self, from: validResp) XCTAssertEqual(docuResp.company_name, "Stark Industries") let emps = [5215, 5500] @@ -139,7 +141,7 @@ final class DocumentTests: XCTestCase { XCTAssertEqual(docuResp.id, "125") XCTAssertEqual(docuResp.country, "USA") print(docuResp) - + } catch ResponseError.documentDoesNotExist(let desc), ResponseError.invalidCollection(let desc) { print(desc) XCTAssertTrue(true) @@ -152,14 +154,14 @@ final class DocumentTests: XCTestCase { XCTAssertTrue(false) } } - + func testDocumentUpdate() async { let config = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) - + let client = Client(config: config) - + let newDoc = PartialCompany(company_name: "Stark Industries", num_employees: 5500) - + do { let docuData = try encoder.encode(newDoc) let (data, _) = try await client.collection(name: "companies").document(id: "125").update(newDocument: docuData) @@ -167,14 +169,14 @@ final class DocumentTests: XCTestCase { guard let validResp = data else { throw DataError.dataNotFound } - + let docuResp = try decoder.decode(Company.self, from: validResp) XCTAssertEqual(docuResp.company_name, "Stark Industries") XCTAssertEqual(docuResp.num_employees, 5500) XCTAssertEqual(docuResp.id, "125") XCTAssertEqual(docuResp.country, "USA") print(docuResp) - + } catch ResponseError.documentDoesNotExist(let desc), ResponseError.invalidCollection(let desc) { print(desc) XCTAssertTrue(true) @@ -187,14 +189,14 @@ final class DocumentTests: XCTestCase { XCTAssertTrue(false) } } - + func testDocumentSearch() async { let config = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) - + let client = Client(config: config) - + let searchParams = SearchParameters(q: "stark", queryBy: "company_name", filterBy: "num_employees:>100", sortBy: "num_employees:desc") - + do { let (data, _) = try await client.collection(name: "companies").documents().search(searchParams, for: Company.self) XCTAssertNotNil(data) @@ -217,19 +219,72 @@ final class DocumentTests: XCTestCase { print(error.localizedDescription) XCTAssertTrue(false) } - + + } + + func testDocumentSearchWithPreset() async { + let productSchema = CollectionSchema(name: "products", fields: [ + Field(name: "name", type: "string"), + Field(name: "price", type: "int32"), + Field(name: "brand", type: "string"), + Field(name: "desc", type: "string"), + ]) + + let preset = PresetUpsertSchema( + value: .singleCollectionSearch( + SearchParameters(q: "Jor", queryBy: "name", filterBy: "price:=[50..120]") + ) + ) + + let _ = try! await client.presets().upsert(presetName: "single-collection-search-preset", params: preset) + + let product1 = Product(name: "Jordan", price: 70, brand: "Nike", desc: "High quality shoe") + + do { + do { + let _ = try await client.collections.create(schema: productSchema) + } catch ResponseError.collectionAlreadyExists(let desc) { + print(desc) + } catch (let error) { + print(error.localizedDescription) + XCTAssertTrue(false) + } + + let (_,_) = try await client.collection(name: "products").documents().create(document: encoder.encode(product1)) + + let (data, _) = try await client.collection(name: "products").documents().search(SearchParameters(preset: "single-collection-search-preset"), for: Product.self) + + + XCTAssertNotNil(data) + guard let validResp = data else { + throw DataError.dataNotFound + } + + XCTAssertNotNil(validResp.hits) + XCTAssertEqual(validResp.hits?.count, 1) + + print(validResp.hits as Any) + } catch HTTPError.serverError(let code, let desc) { + print(desc) + print("The response status code is \(code)") + XCTAssertTrue(false) + } catch (let error) { + print(error.localizedDescription) + XCTAssertTrue(false) + } + try! await tearDownPresets() } - + func testDocumentGroupSearch() async { let config = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) - + let client = Client(config: config) - + let searchParams = SearchParameters(q: "*", queryBy: "company_name", groupBy: "num_employees") - + do { let (data, _) = try await client.collection(name: "companies").documents().search(searchParams, for: Company.self) - + XCTAssertNotNil(data) guard let validResp = data else { throw DataError.dataNotFound @@ -249,19 +304,19 @@ final class DocumentTests: XCTestCase { XCTAssertTrue(false) } } - + func testDocumentImport() async { let config = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) - + let client = Client(config: config) - + let documents = [ Company(id: "124", company_name: "Stark Industries", num_employees: 5125, country: "USA"), Company(id: "125", company_name: "Acme Corp", num_employees: 2133, country: "CA") ] - + do { - + var jsonLStrings:[String] = [] for doc in documents { let data = try encoder.encode(doc) @@ -272,7 +327,7 @@ final class DocumentTests: XCTestCase { let jsonLString = jsonLStrings.joined(separator: "\n") print(jsonLString) let jsonL = Data(jsonLString.utf8) - + let (data, _) = try await client.collection(name: "companies").documents().importBatch(jsonL) XCTAssertNotNil(data) guard let validResp = data else { @@ -288,14 +343,14 @@ final class DocumentTests: XCTestCase { XCTAssertTrue(false) } } - + func testDocumentExport() async { let config = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) - + let client = Client(config: config) - + do { - + let (data, _) = try await client.collection(name: "companies").documents().export() XCTAssertNotNil(data) guard let validResp = data else { @@ -311,14 +366,14 @@ final class DocumentTests: XCTestCase { XCTAssertTrue(false) } } - + func testDocumentDeleteByQuery() async { let config = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) - + let client = Client(config: config) - + do { - + let (data, _) = try await client.collection(name: "companies").documents().delete(filter: "num_employees:>100", batchSize: 100) XCTAssertNotNil(data) guard let validResp = data else { @@ -334,6 +389,6 @@ final class DocumentTests: XCTestCase { XCTAssertTrue(false) } } - - + + } diff --git a/Tests/TypesenseTests/MultiSearchTests.swift b/Tests/TypesenseTests/MultiSearchTests.swift index e1067db..6cf92eb 100644 --- a/Tests/TypesenseTests/MultiSearchTests.swift +++ b/Tests/TypesenseTests/MultiSearchTests.swift @@ -2,53 +2,41 @@ import XCTest @testable import Typesense final class MultiSearchTests: XCTestCase { - - struct Product: Codable, Equatable { - var name: String? - var price: Int? - var brand: String? - var desc: String? - - static func == (lhs: Product, rhs: Product) -> Bool { - return - lhs.name == rhs.name && - lhs.price == rhs.price && - lhs.brand == rhs.brand && - lhs.desc == rhs.desc - } + override func tearDown() async throws { + try! await tearDownCollections() } - + struct Brand: Codable { var name: String } - - + + func testMultiSearch() async { let config = Configuration(nodes: [Node(host: "localhost", port: "8108", nodeProtocol: "http")], apiKey: "xyz", logger: Logger(debugMode: true)) - + let client = Client(config: config) - + let productSchema = CollectionSchema(name: "products", fields: [ Field(name: "name", type: "string"), Field(name: "price", type: "int32"), Field(name: "brand", type: "string"), Field(name: "desc", type: "string"), ]) - + let brandSchema = CollectionSchema(name: "brands", fields: [ Field(name: "name", type: "string"), ]) - + let searchRequests = [ MultiSearchCollectionParameters(q: "shoe", filterBy: "price:=[50..120]", collection: "products"), MultiSearchCollectionParameters(q: "Nike", collection: "brands"), ] - + let brand1 = Brand(name: "Nike") let product1 = Product(name: "Jordan", price: 70, brand: "Nike", desc: "High quality shoe") - + let commonParams = MultiSearchParameters(queryBy: "name") - + do { do { let _ = try await client.collections.create(schema: productSchema) @@ -58,7 +46,7 @@ final class MultiSearchTests: XCTestCase { print(error.localizedDescription) XCTAssertTrue(false) } - + do { let _ = try await client.collections.create(schema: brandSchema) } catch ResponseError.collectionAlreadyExists(let desc) { @@ -67,21 +55,19 @@ final class MultiSearchTests: XCTestCase { print(error.localizedDescription) XCTAssertTrue(false) } - + let (_,_) = try await client.collection(name: "products").documents().create(document: encoder.encode(product1)) - + let (_,_) = try await client.collection(name: "brands").documents().create(document: encoder.encode(brand1)) - + let (data, _) = try await client.multiSearch().perform(searchRequests: searchRequests, commonParameters: commonParams, for: Product.self) - - let (_,_) = try await client.collection(name: "products").delete() //Deleting test collection - let (_,_) = try await client.collection(name: "brands").delete() //Deleting test collection - + + XCTAssertNotNil(data) guard let validResp = data else { throw DataError.dataNotFound } - + XCTAssertNotNil(validResp.results) XCTAssertNotEqual(validResp.results.count, 0) XCTAssertNotNil(validResp.results[0].hits) @@ -97,9 +83,86 @@ final class MultiSearchTests: XCTestCase { print(error.localizedDescription) XCTAssertTrue(false) } - + } - - + func testMultiSearchWithPreset() async { + let productSchema = CollectionSchema(name: "products", fields: [ + Field(name: "name", type: "string"), + Field(name: "price", type: "int32"), + Field(name: "brand", type: "string"), + Field(name: "desc", type: "string"), + ]) + + let brandSchema = CollectionSchema(name: "brands", fields: [ + Field(name: "name", type: "string"), + ]) + + let preset = PresetUpsertSchema( + value: .multiSearch(MultiSearchSearchesParameter( + searches:[ + MultiSearchCollectionParameters(q: "shoe", filterBy: "price:=[50..120]", collection: "products"), + MultiSearchCollectionParameters(q: "Nike", collection: "brands"), + ] + )) + ) + + let _ = try! await client.presets().upsert(presetName: "test-multi-search", params: preset) + + let brand1 = Brand(name: "Nike") + let product1 = Product(name: "Jordan", price: 70, brand: "Nike", desc: "High quality shoe") + + let commonParams = MultiSearchParameters(queryBy: "name", preset: "test-multi-search") + + do { + do { + let _ = try await client.collections.create(schema: productSchema) + } catch ResponseError.collectionAlreadyExists(let desc) { + print(desc) + } catch (let error) { + print(error.localizedDescription) + XCTAssertTrue(false) + } + + do { + let _ = try await client.collections.create(schema: brandSchema) + } catch ResponseError.collectionAlreadyExists(let desc) { + print(desc) + } catch (let error) { + print(error.localizedDescription) + XCTAssertTrue(false) + } + + let (_,_) = try await client.collection(name: "products").documents().create(document: encoder.encode(product1)) + + let (_,_) = try await client.collection(name: "brands").documents().create(document: encoder.encode(brand1)) + + let (data, _) = try await client.multiSearch().perform(searchRequests: [], commonParameters: commonParams, for: Product.self) + + + XCTAssertNotNil(data) + guard let validResp = data else { + throw DataError.dataNotFound + } + + XCTAssertNotNil(validResp.results) + XCTAssertNotEqual(validResp.results.count, 0) + XCTAssertNotNil(validResp.results[0].hits) + XCTAssertNotNil(validResp.results[1].hits) + XCTAssertEqual(validResp.results[1].hits?.count, 1) + + print(validResp.results[1].hits as Any) + } catch HTTPError.serverError(let code, let desc) { + print(desc) + print("The response status code is \(code)") + XCTAssertTrue(false) + } catch (let error) { + print(error.localizedDescription) + XCTAssertTrue(false) + } + try! await tearDownPresets() + } + + + } diff --git a/Tests/TypesenseTests/UtilsTests.swift b/Tests/TypesenseTests/UtilsTests.swift index 29b2375..806fe34 100644 --- a/Tests/TypesenseTests/UtilsTests.swift +++ b/Tests/TypesenseTests/UtilsTests.swift @@ -54,4 +54,19 @@ func createMultiSearchPreset() async throws { value: .multiSearch(MultiSearchSearchesParameter(searches: [MultiSearchCollectionParameters(q: "banana")])) ) ) +} + +struct Product: Codable, Equatable { + var name: String? + var price: Int? + var brand: String? + var desc: String? + + static func == (lhs: Product, rhs: Product) -> Bool { + return + lhs.name == rhs.name && + lhs.price == rhs.price && + lhs.brand == rhs.brand && + lhs.desc == rhs.desc + } } \ No newline at end of file From 96b8eb63cd8b41ebaebbfd10371f42a342139587 Mon Sep 17 00:00:00 2001 From: Hayden Date: Wed, 7 Aug 2024 09:51:32 +0700 Subject: [PATCH 4/5] rename file --- Tests/TypesenseTests/{UtilsTests.swift => TestUtils.swift} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Tests/TypesenseTests/{UtilsTests.swift => TestUtils.swift} (100%) diff --git a/Tests/TypesenseTests/UtilsTests.swift b/Tests/TypesenseTests/TestUtils.swift similarity index 100% rename from Tests/TypesenseTests/UtilsTests.swift rename to Tests/TypesenseTests/TestUtils.swift From 5e62e20ee65230a469e272b9fd93bc794d05f2d7 Mon Sep 17 00:00:00 2001 From: Hayden Date: Wed, 7 Aug 2024 18:29:41 +0700 Subject: [PATCH 5/5] Squashed commit of the following: commit 01eb6e7a78aef2b9ca98a22f6f63e8afba750540 Author: Hayden Date: Wed Aug 7 18:26:08 2024 +0700 update README: add stopwords documentation commit ba8256b9237a5f7df7ef0d529cda80e301edf8e6 Author: Hayden Date: Wed Aug 7 18:23:22 2024 +0700 feat: add stopwords endpoint --- README.md | 28 +++++++++++ Sources/Typesense/Client.swift | 8 ++++ .../Models/StopwordsSetDeleteSchema.swift | 16 +++++++ .../Models/StopwordsSetRetrieveSchema.swift | 21 +++++++++ .../Typesense/Models/StopwordsSetSchema.swift | 30 ++++++++++++ .../Models/StopwordsSetUpsertSchema.swift | 23 ++++++++++ .../StopwordsSetsRetrieveAllSchema.swift | 21 +++++++++ Sources/Typesense/Stopword.swift | 39 ++++++++++++++++ Sources/Typesense/Stopwords.swift | 44 ++++++++++++++++++ Tests/TypesenseTests/StopwordTests.swift | 42 +++++++++++++++++ Tests/TypesenseTests/StopwordsTests.swift | 46 +++++++++++++++++++ Tests/TypesenseTests/TestUtils.swift | 20 ++++++++ 12 files changed, 338 insertions(+) create mode 100644 Sources/Typesense/Models/StopwordsSetDeleteSchema.swift create mode 100644 Sources/Typesense/Models/StopwordsSetRetrieveSchema.swift create mode 100644 Sources/Typesense/Models/StopwordsSetSchema.swift create mode 100644 Sources/Typesense/Models/StopwordsSetUpsertSchema.swift create mode 100644 Sources/Typesense/Models/StopwordsSetsRetrieveAllSchema.swift create mode 100644 Sources/Typesense/Stopword.swift create mode 100644 Sources/Typesense/Stopwords.swift create mode 100644 Tests/TypesenseTests/StopwordTests.swift create mode 100644 Tests/TypesenseTests/StopwordsTests.swift diff --git a/README.md b/README.md index a552467..f29717e 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,34 @@ switch data?.value { let (data, response) = try await client.preset("listing_view").delete() ``` +### Create or update a stopwords set + +```swift +let schema = StopwordsSetUpsertSchema( + stopwords: ["states","united"], + locale: "en" +) +let (data, response) = try await client.stopwords().upsert(stopwordsSetId: "stopword_set1", params: schema) +``` + +### Retrieve all stopwords sets + +```swift +let (data, response) = try await client.stopwords().retrieve() +``` + +### Retrieve a stopwords set + +```swift +let (data, response) = try await client.stopword("stopword_set1").retrieve() +``` + +### Delete a preset + +```swift +let (data, response) = try await client.stopword("stopword_set1").delete() +``` + ## Contributing Issues and pull requests are welcome on GitHub at [Typesense Swift](https://github.com/typesense/typesense-swift). Do note that the Models used in the Swift client are generated by [Swagger-Codegen](https://github.com/swagger-api/swagger-codegen) and are automated to be modified in order to prevent major errors. So please do use the shell script that is provided in the repo to generate the models: diff --git a/Sources/Typesense/Client.swift b/Sources/Typesense/Client.swift index a00b7b7..acc2324 100644 --- a/Sources/Typesense/Client.swift +++ b/Sources/Typesense/Client.swift @@ -43,4 +43,12 @@ public struct Client { public func preset(_ presetName: String) -> Preset { return Preset(apiCall: apiCall, presetName: presetName) } + + public func stopwords() -> Stopwords { + return Stopwords(apiCall: apiCall) + } + + public func stopword(_ stopwordsSetId: String) -> Stopword { + return Stopword(apiCall: apiCall, stopwordsSetId: stopwordsSetId) + } } diff --git a/Sources/Typesense/Models/StopwordsSetDeleteSchema.swift b/Sources/Typesense/Models/StopwordsSetDeleteSchema.swift new file mode 100644 index 0000000..0fb3f91 --- /dev/null +++ b/Sources/Typesense/Models/StopwordsSetDeleteSchema.swift @@ -0,0 +1,16 @@ +import Foundation + + + +public struct StopwordsSetDeleteSchema: Codable { + public var _id: String + + public init(_id: String) { + self._id = _id + } + + public enum CodingKeys: String, CodingKey { + case _id = "id" + } + +} diff --git a/Sources/Typesense/Models/StopwordsSetRetrieveSchema.swift b/Sources/Typesense/Models/StopwordsSetRetrieveSchema.swift new file mode 100644 index 0000000..fd37ac7 --- /dev/null +++ b/Sources/Typesense/Models/StopwordsSetRetrieveSchema.swift @@ -0,0 +1,21 @@ +// +// StopwordsSetRetrieveSchema.swift +// +// Generated by swagger-codegen +// https://github.com/swagger-api/swagger-codegen +// + +import Foundation + + + +public struct StopwordsSetRetrieveSchema: Codable { + + public var stopwords: StopwordsSetSchema + + public init(stopwords: StopwordsSetSchema) { + self.stopwords = stopwords + } + + +} diff --git a/Sources/Typesense/Models/StopwordsSetSchema.swift b/Sources/Typesense/Models/StopwordsSetSchema.swift new file mode 100644 index 0000000..a452fc1 --- /dev/null +++ b/Sources/Typesense/Models/StopwordsSetSchema.swift @@ -0,0 +1,30 @@ +// +// StopwordsSetSchema.swift +// +// Generated by swagger-codegen +// https://github.com/swagger-api/swagger-codegen +// + +import Foundation + + + +public struct StopwordsSetSchema: Codable { + + public var _id: String + public var stopwords: [String] + public var locale: String? + + public init(_id: String, stopwords: [String], locale: String? = nil) { + self._id = _id + self.stopwords = stopwords + self.locale = locale + } + + public enum CodingKeys: String, CodingKey { + case _id = "id" + case stopwords + case locale + } + +} diff --git a/Sources/Typesense/Models/StopwordsSetUpsertSchema.swift b/Sources/Typesense/Models/StopwordsSetUpsertSchema.swift new file mode 100644 index 0000000..0c6eaf2 --- /dev/null +++ b/Sources/Typesense/Models/StopwordsSetUpsertSchema.swift @@ -0,0 +1,23 @@ +// +// StopwordsSetUpsertSchema.swift +// +// Generated by swagger-codegen +// https://github.com/swagger-api/swagger-codegen +// + +import Foundation + + + +public struct StopwordsSetUpsertSchema: Codable { + + public var stopwords: [String] + public var locale: String? + + public init(stopwords: [String], locale: String? = nil) { + self.stopwords = stopwords + self.locale = locale + } + + +} diff --git a/Sources/Typesense/Models/StopwordsSetsRetrieveAllSchema.swift b/Sources/Typesense/Models/StopwordsSetsRetrieveAllSchema.swift new file mode 100644 index 0000000..52d83cd --- /dev/null +++ b/Sources/Typesense/Models/StopwordsSetsRetrieveAllSchema.swift @@ -0,0 +1,21 @@ +// +// StopwordsSetsRetrieveAllSchema.swift +// +// Generated by swagger-codegen +// https://github.com/swagger-api/swagger-codegen +// + +import Foundation + + + +public struct StopwordsSetsRetrieveAllSchema: Codable { + + public var stopwords: [StopwordsSetSchema] + + public init(stopwords: [StopwordsSetSchema]) { + self.stopwords = stopwords + } + + +} diff --git a/Sources/Typesense/Stopword.swift b/Sources/Typesense/Stopword.swift new file mode 100644 index 0000000..7c9d7db --- /dev/null +++ b/Sources/Typesense/Stopword.swift @@ -0,0 +1,39 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct Stopword { + private var apiCall: ApiCall + private var stopwordsSetId: String + + + init(apiCall: ApiCall, stopwordsSetId: String) { + self.apiCall = apiCall + self.stopwordsSetId = stopwordsSetId + } + + public func retrieve() async throws -> (StopwordsSetSchema?, URLResponse?) { + let (data, response) = try await apiCall.get(endPoint: endpointPath()) + if let result = data { + let decodedData = try decoder.decode(StopwordsSetRetrieveSchema.self, from: result) + return (decodedData.stopwords, response) + } + return (nil, response) + } + + public func delete() async throws -> (StopwordsSetDeleteSchema?, URLResponse?) { + let (data, response) = try await apiCall.delete(endPoint: endpointPath()) + if let result = data { + let decodedData = try decoder.decode(StopwordsSetDeleteSchema.self, from: result) + return (decodedData, response) + } + return (nil, response) + } + + private func endpointPath() -> String { + return "\(Stopwords.RESOURCEPATH)/\(stopwordsSetId)" + } + + +} diff --git a/Sources/Typesense/Stopwords.swift b/Sources/Typesense/Stopwords.swift new file mode 100644 index 0000000..bebdb7a --- /dev/null +++ b/Sources/Typesense/Stopwords.swift @@ -0,0 +1,44 @@ +import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +public struct Stopwords { + static let RESOURCEPATH = "stopwords" + private var apiCall: ApiCall + + + init(apiCall: ApiCall) { + self.apiCall = apiCall + } + + public func upsert(stopwordsSetId: String, params: StopwordsSetUpsertSchema) async throws -> (StopwordsSetSchema?, URLResponse?) { + let schemaData = try encoder.encode(params) + let (data, response) = try await self.apiCall.put(endPoint: endpointPath(stopwordsSetId), body: schemaData) + if let result = data { + let decodedData = try decoder.decode(StopwordsSetSchema.self, from: result) + return (decodedData, response) + } + return (nil, response) + } + + public func retrieve() async throws -> ([StopwordsSetSchema]?, URLResponse?) { + let (data, response) = try await self.apiCall.get(endPoint: endpointPath()) + if let result = data { + let decodedData = try decoder.decode(StopwordsSetsRetrieveAllSchema.self, from: result) + return (decodedData.stopwords, response) + } + return (nil, response) + } + + private func endpointPath(_ operation: String? = nil) -> String { + let baseEndpoint = "\(Stopwords.RESOURCEPATH)" + if let operation = operation { + return "\(baseEndpoint)/\(operation)" + } else { + return baseEndpoint + } + } + + +} diff --git a/Tests/TypesenseTests/StopwordTests.swift b/Tests/TypesenseTests/StopwordTests.swift new file mode 100644 index 0000000..e5345bc --- /dev/null +++ b/Tests/TypesenseTests/StopwordTests.swift @@ -0,0 +1,42 @@ +import XCTest +@testable import Typesense + +final class StopwordTests: XCTestCase { + override func tearDown() async throws { + try! await tearDownStopwords() + } + + func testStopwordRetrieve() async { + try! await createStopwordSet() + do { + let (result, _) = try await client.stopword("test-id-stopword-set").retrieve() + XCTAssertNotNil(result) + guard let validResult = result else { + throw DataError.dataNotFound + } + print(validResult) + XCTAssertEqual("test-id-stopword-set", validResult._id) + XCTAssertEqual(["states","united"], validResult.stopwords) + XCTAssertEqual("en", validResult.locale) + } catch (let error) { + print(error.localizedDescription) + XCTAssertTrue(false) + } + } + + func testStopwordDelete() async { + try! await createStopwordSet() + do { + let (result, _) = try await client.stopword("test-id-stopword-set").delete() + guard let validResult = result else { + throw DataError.dataNotFound + } + print(validResult) + XCTAssertEqual("test-id-stopword-set", validResult._id) + } catch (let error) { + print(error) + XCTAssertTrue(false) + } + } + +} diff --git a/Tests/TypesenseTests/StopwordsTests.swift b/Tests/TypesenseTests/StopwordsTests.swift new file mode 100644 index 0000000..d6191ff --- /dev/null +++ b/Tests/TypesenseTests/StopwordsTests.swift @@ -0,0 +1,46 @@ +import XCTest +@testable import Typesense + +final class StopwordsTests: XCTestCase { + override func tearDown() async throws { + try! await tearDownStopwords() + } + + func testStopwordsUpsert() async { + let schema = StopwordsSetUpsertSchema( + stopwords: ["states","united"], + locale: "en" + ) + do { + let (result, _) = try await client.stopwords().upsert(stopwordsSetId: "test-id", params: schema) + XCTAssertNotNil(result) + guard let validResult = result else { + throw DataError.dataNotFound + } + print(validResult) + XCTAssertEqual("test-id", validResult._id) + XCTAssertEqual(["states","united"], validResult.stopwords) + XCTAssertEqual("en", validResult.locale) + } catch (let error) { + print(error.localizedDescription) + XCTAssertTrue(false) + } + } + + func testStopwordsRetrieveAll() async { + try! await createStopwordSet() + do { + let (result, _) = try await client.stopwords().retrieve() + guard let validResult = result else { + throw DataError.dataNotFound + } + print(validResult) + XCTAssertEqual(1, validResult.count) + XCTAssertEqual("test-id-stopword-set", validResult[0]._id) + } catch (let error) { + print(error) + XCTAssertTrue(false) + } + } + +} diff --git a/Tests/TypesenseTests/TestUtils.swift b/Tests/TypesenseTests/TestUtils.swift index 806fe34..78af5b6 100644 --- a/Tests/TypesenseTests/TestUtils.swift +++ b/Tests/TypesenseTests/TestUtils.swift @@ -23,6 +23,16 @@ func tearDownPresets() async throws { } } +func tearDownStopwords() async throws { + let (data, _) = try await client.stopwords().retrieve() + guard let validData = data else { + throw DataError.dataNotFound + } + for item in validData { + let _ = try! await client.stopword(item._id).delete() + } +} + func setUpCollection() async throws{ let schema = CollectionSchema(name: "test-utils-collection", fields: [Field(name: "company_name", type: "string"), Field(name: "num_employees", type: "int32"), Field(name: "country", type: "string", facet: true)], defaultSortingField: "num_employees") let (collResp, _) = try! await client.collections.create(schema: schema) @@ -56,6 +66,16 @@ func createMultiSearchPreset() async throws { ) } +func createStopwordSet() async throws { + let _ = try! await client.stopwords().upsert( + stopwordsSetId: "test-id-stopword-set", + params: StopwordsSetUpsertSchema( + stopwords: ["states","united"], + locale: "en" + ) + ) +} + struct Product: Codable, Equatable { var name: String? var price: Int?