user11042303
user11042303

Reputation:

flatMap doesn't get invoked

I'm trying to validate user's email and password using two separate function calls.

Both functions return AnyPublisher publishers, and I use combineLatest to collect the returned values (each validate call returns the string it's validating) into a tuple.

Then I'm using flatMap to make a network request to sign the user up using the values returned by combineLatest, however the flatMap operator never gets called.

validator.validate(text: email, with: [.validEmail])
  .combineLatest(validator.validate(text: password, with: [.notEmpty]))
  .flatMap { credentials in
    return self.userSessionRepository.signUp(email: credentials.0, password: credentials.1)
  }
  .sink(receiveCompletion: { completion in
    switch completion {
    case .failure(let error):
      print(error)
      self.indicateErrorSigningIn(error)
    case .finished:
      self.goToSignInNavigator.navigateToOtp()
    }
  }, receiveValue: { _ in })
  .store(in: &subscriptions)

signUp(email:password:) returns AnyPublisher

Here's the validator function:

public func validate(text: String, with rules: [Rule]) -> AnyPublisher<String, ErrorMessage> {
  rules.publisher
    .compactMap { $0.check(text) }
    .setFailureType(to: ErrorMessage.self)
    .flatMap {
      Fail<Void, ErrorMessage>(error: ErrorMessage(title: "Error", message: $0.description))
    }
    .map { text }
    .eraseToAnyPublisher()
}

And the signUp function:

public func signUp(email: String, password: String) -> AnyPublisher<Void, ErrorMessage> {
  remoteAPI.signUp(email: email, password: password)
    .flatMap(dataStore.save)
    .mapError { error -> ErrorMessage in
      return ErrorMessage(title: "Error", message: error.description)
    }
    .eraseToAnyPublisher()
}

It calls these two functions:

public func signUp(email: String, password: String) -> AnyPublisher<Confirmation, RemoteError> {
  guard email == "[email protected]" else {
    return Fail<Confirmation, RemoteError>(error: .invalidCredentials)
      .eraseToAnyPublisher()
  }

  return Just(Confirmation(otp: "", nonce: "abcd"))
    .setFailureType(to: RemoteError.self)
    .eraseToAnyPublisher()
}

public func save(confirmation: Confirmation) -> AnyPublisher<Void, RemoteError> {
  self.nonce = confirmation.nonce

  return Empty().eraseToAnyPublisher()
}

I'm not sure what's wrong, though it's likely my not understanding Combine enough, as I've just started learning it recently.

Upvotes: 1

Views: 352

Answers (1)

user11042303
user11042303

Reputation:

I have figured it out.

The problem was with the validate(text:with:) function.

In the event of an error, the function behaved correctly, but when there was no error the function wasn't emitting any value, and that's why flatMap or any other operator in the pipeline wasn't being invoked.

The reason it wasn't emitting any values boils down to how the check(_:) function, which is called in compactMap, works. It returns an optional string, which is an error message. But if there's no error, there's no string, and thus no value is emitted.

As a result, the call to .map { text } doesn't get evaluated, and the credential doesn't get returned.

I've changed the code to this and now the program behaves correctly:

public func validate(text: String, with rules: [Rule]) -> AnyPublisher<String, ErrorMessage> {
  rules.publisher
    .setFailureType(to: ErrorMessage.self)
    .tryMap { rule -> String in
      if let error = rule.check(text) {
        throw ErrorMessage(title: "Error", message: error)
      }
      return text
    }
    .mapError { error -> ErrorMessage in
      return error as! ErrorMessage
    }
    .eraseToAnyPublisher()
}

Upvotes: 1

Related Questions