I have to migrate an application from ReactiveCocoa 4 to ReactiveCocoa 5 (due to Swift 3 migration)
The old implementation uses some RACSubject instances for triggering (performOperationSubject.sendNext) an operation and for handling (didOperationSubject.subscribeNext) the result
internal class MyClass {
internal var performOperationSubject: RACSubject = RACSubject()
internal var didOperationSubject: RACSubject = RACSubject()
internal overide init() {
super.init()
self.performOperationSubject.subscribeNext { [weak self](_) in
guard let strongSelf = self else { return }
strongSelf.didOperationSubject.sendNext(result)
}
}
and when the MyClass instance is used
myClassInstance.didOperationSubject.subscribeNext { ... }
myClassInstance.performOperationSubject.sendNext(value)
Unfortunately the RACSubject is no more present in ReactiveCocoa 5 (ReactiveSwift)
How can I replace the RACSubject in this context?
You would use pipe which gives you an input observer and an output signal instead of using a RACSubject for both input and output. The example from the ReactiveSwift docs looks like this:
let (signal, observer) = Signal<String, NoError>.pipe()
signal
.map { string in string.uppercased() }
.observeValues { value in print(value) }
observer.send(value: "a") // Prints A
observer.send(value: "b") // Prints B
observer.send(value: "c") // Prints C
Using Signal.pipe instead of RACSubject:
Since the Signal type, like RACSubject, is always “hot”, there is a special class method for creating a controllable signal. The Signal.pipe method can replace the use of subjects, and expresses intent better by separating the observing API from the sending API.
To use a pipe, set up observers on the signal as desired, then send values to the sink:
let (signal, observer) = Signal<String, NoError>.pipe()
signal.observeValue(value in
// use value
})
observer.send(value: "the value")
Related
I want flow output (return type Flow<T>) from a non-flow function (return typeT).
fun getTotalFiles(): Int
// Say, This is a library function it'll return the number of files (Int) in that folder at that specific moment.
//And,
fun getAllFiles(): List<File>
// Say, This is a library function it'll return all the files (List<File>) in that folder.
The files in that folder can and will change in the future.
Now, I want to constantly observe the output, so how do I implement it?
fun getFlowOfTotalFiles(): Flow<Int> =
// A wrapper function that converts the library function return type to an observable flow, Flow<Int>
//And,
fun getFlowOfAllFiles(): Flow<List<File>> =
// A wrapper function that converts the library function return type to an observable flow, Flow<List<File>>
For specifically monitoring a directory for files, you can use WatchService and convert it to a flow with the flow builder. Something like this:
fun getDirectoryMonitorFlow(directory: String) = flow {
FileSystems.getDefault().newWatchService().use { watchService ->
while (true) {
val watchKey = Path.of(directory).register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY)
if (watchKey.pollEvents().isNotEmpty()) {
emit(Unit)
}
yield() // give flow opportunity to be cancelled.
if (!watchKey.reset()) {
println("Directory became unreadable. Finishing flow.")
break
}
}
}
}
.catch { println("Exception while monitoring directory.") }
.flowOn(Dispatchers.IO)
And then your class might look like:
fun getFlowOfTotalFiles(): Flow<Int> = getFlowOfAllFiles()
.map { it.size }
.distinctUntilChanged()
fun getFlowOfAllFiles(): Flow<List<File>> = flow {
emit(Unit) // so current state is always emitted
emitAll(getDirectoryMonitorFlow(directory))
}
.map {
File(directory).listFiles()?.toList().orEmpty()
}
.flowOn(Dispatchers.IO)
.distinctUntilChanged()
Although you might consider making the first flow a private SharedFlow so you aren't running multiple WatchServices to monitor the same directory concurrently.
I believe you need an infinite loop inside a flow builder, something like the following:
fun getFlowOfTotalFiles(): Flow<Int> = flow {
while (true) {
emit(getTotalFiles())
// delays for 5 sec before next request and
// terminates the infinite cycle when a coroutine,
// that collects this Flow, is canceled
delay(5000)
}
}
fun getAllFilesFlow(): Flow<List<File>> = flow {
while (true) {
emit(getAllFiles())
delay(5000)
}
}
I am experiencing a bit of trouble, while working on my app in SwiftUI.
I want to append relevant data, summarized in an object, to an array and return this.
While returning the array, I could see by debugging, that it is empty. Debugging in the for loop showed me, that location objects are created and appended, but are not being "saved" in the array. The "mapItems" array on the other hand has lots of members.
What am I missing?
Here is the method I came up with:
func searchForLocation(searchTerm: String) -> Array<Location>{
var locations = [Location]
let searchReqeust = MKLocalSearch.Request()
searchRequest.region = region //region is a published variable and is determined before
searchRequest.naturalLanguageQuery = searchTerm
let search = MKLocalSearch(request: searchRequest)
search.start{response, error in
//error handling
.
.
.
//empty response.mapItems handling
.
.
.
for item in response!mapItems{
let location = createLocationFromItem(item: item)
locations.append(location)
}
}
return locations
}
My locations class if following:
class Location: Identifiable{
var id= UUID()
var coordinates: CLLocationCoordinate2d
//And its proper init method
}
Your searchForLocation has an asynchronous function inside (search.start{...}),
and your code returns before it has finished getting the results.
To "wait" for the results use a completion/closure handler,
something like this:
func searchForLocation(searchTerm: String, completion: #escaping ([Location]) -> ()) {
var locations = [Location]() // <-- here
// ....
search.start{response, error in // <-- here asynchronous function
//... todo deal with errors, eg return completion([])
for item in response!mapItems {
let location = createLocationFromItem(item: item)
locations.append(location)
}
completion(locations) // <- here return when finished
}
}
and call the function like this:
searchForLocation(searchTerm: "Tokyo") { results in
print("\(results)") // <-- here results available, not before
}
I suggest you read-up on how to create and use asynchronous functions, these are important concepts to master to code effectively in Swift.
I am creating a service on top of a Ktor client. My payload is XML, and as such a simplified version of my client looks like this :
class MavenClient(private val client : HttpClient) {
private suspend fun getRemotePom(url : String) =
try{ MavenClientSuccess(client.get<POMProject>(url)) }catch (e: Exception) { MavenClientFailure(e)
}
companion object {
fun getDefaultClient(): HttpClient {
return HttpClient(Apache) {
install(JsonFeature) {
serializer = JacksonSerializer(jackson = kotlinXmlMapper)
accept(ContentType.Text.Xml)
accept(ContentType.Application.Xml)
accept(ContentType.Text.Plain)
}
}
}
}
}
Note the use of a custom XMLMapper, attached to a custom data class.
I want to test this class, and follow the documentation.
I end up with the following code for my test client :
private val mockClient = HttpClient(MockEngine) {
engine {
addHandler { request ->
when (request.url.fullUrl) {
"https://lengrand.me/minimal/1.2/minimal-1.2.pom" -> {
respond(minimalResourceStreamPom.readBytes()
, headers = headersOf("Content-Type" to listOf(ContentType.Application.Xml.toString())))
}
"https://lengrand.me/unknown/1.2/unknown-1.2.pom" -> {
respond("", HttpStatusCode.NotFound)
}
else -> error("Unhandled ${request.url.fullUrl}")
}
}
}
// TODO : How do I avoid repeating this again ? That's my implementation?!
install(JsonFeature) {
serializer = JacksonSerializer(jackson = PomParser.kotlinXmlMapper)
accept(ContentType.Text.Xml)
accept(ContentType.Application.Xml)
accept(ContentType.Text.Plain)
}
}
private val Url.hostWithPortIfRequired: String get() = if (port == protocol.defaultPort) host else hostWithPort
private val Url.fullUrl: String get() = "${protocol.name}://$hostWithPortIfRequired$fullPath"
private val mavenClient = MavenClient(mockClient)
Now, I am not worried about the Mapper itself, because I test it directly.
However what bothers me is that I essentially have to duplicate the complete logic of my client to test behaviour?
This seems very brittle, because for example it will cause my tests to fail and have to be updated if I move to Json tomorrow. Same if I start using Response Validation for example.
This is even more true for another client where I am using a defaultRequest, which I have to completely copy over as well:
private val mockClient = HttpClient(MockEngine) {
install(JsonFeature) {
serializer = JacksonSerializer(mapper)
accept(ContentType.Application.Json)
}
defaultRequest {
method = HttpMethod.Get
host = "api.github.com"
header("Accept", "application/vnd.github.v3+json")
if (GithubLogin().hasToken()) header("Authorization", GithubLogin().authToken)
}
Am I doing things wrong? Am I testing too much ? I am curious as to how I can improve this.
Thanks a lot for your input!
P.S : Unrelated but the page about testing on Ktor mentions adding the dependency to the implementation. Sounds like I should use testImplementation instead to avoid shipping the lib with my application ?
The MockEngine is designed for stubbing real HTTP client implementation to test objects that use it. The duplication problem, you encounter, lies in the fact that transforming response body responsibility belongs to the client. So I suggest either use Jackson directly to transform a response body (in this case you don't need to use JsonFeature) or extract common configuration in a extension function and call it for both engines.
There is a Broadcaster, that accepts strings and append them to a StringBuilder.
I want to test it.
I have to use Thread#sleep to wait, while the broadcaster finish processing of strings. I want to remove sleep.
I tried to use Control#debug() unsuccessfully.
public class BroadcasterUnitTest {
#Test
public void test() {
//prepare
Environment.initialize();
Broadcaster<String> sink = Broadcaster.create(Environment.newDispatcher()); //run broadcaster in separate thread (dispatcher)
StringBuilder sb = new StringBuilder();
sink
.observe(s -> sleep(100)) //long-time operation
.consume(sb::append);
//do
sink.onNext("a");
sink.onNext("b");
//assert
sleep(500);//wait while broadcaster finished (if comment this line then the test will fail)
assertEquals("ab", sb.toString());
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
I'm not familiar with Broadcaster (and it's probably deprecated since the question is old), but these 3 ways could be helpful in general:
When testing Project-Reactor's Fluxes and stuff, you're probably better of using their testing library made specially for this. Their reference and the Javadoc on that part are pretty good, and I'll just copy an example that speaks for itself here:
#Test
public void testAppendBoomError() {
Flux<String> source = Flux.just("foo", "bar");
StepVerifier.create(
appendBoomError(source))
.expectNext("foo")
.expectNext("bar")
.expectErrorMessage("boom")
.verify();
}
You could just block() by yourself on the Fluxes and Monos and then run checks. And note that if an error is emitted, this will result in an exception. But have a feeling you'll find yourself needing to write more code for some cases (e.g., checking the Flux has emitted 2 items X & Y then terminated with error) and you'd be then re-implementing StepVerifier.
#Test
public void testFluxOrMono() {
Flux<String> source = Flux.just(2, 3);
List<Integer> result = source
.flatMap(i -> multiplyBy2Async(i))
.collectList()
.block();
// run your asserts on the list. Reminder: the order may not be what you expect because of the `flatMap`
// Or with a Mono:
Integer resultOfMono = Mono.just(5)
.flatMap(i -> multiplyBy2Async(i))
.map(i -> i * 4)
.block();
// run your asserts on the integer
}
You could use the general solutions to async testing like CountDownLatch, but, again, wouldn't recommend and would give you trouble in some cases. For example, if you don't know the number of receivers in advance you'll need to use something else.
Per answer above, I found blockLast() helped.
#Test
public void MyTest()
{
Logs.Info("Start test");
/* 1 */
// Make a request
WebRequest wr1 = new WebRequest("1", "2", "3", "4");
String json1 = wr1.toJson(wr1);
Logs.Info("Flux");
Flux<String> responses = controller.getResponses(json1);
/* 2 */
Logs.Info("Responses in");
responses.subscribe(s -> mySub.myMethod(s)); // Test for strings is in myMethod
Logs.Info("Test thread sleeping");
Thread.sleep(2000);
/* 3 */
Logs.Info("Test thread blocking");
responses.blockLast();
Logs.Info("Finish test");
}
I have two webservice calls. Webservice1 returns Promise[Option[Model]] and Webservice2 should take Model as a parameter and then return a Promise[Option[String]]. This is how I have structured my code:
def call1: Promise[Option[Model]] = for {
response1 <- callService1(requestHolderForService1)
} yield {
for {
response <- response1
} yield ParserForResponse(response)
}
After, this I want to chain my call to service 2 that takes the result from service 1 as a parameter:
def call2:Promise[Option[String]] = call1.flatMap{
optionModel => optionModel.flatMap{
model => callService2(requestHolderForService2(model)).map{
service2Option => service2Option.map{
service2Result => ResultParse(service2Result)
}
}
}
}
The problem is that my call1 returns a Promise[Option[Model]] whereas the return from call2 needs to be Promise[Option[String]]. The issue stems from the intermediate service call
callService2
which returns Promise[Option[JsValue]] and I am unable to figure out the transition from Promise[Option[Model]] -> Promise[Option[JsValue]] -> Promise[Option[String]]
Can somebody point out how I might be able to chain these two calls together using map or flatMap?
Thanks
The "normal" way would not to be working with the promises directly, but futures for those promises, which you can access from scala.concurrent.Promise.future
First you map the future, think of it as when the option arrives, do this transformation with it, then you need to handle the fact that the option may not exist, which also is a map, think of it as if there is a value to this tranformation on it.
so:
val future1: Future[Option[Model]] = (...).future
val future2: Future[Option[String]] = future1.map { maybeModel =>
maybeModel.map(model => model.toString) // or hovewer you make it a string
}
Which you then can use in your second web service call