Skip to content

Added getOrCreateIndex function #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## vX.X.X

- Added getOrCreateIndex function (#4)
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ The current functions implemented are:
| Swift function | Similar to API call |
| ------------- | ------------- |
| `createIndex(UID:_)` | `POST - /indexes` |
| `getOrCreateIndex(UID:_)` | `POST - /indexes` `GET - /indexes/:index_uid` |
| `getIndex(UID:_)` | `GET - /indexes/:index_uid` |
| `getIndexes(:_)` | `GET - /indexes` |
| `updateIndex(UID:_)` | `POST - /indexes/:index_uid` |
Expand Down
6 changes: 6 additions & 0 deletions Sources/MeiliSearch/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ public struct MeiliSearch {
self.indexes.create(UID, completion)
}

public func getOrCreateIndex(
UID: String,
_ completion: @escaping (Result<Index, Swift.Error>) -> Void) {
self.indexes.getOrCreate(UID, completion)
}

/**
Get the Index for the given `uid`.

Expand Down
60 changes: 59 additions & 1 deletion Sources/MeiliSearch/Indexes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,57 @@ struct Indexes {

}

func getOrCreate(_ UID: String, _ completion: @escaping (Result<Index, Swift.Error>) -> Void) {

self.create(UID) { result in

switch result {

case .success(let index):
completion(.success(index))

case .failure(let error):
switch error {
case CreateError.indexAlreadyExists:
self.get(UID, completion)
default:
completion(.failure(error))
}

}

}

}
Comment on lines +75 to +82
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ppamorim No, I tell that because I see sometimes an empty line behind a scope close bracket. Like on these lines.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From where this rule is coming from? swiftlint does not complain about it. If you want you can open an issue that we can fix it since this is all over the project.

Please check this out: https://google.github.io/swift/#braces

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ppamorim. It's not a problem. I'm sorry I slowed things down.


enum CreateError: Swift.Error {
case indexAlreadyExists

static func decode(_ error: MSError) -> Swift.Error {

let underlyingError: NSError = error.underlying as NSError

if let data = error.data {

let msErrorResponse: MSErrorResponse?
do {
let decoder: JSONDecoder = JSONDecoder()
msErrorResponse = try decoder.decode(MSErrorResponse.self, from: data)
} catch {
msErrorResponse = nil
}

if underlyingError.code == 400 && msErrorResponse?.message == "Impossible to create index; index already exists" {
return CreateError.indexAlreadyExists
}
return error

}

return error
}
}

func create(
_ UID: String,
_ completion: @escaping (Result<Index, Swift.Error>) -> Void) {
Expand All @@ -80,7 +131,14 @@ struct Indexes {
Indexes.decodeJSON(data, completion)

case .failure(let error):
completion(.failure(error))

switch error {
case let msError as MSError:
completion(.failure(CreateError.decode(msError)))
default:
completion(.failure(error))
}

}

}
Expand Down
2 changes: 1 addition & 1 deletion Sources/MeiliSearch/Model/Update.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public struct Update: Codable, Equatable {

///Duration of the update process.
public let duration: TimeInterval

///Date when the update has been enqueued.
public let enqueuedAt: Date

Expand Down
22 changes: 16 additions & 6 deletions Sources/MeiliSearch/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public protocol URLSessionProtocol {

/// Result for the `execute` function.
typealias DataTaskResult = (Data?, URLResponse?, Error?) -> Void

///Function that will trigger the HTTP request.
func execute(
with request: URLRequest,
Expand All @@ -21,6 +21,15 @@ public protocol URLSessionDataTaskProtocol {
func resume()
}

struct MSError: Swift.Error {
let data: Data?
let underlying: Swift.Error
}

struct MSErrorResponse: Decodable {
let message: String
}

final class Request {

private let config: Config
Expand Down Expand Up @@ -51,7 +60,7 @@ final class Request {
}

let task: URLSessionDataTaskProtocol = session.execute(with: request) { (data, _, error) in
if let error = error {
if let error: Swift.Error = error {
completion(.failure(error))
return
}
Expand All @@ -74,8 +83,9 @@ final class Request {
request.httpBody = data

let task: URLSessionDataTaskProtocol = session.execute(with: request) { (data, _, error) in
if let error = error {
completion(.failure(error))
if let error: Swift.Error = error {
let msError = MSError(data: data, underlying: error)
completion(.failure(msError))
return
}
guard let data = data else {
Expand All @@ -99,7 +109,7 @@ final class Request {
request.httpBody = body

let task: URLSessionDataTaskProtocol = session.execute(with: request) { (data, _, error) in
if let error = error {
if let error: Swift.Error = error {
completion(.failure(error))
return
}
Expand All @@ -119,7 +129,7 @@ final class Request {
request.httpMethod = "DELETE"

let task: URLSessionDataTaskProtocol = session.execute(with: request) { (data, _, error) in
if let error = error {
if let error: Swift.Error = error {
completion(.failure(error))
return
}
Expand Down
88 changes: 88 additions & 0 deletions Tests/MeiliSearchTests/IndexesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,94 @@ class IndexesTests: XCTestCase {

}

func testGetOrCreateIndex() {

//Prepare the mock server

let jsonString = """
{
"name":"Movies",
"uid":"Movies",
"createdAt":"2020-04-04T19:59:49.259572Z",
"updatedAt":"2020-04-04T19:59:49.259579Z",
"primaryKey":null
}
"""

let decoder: JSONDecoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(Formatter.iso8601)
let jsonData = jsonString.data(using: .utf8)!
let stubIndex: Index = try! decoder.decode(Index.self, from: jsonData)

session.pushData(jsonString)

// Start the test with the mocked server

let uid: String = "Movies"

let expectation = XCTestExpectation(description: "Get or create Movies index")

self.client.getOrCreateIndex(UID: uid) { result in
switch result {
case .success(let index):
XCTAssertEqual(stubIndex, index)
expectation.fulfill()
case .failure:
XCTFail("Failed to get or create Movies index")
}
}

self.wait(for: [expectation], timeout: 1.0)

}

func testGetOrCreateIndexAlreadyExists() {

//Prepare the mock server

let createJsonString = """
{"message":"Impossible to create index; index already exists"}
"""

session.pushError(createJsonString, nil, code: 400)

let getJsonString = """
{
"name":"Movies",
"uid":"Movies",
"createdAt":"2020-04-04T19:59:49.259572Z",
"updatedAt":"2020-04-04T19:59:49.259579Z",
"primaryKey":null
}
"""

let decoder: JSONDecoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(Formatter.iso8601)
let jsonData = getJsonString.data(using: .utf8)!
let stubIndex: Index = try! decoder.decode(Index.self, from: jsonData)

session.pushData(getJsonString)

// Start the test with the mocked server

let uid: String = "Movies"

let expectation = XCTestExpectation(description: "Get or create Movies index")

self.client.getOrCreateIndex(UID: uid) { result in
switch result {
case .success(let index):
XCTAssertEqual(stubIndex, index)
expectation.fulfill()
case .failure(let error):
XCTFail("Failed to get or create Movies index, error: \(error)")
}
}

self.wait(for: [expectation], timeout: 1.0)

}

func testGetIndex() {

//Prepare the mock server
Expand Down
60 changes: 43 additions & 17 deletions Tests/MeiliSearchTests/MockURLSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,48 +4,63 @@ import Foundation
class MockURLSession: URLSessionProtocol {

private(set) var nextDataTask = MockURLSessionDataTask()
private(set) var nextData: Data?
private(set) var nextError: Error?
private(set) var nextCode: Int = -1

private(set) var lastURL: URL?

func successHttpURLResponse(request: URLRequest) -> URLResponse {
private(set) var responses: [ResponsePayload] = []

func successHttpURLResponse(_ request: URLRequest, _ nextCode: Int) -> URLResponse {
HTTPURLResponse(url: request.url!, statusCode: nextCode, httpVersion: "HTTP/1.1", headerFields: nil)!
}

func failureHttpURLResponse(request: URLRequest) -> URLResponse {
func failureHttpURLResponse(_ request: URLRequest, _ nextCode: Int) -> URLResponse {
HTTPURLResponse(url: request.url!, statusCode: nextCode, httpVersion: "HTTP/1.1", headerFields: nil)!
}

func execute(with request: URLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTaskProtocol {

if responses.isEmpty {
fatalError()
}

lastURL = request.url

if nextError == nil {
completionHandler(nextData, successHttpURLResponse(request: request), nextError)
let first: ResponsePayload = responses.removeFirst()

if first.nextType == ResponseStatus.success {
completionHandler(first.nextData, successHttpURLResponse(request, first.nextCode), first.nextError)
} else {
completionHandler(nextData, failureHttpURLResponse(request: request), nextError)
completionHandler(first.nextData, failureHttpURLResponse(request, first.nextCode), first.nextError)
}

return nextDataTask
}

func pushData(_ string: String, code: Int = 200) {
self.nextData = string.data(using: .utf8)!
self.nextError = nil
self.nextCode = code
let payload = ResponsePayload(
nextType: ResponseStatus.success,
nextData: string.data(using: .utf8),
nextError: nil,
nextCode: code)
responses.append(payload)
}

func pushEmpty(code: Int) {
self.nextData = nil
self.nextError = nil
self.nextCode = code
let payload = ResponsePayload(
nextType: ResponseStatus.success,
nextData: nil,
nextError: nil,
nextCode: code)
responses.append(payload)
}

func pushError(_ string: String? = nil, _ error: Error? = nil, code: Int) {
self.nextData = string?.data(using: .utf8)
self.nextError = error
self.nextCode = code
let payload = ResponsePayload(
nextType: ResponseStatus.error,
nextData: string?.data(using: .utf8),
nextError: error ?? NSError(domain: "", code: code, userInfo: nil),
nextCode: code)
responses.append(payload)
}

}
Expand All @@ -57,3 +72,14 @@ class MockURLSessionDataTask: URLSessionDataTaskProtocol {
resumeWasCalled = true
}
}

enum ResponseStatus {
case success, error
}

struct ResponsePayload {
let nextType: ResponseStatus
let nextData: Data?
let nextError: Error?
let nextCode: Int
}