diff --git a/Sources/Typesense/Alias.swift b/Sources/Typesense/Alias.swift index 61e1577..a42a69b 100644 --- a/Sources/Typesense/Alias.swift +++ b/Sources/Typesense/Alias.swift @@ -12,46 +12,47 @@ public struct Alias { } public func upsert(name: String, collection: CollectionAliasSchema) async throws -> (CollectionAlias?, URLResponse?) { - let schemaData: Data? - - schemaData = try encoder.encode(collection) - - if let validSchema = schemaData { - let (data, response) = try await apiCall.put(endPoint: "\(RESOURCEPATH)/\(name)", body: validSchema) - if let result = data { - let alias = try decoder.decode(CollectionAlias.self, from: result) - return (alias, response) - } + let schemaData = try encoder.encode(collection) + let (data, response) = try await apiCall.put(endPoint: endpointPath(name), body: schemaData) + if let result = data { + let alias = try decoder.decode(CollectionAlias.self, from: result) + return (alias, response) } - return (nil, nil) + return (nil, response) } public func retrieve(name: String) async throws -> (CollectionAlias?, URLResponse?) { - let (data, response) = try await apiCall.get(endPoint: "\(RESOURCEPATH)/\(name)") + let (data, response) = try await apiCall.get(endPoint: endpointPath(name)) if let result = data { let alias = try decoder.decode(CollectionAlias.self, from: result) return (alias, response) } - return (nil, nil) + return (nil, response) } public func retrieve() async throws -> (CollectionAliasesResponse?, URLResponse?) { - let (data, response) = try await apiCall.get(endPoint: "\(RESOURCEPATH)") + let (data, response) = try await apiCall.get(endPoint: endpointPath()) if let result = data { let aliases = try decoder.decode(CollectionAliasesResponse.self, from: result) return (aliases, response) } - return (nil, nil) + return (nil, response) } public func delete(name: String) async throws -> (CollectionAlias?, URLResponse?) { - let (data, response) = try await apiCall.delete(endPoint: "\(RESOURCEPATH)/\(name)") + let (data, response) = try await apiCall.delete(endPoint: endpointPath(name)) if let result = data { let alias = try decoder.decode(CollectionAlias.self, from: result) return (alias, response) } - return (nil, nil) + return (nil, response) } - + private func endpointPath(_ operation: String? = nil) throws -> String { + if let operation: String = operation { + return try "\(RESOURCEPATH)/\(operation.encodeURL())" + } else { + return RESOURCEPATH + } + } } diff --git a/Sources/Typesense/AnalyticsRule.swift b/Sources/Typesense/AnalyticsRule.swift index 36587f8..3a13f01 100644 --- a/Sources/Typesense/AnalyticsRule.swift +++ b/Sources/Typesense/AnalyticsRule.swift @@ -29,7 +29,7 @@ public struct AnalyticsRule { return (nil, response) } - private func endpointPath() -> String { - return "\(AnalyticsRules.resourcePath)/\(name)" + private func endpointPath() throws -> String { + return try "\(AnalyticsRules.resourcePath)/\(name.encodeURL())" } } diff --git a/Sources/Typesense/AnalyticsRules.swift b/Sources/Typesense/AnalyticsRules.swift index 59d43c1..3a122ab 100644 --- a/Sources/Typesense/AnalyticsRules.swift +++ b/Sources/Typesense/AnalyticsRules.swift @@ -33,9 +33,9 @@ public struct AnalyticsRules { return (nil, response) } - private func endpointPath(_ operation: String? = nil) -> String { + private func endpointPath(_ operation: String? = nil) throws -> String { if let operation = operation { - return "\(AnalyticsRules.resourcePath)/\(operation)" + return try "\(AnalyticsRules.resourcePath)/\(operation.encodeURL())" } else { return AnalyticsRules.resourcePath } diff --git a/Sources/Typesense/Collection.swift b/Sources/Typesense/Collection.swift index 0447e9a..95eef68 100644 --- a/Sources/Typesense/Collection.swift +++ b/Sources/Typesense/Collection.swift @@ -6,8 +6,6 @@ import Foundation public struct Collection { var apiCall: ApiCall - let RESOURCEPATH = "collections" - var collectionName: String init(apiCall: ApiCall, collectionName: String) { @@ -24,7 +22,7 @@ public struct Collection { } public func delete() async throws -> (CollectionResponse?, URLResponse?) { - let (data, response) = try await apiCall.delete(endPoint: "\(RESOURCEPATH)/\(collectionName)") + let (data, response) = try await apiCall.delete(endPoint: endpointPath()) if let result = data { let fetchedCollection = try decoder.decode(CollectionResponse.self, from: result) return (fetchedCollection, response) @@ -33,7 +31,7 @@ public struct Collection { } public func retrieve() async throws -> (CollectionResponse?, URLResponse?) { - let (data, response) = try await apiCall.get(endPoint: "\(RESOURCEPATH)/\(collectionName)") + let (data, response) = try await apiCall.get(endPoint: endpointPath()) if let result = data { let fetchedCollection = try decoder.decode(CollectionResponse.self, from: result) return (fetchedCollection, response) @@ -52,4 +50,8 @@ public struct Collection { public func override(_ overrideId: String) -> Override{ return Override(apiCall: self.apiCall, collectionName: self.collectionName, overrideId: overrideId) } + + private func endpointPath() throws -> String { + return "\(Collections.RESOURCEPATH)/\(try collectionName.encodeURL())" + } } diff --git a/Sources/Typesense/Document.swift b/Sources/Typesense/Document.swift index 27c0bd4..a4ea56b 100644 --- a/Sources/Typesense/Document.swift +++ b/Sources/Typesense/Document.swift @@ -8,29 +8,30 @@ public struct Document { var apiCall: ApiCall var collectionName: String var id: String - let RESOURCEPATH: String init(apiCall: ApiCall, collectionName: String, id: String) { self.apiCall = apiCall self.collectionName = collectionName self.id = id - self.RESOURCEPATH = "collections/\(collectionName)/documents" } public func delete() async throws -> (Data?, URLResponse?) { - let (data, response) = try await apiCall.delete(endPoint: "\(RESOURCEPATH)/\(self.id)") + let (data, response) = try await apiCall.delete(endPoint: endpointPath()) return (data, response) } public func retrieve() async throws -> (Data?, URLResponse?) { - let (data, response) = try await apiCall.get(endPoint: "\(RESOURCEPATH)/\(self.id)") + let (data, response) = try await apiCall.get(endPoint: endpointPath()) return (data, response) } public func update(newDocument: Data, options: DocumentIndexParameters? = nil) async throws -> (Data?, URLResponse?) { let queryParams = try createURLQuery(forSchema: options) - let (data, response) = try await apiCall.patch(endPoint: "\(RESOURCEPATH)/\(self.id)", body: newDocument, queryParameters: queryParams) + let (data, response) = try await apiCall.patch(endPoint: endpointPath(), body: newDocument, queryParameters: queryParams) return (data, response) } + private func endpointPath() throws -> String { + return try "\(Collections.RESOURCEPATH)/\(collectionName.encodeURL())/documents/\(id.encodeURL())" + } } diff --git a/Sources/Typesense/Documents.swift b/Sources/Typesense/Documents.swift index 3741738..e42bb0d 100644 --- a/Sources/Typesense/Documents.swift +++ b/Sources/Typesense/Documents.swift @@ -7,24 +7,22 @@ import Foundation public struct Documents { var apiCall: ApiCall var collectionName: String - let RESOURCEPATH: String init(apiCall: ApiCall, collectionName: String) { self.apiCall = apiCall self.collectionName = collectionName - self.RESOURCEPATH = "collections/\(collectionName)/documents" } public func create(document: Data, options: DocumentIndexParameters? = nil) async throws -> (Data?, URLResponse?) { let queryParams = try createURLQuery(forSchema: options) - let (data, response) = try await apiCall.post(endPoint: RESOURCEPATH, body: document, queryParameters: queryParams) + let (data, response) = try await apiCall.post(endPoint: endpointPath(), body: document, queryParameters: queryParams) return (data, response) } public func upsert(document: Data, options: DocumentIndexParameters? = nil) async throws -> (Data?, URLResponse?) { var queryParams = try createURLQuery(forSchema: options) queryParams.append(URLQueryItem(name: "action", value: "upsert")) - let (data, response) = try await apiCall.post(endPoint: RESOURCEPATH, body: document, queryParameters: queryParams) + let (data, response) = try await apiCall.post(endPoint: endpointPath(), body: document, queryParameters: queryParams) return (data, response) } @@ -32,7 +30,7 @@ public struct Documents { var queryParams = try createURLQuery(forSchema: options) queryParams.append(URLQueryItem(name: "action", value: "update")) let jsonData = try encoder.encode(document) - let (data, response) = try await apiCall.post(endPoint: RESOURCEPATH, body: jsonData, queryParameters: queryParams) + let (data, response) = try await apiCall.post(endPoint: endpointPath(), body: jsonData, queryParameters: queryParams) if let validData = data { let decodedData = try decoder.decode(T.self, from: validData) return (decodedData, response) @@ -43,7 +41,7 @@ public struct Documents { public func update(document: T, options: UpdateDocumentsByFilterParameters) async throws -> (UpdateByFilterResponse?, URLResponse?) { let queryParams = try createURLQuery(forSchema: options) let jsonData = try encoder.encode(document) - let (data, response) = try await apiCall.patch(endPoint: RESOURCEPATH, body: jsonData, queryParameters: queryParams) + let (data, response) = try await apiCall.patch(endPoint: endpointPath(), body: jsonData, queryParameters: queryParams) if let validData = data { let decodedData = try decoder.decode(UpdateByFilterResponse.self, from: validData) return (decodedData, response) @@ -53,7 +51,7 @@ public struct Documents { public func delete(options: DeleteDocumentsParameters) async throws -> (DeleteDocumentsResponse?, URLResponse?) { let queryParams = try createURLQuery(forSchema: options) - let (data, response) = try await apiCall.delete(endPoint: "\(RESOURCEPATH)", queryParameters: queryParams) + let (data, response) = try await apiCall.delete(endPoint: endpointPath(), queryParameters: queryParams) if let validData = data { let decodedData = try decoder.decode(DeleteDocumentsResponse.self, from: validData) return (decodedData, response) @@ -70,14 +68,14 @@ public struct Documents { if let givenBatchSize = batchSize { deleteQueryParams.append(URLQueryItem(name: "batch_size", value: String(givenBatchSize))) } - let (data, response) = try await apiCall.delete(endPoint: "\(RESOURCEPATH)", queryParameters: deleteQueryParams) + let (data, response) = try await apiCall.delete(endPoint: endpointPath(), queryParameters: deleteQueryParams) return (data, response) } public func search(_ searchParameters: SearchParameters) async throws -> (Data?, URLResponse?) { let queryParams = try createURLQuery(forSchema: searchParameters) - return try await apiCall.get(endPoint: "\(RESOURCEPATH)/search", queryParameters: queryParams) + return try await apiCall.get(endPoint: endpointPath("search"), queryParameters: queryParams) } public func search(_ searchParameters: SearchParameters, for: T.Type) async throws -> (SearchResult?, URLResponse?) { @@ -307,7 +305,7 @@ public struct Documents { searchQueryParams.append(URLQueryItem(name: "facet_strategy", value: facetReturnParent)) } - let (data, response) = try await apiCall.get(endPoint: "\(RESOURCEPATH)/search", queryParameters: searchQueryParams) + let (data, response) = try await apiCall.get(endPoint: endpointPath("search"), queryParameters: searchQueryParams) if let validData = data { let searchRes = try decoder.decode(SearchResult.self, from: validData) @@ -319,7 +317,7 @@ public struct Documents { public func importBatch(_ documents: Data, options: ImportDocumentsParameters) async throws -> (Data?, URLResponse?) { let queryParams = try createURLQuery(forSchema: options) - let (data, response) = try await apiCall.post(endPoint: "\(RESOURCEPATH)/import", body: documents, queryParameters: queryParams) + let (data, response) = try await apiCall.post(endPoint: endpointPath("import"), body: documents, queryParameters: queryParams) return (data, response) } @@ -329,13 +327,22 @@ public struct Documents { if let specifiedAction = action { importAction.value = specifiedAction.rawValue } - let (data, response) = try await apiCall.post(endPoint: "\(RESOURCEPATH)/import", body: documents, queryParameters: [importAction]) + let (data, response) = try await apiCall.post(endPoint: endpointPath("import"), body: documents, queryParameters: [importAction]) return (data, response) } public func export(options: ExportDocumentsParameters? = nil) async throws -> (Data?, URLResponse?) { let searchQueryParams = try createURLQuery(forSchema: options) - let (data, response) = try await apiCall.get(endPoint: "\(RESOURCEPATH)/export", queryParameters: searchQueryParams) + let (data, response) = try await apiCall.get(endPoint: endpointPath("export"), queryParameters: searchQueryParams) return (data, response) } + + private func endpointPath(_ operation: String? = nil) throws -> String { + let baseEndpoint = try "collections/\(collectionName.encodeURL())/documents" + if let operation: String = operation { + return "\(baseEndpoint)/\(operation)" + } else { + return baseEndpoint + } + } } diff --git a/Sources/Typesense/Errors.swift b/Sources/Typesense/Errors.swift index 0fa84b7..378078a 100644 --- a/Sources/Typesense/Errors.swift +++ b/Sources/Typesense/Errors.swift @@ -21,6 +21,7 @@ extension HTTPError: LocalizedError { public enum URLError: Error { case invalidURL + case encodingError(message: String) } public enum DataError: Error { diff --git a/Sources/Typesense/Override.swift b/Sources/Typesense/Override.swift index 2d9114c..9551615 100644 --- a/Sources/Typesense/Override.swift +++ b/Sources/Typesense/Override.swift @@ -34,8 +34,8 @@ public struct Override { return (nil, response) } - private func endpointPath() -> String { - return "\(Collections.RESOURCEPATH)/\(collectionName)/\(Overrides.RESOURCEPATH)/\(overrideId)" + private func endpointPath() throws -> String { + return try "\(Collections.RESOURCEPATH)/\(collectionName.encodeURL())/\(Overrides.RESOURCEPATH)/\(overrideId.encodeURL())" } diff --git a/Sources/Typesense/Overrides.swift b/Sources/Typesense/Overrides.swift index 6c71ecf..722064c 100644 --- a/Sources/Typesense/Overrides.swift +++ b/Sources/Typesense/Overrides.swift @@ -35,10 +35,10 @@ public struct Overrides { return (nil, nil) } - private func endpointPath(_ operation: String? = nil) -> String { - let baseEndpoint = "\(Collections.RESOURCEPATH)/\(collectionName)/\(Overrides.RESOURCEPATH)" + private func endpointPath(_ operation: String? = nil) throws -> String { + let baseEndpoint = try "\(Collections.RESOURCEPATH)/\(collectionName.encodeURL())/\(Overrides.RESOURCEPATH)" if let operation = operation { - return "\(baseEndpoint)/\(operation)" + return try "\(baseEndpoint)/\(operation.encodeURL())" } else { return baseEndpoint } diff --git a/Sources/Typesense/Preset.swift b/Sources/Typesense/Preset.swift index 9cd107a..91da903 100644 --- a/Sources/Typesense/Preset.swift +++ b/Sources/Typesense/Preset.swift @@ -31,8 +31,8 @@ public struct Preset { return (nil, response) } - private func endpointPath() -> String { - return "\(Presets.RESOURCEPATH)/\(presetName)" + private func endpointPath() throws -> String { + return try "\(Presets.RESOURCEPATH)/\(presetName.encodeURL())" } diff --git a/Sources/Typesense/Presets.swift b/Sources/Typesense/Presets.swift index e4399f2..e24338f 100644 --- a/Sources/Typesense/Presets.swift +++ b/Sources/Typesense/Presets.swift @@ -31,10 +31,10 @@ public struct Presets { return (nil, response) } - private func endpointPath(_ operation: String? = nil) -> String { + private func endpointPath(_ operation: String? = nil) throws -> String { let baseEndpoint = "\(Presets.RESOURCEPATH)" if let operation = operation { - return "\(baseEndpoint)/\(operation)" + return try "\(baseEndpoint)/\(operation.encodeURL())" } else { return baseEndpoint } diff --git a/Sources/Typesense/Stopword.swift b/Sources/Typesense/Stopword.swift index 7c9d7db..e8ef103 100644 --- a/Sources/Typesense/Stopword.swift +++ b/Sources/Typesense/Stopword.swift @@ -31,8 +31,8 @@ public struct Stopword { return (nil, response) } - private func endpointPath() -> String { - return "\(Stopwords.RESOURCEPATH)/\(stopwordsSetId)" + private func endpointPath() throws -> String { + return try "\(Stopwords.RESOURCEPATH)/\(stopwordsSetId.encodeURL())" } diff --git a/Sources/Typesense/Stopwords.swift b/Sources/Typesense/Stopwords.swift index bebdb7a..cbf34f8 100644 --- a/Sources/Typesense/Stopwords.swift +++ b/Sources/Typesense/Stopwords.swift @@ -31,10 +31,10 @@ public struct Stopwords { return (nil, response) } - private func endpointPath(_ operation: String? = nil) -> String { + private func endpointPath(_ operation: String? = nil) throws -> String { let baseEndpoint = "\(Stopwords.RESOURCEPATH)" if let operation = operation { - return "\(baseEndpoint)/\(operation)" + return try "\(baseEndpoint)/\(operation.encodeURL())" } else { return baseEndpoint } diff --git a/Sources/Typesense/Synonyms.swift b/Sources/Typesense/Synonyms.swift index d87d28d..022d81c 100644 --- a/Sources/Typesense/Synonyms.swift +++ b/Sources/Typesense/Synonyms.swift @@ -6,21 +6,18 @@ import Foundation public struct Synonyms { var apiCall: ApiCall var collectionName: String - let RESOURCEPATH: String init(apiCall: ApiCall, collectionName: String) { self.apiCall = apiCall self.collectionName = collectionName - self.RESOURCEPATH = "collections/\(collectionName)/synonyms" } public func upsert(id: String, _ searchSynonym: SearchSynonymSchema) async throws -> (SearchSynonym?, URLResponse?) { var schemaData: Data? = nil - schemaData = try encoder.encode(searchSynonym) if let validSchema = schemaData { - let (data, response) = try await apiCall.put(endPoint: "\(RESOURCEPATH)/\(id)", body: validSchema) + let (data, response) = try await apiCall.put(endPoint: endpointPath(id), body: validSchema) if let result = data { let synonym = try decoder.decode(SearchSynonym.self, from: result) return (synonym, response) @@ -31,7 +28,7 @@ public struct Synonyms { } public func retrieve(id: String) async throws -> (SearchSynonym?, URLResponse?) { - let (data, response) = try await apiCall.get(endPoint: "\(RESOURCEPATH)/\(id)") + let (data, response) = try await apiCall.get(endPoint: endpointPath(id)) if let result = data { let synonym = try decoder.decode(SearchSynonym.self, from: result) return (synonym, response) @@ -40,7 +37,7 @@ public struct Synonyms { } public func retrieve() async throws -> (SearchSynonymsResponse?, URLResponse?) { - let (data, response) = try await apiCall.get(endPoint: "\(RESOURCEPATH)") + let (data, response) = try await apiCall.get(endPoint: endpointPath()) if let result = data { let synonym = try decoder.decode(SearchSynonymsResponse.self, from: result) return (synonym, response) @@ -49,8 +46,16 @@ public struct Synonyms { } public func delete(id: String) async throws -> (Data?, URLResponse?) { - let (data, response) = try await apiCall.get(endPoint: "\(RESOURCEPATH)") + let (data, response) = try await apiCall.get(endPoint: endpointPath()) return (data, response) } + private func endpointPath(_ operation: String? = nil) throws -> String { + let baseEndpoint = try "\(Collections.RESOURCEPATH)/\(collectionName.encodeURL())/synonyms" + if let operation: String = operation { + return "\(baseEndpoint)/\(try operation.encodeURL())" + } else { + return baseEndpoint + } + } } diff --git a/Sources/Typesense/utils/Extensions.swift b/Sources/Typesense/utils/Extensions.swift new file mode 100644 index 0000000..16ef5a2 --- /dev/null +++ b/Sources/Typesense/utils/Extensions.swift @@ -0,0 +1,13 @@ +import Foundation + +private let urlAllowed: CharacterSet = .alphanumerics.union(.init(charactersIn: "-._~")) + +internal extension String { + func encodeURL() throws -> String { + let percentEncoded = self.addingPercentEncoding(withAllowedCharacters: urlAllowed) + guard let valid = percentEncoded else{ + throw URLError.encodingError(message: "Failed to encode URL for string `\(self)`") + } + return valid + } +} \ No newline at end of file diff --git a/Tests/TypesenseTests/CollectionAliasTests.swift b/Tests/TypesenseTests/CollectionAliasTests.swift index 27cd108..2213774 100644 --- a/Tests/TypesenseTests/CollectionAliasTests.swift +++ b/Tests/TypesenseTests/CollectionAliasTests.swift @@ -9,13 +9,13 @@ final class CollectionAliasTests: XCTestCase { func testAliasUpsert() async { do { let aliasCollection = CollectionAliasSchema(collectionName: "companies_june") - let (data, _) = try await client.aliases().upsert(name: "companies", collection: aliasCollection) + let (data, _) = try await client.aliases().upsert(name: "companies-_-~.?test=&123 + # /h/()", collection: aliasCollection) XCTAssertNotNil(data) guard let validData = data else { throw DataError.dataNotFound } print(validData) - XCTAssertEqual(validData.name, "companies") + XCTAssertEqual(validData.name, "companies-_-~.?test=&123 + # /h/()") XCTAssertEqual(validData.collectionName, "companies_june") } catch (let error) { print(error.localizedDescription)