diff --git a/.gitignore b/.gitignore index f69630c23c..fb5b67685f 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ Thumbs.db # Gradle Files # ################ .gradle +.gradletasknamecache .m2 # Build output directies @@ -62,6 +63,9 @@ bin/ # NetBeans specific files/directories .nbattrs +/.nb-gradle/profiles/private/ +.nb-gradle-properties # Scala build *.cache +/.nb-gradle/private/ diff --git a/CHANGES.md b/CHANGES.md index 527d52ba3c..31e703ad39 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,941 @@ # RxJava Releases # +### Version 0.18.3 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.18.3%22)) ### + +* [Pull 1161] (https://github.com/Netflix/RxJava/pull/1161) Removed use of deprecated API from tests & operators +* [Pull 1162] (https://github.com/Netflix/RxJava/pull/1162) fix to remove drift from schedulePeriodic +* [Pull 1159] (https://github.com/Netflix/RxJava/pull/1159) Rxscala improvement +* [Pull 1164] (https://github.com/Netflix/RxJava/pull/1164) JMH Perf Tests for Schedulers.computation +* [Pull 1158] (https://github.com/Netflix/RxJava/pull/1158) Scheduler correctness improvements. + +### Version 0.18.2 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.18.2%22)) ### + + +* [Pull 1150] (https://github.com/Netflix/RxJava/pull/1150) Fix ReplaySubject Terminal State Race Condition +* [Pull 1144] (https://github.com/Netflix/RxJava/pull/1144) Operator Delay rebase & fixes +* [Pull 1142] (https://github.com/Netflix/RxJava/pull/1142) Update 'contains' signature to 'contains(Object)' +* [Pull 1134] (https://github.com/Netflix/RxJava/pull/1134) OperatorTakeLast +* [Pull 1135] (https://github.com/Netflix/RxJava/pull/1135) OperatorTakeUntil +* [Pull 1137] (https://github.com/Netflix/RxJava/pull/1137) Random fixes to operators multicast, sample, refCount +* [Pull 1138] (https://github.com/Netflix/RxJava/pull/1138) Operator Window and other changes +* [Pull 1131] (https://github.com/Netflix/RxJava/pull/1131) Operator TakeTimed +* [Pull 1130] (https://github.com/Netflix/RxJava/pull/1130) Operator Switch +* [Pull 1129] (https://github.com/Netflix/RxJava/pull/1129) Conditional statements contribution to Operator +* [Pull 1128] (https://github.com/Netflix/RxJava/pull/1128) Fix for SerializedObserverTest +* [Pull 1126] (https://github.com/Netflix/RxJava/pull/1126) Operator When +* [Pull 1125] (https://github.com/Netflix/RxJava/pull/1125) Operator contrib math +* [Pull 1124] (https://github.com/Netflix/RxJava/pull/1124) Add lift to rxscala +* [Pull 1122] (https://github.com/Netflix/RxJava/pull/1122) OperatorSkipUntil +* [Pull 1121] (https://github.com/Netflix/RxJava/pull/1121) OperatorSkipTimed +* [Pull 1120] (https://github.com/Netflix/RxJava/pull/1120) OperatorSequenceEqual +* [Pull 1119] (https://github.com/Netflix/RxJava/pull/1119) OperatorRefCount +* [Pull 1118] (https://github.com/Netflix/RxJava/pull/1118) Operator ParallelMerge +* [Pull 1117] (https://github.com/Netflix/RxJava/pull/1117) Operator OnExceptionResumeNextViaObservable +* [Pull 1115] (https://github.com/Netflix/RxJava/pull/1115) OperatorTakeWhile +* [Pull 1112] (https://github.com/Netflix/RxJava/pull/1112) OperatorThrottleFirst +* [Pull 1111] (https://github.com/Netflix/RxJava/pull/1111) OperatorTimeInterval +* [Pull 1110] (https://github.com/Netflix/RxJava/pull/1110) OperatorOnErrorReturn +* [Pull 1109] (https://github.com/Netflix/RxJava/pull/1109) OperatorOnErrorResumeNextViaObservable +* [Pull 1108] (https://github.com/Netflix/RxJava/pull/1108) OperatorMulticastAndReplay +* [Pull 1107] (https://github.com/Netflix/RxJava/pull/1107) Fix ReplaySubject's double termination problem. +* [Pull 1106] (https://github.com/Netflix/RxJava/pull/1106) OperatorMergeMaxConcurrent +* [Pull 1104] (https://github.com/Netflix/RxJava/pull/1104) Operator merge delay error +* [Pull 1103] (https://github.com/Netflix/RxJava/pull/1103) OperatorJoin +* [Pull 1101] (https://github.com/Netflix/RxJava/pull/1101) Operator async +* [Pull 1100] (https://github.com/Netflix/RxJava/pull/1100) OperatorUsing +* [Pull 1099] (https://github.com/Netflix/RxJava/pull/1099) OperatorToMap +* [Pull 1098] (https://github.com/Netflix/RxJava/pull/1098) OperatorTimerAndSample +* [Pull 1097] (https://github.com/Netflix/RxJava/pull/1097) OperatorToMultimap +* [Pull 1096] (https://github.com/Netflix/RxJava/pull/1096) OperatorGroupJoin +* [Pull 1095] (https://github.com/Netflix/RxJava/pull/1095) OperatorGroupByUntil +* [Pull 1094] (https://github.com/Netflix/RxJava/pull/1094) Operator debounce + + +### Version 0.18.1 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.18.1%22)) ### + +* [Pull 1065] (https://github.com/Netflix/RxJava/pull/1065) Optimize OperatorSkipLastTimed +* [Pull 1073] (https://github.com/Netflix/RxJava/pull/1073) OperatorBuffer +* [Pull 1074] (https://github.com/Netflix/RxJava/pull/1074) OperatorConcat +* [Pull 1088] (https://github.com/Netflix/RxJava/pull/1088) OperatorToObservableFuture +* [Pull 1087] (https://github.com/Netflix/RxJava/pull/1087) OperatorMergeMap +* [Pull 1086] (https://github.com/Netflix/RxJava/pull/1086) OperatorFinallyDo +* [Pull 1085] (https://github.com/Netflix/RxJava/pull/1085) OperatorDistinctUntilChanged +* [Pull 1084] (https://github.com/Netflix/RxJava/pull/1084) OperatorDistinct +* [Pull 1083] (https://github.com/Netflix/RxJava/pull/1083) OperatorDematerialize +* [Pull 1081] (https://github.com/Netflix/RxJava/pull/1081) OperatorDefer +* [Pull 1080] (https://github.com/Netflix/RxJava/pull/1080) OperatorDefaultIfEmpty +* [Pull 1079] (https://github.com/Netflix/RxJava/pull/1079) OperatorCombineLatest +* [Pull 1074] (https://github.com/Netflix/RxJava/pull/1074) OperatorConcat +* [Pull 1073] (https://github.com/Netflix/RxJava/pull/1073) OperatorBuffer +* [Pull 1091] (https://github.com/Netflix/RxJava/pull/1091) Handle Thrown Errors with UnsafeSubscribe +* [Pull 1092] (https://github.com/Netflix/RxJava/pull/1092) Restore ObservableExecutionHook.onCreate + +### Version 0.18.0 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.18.0%22)) ### + +This release takes us a step closer to [1.0](https://github.com/Netflix/RxJava/issues/1001) by completing some of the remaining work on the roadmap. + +##### Scheduler + +The first is simplifying the [Scheduler API](https://github.com/Netflix/RxJava/issues/997). + +The Scheduler API is now simplified to this: + +```java +class Scheduler { + public abstract Worker createWorker(); + public int parallelism(); + public long now(); + + public abstract static class Worker implements Subscription { + public abstract Subscription schedule(Action0 action, long delayTime, TimeUnit unit); + public abstract Subscription schedule(Action0 action); + public Subscription schedulePeriodically(Action0 action, long initialDelay, long period, TimeUnit unit); + public long now(); + } +} +``` + +This is a breaking change if you have a custom `Scheduler` implementation or use a `Scheduler` directly. If you only ever pass in a `Scheduler` via the `Schedulers` factory methods, this change does not affect you. + +Additionally, the `ExecutionScheduler` was removed because a general threadpool does not meet the requirements of sequential execution for an `Observable`. It was replaced with `rx.schedulers.EventLoopScheduler` which is the new default for `Schedulers.computation()`. It is a pool of event loops. + +##### rx.joins + +The `rx.joins` package and associated `when`, `and` and `then` operators were moved out of rxjava-core into a new module rxjava-joins. This is done as the rx.joins API was not yet matured and is not going to happen before 1.0. It was determined low priority and not worth blocking a 1.0 release. If the API matures inside the separate module to the point where it makes sense to bring it back into the core it can be done in the 1.x series. + +##### Deprecation Cleanup + +This releases removes many of the classes and methods that have been deprecated in previous releases. Most of the removed functionality was migrated in previous releases to contrib modules such as rxjava-math, rxjava-async and rxjava-computation-expressions. + +A handful of deprecated items still remain but can not yet be removed until all internal operators are finished migrating to using the `lift`/`Subscriber` design changes done in 0.17.0. + + +The full list of changes in 0.18.0: + +* [Pull 1047] (https://github.com/Netflix/RxJava/pull/1047) Scheduler Simplification +* [Pull 1072] (https://github.com/Netflix/RxJava/pull/1072) Scheduler.Inner -> Scheduler.Worker +* [Pull 1053] (https://github.com/Netflix/RxJava/pull/1053) Deprecation Cleanup +* [Pull 1052] (https://github.com/Netflix/RxJava/pull/1052) Scheduler Cleanup +* [Pull 1048] (https://github.com/Netflix/RxJava/pull/1048) Remove ExecutorScheduler - New ComputationScheduler +* [Pull 1049] (https://github.com/Netflix/RxJava/pull/1049) Move rx.joins to rxjava-joins module +* [Pull 1068] (https://github.com/Netflix/RxJava/pull/1068) add synchronous test of resubscribe after error +* [Pull 1066] (https://github.com/Netflix/RxJava/pull/1066) CompositeSubscription fix +* [Pull 1071] (https://github.com/Netflix/RxJava/pull/1071) Manual Merge of AsObservable +* [Pull 1063] (https://github.com/Netflix/RxJava/pull/1063) Fix bugs in equals and hashCode of Timestamped +* [Pull 1070] (https://github.com/Netflix/RxJava/pull/1070) OperationAny -> OperatorAny +* [Pull 1069] (https://github.com/Netflix/RxJava/pull/1069) OperationAll -> OperatorAll +* [Pull 1058] (https://github.com/Netflix/RxJava/pull/1058) Typo in javadoc +* [Pull 1056] (https://github.com/Netflix/RxJava/pull/1056) Add drop(skip) and dropRight(skipLast) to rxscala +* [Pull 1057] (https://github.com/Netflix/RxJava/pull/1057) Fix: Retry in Scala adaptor is ambiguous +* [Pull 1055] (https://github.com/Netflix/RxJava/pull/1055) Fix: Missing Quasar instrumentation on Observable$2.call +* [Pull 1050] (https://github.com/Netflix/RxJava/pull/1050) Reimplement the 'SkipLast' operator +* [Pull 967] (https://github.com/Netflix/RxJava/pull/967) Reimplement the 'single' operator + + + +### Version 0.17.6 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.17.6%22)) ### + +* [Pull 1031] (https://github.com/Netflix/RxJava/pull/1031) Fix NPE in SubjectSubscriptionManager +* [Pull 1030] (https://github.com/Netflix/RxJava/pull/1030) Benchmarking: Add JMH benchmark for ReplaySubject +* [Pull 1033] (https://github.com/Netflix/RxJava/pull/1033) isolate subscriber used for retries, cleanup tests +* [Pull 1021] (https://github.com/Netflix/RxJava/pull/1021) OperatorWeakBinding to not use WeakReferences anymore +* [Pull 1005] (https://github.com/Netflix/RxJava/pull/1005) add toMap from Java Observable +* [Pull 1040] (https://github.com/Netflix/RxJava/pull/1040) Fixed deadlock in Subjects + OperatorCache +* [Pull 1042] (https://github.com/Netflix/RxJava/pull/1042) Kotlin M7 and full compatibility with 0.17.0 +* [Pull 1035] (https://github.com/Netflix/RxJava/pull/1035) Scala cleanup +* [Pull 1009] (https://github.com/Netflix/RxJava/pull/1009) Android - Adding a new RetainedFragment example +* [Pull 1020] (https://github.com/Netflix/RxJava/pull/1020) Upgrade Gradle wrapper for Android samples to Gradle 1.11 +* [Pull 1038] (https://github.com/Netflix/RxJava/pull/1038) rxjava-android: parameterize OperatorViewClick by concrete view type + +### Version 0.17.5 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.17.5%22)) ### + +* [Pull 1010] (https://github.com/Netflix/RxJava/pull/1010) Observable.unsafeSubscribe +* [Pull 1015] (https://github.com/Netflix/RxJava/pull/1015) Remove Redundant protectivelyWrap Method +* [Pull 1019] (https://github.com/Netflix/RxJava/pull/1019) Fix: retry() never unsubscribes from source until operator completes + +### Version 0.17.4 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.17.4%22)) ### + +* [Pull 990] (https://github.com/Netflix/RxJava/pull/990) Quasar Lightweight Threads/Fibers Contrib Module +* [Pull 1012] (https://github.com/Netflix/RxJava/pull/1012) SerializedObserver: Removed window between the two synchronized blocks + + +### Version 0.17.3 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.17.3%22)) ### + +* [Pull 991] (https://github.com/Netflix/RxJava/pull/991) JMH Benchmark Build Config +* [Pull 993] (https://github.com/Netflix/RxJava/pull/993) JMH Perf Tests +* [Pull 995] (https://github.com/Netflix/RxJava/pull/995) Support Custom JMH Args +* [Pull 996] (https://github.com/Netflix/RxJava/pull/996) JMH Perfshadowjar +* [Pull 1003] (https://github.com/Netflix/RxJava/pull/1003) Func0 can transparently implement java.util.concurrent.Callable +* [Pull 999] (https://github.com/Netflix/RxJava/pull/999) New Implementation of SerializedObserver + +### Version 0.17.2 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.17.2%22)) ### + +* [Pull 963](https://github.com/Netflix/RxJava/pull/963) A more robust JMH benchmarking set-up +* [Pull 964](https://github.com/Netflix/RxJava/pull/964) SubjectSubscriptionManager fix. +* [Pull 970](https://github.com/Netflix/RxJava/pull/970) Notifications for the allocation averse. +* [Pull 973](https://github.com/Netflix/RxJava/pull/973) Merge - Handle Bad Observables +* [Pull 974](https://github.com/Netflix/RxJava/pull/974) TestSubject, TestObserver and TestScheduler Improvements +* [Pull 975](https://github.com/Netflix/RxJava/pull/975) GroupBy & Time Gap Fixes +* [Pull 976](https://github.com/Netflix/RxJava/pull/976) parallel-merge unit test assertions +* [Pull 977](https://github.com/Netflix/RxJava/pull/977) Dematerialize - handle non-materialized terminal events +* [Pull 982](https://github.com/Netflix/RxJava/pull/982) Pivot Operator +* [Pull 984](https://github.com/Netflix/RxJava/pull/984) Tests and Javadoc for Pivot +* [Pull 966](https://github.com/Netflix/RxJava/pull/966) Reimplement the ElementAt operator and add it to rxjava-scala +* [Pull 965](https://github.com/Netflix/RxJava/pull/965) BugFix: Chain Subscription in TimeoutSubscriber and SerializedSubscriber +* [Pull 986](https://github.com/Netflix/RxJava/pull/986) Fix SynchronizedObserver.runConcurrencyTest +* [Pull 987](https://github.com/Netflix/RxJava/pull/987) Fix Non-Deterministic Pivot Test +* [Pull 988](https://github.com/Netflix/RxJava/pull/988) OnErrorFailedException + + +### Version 0.17.1 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.17.1%22)) ### + +* [Pull 953](https://github.com/Netflix/RxJava/pull/953) Make ObserveOnTest.testNonBlockingOuterWhileBlockingOnNext deterministic +* [Pull 930](https://github.com/Netflix/RxJava/pull/930) Initial commit of the Android samples module +* [Pull 938](https://github.com/Netflix/RxJava/pull/938) OperatorWeakBinding (deprecates OperatorObserveFromAndroidComponent) +* [Pull 952](https://github.com/Netflix/RxJava/pull/952) rxjava-scala improvements and reimplemented the `amb` operator +* [Pull 955](https://github.com/Netflix/RxJava/pull/955) Fixed ReplaySubject leak +* [Pull 956](https://github.com/Netflix/RxJava/pull/956) Fixed byLine test to use line.separator system property instead of \n. +* [Pull 958](https://github.com/Netflix/RxJava/pull/958) OperatorSkipWhile +* [Pull 959](https://github.com/Netflix/RxJava/pull/959) OperationToFuture must throw CancellationException on get() if cancelled +* [Pull 928](https://github.com/Netflix/RxJava/pull/928) Fix deadlock in SubscribeOnBounded +* [Pull 960](https://github.com/Netflix/RxJava/pull/960) Unit test for "Cannot subscribe to a Retry observable once all subscribers unsubscribed" +* [Pull 962](https://github.com/Netflix/RxJava/pull/962) Migrate from SynchronizedObserver to SerializedObserver + + +### Version 0.17.0 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.17.0%22)) ### + + +Version 0.17.0 contains some significant signature changes that allow us to significantly improve handling of synchronous Observables and simplify Schedulers. Many of the changes have backwards compatible deprecated methods to ease the migration while some are breaking. + +The new signatures related to `Observable` in this release are: + +```java +// A new create method takes `OnSubscribe` instead of `OnSubscribeFunc` +public final static Observable create(OnSubscribe f) + +// The new OnSubscribe type accepts a Subscriber instead of Observer and does not return a Subscription +public static interface OnSubscribe extends Action1> + +// Subscriber is an Observer + Subscription +public abstract class Subscriber implements Observer, Subscription + +// The main `subscribe` behavior receives a Subscriber instead of Observer +public final Subscription subscribe(Subscriber subscriber) + +// Subscribing with an Observer however is still appropriate +// and the Observer is automatically converted into a Subscriber +public final Subscription subscribe(Observer observer) + +// A new 'lift' function allows composing Operator implementations together +public Observable lift(final Operator lift) + +// The `Operator` used with `lift` +public interface Operator extends Func1, Subscriber> + +``` + +Also changed is the `Scheduler` interface which is much simpler: + +```java +public abstract class Scheduler { + public Subscription schedule(Action1 action); + public Subscription schedule(Action1 action, long delayTime, TimeUnit unit); + public Subscription schedulePeriodically(Action1 action, long initialDelay, long period, TimeUnit unit); + public final Subscription scheduleRecursive(final Action1 action) + public long now(); + public int degreeOfParallelism(); + + public static class Inner implements Subscription { + public abstract void schedule(Action1 action, long delayTime, TimeUnit unit); + public abstract void schedule(Action1 action); + public long now(); + } + + public static final class Recurse { + public final void schedule(); + public final void schedule(long delay, TimeUnit unit); + } +} +``` + + +This release applies many lessons learned over the past year and seeks to streamline the API before we hit 1.0. + +As shown in the code above the changes fall into 2 major sections: + +##### 1) Lift/Operator/OnSubscribe/Subscriber + +Changes that allow unsubscribing from synchronous Observables without needing to add concurrency. + +##### 2) Schedulers + +Simplification of the `Scheduler` interface and make clearer the concept of "outer" and "inner" Schedulers for recursion. + + +#### Lift/Operator/OnSubscribe/Subscriber + +New types `Subscriber` and `OnSubscribe` along with the new `lift` function have been added. The reasons and benefits are as follows: + +##### 1) Synchronous Unsubscribe + +RxJava versions up until 0.16.x are unable to unsubscribe from a synchronous Observable such as this: + +```java +Observable oi = Observable.create(new OnSubscribe() { + + @Override + public void call(Observer Observer) { + for (int i = 1; i < 1000000; i++) { + subscriber.onNext(i); + } + subscriber.onCompleted(); + } +}); +``` + +Subscribing to this `Observable` will always emit all 1,000,000 values even if unsubscribed such as via `oi.take(10)`. + +Version 0.17.0 fixes this issue by injecting the `Subscription` into the `OnSubscribe` function to allow code like this: + +```java +Observable oi = Observable.create(new OnSubscribe() { + + @Override + public void call(Subscriber subscriber) { + // we now receive a Subscriber instead of Observer + for (int i = 1; i < 1000000; i++) { + // the OnSubscribe can now check for isUnsubscribed + if (subscriber.isUnsubscribed()) { + return; + } + subscriber.onNext(i); + } + subscriber.onCompleted(); + } + +}); +``` + +Subscribing to this will now correctly only emit 10 `onNext` and unsubscribe: + +```java +// subscribe with an Observer +oi.take(10).subscribe(new Observer() { + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + println("Received: " + t); + } + +}) +``` + +Or the new `Subscriber` type can be used and the `Subscriber` itself can `unsubscribe`: + +```java +// or subscribe with a Subscriber which supports unsubscribe +oi.subscribe(new Subscriber() { + + @Override + public void onCompleted() { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onNext(Integer t) { + println("Received: " + t); + if(t >= 10) { + // a Subscriber can unsubscribe + this.unsubscribe(); + } + } + +}) +``` + + +##### 2) Custom Operator Chaining + +Because Java doesn't support extension methods, the only approach to applying custom operators without getting them added to `rx.Observable` is using static methods. This has meant code like this: + +```java +MyCustomerOperators.operate(observable.map(...).filter(...).take(5)).map(...).subscribe() +``` + +In reality we want: + +```java +observable.map(...).filter(...).take(5).myCustomOperator().map(...).subscribe() +``` + +Using the newly added `lift` we can get quite close to this: + + +```java +observable.map(...).filter(...).take(5).lift(MyCustomOperator.operate()).map(...).subscribe() +``` + +Here is how the proposed `lift` method looks if all operators were applied with it: + +```java +Observable os = OBSERVABLE_OF_INTEGERS.lift(TAKE_5).lift(MAP_INTEGER_TO_STRING); +``` + +Along with the `lift` function comes a new `Operator` signature: + +```java +public interface Operator extends Func1, Subscriber> +``` + +All operator implementations in the `rx.operators` package will over time be migrated to this new signature. + +NOTE: Operators that have not yet been migrated do not work with synchronous unsubscribe. + + +##### 3) Simpler Operator Implementations + +The `lift` operator injects the necessary `Observer` and `Subscription` instances (via the new `Subscriber` type) and eliminates (for most use cases) the need for manual subscription management. Because the `Subscription` is available in-scope there are no awkward coding patterns needed for creating a `Subscription`, closing over it and returning and taking into account synchronous vs asynchronous. + +For example, the body of `fromIterable` is simply: + +```java +public void call(Subscriber o) { + for (T i : is) { + if (o.isUnsubscribed()) { + return; + } + o.onNext(i); + } + o.onCompleted(); +} +``` + +The `take` operator is: + +```java +public Subscriber call(final Subscriber child) { + final CompositeSubscription parent = new CompositeSubscription(); + if (limit == 0) { + child.onCompleted(); + parent.unsubscribe(); + } + + child.add(parent); + return new Subscriber(parent) { + + int count = 0; + boolean completed = false; + + @Override + public void onCompleted() { + if (!completed) { + child.onCompleted(); + } + } + + @Override + public void onError(Throwable e) { + if (!completed) { + child.onError(e); + } + } + + @Override + public void onNext(T i) { + if (!isUnsubscribed()) { + child.onNext(i); + if (++count >= limit) { + completed = true; + child.onCompleted(); + unsubscribe(); + } + } + } + + }; + } +``` + + +##### 4) Recursion/Loop Performance with Unsubscribe + +The `fromIterable` use case is 20x faster when implemented as a loop instead of recursive scheduler (see https://github.com/Netflix/RxJava/commit/a18b8c1a572b7b9509b7a7fe1a5075ce93657771). + +Several places we can remove recursive scheduling used originally for unsubscribe support and use a loop instead. + + + + + +#### Schedulers + + +Schedulers were greatly simplified to a design based around `Action1`. + +```java +public abstract class Scheduler { + public Subscription schedule(Action1 action); + public Subscription schedule(Action1 action, long delayTime, TimeUnit unit); + public Subscription schedulePeriodically(Action1 action, long initialDelay, long period, TimeUnit unit); + public final Subscription scheduleRecursive(final Action1 action) + public long now(); + public int degreeOfParallelism(); + + public static class Inner implements Subscription { + public abstract void schedule(Action1 action, long delayTime, TimeUnit unit); + public abstract void schedule(Action1 action); + public long now(); + } + + public static final class Recurse { + public final void schedule(); + public final void schedule(long delay, TimeUnit unit); + } +} +``` + +This design change originated from three findings: + +1) It was very easy to cause memory leaks or inadvertent parallel execution since the distinction between outer and inner scheduling was not obvious. + +To solve this the new design explicitly has the outer `Scheduler` and then `Scheduler.Inner` for recursion. + +2) The passing of state is not useful since scheduling over network boundaries with this model does not work. + +In this new design all state passing signatures have been removed. This was determined while implementing a `RemoteScheduler` that attempted to use `observeOn` to transition execution from one machine to another. This does not work because of the requirement for serializing/deserializing the state of the entire execution stack. Migration of work over the network has been bound to be better suited to explicit boundaries established by Subjects. Thus, the complications within the Schedulers are unnecessary. + +3) The number of overloads with different ways of doing the same things were confusing. + +This new design removes all but the essential and simplest methods. + +4) A scheduled task could not do work in a loop and easily be unsubscribed which generally meant less efficient recursive scheduling. + +This new design applies similar principles as done with `lift`/`create`/`OnSubscribe`/`Subscriber` and injects the `Subscription` via the `Inner` interface so a running task can check `isUnsubscribed()`. + + + +WIth this new design, the simplest execution of a single task is: + +```java +Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + doWork(); + } + +}); +``` + +Recursion is easily invoked like this: + +```java +Schedulers.newThread().scheduleRecursive(new Action1() { + + @Override + public void call(Recurse recurse) { + doWork(); + // recurse until unsubscribed (the schedule will do nothing if unsubscribed) + recurse.schedule(); + } + + +}); +``` + +or like this if the outer and inner actions need different behavior: + +```java +Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + doWork(); + // recurse until unsubscribed (the schedule will do nothing if unsubscribed) + inner.schedule(this); + } + +}); +``` + +The use of `Action1` on both the outer and inner levels makes it so recursion that refer to `this` and it works easily. + + +Similar to the new `lift`/`create` pattern with `Subscriber` the `Inner` is also a `Subscription` so it allows efficient loops with `unsubscribe` support: + +```java +Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + while(!inner.isUnsubscribed()) { + doWork(); + } + } + +}); +``` + +An action can now `unsubscribe` the `Scheduler.Inner`: + +```java +Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + while(!inner.isUnsubscribed()) { + int i = doOtherWork(); + if(i > 100) { + // an Action can cause the Scheduler to unsubscribe and stop + inner.unsubscribe(); + } + } + } + +}); +``` + +Typically just stopping is sufficient: + +```java +Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + int i = doOtherWork(); + if (i < 10) { + // recurse until done 10 + inner.schedule(this); + } + } + +}); +``` + +but if other work in other tasks is being done and you want to unsubscribe conditionally you could: + +```java +Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + int i = doOtherWork(); + if (i < 10) { + // recurse until done 10 + inner.schedule(this); + } else { + inner.unsubscribe(); + } + } + +}); +``` + +and the recursion can be delayed: + +```java +Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + doWork(); + // recurse until unsubscribed ... but delay the recursion + inner.schedule(this, 500, TimeUnit.MILLISECONDS); + } + +}); +``` + +The same pattern works with the `Recurse` signature: + +```java +Schedulers.newThread().scheduleRecursive(new Action1() { + + @Override + public void call(Recurse recurse) { + doWork(); + // recurse until unsubscribed (the schedule will do nothing if unsubscribed) + recurse.schedule(500, TimeUnit.MILLISECONDS); + } + + +}); +``` + +The methods on the `Inner` never return a `Subscription` because they are always a single thread/event-loop/actor/etc and controlled by the `Subscription` returned by the initial `Scheduler.schedule` method. This is part of clarifying the contract. + +Thus an `unsubscribe` controlled from the outside would be done like this: + +```java +Subscription s = Schedulers.newThread().schedule(new Action1() { + + @Override + public void call(Inner inner) { + while(!inner.isUnsubscribed()) { + doWork(); + } + } + +}); + +// unsubscribe from outside +s.unsubscribe(); +``` + + + +#### Migration Path + +##### 1) Lift/OnSubscribe/Subscriber + +The `lift` function will not be used by most and is additive so will not affect backwards compatibility. The `Subscriber` type is also additive and for most use cases does not need to be used directly, the `Observer` interface can continue being used. + +The previous `create(OnSubscribeFunc f)` signature has been deprecated so code will work but now have warnings. Please begin migrating code as this will be deleted prior to the 1.0 release. + +Code such as this: + +```java +Observable.create(new OnSubscribeFunc() { + + @Override + public Subscription onSubscribe(Observer o) { + o.onNext(1); + o.onCompleted(); + return Subscriptions.empty(); + } +}); +``` + +should change to this: + +```java +Observable.create(new OnSubscribe() { + + @Override + public void call(Subscriber subscriber) { + subscriber.onNext(1); + subscriber.onCompleted(); + } +}); +``` + +If concurrency was being injected to allow unsubscribe support: + +```java +Observable.create(new OnSubscribeFunc() { + + @Override + public Subscription onSubscribe(final Observer o) { + final BooleanSubscription s = new BooleanSubscription(); + Thread t = new Thread(new Runnable() { + + @Override + public void run() { + int i = 0; + while (!s.isUnsubscribed()) { + o.onNext(i++); + } + } + + }); + t.start(); + return s; + } +}); +``` + +you may no longer need it and can implement like this instead: + +```java +Observable.create(new OnSubscribe() { + + @Override + public void call(Subscriber subscriber) { + int i = 0; + while (!subscriber.isUnsubscribed()) { + subscriber.onNext(i++); + } + } +}); +``` + +or if the concurreny is still desired you can simplify the `Subscription` management: + +```java +Observable.create(new OnSubscribe() { + + @Override + public void call(final Subscriber subscriber) { + Thread t = new Thread(new Runnable() { + + @Override + public void run() { + int i = 0; + while (!subscriber.isUnsubscribed()) { + subscriber.onNext(i++); + } + } + + }); + t.start(); + } +}); +``` + +or use `subscribeOn` which now works to make synchronous `Observables` async while supporting `unsubscribe` (this didn't work before): + +```java +Observable.create(new OnSubscribe() { + + @Override + public void call(Subscriber subscriber) { + int i = 0; + while (!subscriber.isUnsubscribed()) { + subscriber.onNext(i++); + } + } +}).subscribeOn(Schedulers.newThread()); +``` + + + +##### 2) Schedulers + +Custom `Scheduler` implementations will need to be re-implemented and any direct use of the `Scheduler` interface will also need to be updated. + + +##### 3) Subscription + +If you have custom `Subscription` implementations you will see they now need an `isUnsubscribed()` method. + +You can either add this method, or wrap your function using `Subscriptions.create` and it will handle the `isUnsubscribed` behavior and execute your function when `unsubscribe()` is called. + +It is recommended to use `Subscriptions.create` for most `Subscription` usage. + + + +#### The Future... + +We have most if not all operators from Rx.Net that we want or intend to port. We think we have got the `create`/`subscribe` signatures as we want and the `Subscription` and `Scheduler` interfaces are now clean. There is at least one more major topic related to back pressure that may result in signature change in a future release. Beyond that no further major signature changing work is expected prior to 1.0. + +We still need to improve on some of the `Subject` implementations still, particularly `ReplaySubject`. We are beginning to focus after this release on cleaning up all of the operator implementations, stabilizing, fixing bugs and performance tuning. + +As we get closer to 1.0 there will be a release that focused on deleting all deprecated methods so it is suggested to start migrating off of them. + +We appreciate your usage, feedback and contributions and hope the library is creating value for you! + + +#### Pull Requests + + +* [Pull 767](https://github.com/Netflix/RxJava/pull/767) Zip fix for multiple onCompleted and moved unsubscribe outside the lock. +* [Pull 770](https://github.com/Netflix/RxJava/pull/770) Bind Operator +* [Pull 778](https://github.com/Netflix/RxJava/pull/778) Fix zip race condition +* [Pull 784](https://github.com/Netflix/RxJava/pull/784) Lift and Observer+Subscription +* [Pull 793](https://github.com/Netflix/RxJava/pull/793) Observer + Subscriber +* [Pull 796](https://github.com/Netflix/RxJava/pull/796) Add Subscription.isUnsubscribed() +* [Pull 797](https://github.com/Netflix/RxJava/pull/797) Scheduler Outer/Inner [Preview] +* [Pull 805](https://github.com/Netflix/RxJava/pull/805) Fix CompositeException +* [Pull 785](https://github.com/Netflix/RxJava/pull/785) Reimplement Zip Operator Using Lift [Preview] +* [Pull 814](https://github.com/Netflix/RxJava/pull/814) RunAsync method for outputting multiple values +* [Pull 812](https://github.com/Netflix/RxJava/pull/812) Fixed OperationSubscribeOn so OperationConditionalsTest works again. +* [Pull 816](https://github.com/Netflix/RxJava/pull/816) One global onCompleted object +* [Pull 818](https://github.com/Netflix/RxJava/pull/818) CompositeSubscription memory reduction +* [Pull 817](https://github.com/Netflix/RxJava/pull/817) Scala Scheduler Bindings Fix +* [Pull 819](https://github.com/Netflix/RxJava/pull/819) CompositeSubscription performance increase +* [Pull 781](https://github.com/Netflix/RxJava/pull/781) Fixed buglet in join binding, simplified types +* [Pull 783](https://github.com/Netflix/RxJava/pull/783) Implement some Android UI related operators +* [Pull 821](https://github.com/Netflix/RxJava/pull/821) Update to use Subscriber/Subscriptions.create +* [Pull 826](https://github.com/Netflix/RxJava/pull/826) Return wrapped Subscription +* [Pull 824](https://github.com/Netflix/RxJava/pull/824) Set setDaemon on NewThreadScheduler +* [Pull 828](https://github.com/Netflix/RxJava/pull/828) Repeat Operator +* [Pull 827](https://github.com/Netflix/RxJava/pull/827) Fixed cut & paster error in io scheduler +* [Pull 833](https://github.com/Netflix/RxJava/pull/833) Take operator was breaking the unsubscribe chain +* [Pull 822](https://github.com/Netflix/RxJava/pull/822) Reimplement 'subscribeOn' using 'lift' +* [Pull 832](https://github.com/Netflix/RxJava/pull/832) Issue #831 Fix for OperationJoin race condition +* [Pull 834](https://github.com/Netflix/RxJava/pull/834) Update clojure for 0.17 +* [Pull 839](https://github.com/Netflix/RxJava/pull/839) Error Handling: OnErrorNotImplemented and java.lang.Error +* [Pull 838](https://github.com/Netflix/RxJava/pull/838) Make Scala OnCompleted Notification an object +* [Pull 837](https://github.com/Netflix/RxJava/pull/837) Perf with JMH +* [Pull 841](https://github.com/Netflix/RxJava/pull/841) Range OnSubscribe +* [Pull 842](https://github.com/Netflix/RxJava/pull/842) Test Unsubscribe +* [Pull 845](https://github.com/Netflix/RxJava/pull/845) Fix problem with Subscription +* [Pull 847](https://github.com/Netflix/RxJava/pull/847) Various Changes While Fixing GroupBy +* [Pull 849](https://github.com/Netflix/RxJava/pull/849) Add 'Fragment-Host' to rxjava-contrib modules for OSGi +* [Pull 851](https://github.com/Netflix/RxJava/pull/851) Reimplement the timeout operator and fix timeout bugs +* [Pull 846](https://github.com/Netflix/RxJava/pull/846) Added overloaded createRequest method that takes an HttpContext instance +* [Pull 777](https://github.com/Netflix/RxJava/pull/777) Fixed testSingleSourceManyIterators +* [Pull 852](https://github.com/Netflix/RxJava/pull/852) rxjava-debug +* [Pull 853](https://github.com/Netflix/RxJava/pull/853) StringObservable Update +* [Pull 763](https://github.com/Netflix/RxJava/pull/763) Added support for custom functions in combineLatest. +* [Pull 854](https://github.com/Netflix/RxJava/pull/854) The onCreate hook disappeared +* [Pull 857](https://github.com/Netflix/RxJava/pull/857) Change Lift to use rx.Observable.Operator +* [Pull 859](https://github.com/Netflix/RxJava/pull/859) Add 'Fragment-Host' to rxjava-contrib/debug module for OSGi +* [Pull 860](https://github.com/Netflix/RxJava/pull/860) Fixing the generics for merge and lift +* [Pull 863](https://github.com/Netflix/RxJava/pull/863) Optimize SwingMouseEventSource.fromRelativeMouseMotion +* [Pull 862](https://github.com/Netflix/RxJava/pull/862) Update the timeout docs +* [Pull 790](https://github.com/Netflix/RxJava/pull/790) Convert to scan to use lift +* [Pull 866](https://github.com/Netflix/RxJava/pull/866) Update OperationScan to OperatorScan +* [Pull 870](https://github.com/Netflix/RxJava/pull/870) Add the selector variants of timeout in RxScala +* [Pull 874](https://github.com/Netflix/RxJava/pull/874) Update CompositeSubscriptionTest.java +* [Pull 869](https://github.com/Netflix/RxJava/pull/869) subscribeOn + groupBy +* [Pull 751](https://github.com/Netflix/RxJava/pull/751) Provide Observable.timestamp(Scheduler) to be used in the tests. +* [Pull 878](https://github.com/Netflix/RxJava/pull/878) Scheduler.scheduleRecursive +* [Pull 877](https://github.com/Netflix/RxJava/pull/877) Correct synchronization guard in groupByUntil +* [Pull 880](https://github.com/Netflix/RxJava/pull/880) Force ViewObservable be subscribed and unsubscribed in the UI thread +* [Pull 887](https://github.com/Netflix/RxJava/pull/887) Remove Bad Filter Logic +* [Pull 890](https://github.com/Netflix/RxJava/pull/890) Split SubscribeOn into SubscribeOn/UnsubscribeOn +* [Pull 891](https://github.com/Netflix/RxJava/pull/891) Eliminate rx.util.* dumping grounds +* [Pull 881](https://github.com/Netflix/RxJava/pull/881) Lift Performance +* [Pull 893](https://github.com/Netflix/RxJava/pull/893) Change Parallel to use Long instead of Int +* [Pull 894](https://github.com/Netflix/RxJava/pull/894) Synchronized Operator Check for isTerminated +* [Pull 885](https://github.com/Netflix/RxJava/pull/885) Fixed an issue with the from(Reader) added a bunch of unit tests. +* [Pull 896](https://github.com/Netflix/RxJava/pull/896) removing java 7 dep +* [Pull 883](https://github.com/Netflix/RxJava/pull/883) Make Subscriptions of SwingObservable thread-safe +* [Pull 895](https://github.com/Netflix/RxJava/pull/895) Rewrite OperationObserveFromAndroidComponent to OperatorObserveFromAndroid +* [Pull 892](https://github.com/Netflix/RxJava/pull/892) onErrorFlatMap + OnErrorThrowable +* [Pull 898](https://github.com/Netflix/RxJava/pull/898) Handle illegal errors thrown from plugin +* [Pull 901](https://github.com/Netflix/RxJava/pull/901) GroupBy Unit Test from #900 +* [Pull 902](https://github.com/Netflix/RxJava/pull/902) Fixed NullPointerException that may happen on timeout +* [Pull 903](https://github.com/Netflix/RxJava/pull/903) Scheduler.Recurse fields should be private +* [Pull 904](https://github.com/Netflix/RxJava/pull/904) Merge: Unsubscribe Completed Inner Observables +* [Pull 905](https://github.com/Netflix/RxJava/pull/905) RxJavaSchedulers Plugin +* [Pull 909](https://github.com/Netflix/RxJava/pull/909) Scheduler Plugin Refactor +* [Pull 910](https://github.com/Netflix/RxJava/pull/910) Remove groupBy with selector +* [Pull 918](https://github.com/Netflix/RxJava/pull/918) Operator: doOnTerminate +* [Pull 919](https://github.com/Netflix/RxJava/pull/919) BugFix: Zip Never Completes When Zero Observables +* [Pull 920](https://github.com/Netflix/RxJava/pull/920) Delete Deprecated onSubscribeStart That Doesn't Work +* [Pull 922](https://github.com/Netflix/RxJava/pull/922) Changes made while integrating it with our internal system +* [Pull 924](https://github.com/Netflix/RxJava/pull/924) Localized Operator Error Handling +* [Pull 925](https://github.com/Netflix/RxJava/pull/925) Rxjava clojure bindings final +* [Pull 926](https://github.com/Netflix/RxJava/pull/926) TestSubscriber: Default onError and Terminal Latch Behavior +* [Pull 927](https://github.com/Netflix/RxJava/pull/927) TestSubscriber lastSeenThread +* [Pull 936](https://github.com/Netflix/RxJava/pull/936) Skip fixed +* [Pull 942](https://github.com/Netflix/RxJava/pull/942) MathObservable +* [Pull 944](https://github.com/Netflix/RxJava/pull/944) OperationRetry -> OperatorRetry +* [Pull 945](https://github.com/Netflix/RxJava/pull/945) refactor the debug hooks before they become a breaking change. +* [Pull 934](https://github.com/Netflix/RxJava/pull/934) add Observable.startWith(Observable) method and unit test +* [Pull 929](https://github.com/Netflix/RxJava/pull/929) correct link to maven search +* [Pull 923](https://github.com/Netflix/RxJava/pull/923) Observable creation from Subscriber[T]=>Unit for Scala +* [Pull 931](https://github.com/Netflix/RxJava/pull/931) A number of improvements to OperatorObserveFromAndroidComponent +* [Pull 950](https://github.com/Netflix/RxJava/pull/950) Add support for Eclipse PDE + + ### Version 0.16.1 ([Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.netflix.rxjava%22%20AND%20v%3A%220.16.1%22)) ### * [Pull 730](https://github.com/Netflix/RxJava/pull/730) Improve Error Handling and Stacktraces When Unsubscribe Fails diff --git a/README.md b/README.md index 4486b6769d..875c7746ac 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# RxJava: Functional Reactive Programming on the JVM +# RxJava: Reactive Extensions for the JVM This library is a Java implementation of Rx Observables. diff --git a/build.gradle b/build.gradle index 304a26643c..750537e93b 100644 --- a/build.gradle +++ b/build.gradle @@ -9,9 +9,11 @@ apply from: file('gradle/release.gradle') buildscript { repositories { mavenLocal() - mavenCentral() // maven { url 'http://jcenter.bintray.com' } + mavenCentral() + jcenter() } - apply from: file('gradle/buildscript.gradle'), to: buildscript + + apply from: file('gradle/buildscript.gradle'), to: buildscript } allprojects { @@ -19,20 +21,21 @@ allprojects { apply plugin: 'idea' repositories { mavenLocal() - mavenCentral() // maven { url: 'http://jcenter.bintray.com' } + mavenCentral() } } subprojects { apply plugin: 'java' + apply plugin: 'shadow' group = "com.netflix.rxjava" // make 'examples' use the same classpath configurations { examplesCompile.extendsFrom compile examplesRuntime.extendsFrom runtime - perfCompile.extendsFrom compile - perfRuntime.extendsFrom runtime + perfCompile.extendsFrom compile + perfRuntime.extendsFrom runtime } @@ -40,49 +43,102 @@ subprojects { it.classpath = sourceSets.main.compileClasspath } - sourceSets { - //include /src/examples folder + sourceSets { examples - //include /src/perf folder perf { - java { - srcDir 'src/perf/java' - compileClasspath += main.output - runtimeClasspath += main.output - } + compileClasspath += sourceSets.main.output } } + tasks.build { + //include 'examples' in build task + dependsOn(examplesClasses) + dependsOn(perfClasses) + } + + task perfJar(type: Jar, dependsOn: perfClasses) { + from sourceSets.perf.output + sourceSets.main.output + } + dependencies { - perfCompile 'org.openjdk.jmh:jmh-core:0.2' + perfCompile 'org.openjdk.jmh:jmh-core:0.5.3' + perfCompile 'org.openjdk.jmh:jmh-generator-annprocess:0.5.3' + //perfCompile project } - tasks.build { - //include 'examples' in build task - dependsOn(examplesClasses) - //include 'perf' in build task - // dependsOn(perfClasses) //-> Not working so commented out + artifacts { + perfRuntime perfJar } eclipse { - classpath { - // include 'provided' dependencies on the classpath - plusConfigurations += configurations.provided + classpath { plusConfigurations += configurations.perfCompile downloadSources = true downloadJavadoc = true } } - + idea { module { - // include 'provided' dependencies on the classpath - scopes.PROVIDED.plus += configurations.provided - // TODO not sure what to add it to - //scopes.PROVIDED.plus += configurations.perfCompile + scopes.PROVIDED.plus += configurations.perfCompile + scopes.PROVIDED.minus += configurations.compile + } + } + + /** + * By default: Run without arguments this will execute all benchmarks that are found (can take a long time). + * + * Optionally pass arguments for custom execution. Example: + * + * ../gradlew benchmarks '-Pjmh=-f 1 -tu ns -bm avgt -wi 5 -i 5 -r 1 .*OperatorSerializePerf.*' + * + * To see all options: + * + * ../gradlew benchmarks '-Pjmh=-h' + */ + task benchmarks(type: JavaExec) { + main = 'org.openjdk.jmh.Main' + classpath = sourceSets.perf.runtimeClasspath + sourceSets.main.output + maxHeapSize = "512m" + + if (project.hasProperty('jmh')) { + args(jmh.split(' ')) + } else { + //args '-h' // help output + args '-f' // fork + args '1' + args '-tu' // time unit + args 'ns' + args '-bm' // benchmark mode + args 'avgt' + args '-wi' // warmup iterations + args '5' + args '-i' // test iterations + args '5' + args '-r' // time per execution in seconds + args '5' + //args '-prof' // profilers + //args 'HS_GC' // HotSpot (tm) memory manager (GC) profiling via implementation-specific MBeans + //args 'HS_RT' // HotSpot (tm) runtime profiling via implementation-specific MBeans + //args 'HS_THR' // HotSpot (tm) threading subsystem via implementation-specific MBeans + //args 'HS_COMP' // HotSpot (tm) JIT compiler profiling via implementation-specific MBeans + //args 'HS_CL' // HotSpot (tm) classloader profiling via implementation-specific MBeans + //args 'STACK' // Simple and naive Java stack profiler + args '.*OperatorSerializePerf.*' // for running only a specific test } } + + shadow { + classifier = "benchmarks" + includeDependenciesFor = ["runtime", "perfRuntime"] + + transformer(com.github.jengelman.gradle.plugins.shadow.transformers.ManifestResourceTransformer) { + mainClass = "org.openjdk.jmh.Main" + } + } + + shadowJar.dependsOn perfJar } project(':rxjava-core') { diff --git a/gradle.properties b/gradle.properties index 80d3fa81f1..733ef16f47 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.17.0-RC4-SNAPSHOT +version=0.18.4-SNAPSHOT diff --git a/gradle/buildscript.gradle b/gradle/buildscript.gradle index 0b6da7ce84..2d8643103e 100644 --- a/gradle/buildscript.gradle +++ b/gradle/buildscript.gradle @@ -2,10 +2,16 @@ repositories { // Repo in addition to maven central repositories { maven { url 'http://dl.bintray.com/content/netflixoss/external-gradle-plugins/' } } // For gradle-release + maven { + //FIXME: waiting for https://github.com/johnrengelman/shadow/pull/38 to merge + name 'Shadow' + url 'http://dl.bintray.com/content/gvsmirnov/gradle-plugins' + } } dependencies { classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.6.1' classpath 'com.mapvine:gradle-cobertura-plugin:0.1' classpath 'gradle-release:gradle-release:1.1.5' classpath 'org.ajoberstar:gradle-git:0.5.0' + classpath 'com.github.jengelman.gradle.plugins:shadow:0.8.1' } diff --git a/language-adaptors/rxjava-clojure/README.md b/language-adaptors/rxjava-clojure/README.md index b2f69b3853..e3729bdc94 100644 --- a/language-adaptors/rxjava-clojure/README.md +++ b/language-adaptors/rxjava-clojure/README.md @@ -1,10 +1,96 @@ -# Clojure Adaptor for RxJava +Clojure bindings for RxJava. +# Binaries + +Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22rxjava-clojure%22). + +Example for Leiningen: + +```clojure +[com.netflix.rxjava/rxjava-clojure "x.y.z"] +``` + +and for Gradle: + +```groovy +compile 'com.netflix.rxjava:rxjava-clojure:x.y.z' +``` + +and for Maven: + +```xml + + com.netflix.rxjava + rxjava-clojure + x.y.z + +``` + +and for Ivy: + +```xml + +``` + +# Clojure Bindings +This library provides convenient, idiomatic Clojure bindings for RxJava. + +The bindings try to present an API that will be comfortable and familiar to a Clojure programmer that's familiar with the sequence operations in `clojure.core`. It "fixes" several issues with using RxJava with raw Java interop, for example: + +* Argument lists are in the "right" order. So in RxJava, the function applied in `Observable.map` is the second argument, while here it's the first argument with one or more Observables as trailing arguments +* Operators take normal Clojure functions as arguments, bypassing need for the interop described below +* Predicates accomodate Clojure's notion of truth +* Operators are generally names as they would be in `clojure.core` rather than the Rx names + +There is no object wrapping going on. That is, all functions return normal `rx.Observable` objects, so you can always drop back to Java interop for anything that's missing in this wrapper. + +## Basic Usage +Most functionality resides in the `rx.lang.clojure.core` namespace and for the most part looks like normal Clojure sequence manipulation: + +```clojure +(require '[rx.lang.clojure.core :as rx]) + +(->> my-observable + (rx/map (comp clojure.string/lower-case :first-name)) + (rx/map clojure.string/lower-case) + (rx/filter #{"bob"}) + (rx/distinct) + (rx/into [])) +;=> An Observable that emits a single vector of names +``` + +Blocking operators, which are useful for testing, but should otherwise be avoided, reside in `rx.lang.clojure.blocking`. For example: + +```clojure +(require '[rx.lang.clojure.blocking :as rxb]) + +(rxb/doseq [{:keys [first-name]} users-observable] + (println "Hey," first-name)) +;=> nil +``` + +## Open Issues + +* The missing stuff mentioned below +* `group-by` val-fn variant isn't implemented in RxJava +* There are some functions for defining customer Observables and Operators (`subscriber`, `operator*`, `observable*`). I don't think these are really enough for serious operator implementation, but I'm hesitant to guess at an abstraction at this point. These will probably change dramatically. + +## What's Missing +This library is an ongoing work in progress driven primarily by the needs of one team at Netflix. As such some things are currently missing: + +* Highly-specific operators that we felt cluttered the API and were easily composed from existing operators, especially since we're in not-Java land. For example, `Observable.sumLong()`. +* Most everything involving schedulers +* Most everything involving time +* `Observable.window` and `Observable.buffer`. Who knows which parts of these beasts to wrap? + +Of course, contributions that cover these cases are welcome. + +# Low-level Interop This adaptor provides functions and macros to ease Clojure/RxJava interop. In particular, there are functions and macros for turning Clojure functions and code into RxJava `Func*` and `Action*` interfaces without the tedium of manually reifying the interfaces. -# Basic Usage +## Basic Usage -## Requiring the interop namespace +### Requiring the interop namespace The first thing to do is to require the namespace: ```clojure @@ -19,23 +105,23 @@ or, at the REPL: (require '[rx.lang.clojure.interop :as rx]) ``` -## Using rx/fn -Once the namespace is required, you can use the `rx/fn` macro anywhere RxJava wants a `rx.util.functions.Func` object. The syntax is exactly the same as `clojure.core/fn`: +### Using rx/fn +Once the namespace is required, you can use the `rx/fn` macro anywhere RxJava wants a `rx.functions.Func` object. The syntax is exactly the same as `clojure.core/fn`: ```clojure (-> my-observable (.map (rx/fn [v] (* 2 v)))) ``` -If you already have a plain old Clojure function you'd like to use, you can pass it to the `rx/fn*` function to get a new object that implements `rx.util.functions.Func`: +If you already have a plain old Clojure function you'd like to use, you can pass it to the `rx/fn*` function to get a new object that implements `rx.functions.Func`: ```clojure (-> my-numbers (.reduce (rx/fn* +))) ``` -## Using rx/action -The `rx/action` macro is identical to `rx/fn` except that the object returned implements `rx.util.functions.Action` interfaces. It's used in `subscribe` and other side-effect-y contexts: +### Using rx/action +The `rx/action` macro is identical to `rx/fn` except that the object returned implements `rx.functions.Action` interfaces. It's used in `subscribe` and other side-effect-y contexts: ```clojure (-> my-observable @@ -46,8 +132,8 @@ The `rx/action` macro is identical to `rx/fn` except that the object returned im (rx/action [] (println "Sequence complete")))) ``` -## Using Observable/create -As of 0.17, `rx.Observable/create` takes an implementation of `rx.Observable$OnSubscribe` which is basically an alias for `rx.util.functions.Action1` that takes an `rx.Subscriber` as its argument. Thus, you can just use `rx/action` when creating new observables: +### Using Observable/create +As of 0.17, `rx.Observable/create` takes an implementation of `rx.Observable$OnSubscribe` which is basically an alias for `rx.functions.Action1` that takes an `rx.Subscriber` as its argument. Thus, you can just use `rx/action` when creating new observables: ```clojure ; A simple observable that emits 0..9 taking unsubscribe into account @@ -59,35 +145,10 @@ As of 0.17, `rx.Observable/create` takes an implementation of `rx.Observable$OnS (.onCompleted s))) ``` -# Gotchas +## Gotchas Here are a few things to keep in mind when using this interop: * Keep in mind the (mostly empty) distinction between `Func` and `Action` and which is used in which contexts * If there are multiple Java methods overloaded by `Func` arity, you'll need to use a type hint to let the compiler know which one to choose. * Methods that take a predicate (like filter) expect the predicate to return a boolean value. A function that returns a non-boolean value will result in a `ClassCastException`. -# Binaries - -Binaries and dependency information for Maven, Ivy, Gradle and others can be found at [http://search.maven.org](http://search.maven.org/#search%7Cga%7C1%7Ca%3A%22rxjava-clojure%22). - -Example for Maven: - -```xml - - com.netflix.rxjava - rxjava-clojure - x.y.z - -``` - -and for Ivy: - -```xml - -``` - -and for Leiningen: - -```clojure -[com.netflix.rxjava/rxjava-clojure "x.y.z"] -``` diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj new file mode 100644 index 0000000000..feee933225 --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/blocking.clj @@ -0,0 +1,140 @@ +(ns rx.lang.clojure.blocking + "Blocking operators and functions. These should never be used in + production code except at the end of an async chain to convert from + rx land back to sync land. For example, to produce a servlet response. + + If you use these, you're a bad person. + " + (:refer-clojure :exclude [first into doseq last]) + (:require [rx.lang.clojure.interop :as iop] [rx.lang.clojure.core :as rx]) + (:import [rx Observable] + [rx.observables BlockingObservable])) + +(def ^:private -ns- *ns*) +(set! *warn-on-reflection* true) + +(defmacro ^:private with-ex-unwrap + "The blocking ops wrap errors stuff in RuntimeException because of stupid Java. + This tries to unwrap them so callers get the exceptions they expect." + [& body] + `(try + ~@body + (catch RuntimeException e# + (throw (or + (and (identical? RuntimeException (class e#)) + (.getCause e#)) + e#))))) + +(defn ^BlockingObservable ->blocking + "Convert an Observable to a BlockingObservable. + + If o is already a BlockingObservable it's returned unchanged. + " + [o] + (if (instance? BlockingObservable o) + o + (.toBlockingObservable ^Observable o))) + +(defn o->seq + "Returns a lazy sequence of the items emitted by o + + See: + rx.observables.BlockingObservable/getIterator + rx.lang.clojure.core/seq->o + " + [o] + (-> (->blocking o) + (.getIterator) + (iterator-seq))) + +(defn first + "*Blocks* and waits for the first value emitted by the given observable. + + If the Observable is empty, returns nil + + If an error is produced it is thrown. + + See: + clojure.core/first + rx/first + rx.observables.BlockingObservable/first + " + [observable] + (with-ex-unwrap + (.firstOrDefault (->blocking observable) nil))) + +(defn last + "*Blocks* and waits for the last value emitted by the given observable. + + If the Observable is empty, returns nil + + If an error is produced it is thrown. + + See: + clojure.core/last + rx/last + rx.observable.BlockingObservable/last + " + [observable] + (with-ex-unwrap + (.lastOrDefault (->blocking observable) nil))) + +(defn single + "*Blocks* and waits for the first value emitted by the given observable. + + An error is thrown if zero or more then one value is produced. + " + [observable] + (with-ex-unwrap + (.single (->blocking observable)))) + +(defn into + "*Blocks* and pours the elements emitted by the given observables into + to. + + If an error is produced it is thrown. + + See: + clojure.core/into + rx/into + " + [to from-observable] + (with-ex-unwrap + (clojure.core/into to (o->seq from-observable)))) + +(defn doseq* + "*Blocks* and executes (f x) for each x emitted by xs + + Returns nil. + + See: + doseq + clojure.core/doseq + " + [xs f] + (with-ex-unwrap + (-> (->blocking xs) + (.forEach (rx.lang.clojure.interop/action* f))))) + +(defmacro doseq + "Like clojure.core/doseq except iterates over an observable in a blocking manner. + + Unlike clojure.core/doseq, only supports a single binding + + Returns nil. + + Example: + + (rx-blocking/doseq [{:keys [name]} users-observable] + (println \"User:\" name)) + + See: + doseq* + clojure.core/doseq + " + [bindings & body] + (when (not= (count bindings) 2) + (throw (IllegalArgumentException. (str "sorry, rx/doseq only supports one binding")))) + (let [[k v] bindings] + `(doseq* ~v (fn [~k] ~@body)))) + diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj new file mode 100644 index 0000000000..ec07e51055 --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/chunk.clj @@ -0,0 +1,100 @@ +(ns rx.lang.clojure.chunk + (:refer-clojure :exclude [chunk]) + (:require [rx.lang.clojure.core :as rx])) + +(def ^:private -ns- *ns*) +(set! *warn-on-reflection* true) + +(defn chunk + "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION + + TODO RxJava's much bigger since this was written. Is there something built in? + + Same as rx.Observable.merge(Observable>) but the input Observables + are \"chunked\" so that at most chunk-size of them are \"in flight\" at any given + time. + + The order of the input Observables is not preserved. + + The main purpose here is to allow a large number of Hystrix observables to + be processed in a controlled way so that the Hystrix execution queues aren't + overwhelmed. + + Example: + + (->> users + (rx/map #(-> (GetUserCommand. %) .toObservable)) + (chunk 10)) + + See: + http://netflix.github.io/RxJava/javadoc/rx/Observable.html#merge(rx.Observable) + http://netflix.github.io/RxJava/javadoc/rx/Observable.html#mergeDelayError(rx.Observable) + " + ([chunk-size observable-source] (chunk chunk-size {} observable-source)) + ([chunk-size options observable-source] + (let [new-state-atom #(atom {:in-flight #{} ; observables currently in-flight + :buffered [] ; observables waiting to be emitted + :complete false ; true if observable-source is complete + :observer % }) ; the observer + ps #(do (printf "%s/%d/%d%n" + (:complete %) + (-> % :buffered count) + (-> % :in-flight count)) + (flush)) + + ; Given the current state, returns [action new-state]. action is the + ; next Observable or Throwable to emit, or :complete if we're done. + next-state (fn [{:keys [complete buffered in-flight] :as old}] + (cond + (empty? buffered) [complete old] + + (< (count in-flight) chunk-size) (let [next-o (first buffered)] + [next-o + (-> old + (update-in [:buffered] next) + (update-in [:in-flight] conj next-o))]) + + :else [nil old])) + + ; Advance the state, performing side-effects as necessary + advance! (fn advance! [state-atom] + (let [old-state @state-atom + [action new-state] (next-state old-state)] + (if (compare-and-set! state-atom old-state new-state) + (let [observer (:observer new-state)] + (if (:debug options) (ps new-state)) + (cond + (= :complete action) + (rx/on-completed observer) + + (instance? Throwable action) + (rx/on-error observer action) + + (instance? rx.Observable action) + (rx/on-next observer + (.finallyDo ^rx.Observable action + (reify rx.functions.Action0 + (call [this] + (swap! state-atom update-in [:in-flight] disj action) + (advance! state-atom))))))) + (recur state-atom)))) + + subscribe (fn [state-atom] + (rx/subscribe observable-source + (fn [o] + (swap! state-atom update-in [:buffered] conj o) + (advance! state-atom)) + + (fn [e] + (swap! state-atom assoc :complete e) + (advance! state-atom)) + + (fn [] + (swap! state-atom assoc :complete :complete) + (advance! state-atom)))) + observable (rx/observable* (fn [observer] + (subscribe (new-state-atom observer)))) ] + (if (:delay-error? options) + (rx.Observable/mergeDelayError observable) + (rx.Observable/merge observable))))) + diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj new file mode 100644 index 0000000000..9af3891494 --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/core.clj @@ -0,0 +1,962 @@ +(ns rx.lang.clojure.core + (:refer-clojure :exclude [concat cons count cycle + distinct do drop drop-while + empty every? + filter first future + group-by + interleave interpose into iterate + keep keep-indexed + map mapcat map-indexed + merge next nth partition-all + range reduce reductions + rest seq some sort sort-by split-with + take take-while throw]) + (:require [rx.lang.clojure.interop :as iop] + [rx.lang.clojure.graph :as graph] + [rx.lang.clojure.realized :as realized]) + (:import [rx + Observable + Observer Observable$Operator Observable$OnSubscribe + Subscriber Subscription] + [rx.observables + BlockingObservable + GroupedObservable] + [rx.subscriptions Subscriptions] + [rx.functions Action0 Action1 Func0 Func1 Func2])) + +(set! *warn-on-reflection* true) + +(declare concat* concat map* map map-indexed reduce take take-while) + +(defn ^Func1 fn->predicate + "Turn f into a predicate that returns true/false like Rx predicates should" + [f] + (iop/fn* (comp boolean f))) + +;################################################################################ + +(defn observable? + "Returns true if o is an rx.Observable" + [o] + (instance? Observable o)) + +;################################################################################ + +(defn on-next + "Call onNext on the given observer and return o." + [^Observer o value] + (.onNext o value) + o) + +(defn on-completed + "Call onCompleted on the given observer and return o." + [^Observer o] + (.onCompleted o) + o) + +(defn on-error + "Call onError on the given observer and return o." + [^Observer o e] + (.onError o e) + o) + +(defmacro catch-error-value + "Experimental + + TODO: Better name, better abstraction. + + Evaluate body and return its value. If an exception e is thrown, inject the + given value into the exception's cause and call (on-error error-observer e), + returning e. + + This is meant to facilitate implementing Observers that call user-supplied code + safely. The general pattern is something like: + + (fn [o v] + (rx/catch-error-value o v + (rx/on-next o (some-func v)))) + + If (some-func v) throws an exception, it is caught, v is injected into the + exception's cause (with OnErrorThrowable/addValueAsLastCause) and + (rx/on-error o e) is invoked. + + See: + rx.exceptions.OnErrorThrowable/addValueAsLastCause + " + [error-observer value & body] + `(try + ~@body + (catch Throwable e# + (on-error ~error-observer + (rx.exceptions.OnErrorThrowable/addValueAsLastCause e# ~value)) + e#))) + +;################################################################################ +; Tools for creating new operators and observables + +(declare unsubscribed?) + +(defn ^Subscriber subscriber + "Experimental, subject to change or deletion." + ([o on-next-action] (subscriber o on-next-action nil nil)) + ([o on-next-action on-error-action] (subscriber o on-next-action on-error-action nil)) + ([^Subscriber o on-next-action on-error-action on-completed-action] + (proxy [Subscriber] [o] + (onCompleted [] + (if on-completed-action + (on-completed-action o) + (on-completed o))) + (onError [e] + (if on-error-action + (on-error-action o e) + (on-error o e))) + (onNext [t] + (if on-next-action + (on-next-action o t) + (on-next o t)))))) + +(defn ^Subscription subscription + "Create a new subscription that calls the given no-arg handler function when + unsubscribe is called + + See: + rx.subscriptions.Subscriptions/create + " + [handler] + (Subscriptions/create ^Action0 (iop/action* handler))) + +(defn ^Observable$Operator operator* + "Experimental, subject to change or deletion. + + Returns a new implementation of rx.Observable$Operator that calls the given + function with a rx.Subscriber. The function should return a rx.Subscriber. + + See: + lift + rx.Observable$Operator + " + [f] + {:pre [(fn? f)]} + (reify Observable$Operator + (call [this o] + (f o)))) + +(defn ^Observable observable* + "Create an Observable from the given function. + + When subscribed to, (f subscriber) is called at which point, f can start emitting values, etc. + The passed subscriber is of type rx.Subscriber. + + See: + rx.Subscriber + rx.Observable/create + " + [f] + (Observable/create ^Observable$OnSubscribe (iop/action* f))) + +(defn wrap-on-completed + "Wrap handler with code that automaticaly calls rx.Observable.onCompleted." + [handler] + (fn [^Observer observer] + (handler observer) + (when-not (unsubscribed? observer) + (.onCompleted observer)))) + +(defn wrap-on-error + "Wrap handler with code that automaticaly calls (on-error) if an exception is thrown" + [handler] + (fn [^Observer observer] + (try + (handler observer) + (catch Throwable e + (when-not (unsubscribed? observer) + (.onError observer e)))))) + +(defn lift + "Lift the Operator op over the given Observable xs + + Example: + + (->> my-observable + (rx/lift (rx/operator ...)) + ...) + + See: + rx.Observable/lift + operator + " + [^Observable$Operator op ^Observable xs] + (.lift xs op)) + +;################################################################################ + +(defn ^Subscription subscribe + "Subscribe to the given observable. + + on-X-action is a normal clojure function. + + See: + rx.Observable/subscribe + " + + ([^Observable o on-next-action] + (.subscribe o + ^Action1 (iop/action* on-next-action))) + + ([^Observable o on-next-action on-error-action] + (.subscribe o + ^Action1 (iop/action* on-next-action) + ^Action1 (iop/action* on-error-action))) + + ([^Observable o on-next-action on-error-action on-completed-action] + (.subscribe o + ^Action1 (iop/action* on-next-action) + ^Action1 (iop/action* on-error-action) + ^Action0 (iop/action* on-completed-action)))) + +(defn unsubscribe + "Unsubscribe from Subscription s and return it." + [^Subscription s] + (.unsubscribe s) + s) + +(defn subscribe-on + "Cause subscriptions to the given observable to happen on the given scheduler. + + Returns a new Observable. + + See: + rx.Observable/subscribeOn + " + [^rx.Scheduler s ^Observable xs] + (.subscribeOn xs s)) + +(defn unsubscribe-on + "Cause unsubscriptions from the given observable to happen on the given scheduler. + + Returns a new Observable. + + See: + rx.Observable/unsubscribeOn + " + [^rx.Scheduler s ^Observable xs] + (.unsubscribeOn xs s)) + +(defn unsubscribed? + "Returns true if the given Subscription (or Subscriber) is unsubscribed. + + See: + rx.Observable/create + observable* + " + [^Subscription s] + (.isUnsubscribed s)) + +;################################################################################ +; Functions for creating Observables + +(defn ^Observable never + "Returns an Observable that never emits any values and never completes. + + See: + rx.Observable/never + " + [] + (Observable/never)) + +(defn ^Observable empty + "Returns an Observable that completes immediately without emitting any values. + + See: + rx.Observable/empty + " + [] + (Observable/empty)) + +(defn ^Observable return + "Returns an observable that emits a single value. + + See: + rx.Observable/just + " + [value] + (Observable/just value)) + +(defn ^Observable seq->o + "Make an observable out of some seq-able thing. The rx equivalent of clojure.core/seq." + [xs] + (if-let [s (clojure.core/seq xs)] + (Observable/from ^Iterable s) + (empty))) + +;################################################################################ +; Operators + +(defn serialize + "Serialize execution. + + See: + rx.Observable/serialize + " + ([^Observable xs] + (.serialize xs))) + +(defn merge* + "Merge an Observable of Observables into a single Observable + + If you want clojure.core/merge, it's just this: + + (rx/reduce clojure.core/merge {} maps) + + See: + merge + merge-delay-error* + rx.Observable/merge + " + [^Observable xs] + (Observable/merge xs)) + +(defn ^Observable merge + "Merge one or more Observables into a single observable. + + If you want clojure.core/merge, it's just this: + + (rx/reduce clojure.core/merge {} maps) + + See: + merge* + merge-delay-error + rx.Observable/merge + " + [& os] + (merge* (seq->o os))) + +(defn ^Observable merge-delay-error* + "Same as merge*, but all values are emitted before errors are propagated" + [^Observable xs] + (Observable/mergeDelayError xs)) + +(defn ^Observable merge-delay-error + "Same as merge, but all values are emitted before errors are propagated" + [& os] + (merge-delay-error* (seq->o os))) + +(defn cache + "caches the observable value so that multiple subscribers don't re-evaluate it. + + See: + rx.Observable/cache" + [^Observable xs] + (.cache xs)) + +(defn cons + "cons x to the beginning of xs" + [x xs] + (concat (return x) xs)) + +(defn ^Observable concat + "Concatenate the given Observables one after the another. + + Note that xs is separate Observables which are concatentated. To concatenate an + Observable of Observables, use concat* + + See: + rx.Observable/concat + concat* + " + [& xs] + (Observable/concat (seq->o xs))) + +(defn ^Observable concat* + "Concatenate the given Observable of Observables one after another. + + See: + rx.Observable/concat + concat + " + [^Observable os] + (Observable/concat os)) + +(defn count + "Returns an Observable that emits the number of items is xs as a long. + + See: + rx.Observable/longCount + " + [^Observable xs] + (.longCount xs)) + +(defn cycle + "Returns an Observable that emits the items of xs repeatedly, forever. + + TODO: Other sigs. + + See: + rx.Observable/repeat + clojure.core/cycle + " + [^Observable xs] + (.repeat xs)) + +(defn distinct + "Returns an Observable of the elements of Observable xs with duplicates + removed. key-fn, if provided, is a one arg function that determines the + key used to determined duplicates. key-fn defaults to identity. + + This implementation doesn't use rx.Observable/distinct because it doesn't + honor Clojure's equality semantics. + + See: + clojure.core/distinct + " + ([xs] (distinct identity xs)) + ([key-fn ^Observable xs] + (let [op (operator* (fn [o] + (let [seen (atom #{})] + (subscriber o + (fn [o v] + (let [key (key-fn v)] + (when-not (contains? @seen key) + (swap! seen conj key) + (on-next o v))))))))] + (lift op xs)))) + +(defn ^Observable do + "Returns a new Observable that, for each x in Observable xs, executes (do-fn x), + presumably for its side effects, and then passes x along unchanged. + + If do-fn throws an exception, that exception is emitted via onError and the sequence + is finished. + + Example: + + (->> (rx/seq->o [1 2 3]) + (rx/do println) + ...) + + Will print 1, 2, 3. + + See: + rx.Observable/doOnNext + " + [do-fn ^Observable xs] + (.doOnNext xs (iop/action* do-fn))) + +(defn ^Observable drop + [n ^Observable xs] + (.skip xs n)) + +(defn ^Observable drop-while + [p ^Observable xs] + (.skipWhile xs (fn->predicate p))) + +(defn ^Observable every? + "Returns an Observable that emits a single true value if (p x) is true for + all x in xs. Otherwise emits false. + + See: + clojure.core/every? + rx.Observable/all + " + [p ^Observable xs] + (.all xs (fn->predicate p))) + +(defn ^Observable filter + [p ^Observable xs] + (.filter xs (fn->predicate p))) + +(defn ^Observable first + "Returns an Observable that emits the first item emitted by xs, or an + empty Observable if xs is empty. + + See: + rx.Observable/take(1) + " + [^Observable xs] + (.take xs 1)) + +(defn ^Observable group-by + "Returns an Observable of clojure.lang.MapEntry where the key is the result of + (key-fn x) and the val is an Observable of x for each key. + + This returns a clojure.lang.MapEntry rather than rx.observables.GroupedObservable + for some vague consistency with clojure.core/group-by and so that clojure.core/key, + clojure.core/val and destructuring will work as expected. + + See: + clojure.core/group-by + rx.Observable/groupBy + rx.observables.GroupedObservable + " + ([key-fn ^Observable xs] + (->> (.groupBy xs (iop/fn* key-fn)) + (map (fn [^GroupedObservable go] + (clojure.lang.MapEntry. (.getKey go) go)))))) + +(defn interleave* + "Returns an Observable of the first item in each Observable emitted by observables, then + the second etc. + + observables is an Observable of Observables + + See: + interleave + clojure.core/interleave + " + [observables] + (->> (map* #(seq->o %&) observables) + (concat*))) + +(defn interleave + "Returns an Observable of the first item in each Observable, then the second etc. + + Each argument is an individual Observable + + See: + observable* + clojure.core/interleave + " + [o1 & observables] + (->> (apply map #(seq->o %&) o1 observables) + (concat*))) + +(defn interpose + "Returns an Observable of the elements of xs separated by sep + + See: + clojure.core/interpose + " + [sep xs] + (let [op (operator* (fn [o] + (let [first? (atom true)] + (subscriber o (fn [o v] + (if-not (compare-and-set! first? true false) + (on-next o sep)) + (on-next o v))))))] + (lift op xs))) + +(defn into + "Returns an observable that emits a single value which is all of the + values of from-observable conjoined onto to + + See: + clojure.core/into + rx.Observable/toList + " + [to ^Observable from] + ; clojure.core/into uses transients if to is IEditableCollection + ; I don't think we have any guarantee that all on-next calls will be on the + ; same thread, so we can't do that here. + (reduce conj to from)) + +(defn iterate + "Returns an Observable of x, (f x), (f (f x)) etc. f must be free of side-effects + + See: + clojure.core/iterate + " + [f x] + (observable* (fn [s] + (loop [x x] + (when-not (unsubscribed? s) + (on-next s x) + (recur (f x))))))) + +(defn keep + [f xs] + (filter (complement nil?) (map f xs))) + +(defn keep-indexed + [f xs] + (filter (complement nil?) (map-indexed f xs))) + +(defn ^Observable map* + "Map a function over an Observable of Observables. + + Each item from the first emitted Observable is the first arg, each + item from the second emitted Observable is the second arg, and so on. + + See: + map + clojure.core/map + rx.Observable/zip + " + [f ^Observable observable] + (Observable/zip observable + ^rx.functions.FuncN (iop/fnN* f))) + +(defn ^Observable map + "Map a function over one or more observable sequences. + + Each item from the first Observable is the first arg, each item + from the second Observable is the second arg, and so on. + + See: + clojure.core/map + rx.Observable/zip + " + [f & observables] + (Observable/zip ^Iterable observables + ^rx.functions.FuncN (iop/fnN* f))) + +(defn ^Observable mapcat* + "Same as multi-arg mapcat, but input is an Observable of Observables. + + See: + mapcat + clojure.core/mapcat + " + [f ^Observable xs] + (->> xs + (map* f) + (concat*))) + +(defn ^Observable mapcat + "Returns an observable which, for each value x in xs, calls (f x), which must + return an Observable. The resulting observables are concatentated together + into one observable. + + If multiple Observables are given, the arguments to f are the first item from + each observable, then the second item, etc. + + See: + clojure.core/mapcat + rx.Observable/flatMap + " + [f & xs] + (if (clojure.core/next xs) + (mapcat* f (seq->o xs)) + ; use built-in flatMap for single-arg case + (.flatMap ^Observable (clojure.core/first xs) (iop/fn* f)))) + +(defn map-indexed + "Returns an observable that invokes (f index value) for each value of the input + observable. index starts at 0. + + See: + clojure.core/map-indexed + " + [f xs] + (let [op (operator* (fn [o] + (let [n (atom -1)] + (subscriber o + (fn [o v] + (catch-error-value o v + (on-next o (f (swap! n inc) v))))))))] + (lift op xs))) + +(def next + "Returns an observable that emits all but the first element of the input observable. + + See: + clojure.core/next + " + (partial drop 1)) + +(defn nth + "Returns an Observable that emits the value at the index in the given + Observable. nth throws an IndexOutOfBoundsException unless not-found + is supplied. + + Note that the Observable is the *first* arg! + " + ([^Observable xs index] + (.elementAt xs index)) + ([^Observable xs index not-found] + (.elementAtOrDefault xs index not-found))) + +(defn ^Observable partition-all + "Returns an Observable of Observables of n items each, at offsets step + apart. If step is not supplied, defaults to n, i.e. the partitions + do not overlap. May include partitions with fewer than n items at the end. + + See: + clojure.core/partition-all + rx.Observable/window + " + ([n ^Observable xs] (.window xs (int n))) + ([n step ^Observable xs] (.window xs (int n) (int step)))) + +(defn range + "Returns an Observable nums from start (inclusive) to end + (exclusive), by step, where start defaults to 0, step to 1, and end + to infinity. + + Note: this is not implemented on rx.Observable/range + + See: + clojure.core/range + " + ([] (range 0 Double/POSITIVE_INFINITY 1)) + ([end] (range 0 end 1)) + ([start end] (range start end 1)) + ([start end step] + (observable* (fn [s] + (let [comp (if (pos? step) < >)] + (loop [i start] + (if-not (unsubscribed? s) + (if (comp i end) + (do + (on-next s i) + (recur (+ i step))) + (on-completed s))))))))) + +(defn ^Observable reduce + ([f ^Observable xs] (.reduce xs (iop/fn* f))) + ([f val ^Observable xs] (.reduce xs val (iop/fn* f)))) + +(defn ^Observable reductions + ([f ^Observable xs] (.scan xs (iop/fn* f))) + ([f val ^Observable xs] (.scan xs val (iop/fn* f)))) + +(def rest + "Same as rx/next" + next) + +(defn some + "Returns an observable that emits the first logical true value of (pred x) for + any x in xs, else completes immediately. + + See: + clojure.core/some + " + [p ^Observable xs] + (->> xs + (map p) + (filter identity) + first)) + +(defn ^:private sorted-list-by + ([keyfn coll] (sorted-list-by keyfn clojure.core/compare coll)) + ([keyfn comp ^Observable coll] + (.toSortedList coll (iop/fn [a b] + ; force to int so rxjava doesn't have a fit + (int (comp (keyfn a) (keyfn b))))))) + +(defn sort + "Returns an observable that emits the items in xs, where the sort order is + determined by comparing items. If no comparator is supplied, uses compare. + comparator must implement java.util.Comparator. + + See: + clojure.core/sort + " + ([xs] + (sort clojure.core/compare xs)) + ([comp xs] + (->> xs + (sorted-list-by identity comp) + (mapcat seq->o)))) + +(defn sort-by + "Returns an observable that emits the items in xs, where the sort order is + determined by comparing (keyfn item). If no comparator is supplied, uses + compare. comparator must implement java.util.Comparator. + + See: + clojure.core/sort-by + " + ([keyfn xs] + (sort-by keyfn clojure.core/compare xs)) + ([keyfn comp ^Observable xs] + (->> xs + (sorted-list-by keyfn comp) + (mapcat seq->o)))) + +(defn split-with + "Returns an observable that emits a pair of observables + + [(take-while p xs) (drop-while p xs)] + + See: + rx.lang.clojure/take-while + rx.lang.clojure/drop-while + clojure.core/split-with + " + [p xs] + (return [(take-while p xs) (drop-while p xs)])) + +(defn ^Observable take + "Returns an observable that emits the first n elements of xs. + + See: + clojure.core/take + " + [n ^Observable xs] + {:pre [(>= n 0)]} + (.take xs n)) + +(defn take-while + "Returns an Observable that emits xs until the first x such that + (p x) is falsey. + + See: + clojure.core/take-while + rx.Observable/takeWhile + " + [p ^Observable xs] + (.takeWhile xs (fn->predicate p))) + +;################################################################################; + +(defn throw + "Returns an Observable the simply emits the given exception with on-error + + See: + rx.Observable/error + " + [^Throwable e] + (Observable/error e)) + +(defn catch* + "Returns an observable that, when Observable o triggers an error, e, continues with + Observable returned by (f e) if (p e) is true. If (p e) returns a Throwable + that value is passed as e. + + If p is a class object, a normal instance? check is performed rather than calling it + as a function. If the value returned by (p e) is not true, the error is propagated. + + Examples: + + (->> my-observable + + ; On IllegalArgumentException, just emit 1 + (catch* IllegalArgumentException + (fn [e] (rx/return 1))) + + ; If exception message contains \"WAT\", emit [\\W \\A \\T] + (catch* (fn [e] (-> e .getMessage (.contains \"WAT\"))) + (fn [e] (rx/seq->o [\\W \\A \\T])))) + + See: + rx.Observable/onErrorResumeNext + http://netflix.github.io/RxJava/javadoc/rx/Observable.html#onErrorResumeNext(rx.functions.Func1) + " + [p f ^Observable o] + (let [p (if (class? p) + (fn [e] (.isInstance ^Class p e)) + p)] + (.onErrorResumeNext o + ^Func1 (iop/fn [e] + (if-let [maybe-e (p e)] + (f (if (instance? Throwable maybe-e) + maybe-e + e)) + (rx.lang.clojure.core/throw e)))))) + +(defmacro catch + "Macro version of catch*. + + The body of the catch is wrapped in an implicit (do). It must evaluate to an Observable. + + Note that the source observable is the last argument so this works with ->> but may look + slightly odd when used standalone. + + Example: + + (->> my-observable + ; just emit 0 on IllegalArgumentException + (catch IllegalArgumentException e + (rx/return 0)) + + (catch DependencyException e + (if (.isMinor e) + (rx/return 0) + (rx/throw (WebException. 503))))) + + See: + catch* + " + {:arglists '([p binding & body observable])} + [p binding & body] + (let [o (last body) + body (butlast body)] + `(catch* ~p + (fn [~binding] ~@body) + ~o))) + +(defn finally* + "Returns an Observable that, as a side-effect, executes (f) when the given + Observable completes regardless of success or failure. + + Example: + + (->> my-observable + (finally* (fn [] (println \"Done\")))) + + " + [f ^Observable o] + (.finallyDo o ^Action0 (iop/action* f))) + +(defmacro finally + "Macro version of finally*. + + Note that the source observable is the last argument so this works with ->> but may look + slightly odd when used standalone. + + Example: + + (->> my-observable + (finally (println \"Done\"))) + + See: + finally* + " + {:arglists '([& body observable])} + [& body] + (let [o (last body) + body (butlast body)] + `(finally* (fn [] ~@body) ~o))) + +;################################################################################; + +(defn generator* + "Creates an observable that calls (f observer & args) which should emit values + with (rx/on-next observer value). + + Automatically calls on-completed on return, or on-error if any exception is thrown. + + f should exit early if (rx/unsubscribed? observable) returns true + + Examples: + + ; An observable that emits just 99 + (rx/generator* on-next 99) + " + [f & args] + (observable* (-> #(apply f % args) + wrap-on-completed + wrap-on-error))) + +(defmacro generator + "Create an observable that executes body which should emit values with + (rx/on-next observer value) where observer comes from bindings. + + Automatically calls on-completed on return, or on-error if any exception is thrown. + + The body should exit early if (rx/unsubscribed? observable) returns true + + Examples: + + ; make an observer that emits [0 1 2 3 4] + (generator [observer] + (dotimes [i 5] + (on-next observer i))) + + " + [bindings & body] + `(generator* (fn ~bindings ~@body))) + +;################################################################################; + +; Import public graph symbols here. I want them in this namespace, but implementing +; them here with all the clojure.core symbols excluded is a pain. +(intern *ns* (with-meta 'let-o* (meta #'graph/let-o*)) @#'graph/let-o*) +(intern *ns* (with-meta 'let-o (meta #'graph/let-o)) @#'graph/let-o) + +;################################################################################; + +; Import some public realized symbols here. I want them in this namespace, but implementing +; them here with all the clojure.core symbols excluded is a pain. +(intern *ns* (with-meta 'let-realized (meta #'realized/let-realized)) @#'realized/let-realized) + diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj new file mode 100644 index 0000000000..83b56b27b6 --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/future.clj @@ -0,0 +1,71 @@ +(ns rx.lang.clojure.future + "Functions and macros for making rx-ified futures. That is, run some code in some + other thread and return an Observable of its result. + " + (:require [rx.lang.clojure.interop :as iop] + [rx.lang.clojure.core :as rx])) + +(def ^:private -ns- *ns*) +(set! *warn-on-reflection* true) + +(defn future* + "Exerimental/Possibly a bad idea + + Execute (f & args) in a separate thread and pass the result to onNext. + If an exception is thrown, onError is called with the exception. + + runner is a function that takes a no-arg function argument and returns a future + representing the execution of that function. + + Returns an Observable. If the subscriber unsubscribes, the future will be canceled + with clojure.core/future-cancel + + Examples: + + (subscribe (rx/future future-call + #(slurp \"input.txt\")) + (fn [v] (println \"Got: \" v))) + ; eventually outputs content of input.txt + " + [runner f & args] + {:pre [(ifn? runner) (ifn? f)]} + (rx/observable* (fn [^rx.Subscriber observer] + (let [wrapped (-> #(rx/on-next % (apply f args)) + rx/wrap-on-completed + rx/wrap-on-error) + fu (runner #(wrapped observer))] + (.add observer + (rx/subscription #(future-cancel fu))))))) + +(defn future-generator* + "Exerimental/Possibly a bad idea + + Same as rx/generator* except f is invoked in a separate thread. + + runner is a function that takes a no-arg function argument and returns a future + representing the execution of that function. + + Returns an Observable. If the subscriber unsubscribes, the future will be canceled + with clojure.core/future-cancel + + Example: + + (future-generator* future-call + (fn [o] + (rx/on-next o 1) + (Thread/sleep 1000) + (rx/on-next o 2))) + + See: + rx.lang.clojure.core/generator* + " + [runner f & args] + {:pre [(ifn? runner) (ifn? f)]} + (rx/observable* (fn [^rx.Subscriber observer] + (let [wrapped (-> (fn [o] + (apply f o args)) + rx/wrap-on-completed + rx/wrap-on-error) + fu (runner #(wrapped observer))] + (.add observer + (rx/subscription #(future-cancel fu))))))) diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/graph.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/graph.clj new file mode 100644 index 0000000000..a67ebba47c --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/graph.clj @@ -0,0 +1,141 @@ +(ns rx.lang.clojure.graph + "This is an implementation namespace. Don't use it directly. Use the symbols + in rx.lang.clojure.core + " + (:require [clojure.set :as set])) + +(def ^:private -ns- *ns*) +(set! *warn-on-reflection* true) + +(defn ^:private ->let-o*-observable + [^rx.Observable o n name] + (if (= n 1) + o + ; TODO This is a shortcut. We know the expected number of subscriptions so + ; we only need to cache values until we get the nth subscription at which + ; point, it just becomes a pass through. I haven't found a cache/replay-ish + ; operator that gives this level of control over the cached values + (.cache o))) + +(defn let-o* + "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION + + Given a graph description, returns an observable that emits a single + map of observables all hooked up and ready for subscription. + + A graph is a map from name to a map with keys: + + :deps A vector of dependency names + :factory A function that takes a map from name to Observable + for the names in :deps and returns an Observable + + Returns a map from name to Observable. Additionally, there will be a + ::non-terminals key in the map with a vector of non-terminal names. + + See: + let-o + " + [description] + (let [in-dep-counts (->> description + vals + (mapcat :deps) + frequencies) + terminals (set/difference (set (keys description)) (set (keys in-dep-counts))) + non-terminals (vec (keys in-dep-counts)) + + resolve-node (fn resolve-node [state {:keys [id deps factory] :as node}] + (let [existing (state id)] + (cond + ; It's already resolving up the stack. We've hit a cycle. + (= ::resolving existing) (throw (IllegalArgumentException. (format "Cycle found at '%s'" id))) + + ; It's already resolved. Done. + (not (nil? existing)) state + + :else + ; recursively resolve dependencies + (let [new-state (reduce (fn [s dep] + (if-let [dep-node (description dep)] + (resolve-node s (assoc dep-node :id dep)) + (throw (IllegalArgumentException. (format "Unknown node '%s' referenced from '%s'" dep id))))) + (assoc state id ::resolving) + deps) + ; execute the factory function and wrap it in an observable that delays dependencies + o (-> (select-keys new-state deps) + factory + (->let-o*-observable (in-dep-counts id 1) id))] + ; return the updated state with the resolved node + (assoc new-state id o)))))] + ; resolve the graph and build the result map + (-> (reduce (fn [s [id node]] + (resolve-node s (assoc node :id id))) + {} + description) + (select-keys terminals) + (assoc ::non-terminals non-terminals)))) + +(defmacro let-o + "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION + + Similar to clojure.core/let, but bindings are Observables and the result of the body + must be an Observable. Binding names must start with ?. Binding order doesn't matter + and any binding is visible to all other expressions as long as no cycles are produced + in the resulting Observable expression. + + The key difference here is that the macro can identify the dependencies between Observables + and correctly connect them, protecting from variations in subscribe behavior as well as + the idiosyncracies of setting up multiple subscriptions to Observables. + + This is only very useful for constructing graphs of Observables where you'd usually have + to fiddle around with publish, connect, replay and all that stuff. If you have a linear + sequence of operators, just chain them together. + + Current limitations: + + * All Observables are cache()'d so watch out for large sequences. This will be + fixed eventually. + * let-o cannot be nested. Some deep-walking macro-magic will be required for this. + + Example: + + ; Note that both ?c and ?d depend on ?b and the result Observable depends on + ; ?c and ?d. + (let-o [?a (rx/return 99) + ?b (... some observable network request ...) + ?c (rx/map vector ?a ?b) + ?d (rx/map ... ?b)] + (rx/map vector ?c ?d)) + + See: + let-o* + " + [bindings & result-body] + (let [sym->dep-sym (fn [s] + (when (and (symbol? s) + (not (namespace s)) + (.startsWith (name s) "?")) + s)) + body->dep-syms (fn [body] + (->> body + flatten + (keep sym->dep-sym) + distinct + vec)) + ->node-map (fn [[id & body]] + (let [dep-syms (body->dep-syms body) + dep-keys (->> dep-syms (map (comp keyword name)) vec)] + [(keyword (name id)) {:deps dep-keys + :factory `(fn [{:keys ~dep-syms}] ~@body) }])) + node-map (let [base-map (->> bindings + (partition 2) + (map ->node-map) + (into {})) + result-dep-syms (body->dep-syms result-body)] + (assoc base-map + :rx.lang.clojure.core/result + {:deps (mapv keyword result-dep-syms) + :factory `(fn [{:keys ~result-dep-syms}] ~@result-body) }))] + `(->> ~node-map + let-o* + :rx.lang.clojure.core/result))) + diff --git a/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/realized.clj b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/realized.clj new file mode 100644 index 0000000000..2926633924 --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/main/clojure/rx/lang/clojure/realized.clj @@ -0,0 +1,134 @@ +(ns rx.lang.clojure.realized + (:require [rx.lang.clojure.interop :as iop])) + +(def ^:private -ns- *ns*) +(set! *warn-on-reflection* true) + +(defrecord ^:private PostProc [o f]) + +(defn all + "Tell realized map to capture all output of the observable, not just the last one" + [o] + (->PostProc o identity)) + +(defn only + "Tell realized map to capture the only value emitted by the observable. + If there are 0 or more than one values, an IllegalStateException is thrown + which should propagate to onError. + + This is the default mode of realized-map and let-realized. + " + [o] + (->PostProc o (fn [values] + (condp = (count values) + 1 (first values) + (throw (IllegalStateException. "Observable did not produce exactly one value")))))) + +(defn ^:private ->post-proc + [v] + (cond + (instance? rx.Observable v) (only v) + (instance? PostProc v) v + (vector? v) (->PostProc (first v) + (apply comp (reverse (next v)))) + :else (->post-proc (rx.Observable/just v)))) + +(defn realized-map + "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION + + See let-realized. + + Given a map from key to observable, returns an observable that emits a single + map from the same keys to the values emitted by their corresponding observable. + + keyvals is a list of key/value pairs where key is a key in the emitted map and val + can be one of the following: + + rx.Observable The only value of the emitted sequence is bound to the key. This is the + default since this is often a singleton response from a request. If the + Observable produces 0 or more than 1 values, an IllegalStateException is + produced. + + vector The first element of the vector must be an Observable. Remaining elements + are functions applied in sequence to the list of values emitted by the + observable. For example [my-observable first] will result in a single + value in the emitted map rather than a vector of values. + + other The value is placed in the emitted map as is + + Note the observable can also be wrapped with realized/all to get the full list rather than + just the last value. + + The purpose of this is to simplify the messy pattern of mapping observables to + single key maps, merging and then folding all the separate maps together. So code + like this: + + ; TODO update + (->> (rx/merge (->> (user-info-o user-id) + (rx/map (fn [u] {:user u}))) + (->> (user-likes-o user-id) + (rx/map (fn [u] {:likes u})))) + (rx/reduce merge {})) + + becomes: + + (realized-map :user (user-info-o user-id) + :likes (user-likes-o user-id)) + + See: + let-realized + " + [& keyvals] + (let [o (->> keyvals + (partition 2) + ; generate a sequence of observables + (map (fn [[k v]] + (let [{:keys [^rx.Observable o f]} (->post-proc v)] + ; pour the observable into a single list and apply post-proc func to it + (-> o + .toList + (.map (iop/fn [list] {k (f list)})))))))] + + (-> ^Iterable o + (rx.Observable/merge) ; funnel all the observables into a single sequence + (.reduce {} (iop/fn* merge))))) ; do the map merge dance + +(defn ^rx.Observable realized-map* + "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION + + Same as realized-map, but takes a map argument rather than key-value pairs." + [map-description] + (apply realized-map (apply concat map-description))) + +(defmacro let-realized + "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION + + 'let' version of realized map. + + (let-realized [a (make-observable)] + (* 2 a)) + + is equivalent to: + + (->> (realized-map :a (make-observable)) + (map (fn [{:keys [a]}] (* 2 a)))) + + That is, it eliminates the repition of the map keys when you want to do something + with the final result. + + Evaluates to an Observable that emits the value of the let body. + + See: + rx.lang.clojure.realized/realized-map + rx.lang.clojure.realized/all + " + [bindings & body] + (let [b-parts (partition 2 bindings) + b-map (->> b-parts + (map (fn [[k v]] + [(keyword (name k)) v])) + (into {})) + b-names (mapv first b-parts)] + `(.map (realized-map* ~b-map) + (iop/fn [{:keys ~b-names}] ~@body)))) + diff --git a/language-adaptors/rxjava-clojure/src/main/java/rx/lang/clojure/interop/DummyObservable.java b/language-adaptors/rxjava-clojure/src/main/java/rx/lang/clojure/interop/DummyObservable.java index 44a5a0b66d..805198b6fa 100644 --- a/language-adaptors/rxjava-clojure/src/main/java/rx/lang/clojure/interop/DummyObservable.java +++ b/language-adaptors/rxjava-clojure/src/main/java/rx/lang/clojure/interop/DummyObservable.java @@ -1,5 +1,5 @@ /** - * Copyright 2013 Netflix, Inc. + * Copyright 2014 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/blocking_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/blocking_test.clj new file mode 100644 index 0000000000..260e6a3dc5 --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/blocking_test.clj @@ -0,0 +1,65 @@ +(ns rx.lang.clojure.blocking-test + (:require [rx.lang.clojure.blocking :as b] + [rx.lang.clojure.core :as rx] + [clojure.test :refer [deftest testing is]])) + +(deftest test-->blocking + (testing "returns a BlockingObservable from an Observable" + (is (instance? rx.observables.BlockingObservable (b/->blocking (rx/return 0))))) + + (testing "is idempotent" + (is (instance? rx.observables.BlockingObservable (b/->blocking (b/->blocking (rx/return 0))))))) + + +(deftest test-o->seq + (is (= [1 2 3] (b/o->seq (rx/seq->o [1 2 3]))))) + +(deftest test-first + (testing "returns first element of observable" + (is (= 1 (b/first (rx/seq->o [1 2 3]))))) + (testing "returns nil for empty observable" + (is (nil? (b/first (rx/empty))))) + (testing "rethrows errors" + (is (thrown? java.io.FileNotFoundException + (b/first (rx/throw (java.io.FileNotFoundException. "boo"))))))) + +(deftest test-last + (testing "returns last element of observable" + (is (= 3 (b/last (rx/seq->o [1 2 3]))))) + (testing "returns nil for empty observable" + (is (nil? (b/last (rx/empty))))) + (testing "rethrows errors" + (is (thrown? java.io.FileNotFoundException + (b/last (rx/throw (java.io.FileNotFoundException. "boo"))))))) + +(deftest test-single + (testing "returns one element" + (is (= 1 (b/single (rx/return 1))))) + (testing "throw if empty" + (is (thrown? java.util.NoSuchElementException (b/single (rx/empty))))) + (testing "throw if many" + (is (thrown? java.lang.IllegalArgumentException (b/single (rx/seq->o [1 2]))))) + (testing "rethrows errors" + (is (thrown? java.io.FileNotFoundException + (b/single (rx/throw (java.io.FileNotFoundException. "boo"))))))) + +(deftest test-into + (is (= [1 2 3] + (b/into [1] (rx/seq->o [2 3])))) + (testing "rethrows errors" + (is (thrown? java.io.FileNotFoundException + (b/into #{} (rx/throw (java.io.FileNotFoundException. "boo"))))))) + +(deftest test-doseq + (is (= (range 3) + (let [capture (atom [])] + (b/doseq [{:keys [value]} (rx/seq->o (map #(hash-map :value %) (range 3)))] + (println value) + (swap! capture conj value)) + @capture))) + + (testing "rethrows errors" + (is (thrown? java.io.FileNotFoundException + (b/doseq [i (rx/seq->o (range 3))] + (throw (java.io.FileNotFoundException. "boo"))))))) + diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/chunk_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/chunk_test.clj new file mode 100644 index 0000000000..58ef044c9d --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/chunk_test.clj @@ -0,0 +1,65 @@ +(ns rx.lang.clojure.chunk-test + (:require [rx.lang.clojure.chunk :as rx-chunk] + [rx.lang.clojure.core :as rx] + [rx.lang.clojure.future :as rx-future] + [rx.lang.clojure.blocking :as rx-blocking] + [clojure.test :refer [deftest testing is]])) + + +(deftest test-chunk + (let [n 20 + chunk-size 10 + factory (rx-future/future-generator* + future-call + (fn[o] + (doseq [i (range n)] + (Thread/sleep (rand-int 50)) + (rx/on-next o (rx-future/future* + future-call + #(let [t (rand-int 500)] + (Thread/sleep t) + i))))))] + (is (= (range n) + (sort (rx-blocking/into [] + (rx-chunk/chunk chunk-size {:debug true} factory))))))) + +(deftest test-chunk-with-error + (testing "error from source is propagated" + (let [n 20 + chunk-size 4 + factory (rx-future/future-generator* + future-call + (fn [o] + (doseq [i (range n)] + (Thread/sleep (rand-int 50)) + (rx/on-next o (rx-future/future* + future-call + #(let [t (rand-int 1000)] + (Thread/sleep t) + i)))) + (throw (IllegalArgumentException. "hi"))))] + (is (thrown-with-msg? IllegalArgumentException #"hi" + (rx-blocking/into [] + (rx-chunk/chunk chunk-size {:debug true} factory)))))) + + (testing "error from single observable is propagated" + (let [n 20 + chunk-size 4 + factory (rx-future/future-generator* + future-call + (fn [o] + (doseq [i (range n)] + (Thread/sleep (rand-int 50)) + (rx/on-next o (rx-future/future* + future-call + #(let [t (rand-int 1000)] + (throw (IllegalArgumentException. "byebye")) + (Thread/sleep t) + i))))))] + (is (thrown? rx.exceptions.CompositeException + (rx-blocking/into [] + (rx-chunk/chunk chunk-size + {:debug true + :delay-error? true } + factory))))))) + diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj new file mode 100644 index 0000000000..a4d842e32c --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/core_test.clj @@ -0,0 +1,675 @@ +(ns rx.lang.clojure.core-test + (:require [rx.lang.clojure.core :as rx] + [rx.lang.clojure.blocking :as b] + [rx.lang.clojure.future :as f] + [clojure.test :refer [deftest is testing are]])) + +(deftest test-observable? + (is (rx/observable? (rx/return 99))) + (is (not (rx/observable? "I'm not an observable")))) + +(deftest test-on-next + (testing "calls onNext" + (let [called (atom []) + o (reify rx.Observer (onNext [this value] (swap! called conj value)))] + (is (identical? o (rx/on-next o 1))) + (is (= [1] @called))))) + +(deftest test-on-completed + (testing "calls onCompleted" + (let [called (atom 0) + o (reify rx.Observer (onCompleted [this] (swap! called inc)))] + (is (identical? o (rx/on-completed o))) + (is (= 1 @called))))) + +(deftest test-on-error + (testing "calls onError" + (let [called (atom []) + e (java.io.FileNotFoundException. "yum") + o (reify rx.Observer (onError [this e] (swap! called conj e)))] + (is (identical? o (rx/on-error o e))) + (is (= [e] @called))))) + +(deftest test-catch-error-value + (testing "if no exception, returns body" + (let [o (reify rx.Observer)] + (is (= 3 (rx/catch-error-value o 99 + (+ 1 2)))))) + + (testing "exceptions call onError on observable and inject value in exception" + (let [called (atom []) + e (java.io.FileNotFoundException. "boo") + o (reify rx.Observer + (onError [this e] + (swap! called conj e))) + result (rx/catch-error-value o 100 + (throw e)) + cause (.getCause e)] + (is (identical? e result)) + (is (= [e] @called)) + (when (is (instance? rx.exceptions.OnErrorThrowable$OnNextValue cause)) + (is (= 100 (.getValue cause))))))) + +(deftest test-subscribe + (testing "subscribe overload with only onNext" + (let [o (rx/return 1) + called (atom nil)] + (rx/subscribe o (fn [v] (swap! called (fn [_] v)))) + (is (= 1 @called))))) + +(deftest test-fn->predicate + (are [f arg result] (= result (.call (rx/fn->predicate f) arg)) + identity nil false + identity false false + identity 1 true + identity "true" true + identity true true)) + +(deftest test-subscription + (let [called (atom 0) + s (rx/subscription #(swap! called inc))] + (is (identical? s (rx/unsubscribe s))) + (is (= 1 @called)))) + +(deftest test-unsubscribed? + (let [s (rx/subscription #())] + (is (not (rx/unsubscribed? s))) + (rx/unsubscribe s) + (is (rx/unsubscribed? s)))) + + +(deftest test-observable* + (let [o (rx/observable* (fn [s] + (rx/on-next s 0) + (rx/on-next s 1) + (when-not (rx/unsubscribed? s) (rx/on-next s 2)) + (rx/on-completed s)))] + (is (= [0 1 2] (b/into [] o))))) + +(deftest test-operator* + (let [o (rx/operator* #(rx/subscriber % + (fn [o v] + (if (even? v) + (rx/on-next o v))))) + result (->> (rx/seq->o [1 2 3 4 5]) + (rx/lift o) + (b/into []))] + (is (= [2 4] result)))) + +(deftest test-serialize + ; I'm going to believe serialize works and just exercise it + ; here for sanity. + (is (= [1 2 3] + (->> [1 2 3] + (rx/seq->o) + (rx/serialize) + (b/into []))))) + +(let [expected-result [[1 3 5] [2 4 6]] + sleepy-o #(f/future-generator* + future-call + (fn [o] + (doseq [x %] + (Thread/sleep 10) + (rx/on-next o x)))) + make-inputs (fn [] (mapv sleepy-o expected-result)) + make-output (fn [r] [(keep #{1 3 5} r) + (keep #{2 4 6} r)])] + (deftest test-merge* + (is (= expected-result + (->> (make-inputs) + (rx/seq->o) + (rx/merge*) + (b/into []) + (make-output))))) + (deftest test-merge + (is (= expected-result + (->> (make-inputs) + (apply rx/merge) + (b/into []) + (make-output))))) + (deftest test-merge-delay-error* + (is (= expected-result + (->> (make-inputs) + (rx/seq->o) + (rx/merge-delay-error*) + (b/into []) + (make-output))))) + (deftest test-merge-delay-error + (is (= expected-result + (->> (make-inputs) + (apply rx/merge-delay-error) + (b/into []) + (make-output)))))) + +(deftest test-generator + (testing "calls on-completed automatically" + (let [o (rx/generator [o]) + called (atom nil)] + (rx/subscribe o (fn [v]) (fn [_]) #(reset! called "YES")) + (is (= "YES" @called)))) + + (testing "exceptions automatically go to on-error" + (let [expected (IllegalArgumentException. "hi") + actual (atom nil)] + (rx/subscribe (rx/generator [o] (throw expected)) + #() + #(reset! actual %)) + (is (identical? expected @actual))))) + +(deftest test-seq->o + (is (= [] (b/into [] (rx/seq->o [])))) + (is (= [] (b/into [] (rx/seq->o nil)))) + (is (= [\a \b \c] (b/into [] (rx/seq->o "abc")))) + (is (= [0 1 2 3] (b/first (rx/into [] (rx/seq->o (range 4)))))) + (is (= #{0 1 2 3} (b/first (rx/into #{} (rx/seq->o (range 4)))))) + (is (= {:a 1 :b 2 :c 3} (b/first (rx/into {} (rx/seq->o [[:a 1] [:b 2] [:c 3]])))))) + +(deftest test-return + (is (= [0] (b/into [] (rx/return 0))))) + +(deftest test-cache + (let [value (atom 0) + o (->> + (rx/return 0) + (rx/map (fn [x] (swap! value inc))) + (rx/cache))] + (is (= 1 (b/single o))) + (is (= 1 @value)) + (is (= 1 (b/single o))) + (is (= 1 @value)) + (is (= 1 (b/single o))))) + +(deftest test-cons + (is (= [1] (b/into [] (rx/cons 1 (rx/empty))))) + (is (= [1 2 3 4] (b/into [] (rx/cons 1 (rx/seq->o [2 3 4])))))) + +(deftest test-concat + (is (= [:q :r] + (b/into [] (rx/concat (rx/seq->o [:q :r]))))) + (is (= [:q :r 1 2 3] + (b/into [] (rx/concat (rx/seq->o [:q :r]) + (rx/seq->o [1 2 3])))))) + +(deftest test-concat* + (is (= [:q :r] + (b/into [] (rx/concat* (rx/return (rx/seq->o [:q :r])))))) + (is (= [:q :r 1 2 3] + (b/into [] (rx/concat* (rx/seq->o [(rx/seq->o [:q :r]) + (rx/seq->o [1 2 3])])))))) + +(deftest test-count + (are [xs] (= (count xs) (->> xs (rx/seq->o) (rx/count) (b/single))) + [] + [1] + [5 6 7] + (range 10000))) + +(deftest test-cycle + (is (= [1 2 3 1 2 3 1 2 3 1 2] + (->> [1 2 3] + (rx/seq->o) + (rx/cycle) + (rx/take 11) + (b/into []))))) + +(deftest test-distinct + (let [input [{:a 1} {:a 1} {:b 1} {"a" (int 1)} {:a (int 1)}]] + (is (= (distinct input) + (->> input + (rx/seq->o) + (rx/distinct) + (b/into []))))) + (let [input [{:name "Bob" :x 2} {:name "Jim" :x 99} {:name "Bob" :x 3}]] + (is (= [{:name "Bob" :x 2} {:name "Jim" :x 99}] + (->> input + (rx/seq->o) + (rx/distinct :name) + (b/into [])))))) + +(deftest test-do + (testing "calls a function with each element" + (let [collected (atom [])] + (is (= [1 2 3] + (->> (rx/seq->o [1 2 3]) + (rx/do (fn [v] + (swap! collected conj (* 2 v)))) + (rx/do (partial println "GOT")) + (b/into [])))) + (is (= [2 4 6] @collected)))) + (testing "ends sequence with onError if action code throws an exception" + (let [collected (atom []) + o (->> (rx/seq->o [1 2 3]) + (rx/do (fn [v] + (if (= v 2) + (throw (IllegalStateException. (str "blah" v))) + (swap! collected conj (* 99 v))))))] + (is (thrown-with-msg? IllegalStateException #"blah2" + (b/into [] o))) + (is (= [99] @collected))))) + +(deftest test-drop-while + (is (= (into [] (drop-while even? [2 4 6 8 1 2 3])) + (b/into [] (rx/drop-while even? (rx/seq->o [2 4 6 8 1 2 3]))))) + (is (= (into [] (drop-while even? [2 4 6 8 1 2 3])) + (b/into [] (rx/drop-while even? (rx/seq->o [2 4 6 8 1 2 3])))))) + +(deftest test-every? + (are [xs p result] (= result (->> xs (rx/seq->o) (rx/every? p) (b/single))) + [2 4 6 8] even? true + [2 4 3 8] even? false + [1 2 3 4] #{1 2 3 4} true + [1 2 3 4] #{1 3 4} false)) + +(deftest test-filter + (is (= (into [] (->> [:a :b :c :d :e :f :G :e] + (filter #{:b :e :G}))) + (b/into [] (->> (rx/seq->o [:a :b :c :d :e :f :G :e]) + (rx/filter #{:b :e :G})))))) + +(deftest test-first + (is (= [3] + (b/into [] (rx/first (rx/seq->o [3 4 5]))))) + (is (= [] + (b/into [] (rx/first (rx/empty)))))) + +(deftest test-group-by + (let [xs [{:k :a :v 1} {:k :b :v 2} {:k :a :v 3} {:k :c :v 4}]] + (testing "with just a key-fn" + (is (= [[:a {:k :a :v 1}] + [:b {:k :b :v 2}] + [:a {:k :a :v 3}] + [:c {:k :c :v 4}]] + (->> xs + (rx/seq->o) + (rx/group-by :k) + (rx/mapcat (fn [[k vo :as me]] + (is (instance? clojure.lang.MapEntry me)) + (rx/map #(vector k %) vo))) + (b/into []))))) + + ; TODO reinstate once this is implemented + ; see https://github.com/Netflix/RxJava/commit/02ccc4d727a9297f14219549208757c6e0efce2a + #_(testing "with a val-fn" + (is (= [[:a 1] + [:b 2] + [:a 3] + [:c 4]] + (->> xs + (rx/seq->o) + (rx/group-by :k :v) + (rx/mapcat (fn [[k vo :as me]] + (is (instance? clojure.lang.MapEntry me)) + (rx/map #(vector k %) vo))) + (b/into []))))))) + +(deftest test-interleave + (are [inputs] (= (apply interleave inputs) + (->> (apply rx/interleave (map rx/seq->o inputs)) + (b/into []))) + [[] []] + [[] [1]] + [(range 5) (range 10) (range 10) (range 3)] + [(range 50) (range 10)] + [(range 5) (range 10 60) (range 10) (range 50)]) + + ; one-arg case, not supported by clojure.core/interleave + (is (= (range 10) + (->> (rx/interleave (rx/seq->o (range 10))) + (b/into []))))) + +(deftest test-interleave* + (are [inputs] (= (apply interleave inputs) + (->> (rx/interleave* (->> inputs + (map rx/seq->o) + (rx/seq->o))) + (b/into []))) + [[] []] + [[] [1]] + [(range 5) (range 10) (range 10) (range 3)] + [(range 50) (range 10)] + [(range 5) (range 10 60) (range 10) (range 50)])) + +(deftest test-interpose + (is (= (interpose \, [1 2 3]) + (b/into [] (rx/interpose \, (rx/seq->o [1 2 3])))))) + +(deftest test-into + (are [input to] (= (into to input) + (b/single (rx/into to (rx/seq->o input)))) + [6 7 8] [9 10 [11]] + #{} [1 2 3 2 4 5] + {} [[1 2] [3 2] [4 5]] + {} [] + '() (range 50))) + +(deftest test-iterate + (are [f x n] (= (->> (iterate f x) (take n)) + (->> (rx/iterate f x) (rx/take n) (b/into []))) + inc 0 10 + dec 20 100 + #(conj % (count %)) [] 5 + #(cons (count %) % ) nil 5)) + +(deftest test-keep + (is (= (into [] (keep identity [true true false])) + (b/into [] (rx/keep identity (rx/seq->o [true true false]))))) + + (is (= (into [] (keep #(if (even? %) (* 2 %)) (range 9))) + (b/into [] (rx/keep #(if (even? %) (* 2 %)) (rx/seq->o (range 9))))))) + +(deftest test-keep-indexed + (is (= (into [] (keep-indexed (fn [i v] + (if (even? i) v)) + [true true false])) + (b/into [] (rx/keep-indexed (fn [i v] + (if (even? i) v)) + (rx/seq->o [true true false])))))) + +(deftest test-map + (is (= (into {} (map (juxt identity name) + [:q :r :s :t :u])) + (b/into {} (rx/map (juxt identity name) + (rx/seq->o [:q :r :s :t :u]))))) + (is (= (into [] (map vector + [:q :r :s :t :u] + (range 10) + ["a" "b" "c" "d" "e"] )) + (b/into [] (rx/map vector + (rx/seq->o [:q :r :s :t :u]) + (rx/seq->o (range 10) ) + (rx/seq->o ["a" "b" "c" "d" "e"] ))))) + ; check > 4 arg case + (is (= (into [] (map vector + [:q :r :s :t :u] + [:q :r :s :t :u] + [:q :r :s :t :u] + (range 10) + (range 10) + (range 10) + ["a" "b" "c" "d" "e"] + ["a" "b" "c" "d" "e"] + ["a" "b" "c" "d" "e"])) + (b/into [] (rx/map vector + (rx/seq->o [:q :r :s :t :u]) + (rx/seq->o [:q :r :s :t :u]) + (rx/seq->o [:q :r :s :t :u]) + (rx/seq->o (range 10)) + (rx/seq->o (range 10)) + (rx/seq->o (range 10)) + (rx/seq->o ["a" "b" "c" "d" "e"]) + (rx/seq->o ["a" "b" "c" "d" "e"]) + (rx/seq->o ["a" "b" "c" "d" "e"])))))) + +(deftest test-map* + (is (= [[1 2 3 4 5 6 7 8]] + (b/into [] (rx/map* vector + (rx/seq->o [(rx/seq->o [1]) + (rx/seq->o [2]) + (rx/seq->o [3]) + (rx/seq->o [4]) + (rx/seq->o [5]) + (rx/seq->o [6]) + (rx/seq->o [7]) + (rx/seq->o [8])])))))) +(deftest test-map-indexed + (is (= (map-indexed vector [:a :b :c]) + (b/into [] (rx/map-indexed vector (rx/seq->o [:a :b :c]))))) + (testing "exceptions from fn have error value injected" + (try + (->> (rx/seq->o [:a :b :c]) + (rx/map-indexed (fn [i v] + (if (= 1 i) + (throw (java.io.FileNotFoundException. "blah"))) + v)) + (b/into [])) + (catch java.io.FileNotFoundException e + (is (= :b (-> e .getCause .getValue))))))) + +(deftest test-mapcat* + (let [f (fn [a b c d e] + [(+ a b) (+ c d) e])] + (is (= (->> (range 5) + (map (fn [_] (range 5))) + (apply mapcat f)) + (->> (range 5) + (map (fn [_] (rx/seq->o (range 5)))) + (rx/seq->o) + (rx/mapcat* (fn [& args] (rx/seq->o (apply f args)))) + (b/into [])))))) + +(deftest test-mapcat + (let [f (fn [v] [v (* v v)]) + xs (range 10)] + (is (= (mapcat f xs) + (b/into [] (rx/mapcat (comp rx/seq->o f) (rx/seq->o xs)))))) + + (let [f (fn [a b] [a b (* a b)]) + as (range 10) + bs (range 15)] + (is (= (mapcat f as bs) + (b/into [] (rx/mapcat (comp rx/seq->o f) + (rx/seq->o as) + (rx/seq->o bs))))))) + +(deftest test-next + (let [in [:q :r :s :t :u]] + (is (= (next in) (b/into [] (rx/next (rx/seq->o in))))))) + +(deftest test-nth + (is (= [:a] + (b/into [] (rx/nth (rx/seq->o [:s :b :a :c]) 2)))) + (is (= [:fallback] + (b/into [] (rx/nth (rx/seq->o [:s :b :a :c]) 25 :fallback))))) + +(deftest test-rest + (let [in [:q :r :s :t :u]] + (is (= (rest in) (b/into [] (rx/rest (rx/seq->o in))))))) + +(deftest test-partition-all + (are [input-size part-size step] (= (->> (range input-size) + (partition-all part-size step)) + (->> (range input-size) + (rx/seq->o) + (rx/partition-all part-size step) + (rx/map #(rx/into [] %)) + (rx/concat*) + (b/into []))) + 0 1 1 + 10 2 2 + 10 3 2 + 15 30 4) + + (are [input-size part-size] (= (->> (range input-size) + (partition-all part-size)) + (->> (range input-size) + (rx/seq->o) + (rx/partition-all part-size) + (rx/map #(rx/into [] %)) + (rx/concat*) + (b/into []))) + 0 1 + 10 2 + 10 3 + 15 30)) + +(deftest test-range + (are [start end step] (= (range start end step) + (->> (rx/range start end step) (b/into []))) + 0 10 2 + 0 -100 -1 + 5 100 9) + + (are [start end] (= (range start end) + (->> (rx/range start end) (b/into []))) + 0 10 + 0 -100 + 5 100) + + (are [start] (= (->> (range start) (take 100)) + (->> (rx/range start) (rx/take 100) (b/into []))) + 50 + 0 + 5 + -20) + (is (= (->> (range) (take 500)) + (->> (rx/range) (rx/take 500) (b/into []))))) + +(deftest test-reduce + (is (= (reduce + 0 (range 4)) + (b/first (rx/reduce + 0 (rx/seq->o (range 4))))))) + +(deftest test-reductions + (is (= (into [] (reductions + 0 (range 4))) + (b/into [] (rx/reductions + 0 (rx/seq->o (range 4))))))) + +(deftest test-some + (is (= [:r] (b/into [] (rx/some #{:r :s :t} (rx/seq->o [:q :v :r]))))) + (is (= [] (b/into [] (rx/some #{:r :s :t} (rx/seq->o [:q :v])))))) + +(deftest test-sort + (are [in cmp] (= (if cmp + (sort cmp in) + (sort in)) + (->> in + (rx/seq->o) + (#(if cmp (rx/sort cmp %) (rx/sort %))) + (b/into []))) + [] nil + [] (comp - compare) + [3 1 2] nil + [1 2 3] nil + [1 2 3] (comp - compare) + [2 1 3] (comp - compare))) + +(deftest test-sort-by + (are [rin cmp] (let [in (map #(hash-map :foo %) rin)] + (= (if cmp + (sort-by :foo cmp in) + (sort-by :foo in)) + (->> in + (rx/seq->o) + (#(if cmp (rx/sort-by :foo cmp %) (rx/sort-by :foo %))) + (b/into [])))) + [] nil + [] (comp - compare) + [3 1 2] nil + [1 2 3] nil + [1 2 3] (comp - compare) + [2 1 3] (comp - compare))) + + +(deftest test-split-with + (is (= (split-with (partial >= 3) (range 6)) + (->> (rx/seq->o (range 6)) + (rx/split-with (partial >= 3)) + b/first + (map (partial b/into [])))))) + +(deftest test-take-while + (is (= (into [] (take-while even? [2 4 6 8 1 2 3])) + (b/into [] (rx/take-while even? (rx/seq->o [2 4 6 8 1 2 3])))))) + +(deftest test-throw + (let [expected (IllegalArgumentException. "HI") + called (atom nil)] + (rx/subscribe (rx/throw expected) + (fn [_]) + (fn [e] (reset! called expected)) + (fn [_])) + (is (identical? expected @called)))) + +(deftest test-catch* + (testing "Is just a passthrough if there's no error" + (is (= [1 2 3] + (->> (rx/seq->o [1 2 3]) + (rx/catch* Exception (fn [e] (throw "OH NO"))) + (b/into []))))) + + (testing "Can catch a particular exception type and continue with an observable" + (is (= [1 2 4 5 6 "foo"] + (->> (rx/generator [o] + (rx/on-next o 1) + (rx/on-next o 2) + (rx/on-error o (IllegalStateException. "foo"))) + (rx/catch* IllegalStateException + (fn [e] + (rx/seq->o [4 5 6 (.getMessage e)]))) + (b/into []))))) + + (testing "if exception isn't matched, it's passed to on-error" + (let [expected (IllegalArgumentException. "HI") + called (atom nil)] + (rx/subscribe (->> (rx/generator [o] + (rx/on-next o 1) + (rx/on-next o 2) + (rx/on-error o expected)) + (rx/catch* IllegalStateException (fn [e] + (rx/return "WAT?")))) + (fn [_]) + (fn [e] (reset! called expected)) + (fn [_])) + (is (identical? expected @called)))) + + (testing "if p returns Throwable, that's passed as e" + (let [cause (IllegalArgumentException. "HI") + wrapper (java.util.concurrent.ExecutionException. cause)] + (is (= [cause] + (->> (rx/throw wrapper) + (rx/catch #(.getCause %) e + (rx/return e)) + (b/into []))))))) + + +(deftest test-finally + (testing "Supports a finally clause" + (testing "called on completed" + (let [completed (atom nil) + called (atom nil)] + (rx/subscribe (->> (rx/seq->o [1 2 3]) + (rx/finally* (fn [] (reset! called (str "got it"))))) + (fn [_]) + (fn [_] (throw (IllegalStateException. "WAT"))) + (fn [] (reset! completed "DONE"))) + (is (= "got it" @called)) + (is (= "DONE" @completed)))) + + (testing "called on error" + (let [expected (IllegalStateException. "expected") + completed (atom nil) + called (atom nil)] + (rx/subscribe (->> (rx/generator [o] + (rx/on-next o 1) + (rx/on-next o 2) + (rx/on-error o expected)) + (rx/finally + (reset! called "got it"))) + (fn [_]) + (fn [e] (reset! completed e)) + (fn [] (throw (IllegalStateException. "WAT")))) + (is (= "got it" @called)) + (is (identical? expected @completed)))))) + + +;################################################################################ + +(deftest test-graph-imports + (is (= 99 + (-> {:a {:deps [] :factory (fn [_] (rx/return 99))}} + rx/let-o* + :a + b/single))) + (is (= 100 + (b/single (rx/let-o [?a (rx/return 100)] + ?a))))) + +;################################################################################ + +(deftest test-realized-imports + (is (= {:a 1 :b 2} + (->> (rx/let-realized [a (rx/return 1) + b (rx/return 2)] + {:a a :b b}) + b/single)))) + + diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj new file mode 100644 index 0000000000..ba2344e4e2 --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/future_test.clj @@ -0,0 +1,61 @@ +(ns rx.lang.clojure.future-test + (:require [rx.lang.clojure.core :as rx] + [rx.lang.clojure.blocking :as b] + [rx.lang.clojure.future :as f]) + (:require [clojure.test :refer [deftest testing is]])) + +(deftest test-future-generator + (is (not= [(.getId (Thread/currentThread))] + (b/into [] + (f/future-generator* future-call + #(rx/on-next % (.getId (Thread/currentThread)))))))) + +(deftest test-future + (is (= [15] (b/into [] (f/future* future-call + 1 2 3 4 5))))) + +(deftest test-future-exception + (is (= "Caught: boo" + (->> (f/future* future-call #(throw (java.io.FileNotFoundException. "boo"))) + (rx/catch java.io.FileNotFoundException e + (rx/return (str "Caught: " (.getMessage e)))) + (b/single))))) + +(deftest test-future-cancel + (let [exited? (atom nil) + o (f/future* future-call + (fn [] (Thread/sleep 1000) + (reset! exited? true) + "WAT")) + result (->> o + (rx/take 0) + (b/into []))] + (Thread/sleep 2000) + (is (= [nil []] + [@exited? result])))) + +(deftest test-future-generator-cancel + (let [exited? (atom nil) + o (f/future-generator* future-call + (fn [o] + (rx/on-next o "FIRST") + (Thread/sleep 1000) + (reset! exited? true))) + result (->> o + (rx/take 1) + (b/into []))] + (Thread/sleep 2000) + (is (= [nil ["FIRST"]] + [@exited? result])))) + +(deftest test-future-generator-exception + (let [e (java.io.FileNotFoundException. "snake")] + (is (= [1 2 e] + (->> (f/future-generator* + future-call + (fn [o] + (rx/on-next o 1) + (rx/on-next o 2) + (throw e))) + (rx/catch java.io.FileNotFoundException e + (rx/return e)) + (b/into [])))))) diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/graph_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/graph_test.clj new file mode 100644 index 0000000000..56ddfc9ff3 --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/graph_test.clj @@ -0,0 +1,122 @@ +(ns rx.lang.clojure.graph-test + (:require [rx.lang.clojure.graph :as graph] + [rx.lang.clojure.core :as rx] + [rx.lang.clojure.future :as rx-future] + [rx.lang.clojure.blocking :as rx-blocking] + [clojure.test :refer [deftest testing is]])) + +(deftest test-let-o* + (testing "throws on cycle" + (is (thrown-with-msg? IllegalArgumentException #"Cycle found" + (graph/let-o* {:a {:deps [:a]}})))) + + (testing "throws on unknown" + (is (thrown-with-msg? IllegalArgumentException #"Unknown node" + (graph/let-o* {:a {:deps [:b]}})))) + + (testing "it works in a simple case" + (let [d {:a {:deps [] + :factory (fn [_] (rx/seq->o [1 2 3 4 5]))} + :b {:deps [:a] + :factory (fn [{:keys [a]}] (rx/map #(* % %) a)) } + :c {:deps [:a :b] + :factory (fn [{:keys [a b]}] (rx/map #(+ %1 %2) a b)) } + :d {:deps [:c :b] + :factory (fn [{:keys [c b]}] (rx/map #(+ %1 %2) c b)) } + } + f (graph/let-o* d) ] + (println f) + ; (n^2 + n) + n^2 + (is (= [3 10 21 36 55] + (rx-blocking/into [] (:d f))))))) + +(deftest test-let-o + (testing "it works" + (let [f (graph/let-o [?a (rx/seq->o [1 2 3]) + ?b (rx/seq->o [4 5 6])] + (rx/map + ?a ?b))] + (is (= [5 7 9] + (rx-blocking/into [] f))))) + + (testing "it still works" + (is (= {:a 99 :b 100 :z "hi"} + (rx-blocking/single + (-> (let [z (rx/return "hi")] ; an observable from "somewhere else" + (graph/let-o + [?a (rx-future/future* future-call #(do (Thread/sleep 50) 99)) + ?b (rx-future/future* future-call #(do (Thread/sleep 500) 100)) + ?c (rx/map #(hash-map :a %1 :b %2 :z %3) ?a ?b ?z) + ?z z] + (rx/reduce merge {} ?c))))))))) + +(deftest test-complicated-graph + ; These funcs model network requests for various stuff. They all return observable. + (let [request-vhs (fn [] + (rx-future/future-generator* + future-call + (fn [o] + (Thread/sleep 50) + (doseq [i (range 3)] + (rx/on-next o {:id i}))))) + request-user (fn [id] + (rx-future/future* + future-call + #(do (Thread/sleep (rand-int 250)) + {:id id + :name (str "friend" id) }))) + request-ab (fn [u] + (rx-future/future* + future-call + #(do (Thread/sleep (rand-int 250)) + {:user-id (:id u) + :cell (* 2 (:id u))}))) + + request-video-md (fn [v] + (rx/return {:video v + :title (str "title" (:id v)) })) + + ; Now we can stitch all these requests together into an rx graph to + ; produce a response. + o (graph/let-o [?user-info (rx-future/future* + future-call + #(do (Thread/sleep 20) + {:name "Bob" + :id 12345 + :friend-ids [1 2 3] })) + + ?friends (->> ?user-info + (rx/mapcat (fn [ui] + (rx/mapcat request-user + (rx/seq->o (:friend-ids ui)))))) + + ?ab (->> (rx/concat ?user-info ?friends) + (rx/mapcat request-ab)) + + ?ab-lookup (->> ?ab + (rx/map (juxt :user-id #(dissoc % :user-id))) + (rx/into {})) + + ?vhs (request-vhs) + + + ?metadata (->> ?vhs + (rx/mapcat request-video-md))] + (rx/map (fn [u m f ab-lookup] + {:user (dissoc u :friend-ids) + :videos m + :friends (sort-by :id f) + :ab ab-lookup}) + ?user-info + (rx/into [] ?metadata) + (rx/into [] ?friends) + ?ab-lookup))] + + (is (= {:user {:name "Bob" :id 12345} + :videos [{:video {:id 0} :title "title0"} + {:video {:id 1} :title "title1"} + {:video {:id 2} :title "title2"}] + :friends [{:name "friend1" :id 1}{:name "friend2" :id 2}{:name "friend3" :id 3}] + :ab {12345 {:cell 24690} 1 {:cell 2} 2 {:cell 4} 3 {:cell 6}} } + (rx-blocking/single o))))) + + diff --git a/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/realized_test.clj b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/realized_test.clj new file mode 100644 index 0000000000..3bf9b16872 --- /dev/null +++ b/language-adaptors/rxjava-clojure/src/test/clojure/rx/lang/clojure/realized_test.clj @@ -0,0 +1,130 @@ +(ns rx.lang.clojure.realized-test + (:require [rx.lang.clojure.realized :as r] + [rx.lang.clojure.core :as rx] + [rx.lang.clojure.future :as rx-future] + [rx.lang.clojure.blocking :as rx-blocking] + [clojure.test :refer [deftest testing is]])) + + + +(deftest test-realized-map + (testing "Turns map of observables into observable of map" + (let [o (r/realized-map :a (r/all (rx/seq->o [1 2 3])) + :a2 (rx/seq->o [99 100 101]) + :b (rx/return "hi") + :c [(->> [1 2 3] + rx/seq->o + (rx/map #(* % %))) + next] + :d (rx/return "just one") + :e "just a value") + result (rx-blocking/single o)] + (is (= {:a [1 2 3] + :a2 101 + :b "hi" + :c [4 9] + :d "just one" + :e "just a value" } + result))))) + +(deftest test-realized-map + (testing "works like realized-map, but takes a map instead of key/value pairs" + (is (= {:a [1 2] + :b 500 } + (->> {:a (r/all (rx/seq->o [1 2])) + :b 500 } + r/realized-map* + rx-blocking/single))))) + +(deftest test-let-realized + (is (= {:a* 2 + :b* 500 + :c* 1000 } + (->> (r/let-realized [a [(rx/seq->o [1 2]) last] + b 500 + c (rx/return 1000) ] + {:a* a + :b* b + :c* c }) + rx-blocking/single)))) + +(deftest test-only + (testing "raises IllegalStateException if sequence is empty" + (is (thrown-with-msg? IllegalStateException #"did not produce" + (->> (r/let-realized [a (rx/seq->o [1 2])] + {:a a}) + rx-blocking/single))) + ; Just to be sure, make sure it goes through onError. + (let [values (atom []) + errors (atom [])] + (rx/subscribe (r/let-realized [a (rx/seq->o [1 2])] + {:a a}) + #(swap! values conj %) + #(swap! errors conj %)) + (is (empty? @values)) + (is (= 1 (count @errors))) + (let [[e] @errors] + (is (instance? IllegalStateException e)))))) + +(deftest test-all + (testing "collects all values from an observable" + (is (= [1 2 3] + (->> (r/let-realized [a (r/all (rx/seq->o [1 2 3]))] + a) + rx-blocking/single))))) + +; Playing with some expressing some of the video stuff with this. +(comment + (->> (get-list-of-lists user-id) + (rx/mapcat (fn [list] + (->> (video-list->videos list) + (rx/take 10)))) + (rx/mapcat (fn [video] + (->> (r/let-realized [md (video->metadata video) + bm (video->bookmark video) + rt (video->rating video user-id)] + {:id (:id video) + :title (:title md) + :length (:duration md) + :bookmark bm + :rating {:actual (:actual-star-rating rt) + :average (:average-star-rating rt) + :predicted (:predicted-star-rating rt) } }))))) + + (->> (get-list-of-lists user-id) + (rx/mapcat (fn [list] + (->> (video-list->videos list) + (rx/take 10)))) + (rx/mapcat (fn [video] + (->> (r/realized-map :md (video->metadata video) + :bm (video->bookmark video) + :rt (video->rating video user-id)) + (rx/map (fn [{:keys [md bm rt]}] + {:id (:id video) + :title (:title md) + :length (:duration md) + :bookmark bm + :rating {:actual (:actual-star-rating rt) + :average (:average-star-rating rt) + :predicted (:predicted-star-rating rt) } })))))) + + (->> (get-list-of-lists user-id) + (rx/mapcat (fn [list] + (->> (video-list->videos list) + (rx/take 10)))) + (rx/mapcat (fn [video] + (->> (r/realized-map :id (:id video) + :md [(video->metadata video) + first + #(select-keys % [:title :duration])] + :bookmark (video->bookmark video) + :rating [(video->rating video user-id) + first + #(hash-map :actual (:actual-star-rating %) + :average (:average-star-rating %) + :predicted (:predicted-star-rating %))]) + (rx/map (fn [m] + (-> m + (merge (:md m)) + (dissoc :md))))))))) + diff --git a/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyActionWrapper.java b/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyActionWrapper.java index 4c05c96b4a..dbf99b943d 100644 --- a/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyActionWrapper.java +++ b/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyActionWrapper.java @@ -1,5 +1,5 @@ /** - * Copyright 2013 Netflix, Inc. + * Copyright 2014 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyCreateWrapper.java b/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyCreateWrapper.java index c94b0aaf57..a5149c857e 100644 --- a/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyCreateWrapper.java +++ b/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyCreateWrapper.java @@ -1,5 +1,5 @@ /** - * Copyright 2013 Netflix, Inc. + * Copyright 2014 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyFunctionWrapper.java b/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyFunctionWrapper.java index 4adb8de014..e0b6132de2 100644 --- a/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyFunctionWrapper.java +++ b/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyFunctionWrapper.java @@ -1,5 +1,5 @@ /** - * Copyright 2013 Netflix, Inc. + * Copyright 2014 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyOnSubscribeFuncWrapper.java b/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyOnSubscribeFuncWrapper.java index d8a4369a43..3e2bc1b638 100644 --- a/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyOnSubscribeFuncWrapper.java +++ b/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/GroovyOnSubscribeFuncWrapper.java @@ -1,5 +1,5 @@ /** - * Copyright 2013 Netflix, Inc. + * Copyright 2014 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/RxGroovyExtensionModule.java b/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/RxGroovyExtensionModule.java index 8f12c8430b..0df587bfb5 100644 --- a/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/RxGroovyExtensionModule.java +++ b/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/RxGroovyExtensionModule.java @@ -1,5 +1,5 @@ /** - * Copyright 2013 Netflix, Inc. + * Copyright 2014 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -167,7 +167,7 @@ public String getName() { } @Override - public Class getReturnType() { + public Class getReturnType() { return m.getReturnType(); } @@ -177,6 +177,7 @@ public CachedClass getDeclaringClass() { } @Override + @SuppressWarnings("unchecked") public Object invoke(Object object, final Object[] arguments) { return Observable.create(new GroovyCreateWrapper((Closure) arguments[0])); } diff --git a/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/RxGroovyPropertiesModuleFactory.java b/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/RxGroovyPropertiesModuleFactory.java index 149bca5621..ec80b6401e 100644 --- a/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/RxGroovyPropertiesModuleFactory.java +++ b/language-adaptors/rxjava-groovy/src/main/java/rx/lang/groovy/RxGroovyPropertiesModuleFactory.java @@ -1,5 +1,5 @@ /** - * Copyright 2013 Netflix, Inc. + * Copyright 2014 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/language-adaptors/rxjava-groovy/src/test/groovy/rx/lang/groovy/ObservableTests.groovy b/language-adaptors/rxjava-groovy/src/test/groovy/rx/lang/groovy/ObservableTests.groovy index 1bc87c0ee5..e459e14de4 100644 --- a/language-adaptors/rxjava-groovy/src/test/groovy/rx/lang/groovy/ObservableTests.groovy +++ b/language-adaptors/rxjava-groovy/src/test/groovy/rx/lang/groovy/ObservableTests.groovy @@ -322,7 +322,7 @@ def class ObservableTests { Observable.from("one", "two", "three", "four", "five", "six") .groupBy({String s -> s.length()}) - .mapMany({ + .flatMap({ groupObservable -> return groupObservable.map({ diff --git a/language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyActionWrapper.java b/language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyActionWrapper.java index d85ffbbf41..9bfef70a4c 100644 --- a/language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyActionWrapper.java +++ b/language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyActionWrapper.java @@ -1,5 +1,5 @@ /** - * Copyright 2013 Netflix, Inc. + * Copyright 2014 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyFunctionWrapper.java b/language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyFunctionWrapper.java index d372ee136a..d9c5ae959e 100644 --- a/language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyFunctionWrapper.java +++ b/language-adaptors/rxjava-jruby/src/main/java/rx/lang/jruby/JRubyFunctionWrapper.java @@ -1,5 +1,5 @@ /** - * Copyright 2013 Netflix, Inc. + * Copyright 2014 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,6 +41,11 @@ * @param * @param * @param + * @param + * @param + * @param + * @param + * @param * @param */ public class JRubyFunctionWrapper implements @@ -67,18 +72,21 @@ public JRubyFunctionWrapper(ThreadContext context, RubyProc proc) { } @Override + @SuppressWarnings("unchecked") public R call() { IRubyObject[] array = new IRubyObject[0]; return (R) proc.call(context, array); } @Override + @SuppressWarnings("unchecked") public R call(T1 t1) { IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1)}; return (R) proc.call(context, array); } @Override + @SuppressWarnings("unchecked") public R call(T1 t1, T2 t2) { IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), JavaUtil.convertJavaToRuby(runtime, t2)}; @@ -86,6 +94,7 @@ public R call(T1 t1, T2 t2) { } @Override + @SuppressWarnings("unchecked") public R call(T1 t1, T2 t2, T3 t3) { IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), JavaUtil.convertJavaToRuby(runtime, t2), @@ -94,6 +103,7 @@ public R call(T1 t1, T2 t2, T3 t3) { } @Override + @SuppressWarnings("unchecked") public R call(T1 t1, T2 t2, T3 t3, T4 t4) { IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), JavaUtil.convertJavaToRuby(runtime, t2), @@ -103,6 +113,7 @@ public R call(T1 t1, T2 t2, T3 t3, T4 t4) { } @Override + @SuppressWarnings("unchecked") public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5) { IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), JavaUtil.convertJavaToRuby(runtime, t2), @@ -113,6 +124,7 @@ public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5) { } @Override + @SuppressWarnings("unchecked") public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6) { IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), JavaUtil.convertJavaToRuby(runtime, t2), @@ -124,6 +136,7 @@ public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6) { } @Override + @SuppressWarnings("unchecked") public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7) { IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), JavaUtil.convertJavaToRuby(runtime, t2), @@ -136,6 +149,7 @@ public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7) { } @Override + @SuppressWarnings("unchecked") public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8) { IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), JavaUtil.convertJavaToRuby(runtime, t2), @@ -149,6 +163,7 @@ public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8) { } @Override + @SuppressWarnings("unchecked") public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8, T9 t9) { IRubyObject[] array = {JavaUtil.convertJavaToRuby(runtime, t1), JavaUtil.convertJavaToRuby(runtime, t2), @@ -163,6 +178,7 @@ public R call(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8, T9 t9) { } @Override + @SuppressWarnings("unchecked") public R call(Object... args) { IRubyObject[] array = new IRubyObject[args.length]; for (int i = 0; i < args.length; i++) { diff --git a/language-adaptors/rxjava-kotlin/README.md b/language-adaptors/rxjava-kotlin/README.md index c0eb072c26..037d759654 100644 --- a/language-adaptors/rxjava-kotlin/README.md +++ b/language-adaptors/rxjava-kotlin/README.md @@ -3,25 +3,37 @@ Kotlin has support for SAM (Single Abstract Method) Interfaces as Functions (i.e. Java 8 Lambdas). So you could use Kotlin in RxJava whitout this adaptor ```kotlin -Observable.create(OnSubscribeFunc { - it!!.onNext("Hello") - it.onCompleted() +Observable.create(OnSubscribeFunc { observer -> + observer!!.onNext("Hello") + observer.onCompleted() Subscriptions.empty() })!!.subscribe { result -> a!!.received(result) } ``` -This adaptor exposes a set of Extension functions that allow a more idiomatic Kotlin usage +In RxJava [0.17.0](https://github.com/Netflix/RxJava/releases/tag/0.17.0) version a new Subscriber type was included ```kotlin -import rx.lang.kotlin.* +Observable.create(object:OnSubscribe { + override fun call(subscriber: Subscriber?) { + subscriber!!.onNext("Hello") + subscriber.onCompleted() + } +})!!.subscribe { result -> + a!!.received(result) +} +``` -{(observer: Observer) -> - observer.onNext("Hello") - observer.onCompleted() - Subscriptions.empty()!! -}.asObservableFunc().subscribe { result -> +(Due to a [bug in Kotlin's compiler](http://youtrack.jetbrains.com/issue/KT-4753) you can't use SAM with OnSubscribe) + +This adaptor exposes a set of Extension functions that allow a more idiomatic Kotlin usage + +```kotlin +{(subscriber: Subscriber) -> + subscriber.onNext("Hello") + subscriber.onCompleted() +}.asObservable().subscribe { result -> a!!.received(result) } ``` diff --git a/language-adaptors/rxjava-kotlin/build.gradle b/language-adaptors/rxjava-kotlin/build.gradle index aa44049d05..fc710d89fb 100644 --- a/language-adaptors/rxjava-kotlin/build.gradle +++ b/language-adaptors/rxjava-kotlin/build.gradle @@ -4,7 +4,7 @@ buildscript { } dependencies { - classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:0.6.1673' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:0.7.270' } } @@ -13,7 +13,7 @@ apply plugin: 'osgi' dependencies { compile project(':rxjava-core') - compile 'org.jetbrains.kotlin:kotlin-stdlib:0.6.1673' + compile 'org.jetbrains.kotlin:kotlin-stdlib:0.7.270' provided 'junit:junit-dep:4.10' provided 'org.mockito:mockito-core:1.8.5' } diff --git a/language-adaptors/rxjava-kotlin/src/main/kotlin/rx/lang/kotlin/namespace.kt b/language-adaptors/rxjava-kotlin/src/main/kotlin/rx/lang/kotlin/namespace.kt index d5eb8420f5..006f97e48a 100644 --- a/language-adaptors/rxjava-kotlin/src/main/kotlin/rx/lang/kotlin/namespace.kt +++ b/language-adaptors/rxjava-kotlin/src/main/kotlin/rx/lang/kotlin/namespace.kt @@ -21,14 +21,19 @@ import rx.Observable import rx.Observable.OnSubscribe import rx.Subscription import rx.Observable.OnSubscribeFunc +import rx.Subscriber -public fun Function1, Unit>.asObservable(): Observable { - return Observable.create(OnSubscribe{ t1 -> - this(t1!!) +public fun Function1, Unit>.asObservable(): Observable { + return Observable.create(object:OnSubscribe { + override fun call(t1: Subscriber?) { + this@asObservable(t1!!) + } + })!! } +[deprecated("Use Function1, Unit>.asObservable()")] public fun Function1, Subscription>.asObservableFunc(): Observable { return Observable.create(OnSubscribeFunc{ op -> this(op!!) diff --git a/language-adaptors/rxjava-kotlin/src/test/kotlin/rx/lang/kotlin/BasicKotlinTests.kt b/language-adaptors/rxjava-kotlin/src/test/kotlin/rx/lang/kotlin/BasicKotlinTests.kt index cf4d3787b3..99d6b4278b 100644 --- a/language-adaptors/rxjava-kotlin/src/test/kotlin/rx/lang/kotlin/BasicKotlinTests.kt +++ b/language-adaptors/rxjava-kotlin/src/test/kotlin/rx/lang/kotlin/BasicKotlinTests.kt @@ -16,10 +16,7 @@ package rx.lang.kotlin -import org.mockito.Mock import rx.Observable -import org.junit.Before -import org.mockito.MockitoAnnotations import org.junit.Test import rx.subscriptions.Subscriptions import org.mockito.Mockito.* @@ -31,21 +28,23 @@ import rx.Subscription import kotlin.concurrent.thread import rx.Observable.OnSubscribeFunc import rx.lang.kotlin.BasicKotlinTests.AsyncObservable +import rx.Observable.OnSubscribe +import rx.Subscriber /** * This class use plain Kotlin without extensions from the language adaptor */ -public class BasicKotlinTests:KotlinTests() { - +public class BasicKotlinTests : KotlinTests() { [Test] public fun testCreate() { - Observable.create(OnSubscribeFunc { - it!!.onNext("Hello") - it.onCompleted() - Subscriptions.empty() + Observable.create(object:OnSubscribe { + override fun call(subscriber: Subscriber?) { + subscriber!!.onNext("Hello") + subscriber.onCompleted() + } })!!.subscribe { result -> a!!.received(result) } @@ -310,7 +309,7 @@ public class BasicKotlinTests:KotlinTests() { - public class TestFactory(){ + public class TestFactory() { var counter = 1 val numbers: Observable @@ -330,8 +329,8 @@ public class BasicKotlinTests:KotlinTests() { } - class AsyncObservable : OnSubscribeFunc{ - override fun onSubscribe(op: Observer?): Subscription? { + class AsyncObservable : OnSubscribe { + override fun call(op: Subscriber?) { thread { Thread.sleep(50) op!!.onNext(1) @@ -339,15 +338,14 @@ public class BasicKotlinTests:KotlinTests() { op.onNext(3) op.onCompleted() } - return Subscriptions.empty() + } } - class TestOnSubscribe(val count: Int) : OnSubscribeFunc{ - override fun onSubscribe(op: Observer?): Subscription? { + class TestOnSubscribe(val count: Int) : OnSubscribe { + override fun call(op: Subscriber?) { op!!.onNext("hello_$count") op.onCompleted() - return Subscriptions.empty()!! } } diff --git a/language-adaptors/rxjava-kotlin/src/test/kotlin/rx/lang/kotlin/ExtensionTests.kt b/language-adaptors/rxjava-kotlin/src/test/kotlin/rx/lang/kotlin/ExtensionTests.kt index c9fa67a6a1..c601bd54a1 100644 --- a/language-adaptors/rxjava-kotlin/src/test/kotlin/rx/lang/kotlin/ExtensionTests.kt +++ b/language-adaptors/rxjava-kotlin/src/test/kotlin/rx/lang/kotlin/ExtensionTests.kt @@ -16,19 +16,14 @@ package rx.lang.kotlin -import org.mockito.Mock import rx.Observable -import org.junit.Before -import org.mockito.MockitoAnnotations import org.junit.Test -import rx.subscriptions.Subscriptions import org.mockito.Mockito.* import org.mockito.Matchers.* -import rx.Observer import org.junit.Assert.* import rx.Notification -import rx.Subscription import kotlin.concurrent.thread +import rx.Subscriber /** * This class contains tests using the extension functions provided by the language adaptor. @@ -39,11 +34,10 @@ public class ExtensionTests : KotlinTests() { [Test] public fun testCreate() { - {(observer: Observer) -> - observer.onNext("Hello") - observer.onCompleted() - Subscriptions.empty()!! - }.asObservableFunc().subscribe { result -> + {(subscriber: Subscriber) -> + subscriber.onNext("Hello") + subscriber.onCompleted() + }.asObservable().subscribe { result -> a!!.received(result) } @@ -216,7 +210,7 @@ public class ExtensionTests : KotlinTests() { [Test] public fun testForEach() { - asyncObservable.asObservableFunc().toBlockingObservable()!!.forEach(received()) + asyncObservable.asObservable().toBlockingObservable()!!.forEach(received()) verify(a, times(1))!!.received(1) verify(a, times(1))!!.received(2) verify(a, times(1))!!.received(3) @@ -224,7 +218,7 @@ public class ExtensionTests : KotlinTests() { [Test(expected = javaClass())] public fun testForEachWithError() { - asyncObservable.asObservableFunc().toBlockingObservable()!!.forEach { throw RuntimeException("err") } + asyncObservable.asObservable().toBlockingObservable()!!.forEach { throw RuntimeException("err") } fail("we expect an exception to be thrown") } @@ -259,21 +253,19 @@ public class ExtensionTests : KotlinTests() { assertEquals(listOf(3, 6, 9), values[2]) } - val funOnSubscribe: (Int, Observer) -> Subscription = { counter, observer -> - observer.onNext("hello_$counter") - observer.onCompleted() - Subscriptions.empty()!! + val funOnSubscribe: (Int, Subscriber) -> Unit = { counter, subscriber -> + subscriber.onNext("hello_$counter") + subscriber.onCompleted() } - val asyncObservable: (Observer) -> Subscription = { observer -> + val asyncObservable: (Subscriber) -> Unit = { subscriber -> thread { Thread.sleep(50) - observer.onNext(1) - observer.onNext(2) - observer.onNext(3) - observer.onCompleted() + subscriber.onNext(1) + subscriber.onNext(2) + subscriber.onNext(3) + subscriber.onCompleted() } - Subscriptions.empty()!! } /** @@ -283,7 +275,7 @@ public class ExtensionTests : KotlinTests() { return {(p2: P2) -> this(p1, p2) } } - inner public class TestFactory(){ + inner public class TestFactory() { var counter = 1 val numbers: Observable @@ -291,14 +283,14 @@ public class ExtensionTests : KotlinTests() { return listOf(1, 3, 2, 5, 4).asObservable() } - val onSubscribe: (Observer) -> Subscription + val onSubscribe: (Subscriber) -> Unit get(){ return funOnSubscribe.partially1(counter++) } val observable: Observable get(){ - return onSubscribe.asObservableFunc() + return onSubscribe.asObservable() } } diff --git a/language-adaptors/rxjava-scala/README.md b/language-adaptors/rxjava-scala/README.md index 30b8d75b63..fb56b94eb1 100644 --- a/language-adaptors/rxjava-scala/README.md +++ b/language-adaptors/rxjava-scala/README.md @@ -5,14 +5,14 @@ This adaptor allows to use RxJava in Scala with anonymous functions, e.g. ```scala val o = Observable.interval(200 millis).take(5) o.subscribe(n => println("n = " + n)) -Observable(1, 2, 3, 4).reduce(_ + _) +Observable.items(1, 2, 3, 4).reduce(_ + _) ``` For-comprehensions are also supported: ```scala -val first = Observable(10, 11, 12) -val second = Observable(10, 11, 12) +val first = Observable.items(10, 11, 12) +val second = Observable.items(10, 11, 12) val booleans = for ((n1, n2) <- (first zip second)) yield (n1 == n2) ``` diff --git a/language-adaptors/rxjava-scala/src/examples/java/rx/lang/scala/examples/MovieLibUsage.java b/language-adaptors/rxjava-scala/src/examples/java/rx/lang/scala/examples/MovieLibUsage.java index a8edadaf6b..5d1acf5361 100644 --- a/language-adaptors/rxjava-scala/src/examples/java/rx/lang/scala/examples/MovieLibUsage.java +++ b/language-adaptors/rxjava-scala/src/examples/java/rx/lang/scala/examples/MovieLibUsage.java @@ -1,5 +1,5 @@ /** - * Copyright 2013 Netflix, Inc. + * Copyright 2014 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/Olympics.scala b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/Olympics.scala index e91de4b51d..dba0019cdb 100644 --- a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/Olympics.scala +++ b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/Olympics.scala @@ -22,6 +22,7 @@ object Olympics { case class Medal(val year: Int, val games: String, val discipline: String, val medal: String, val athlete: String, val country: String) def mountainBikeMedals: Observable[Medal] = Observable.items( + duration(100 millis), // a short delay because medals are only awarded some time after the Games began Observable.items( Medal(1996, "Atlanta 1996", "cross-country men", "Gold", "Bart BRENTJENS", "Netherlands"), Medal(1996, "Atlanta 1996", "cross-country women", "Gold", "Paola PEZZO", "Italy"), @@ -69,18 +70,33 @@ object Olympics { ).concat // speed it up :D - val fourYears = 4000.millis + val oneYear = 1000.millis - val neverUsedDummyMedal = Medal(3333, "?", "?", "?", "?", "?") + //val neverUsedDummyMedal = Medal(3333, "?", "?", "?", "?", "?") - def fourYearsEmpty: Observable[Medal] = { + /** runs an infinite loop, and returns Bottom type (Nothing) */ + def getNothing: Nothing = { + println("You shouldn't have called this method ;-)") + getNothing + } + + /** returns an Observable which emits no elements and completes after a duration of d */ + def duration(d: Duration): Observable[Nothing] = Observable.interval(d).take(1).filter(_ => false).map(_ => getNothing) + + def fourYearsEmpty: Observable[Medal] = duration(4*oneYear) + + def yearTicks: Observable[Int] = + (Observable.from(1996 to 2014) zip (Observable.items(-1) ++ Observable.interval(oneYear))).map(_._1) + + /* + def fourYearsEmptyOld: Observable[Medal] = { // TODO this should return an observable which emits nothing during fourYears and then completes // Because of https://github.com/Netflix/RxJava/issues/388, we get non-terminating tests // And this https://github.com/Netflix/RxJava/pull/289#issuecomment-24738668 also causes problems // So we don't use this: - // Observable.interval(fourYears).take(1).map(i => neverUsedDummyMedal).filter(m => false) + Observable.interval(fourYears).take(1).map(i => neverUsedDummyMedal).filter(m => false) // But we just return empty, which completes immediately - Observable.empty - } + // Observable.empty + }*/ } \ No newline at end of file diff --git a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala index 6b6faf4f7d..40ed6bdf1f 100644 --- a/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala +++ b/language-adaptors/rxjava-scala/src/examples/scala/rx/lang/scala/examples/RxScalaDemo.scala @@ -16,6 +16,8 @@ package rx.lang.scala.examples import java.io.IOException +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit import scala.concurrent.duration.Duration import scala.concurrent.duration.DurationInt @@ -25,6 +27,7 @@ import scala.language.implicitConversions import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Assert.assertFalse import org.junit.Ignore import org.junit.Test import org.scalatest.junit.JUnitSuite @@ -32,6 +35,15 @@ import org.scalatest.junit.JUnitSuite import rx.lang.scala._ import rx.lang.scala.schedulers._ +/** + * Demo how the different operators can be used. In Eclipse, you can right-click + * a test and choose "Run As" > "Scala JUnit Test". + * + * For each operator added to Observable.java, we add a little usage demo here. + * It does not need to test the functionality (that's already done by the tests in + * RxJava core), but it should demonstrate how it can be used, to make sure that + * the method signature makes sense. + */ @Ignore // Since this doesn't do automatic testing, don't increase build time unnecessarily class RxScalaDemo extends JUnitSuite { @@ -78,13 +90,9 @@ class RxScalaDemo extends JUnitSuite { val first = Observable.from(List(10, 11, 12)) val second = Observable.from(List(10, 11, 12)) - val b1 = (first zip second) map (p => p._1 == p._2) forall (b => b) - - val equality = (a: Any, b: Any) => a == b - val b2 = (first zip second) map (p => equality(p._1, p._2)) forall (b => b) + val b = (first zip second) forall { case (a, b) => a == b } - assertTrue(b1.toBlockingObservable.single) - assertTrue(b2.toBlockingObservable.single) + assertTrue(b.toBlockingObservable.single) } @Test def testObservableComparisonWithForComprehension() { @@ -93,7 +101,7 @@ class RxScalaDemo extends JUnitSuite { val booleans = for ((n1, n2) <- (first zip second)) yield (n1 == n2) - val b1 = booleans.forall(_ == true) // without `== true`, b1 is assigned the forall function + val b1 = booleans.forall(identity) assertTrue(b1.toBlockingObservable.single) } @@ -136,6 +144,12 @@ class RxScalaDemo extends JUnitSuite { o.buffer(5).subscribe((l: Seq[Int]) => println(l.mkString("[", ", ", "]"))) } + @Test def bufferExample() { + val o = Observable.from(1 to 18).zip(Observable.interval(100 millis)).map(_._1) + val boundary = Observable.interval(500 millis) + o.buffer(boundary).toBlockingObservable.foreach((l: Seq[Int]) => println(l.mkString("[", ", ", "]"))) + } + @Test def windowExample() { (for ((o, i) <- Observable.from(1 to 18).window(5).zipWithIndex; n <- o) yield s"Observable#$i emits $n" @@ -216,7 +230,6 @@ class RxScalaDemo extends JUnitSuite { }).flatten.toBlockingObservable.foreach(println(_)) } - @Ignore // TODO something's bad here @Test def timingTest1() { val numbersByModulo3 = Observable.interval(1000 millis).take(9).groupBy(_ % 3) @@ -224,10 +237,14 @@ class RxScalaDemo extends JUnitSuite { (for ((modulo, numbers) <- numbersByModulo3) yield { println("Observable for modulo" + modulo + " started at t = " + (System.currentTimeMillis - t0)) - numbers.take(1) // <- TODO very unexpected - //numbers + numbers.map(n => s"${n} is in the modulo-$modulo group") }).flatten.toBlockingObservable.foreach(println(_)) } + + @Test def testOlympicYearTicks() { + Olympics.yearTicks.subscribe(println(_)) + waitFor(Olympics.yearTicks) + } @Test def groupByExample() { val medalsByCountry = Olympics.mountainBikeMedals.groupBy(medal => medal.country) @@ -238,8 +255,10 @@ class RxScalaDemo extends JUnitSuite { firstMedalOfEachCountry.subscribe(medal => { println(s"${medal.country} wins its first medal in ${medal.year}") }) + + Olympics.yearTicks.subscribe(year => println(s"\nYear $year starts.")) - waitFor(firstMedalOfEachCountry) + waitFor(Olympics.yearTicks) } @Test def groupByUntilExample() { @@ -250,36 +269,64 @@ class RxScalaDemo extends JUnitSuite { } @Test def combineLatestExample() { - val first_counter = Observable.interval(250 millis) - val second_counter = Observable.interval(550 millis) - val combined_counter = first_counter.combineLatest(second_counter, + val firstCounter = Observable.interval(250 millis) + val secondCounter = Observable.interval(550 millis) + val combinedCounter = firstCounter.combineLatest(secondCounter, (x: Long, y: Long) => List(x,y)) take 10 - combined_counter subscribe {x => println(s"Emitted group: $x")} + combinedCounter subscribe {x => println(s"Emitted group: $x")} + waitFor(combinedCounter) } + @Test def olympicsExampleWithoutPublish() { + val medals = Olympics.mountainBikeMedals.doOnEach(_ => println("onNext")) + medals.subscribe(println(_)) // triggers an execution of medals Observable + waitFor(medals) // triggers another execution of medals Observable + } - @Test def olympicsExample() { - val medals = Olympics.mountainBikeMedals.publish - medals.subscribe(println(_)) + @Test def olympicsExampleWithPublish() { + val medals = Olympics.mountainBikeMedals.doOnEach(_ => println("onNext")).publish + medals.subscribe(println(_)) // triggers an execution of medals Observable medals.connect - //waitFor(medals) + waitFor(medals) // triggers another execution of medals Observable } @Test def exampleWithoutPublish() { - val unshared = List(1 to 4).toObservable + val unshared = Observable.from(1 to 4) unshared.subscribe(n => println(s"subscriber 1 gets $n")) unshared.subscribe(n => println(s"subscriber 2 gets $n")) } @Test def exampleWithPublish() { - val unshared = List(1 to 4).toObservable + val unshared = Observable.from(1 to 4) val shared = unshared.publish shared.subscribe(n => println(s"subscriber 1 gets $n")) shared.subscribe(n => println(s"subscriber 2 gets $n")) shared.connect } + @Test def exampleWithPublish2() { + val unshared = Observable.from(1 to 4) + val shared = unshared.publish(0) + shared.subscribe(n => println(s"subscriber 1 gets $n")) + shared.subscribe(n => println(s"subscriber 2 gets $n")) + shared.connect + } + + @Test def exampleWithPublish3() { + val o = Observable.interval(100 millis).take(5).publish((o: Observable[Long]) => o.map(_ * 2)) + o.subscribe(n => println(s"subscriber 1 gets $n")) + o.subscribe(n => println(s"subscriber 2 gets $n")) + Thread.sleep(1000) + } + + @Test def exampleWithPublish4() { + val o = Observable.interval(100 millis).take(5).publish((o: Observable[Long]) => o.map(_ * 2), -1L) + o.subscribe(n => println(s"subscriber 1 gets $n")) + o.subscribe(n => println(s"subscriber 2 gets $n")) + Thread.sleep(1000) + } + def doLater(waitTime: Duration, action: () => Unit): Unit = { Observable.interval(waitTime).take(1).subscribe(_ => action()) } @@ -296,14 +343,54 @@ class RxScalaDemo extends JUnitSuite { @Test def exampleWithReplay() { val numbers = Observable.interval(1000 millis).take(6) - val (startFunc, sharedNumbers) = numbers.replay + val sharedNumbers = numbers.replay sharedNumbers.subscribe(n => println(s"subscriber 1 gets $n")) - startFunc() + sharedNumbers.connect // subscriber 2 subscribes later but still gets all numbers doLater(3500 millis, () => { sharedNumbers.subscribe(n => println(s"subscriber 2 gets $n")) }) waitFor(sharedNumbers) } + @Test def exampleWithReplay2() { + val numbers = Observable.interval(100 millis).take(10) + val sharedNumbers = numbers.replay(3) + sharedNumbers.subscribe(n => println(s"subscriber 1 gets $n")) + sharedNumbers.connect + // subscriber 2 subscribes later but only gets the 3 buffered numbers and the following numbers + Thread.sleep(700) + sharedNumbers.subscribe(n => println(s"subscriber 2 gets $n")) + waitFor(sharedNumbers) + } + + @Test def exampleWithReplay3() { + val numbers = Observable.interval(100 millis).take(10) + val sharedNumbers = numbers.replay(300 millis) + sharedNumbers.subscribe(n => println(s"subscriber 1 gets $n")) + sharedNumbers.connect + // subscriber 2 subscribes later but only gets the buffered numbers and the following numbers + Thread.sleep(700) + sharedNumbers.subscribe(n => println(s"subscriber 2 gets $n")) + waitFor(sharedNumbers) + } + + @Test def exampleWithReplay4() { + val numbers = Observable.interval(100 millis).take(10) + val sharedNumbers = numbers.replay(2, 300 millis) + sharedNumbers.subscribe(n => println(s"subscriber 1 gets $n")) + sharedNumbers.connect + // subscriber 2 subscribes later but only gets the buffered numbers and the following numbers + Thread.sleep(700) + sharedNumbers.subscribe(n => println(s"subscriber 2 gets $n")) + waitFor(sharedNumbers) + } + + @Test def exampleWithReplay5() { + val numbers = Observable.interval(100 millis).take(10) + val sharedNumbers = numbers.replay[Long, Long]((o: Observable[Long]) => o.map(_ * 2)) + sharedNumbers.subscribe(n => println(s"subscriber gets $n")) + waitFor(sharedNumbers) + } + @Test def testSingleOption() { assertEquals(None, List(1, 2).toObservable.toBlockingObservable.singleOption) assertEquals(Some(1), List(1).toObservable.toBlockingObservable.singleOption) @@ -372,6 +459,53 @@ class RxScalaDemo extends JUnitSuite { assertEquals(10, List(-1, 0, 1).toObservable.filter(condition).firstOrElse(10).toBlockingObservable.single) } + @Test def firstLastSingleExample() { + assertEquals(1, List(1, 2, 3, 4).toObservable.head.toBlockingObservable.single) + assertEquals(1, List(1, 2, 3, 4).toObservable.first.toBlockingObservable.single) + assertEquals(4, List(1, 2, 3, 4).toObservable.last.toBlockingObservable.single) + assertEquals(1, List(1).toObservable.single.toBlockingObservable.single) + + assertEquals(1, List(1, 2, 3, 4).toObservable.toBlockingObservable.head) + assertEquals(1, List(1, 2, 3, 4).toObservable.toBlockingObservable.first) + assertEquals(4, List(1, 2, 3, 4).toObservable.toBlockingObservable.last) + assertEquals(1, List(1).toObservable.toBlockingObservable.single) + } + + @Test def dropExample() { + val o = List(1, 2, 3, 4).toObservable + assertEquals(List(3, 4), o.drop(2).toBlockingObservable.toList) + } + + @Test def dropWithTimeExample() { + val o = List(1, 2, 3, 4).toObservable.zip( + Observable.interval(500 millis, IOScheduler())).map(_._1) // emit every 500 millis + println( + o.drop(1250 millis, IOScheduler()).toBlockingObservable.toList // output List(3, 4) + ) + } + + @Test def dropRightExample() { + val o = List(1, 2, 3, 4).toObservable + assertEquals(List(1, 2), o.dropRight(2).toBlockingObservable.toList) + } + + @Test def dropRightWithTimeExample() { + val o = List(1, 2, 3, 4).toObservable.zip( + Observable.interval(500 millis, IOScheduler())).map(_._1) // emit every 500 millis + println( + o.dropRight(750 millis, IOScheduler()).toBlockingObservable.toList // output List(1, 2) + ) + } + + @Test def dropUntilExample() { + val o = List("Alice", "Bob", "Carlos").toObservable.zip( + Observable.interval(700 millis, IOScheduler())).map(_._1) // emit every 700 millis + val other = List(1).toObservable.delay(1 seconds) + println( + o.dropUntil(other).toBlockingObservable.toList // output List("Bob", "Carlos") + ) + } + def square(x: Int): Int = { println(s"$x*$x is being calculated on thread ${Thread.currentThread().getId}") Thread.sleep(100) // calculating a square is heavy work :) @@ -402,7 +536,7 @@ class RxScalaDemo extends JUnitSuite { } @Test def timestampExample() { - val timestamped = Observable.interval(100 millis).take(3).timestamp.toBlockingObservable + val timestamped = Observable.interval(100 millis).take(6).timestamp.toBlockingObservable for ((millis, value) <- timestamped if value > 0) { println(value + " at t = " + millis) } @@ -441,39 +575,109 @@ class RxScalaDemo extends JUnitSuite { val oc3: rx.Notification[_ <: Int] = oc2.asJavaNotification val oc4: rx.Notification[_ <: Any] = oc2.asJavaNotification } - - @Test def elementAtReplacement() { - assertEquals("b", List("a", "b", "c").toObservable.drop(1).first.toBlockingObservable.single) - } - - @Test def elementAtOrDefaultReplacement() { - assertEquals("b", List("a", "b", "c").toObservable.drop(1).firstOrElse("!").toBlockingObservable.single) - assertEquals("!!", List("a", "b", "c").toObservable.drop(10).firstOrElse("!!").toBlockingObservable.single) - } @Test def takeWhileWithIndexAlternative { val condition = true List("a", "b").toObservable.zipWithIndex.takeWhile{case (elem, index) => condition}.map(_._1) } - @Test def createExample() { + def calculateElement(index: Int): String = { + println("omg I'm calculating so hard") + index match { + case 0 => "a" + case 1 => "b" + case _ => throw new IllegalArgumentException + } + } + + /** + * This is a bad way of using Observable.create, because even if the consumer unsubscribes, + * all elements are calculated. + */ + @Test def createExampleBad() { val o = Observable.create[String](observer => { - // this is bad because you cannot unsubscribe! - observer.onNext("a") - observer.onNext("b") + observer.onNext(calculateElement(0)) + observer.onNext(calculateElement(1)) observer.onCompleted() Subscription {} }) - o.subscribe(println(_)) + o.take(1).subscribe(println(_)) + } + + /** + * This is the good way of doing it: If the consumer unsubscribes, no more elements are + * calculated. + */ + @Test def createExampleGood() { + val o = Observable[String](subscriber => { + var i = 0 + while (i < 2 && !subscriber.isUnsubscribed) { + subscriber.onNext(calculateElement(i)) + i += 1 + } + if (!subscriber.isUnsubscribed) subscriber.onCompleted() + }) + o.take(1).subscribe(println(_)) + } + + @Test def createExampleGood2() { + import scala.io.{Codec, Source} + + val rxscala = Observable[String](subscriber => { + try { + val input = new java.net.URL("http://rxscala.github.io/").openStream() + subscriber.add(Subscription { + input.close() + }) + Source.fromInputStream(input)(Codec.UTF8).getLines() + .takeWhile(_ => !subscriber.isUnsubscribed) + .foreach(subscriber.onNext(_)) + if (!subscriber.isUnsubscribed) { + subscriber.onCompleted() + } + } + catch { + case e: Throwable => if (!subscriber.isUnsubscribed) subscriber.onError(e) + } + }).subscribeOn(IOScheduler()) + + val count = rxscala.flatMap(_.split("\\W+").toSeq.toObservable) + .map(_.toLowerCase) + .filter(_ == "rxscala") + .size + println(s"RxScala appears ${count.toBlockingObservable.single} times in http://rxscala.github.io/") } def output(s: String): Unit = println(s) - // blocks until obs has completed + /** Subscribes to obs and waits until obs has completed. Note that if you subscribe to + * obs yourself and also call waitFor(obs), all side-effects of subscribing to obs + * will happen twice. + */ def waitFor[T](obs: Observable[T]): Unit = { obs.toBlockingObservable.toIterable.last } + @Test def doOnTerminateExample(): Unit = { + val o = List("red", "green", "blue").toObservable.doOnTerminate(() => println("terminate")) + o.subscribe(v => println(v), e => e.printStackTrace, () => println("onCompleted")) + // red + // green + // blud + // terminate + // onCompleted + } + + @Test def finallyDoExample(): Unit = { + val o = List("red", "green", "blue").toObservable.finallyDo(() => println("finally")) + o.subscribe(v => println(v), e => e.printStackTrace, () => println("onCompleted")) + // red + // green + // blud + // onCompleted + // finally + } + @Test def timeoutExample(): Unit = { val other = List(100L, 200L, 300L).toObservable val result = Observable.interval(100 millis).timeout(50 millis, other).toBlockingObservable.toList @@ -495,4 +699,292 @@ class RxScalaDemo extends JUnitSuite { println(result) } + @Test def ambExample(): Unit = { + val o1 = List(100L, 200L, 300L).toObservable.delay(4 seconds) + val o2 = List(1000L, 2000L, 3000L).toObservable.delay(2 seconds) + val result = o1.amb(o2).toBlockingObservable.toList + println(result) + } + + @Test def delayExample(): Unit = { + val o = List(100L, 200L, 300L).toObservable.delay(2 seconds) + val result = o.toBlockingObservable.toList + println(result) + } + + @Test def delayExample2(): Unit = { + val o = List(100L, 200L, 300L).toObservable.delay(2 seconds, IOScheduler()) + val result = o.toBlockingObservable.toList + println(result) + } + + @Test def delayExample3(): Unit = { + val o = List(100, 500, 200).toObservable.delay( + (i: Int) => Observable.items(i).delay(i millis) + ) + o.toBlockingObservable.foreach(println(_)) + } + + @Test def delayExample4(): Unit = { + val o = List(100, 500, 200).toObservable.delay( + () => Observable.interval(500 millis).take(1), + (i: Int) => Observable.items(i).delay(i millis) + ) + o.toBlockingObservable.foreach(println(_)) + } + + @Test def delaySubscriptionExample(): Unit = { + val o = List(100L, 200L, 300L).toObservable.delaySubscription(2 seconds) + val result = o.toBlockingObservable.toList + println(result) + } + + @Test def delaySubscriptionExample2(): Unit = { + val o = List(100L, 200L, 300L).toObservable.delaySubscription(2 seconds, IOScheduler()) + val result = o.toBlockingObservable.toList + println(result) + } + + @Test def elementAtExample(): Unit = { + val o = List("red", "green", "blue").toObservable + println(o.elementAt(2).toBlockingObservable.single) + } + + @Test def elementAtOrDefaultExample(): Unit = { + val o : Observable[Seq[Char]] = List("red".toList, "green".toList, "blue".toList).toObservable.elementAtOrDefault(3, "black".toSeq) + println(o.toBlockingObservable.single) + } + + @Test def toMapExample1(): Unit = { + val o : Observable[String] = List("alice", "bob", "carol").toObservable + val keySelector = (s: String) => s.head + val m = o.toMap(keySelector) + println(m.toBlockingObservable.single) + } + + @Test def toMapExample2(): Unit = { + val o : Observable[String] = List("alice", "bob", "carol").toObservable + val keySelector = (s: String) => s.head + val valueSelector = (s: String) => s.tail + val m = o.toMap(keySelector, valueSelector) + println(m.toBlockingObservable.single) + } + + @Test def toMapExample3(): Unit = { + val o : Observable[String] = List("alice", "bob", "carol").toObservable + val keySelector = (s: String) => s.head + val valueSelector = (s: String) => s.tail + val mapFactory = () => Map(('s',"tart")) + val m = o.toMap(keySelector, valueSelector, mapFactory) + println(m.toBlockingObservable.single) + } + + @Test def containsExample(): Unit = { + val o1 = List(1, 2, 3).toObservable.contains(2) + assertTrue(o1.toBlockingObservable.single) + + val o2 = List(1, 2, 3).toObservable.contains(4) + assertFalse(o2.toBlockingObservable.single) + } + + @Test def repeatExample1(): Unit = { + val o : Observable[String] = List("alice", "bob", "carol").toObservable.repeat.take(6) + assertEquals(List("alice", "bob", "carol", "alice", "bob", "carol"), o.toBlockingObservable.toList) + } + + @Test def repeatExample2(): Unit = { + val o : Observable[String] = List("alice", "bob", "carol").toObservable.repeat(2) + assertEquals(List("alice", "bob", "carol", "alice", "bob", "carol"), o.toBlockingObservable.toList) + } + + @Test def retryExample1(): Unit = { + val o : Observable[String] = List("alice", "bob", "carol").toObservable + assertEquals(List("alice", "bob", "carol"), o.retry.toBlockingObservable.toList) + } + + @Test def retryExample2(): Unit = { + val o : Observable[String] = List("alice", "bob", "carol").toObservable + assertEquals(List("alice", "bob", "carol"), o.retry(3).toBlockingObservable.toList) + } + + @Test def liftExample1(): Unit = { + // Add "No. " in front of each item + val o = List(1, 2, 3).toObservable.lift { + subscriber: Subscriber[String] => + Subscriber[Int]( + subscriber, + (v: Int) => subscriber.onNext("No. " + v), + e => subscriber.onError(e), + () => subscriber.onCompleted + ) + }.toBlockingObservable.toList + println(o) + } + + @Test def liftExample2(): Unit = { + // Split the input Strings with " " + val splitStringsWithSpace = (subscriber: Subscriber[String]) => { + Subscriber[String]( + subscriber, + (v: String) => v.split(" ").foreach(subscriber.onNext(_)), + e => subscriber.onError(e), + () => subscriber.onCompleted + ) + } + + // Convert the input Strings to Chars + val stringsToChars = (subscriber: Subscriber[Char]) => { + Subscriber[String]( + subscriber, + (v: String) => v.foreach(subscriber.onNext(_)), + e => subscriber.onError(e), + () => subscriber.onCompleted + ) + } + + // Skip the first n items. If the length of source is less than n, throw an IllegalArgumentException + def skipWithException[T](n: Int) = (subscriber: Subscriber[T]) => { + var count = 0 + Subscriber[T]( + subscriber, + (v: T) => { + if (count >= n) subscriber.onNext(v) + count += 1 + }, + e => subscriber.onError(e), + () => if (count < n) subscriber.onError(new IllegalArgumentException("There is no enough items")) else subscriber.onCompleted + ) + } + + val o = List("RxJava – Reactive Extensions for the JVM").toObservable + .lift(splitStringsWithSpace) + .map(_.toLowerCase) + .lift(stringsToChars) + .filter(_.isLetter) + .lift(skipWithException(100)) + try { + o.toBlockingObservable.toList + } + catch { + case e: IllegalArgumentException => println("IllegalArgumentException from skipWithException") + } + } + + @Test def multicastExample1(): Unit = { + val unshared = Observable.from(1 to 4) + val shared = unshared.multicast(Subject()) + shared.subscribe(n => println(s"subscriber 1 gets $n")) + shared.subscribe(n => println(s"subscriber 2 gets $n")) + shared.connect + } + + @Test def multicastExample2(): Unit = { + val unshared = Observable.from(1 to 4) + val shared = unshared.multicast[Int, String](() => Subject(), o => o.map("No. " + _)) + shared.subscribe(n => println(s"subscriber 1 gets $n")) + shared.subscribe(n => println(s"subscriber 2 gets $n")) + } + + @Test def startWithExample(): Unit = { + val o1 = List(3, 4).toObservable + val o2 = 1 +: 2 +: o1 + assertEquals(List(1, 2, 3, 4), o2.toBlockingObservable.toList) + } + + @Test def appendExample(): Unit = { + val o = List(1, 2).toObservable :+ 3 :+ 4 + assertEquals(List(1, 2, 3, 4), o.toBlockingObservable.toList) + } + + @Test def sequenceEqualExampe(): Unit = { + val o1 = List(1, 2, 3).toObservable + val o2 = List(1, 2, 3).toObservable + val o3 = List(1, 2).toObservable + val o4 = List(1.0, 2.0, 3.0).toObservable + assertTrue(o1.sequenceEqual(o2).toBlockingObservable.single) + assertFalse(o1.sequenceEqual(o3).toBlockingObservable.single) + assertTrue(o1.sequenceEqual(o4).toBlockingObservable.single) + } + + @Test def takeExample(): Unit = { + val o = (1 to 20).toObservable + .zip(Observable.interval(300 millis)) + .map(_._1) + .take(2 seconds) + println(o.toBlockingObservable.toList) + } + + @Test def takeRightExample(): Unit = { + val o = (1 to 6).toObservable.takeRight(3) + assertEquals(List(4, 5, 6), o.toBlockingObservable.toList) + } + + @Test def takeRightExample2(): Unit = { + val o = (1 to 10).toObservable + .zip(Observable.interval(100 millis)) + .map(_._1) + .takeRight(300 millis) + println(o.toBlockingObservable.toList) + } + + @Test def takeRightExample3(): Unit = { + val o = (1 to 10).toObservable + .zip(Observable.interval(100 millis)) + .map(_._1) + .takeRight(2, 300 millis) + println(o.toBlockingObservable.toList) + } + + @Test def timeIntervalExample(): Unit = { + val o = (1 to 10).toObservable + .zip(Observable.interval(100 millis)) + .map(_._1) + .timeInterval + println(o.toBlockingObservable.toList) + } + + @Test def schedulerExample1(): Unit = { + val latch = new CountDownLatch(1) + val worker = IOScheduler().createWorker + worker.schedule { + println("Hello from Scheduler") + latch.countDown() + } + latch.await(5, TimeUnit.SECONDS) + } + + @Test def schedulerExample2(): Unit = { + val latch = new CountDownLatch(1) + val worker = IOScheduler().createWorker + worker.schedule(1 seconds) { + println("Hello from Scheduler after 1 second") + latch.countDown() + } + latch.await(5, TimeUnit.SECONDS) + } + + @Test def schedulerExample3(): Unit = { + val worker = IOScheduler().createWorker + var no = 1 + val subscription = worker.schedulePeriodically(initialDelay = 1 seconds, period = 100 millis) { + println(s"Hello(${no}) from Scheduler") + no += 1 + } + TimeUnit.SECONDS.sleep(2) + subscription.unsubscribe() + } + + @Test def schedulerExample4(): Unit = { + val worker = IOScheduler().createWorker + var no = 1 + def hello: Unit = { + println(s"Hello(${no}) from Scheduler") + no += 1 + worker.schedule(100 millis)(hello) + } + val subscription = worker.schedule(1 seconds)(hello) + TimeUnit.SECONDS.sleep(2) + subscription.unsubscribe() + } + } diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/ImplicitFunctionConversions.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/ImplicitFunctionConversions.scala index 3c6aa5c29a..423ab119c9 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/ImplicitFunctionConversions.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/ImplicitFunctionConversions.scala @@ -17,10 +17,8 @@ package rx.lang.scala import java.lang.Exception import java.{ lang => jlang } - import scala.language.implicitConversions import scala.collection.Seq - import rx.functions._ import rx.lang.scala.JavaConversions._ @@ -56,6 +54,13 @@ object ImplicitFunctionConversions { } } + implicit def scalaAction1ToOnSubscribe[T](f: Subscriber[T] => Unit) = + new rx.Observable.OnSubscribe[T] { + def call(s: rx.Subscriber[_ >: T]): Unit = { + f(s) + } + } + implicit def scalaByNameParamToFunc0[B](param: => B): Func0[B] = new Func0[B] { def call(): B = param diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/JavaConversions.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/JavaConversions.scala index d003842be4..df86c08f80 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/JavaConversions.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/JavaConversions.scala @@ -32,11 +32,15 @@ object JavaConversions { implicit def toScalaSubscription(s: rx.Subscription): Subscription = Subscription(s) + implicit def toJavaSubscriber[T](s: Subscriber[T]): rx.Subscriber[_ >: T] = s.asJavaSubscriber + + implicit def toScalaSubscriber[T](s: rx.Subscriber[_ >: T]): Subscriber[T] = Subscriber(s) + implicit def scalaSchedulerToJavaScheduler(s: Scheduler): rx.Scheduler = s.asJavaScheduler implicit def javaSchedulerToScalaScheduler(s: rx.Scheduler): Scheduler = Scheduler(s) - implicit def scalaInnerToJavaInner(s: Inner): rx.Scheduler.Inner = s.asJavaInner - implicit def javaInnerToScalaInner(s: rx.Scheduler.Inner): Inner = Inner(s) + implicit def scalaWorkerToJavaWorker(s: Worker): rx.Scheduler.Worker = s.asJavaWorker + implicit def javaWorkerToScalaWorker(s: rx.Scheduler.Worker): Worker = Worker(s) implicit def toJavaObserver[T](s: Observer[T]): rx.Observer[_ >: T] = s.asJavaObserver @@ -50,4 +54,12 @@ object JavaConversions { val asJavaObservable = observable } } + + implicit def toJavaOperator[T, R](operator: Subscriber[R] => Subscriber[T]): rx.Observable.Operator[R, T] = { + new rx.Observable.Operator[R, T] { + override def call(subscriber: rx.Subscriber[_ >: R]): rx.Subscriber[_ >: T] = { + toJavaSubscriber[T](operator(toScalaSubscriber[R](subscriber))) + } + } + } } \ No newline at end of file diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Notification.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Notification.scala index 84f1642dbc..c9e8cd4822 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Notification.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Notification.scala @@ -137,7 +137,7 @@ object Notification { * @param notification * The [[rx.lang.scala.Notification]] to be deconstructed * @return - * The [[java.lang.Throwable]] value contained in this notification. + * The `java.lang.Throwable` value contained in this notification. */ def unapply[U](notification: Notification[U]): Option[Throwable] = notification match { case onError: OnError[U] => Some(onError.error) diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala index 9243ec3e6c..eb3d078516 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observable.scala @@ -20,6 +20,8 @@ import rx.functions.FuncN import rx.Observable.OnSubscribeFunc import rx.lang.scala.observables.ConnectableObservable import scala.concurrent.duration +import java.util +import collection.JavaConversions._ /** @@ -46,6 +48,28 @@ import scala.concurrent.duration * the observer * @define subscribeObserverParamScheduler * the [[rx.lang.scala.Scheduler]] on which Observers subscribe to the Observable + * + * @define subscribeSubscriberMain + * Call this method to subscribe an [[Subscriber]] for receiving items and notifications from the [[Observable]]. + * + * A typical implementation of `subscribe` does the following: + * + * It stores a reference to the Observer in a collection object, such as a `List[T]` object. + * + * It returns a reference to the [[rx.lang.scala.Subscription]] interface. This enables [[Subscriber]]s to + * unsubscribe, that is, to stop receiving items and notifications before the Observable stops + * sending them, which also invokes the Subscriber's [[rx.lang.scala.Observer.onCompleted onCompleted]] method. + * + * An [[Observable]] instance is responsible for accepting all subscriptions + * and notifying all [[Subscriber]]s. Unless the documentation for a particular + * [[Observable]] implementation indicates otherwise, [[Subscriber]]s should make no + * assumptions about the order in which multiple [[Subscriber]]s will receive their notifications. + * + * @define subscribeSubscriberParamObserver + * the [[Subscriber]] + * @define subscribeSubscriberParamScheduler + * the [[rx.lang.scala.Scheduler]] on which [[Subscriber]]s subscribe to the Observable + * * @define subscribeAllReturn * a [[rx.lang.scala.Subscription]] reference whose `unsubscribe` method can be called to stop receiving items * before the Observable has finished sending them @@ -75,8 +99,9 @@ import scala.concurrent.duration */ trait Observable[+T] { + import scala.collection.JavaConverters._ import scala.collection.Seq - import scala.concurrent.duration.{Duration, TimeUnit} + import scala.concurrent.duration.{Duration, TimeUnit, MILLISECONDS} import rx.functions._ import rx.lang.scala.observables.BlockingObservable import ImplicitFunctionConversions._ @@ -97,11 +122,10 @@ trait Observable[+T] * $subscribeObserverMain * * @param observer $subscribeObserverParamObserver - * @param scheduler $subscribeObserverParamScheduler * @return $subscribeAllReturn */ - def subscribe(observer: Observer[T], scheduler: Scheduler): Subscription = { - asJavaObservable.subscribe(observer.asJavaObserver, scheduler) + def subscribe(observer: Observer[T]): Subscription = { + asJavaObservable.subscribe(observer.asJavaObserver) } /** @@ -110,17 +134,27 @@ trait Observable[+T] * @param observer $subscribeObserverParamObserver * @return $subscribeAllReturn */ - def subscribe(observer: Observer[T]): Subscription = { - asJavaObservable.subscribe(observer.asJavaObserver) + def apply(observer: Observer[T]): Subscription = subscribe(observer) + + /** + * $subscribeSubscriberMain + * + * @param subscriber $subscribeSubscriberParamObserver + * @return $subscribeAllReturn + */ + def subscribe(subscriber: Subscriber[T]): Subscription = { + // Add the casting to avoid compile error "ambiguous reference to overloaded definition" + val thisJava = asJavaObservable.asInstanceOf[rx.Observable[T]] + thisJava.subscribe(subscriber.asJavaSubscriber) } /** - * $subscribeObserverMain + * $subscribeSubscriberMain * - * @param observer $subscribeObserverParamObserver + * @param subscriber $subscribeSubscriberParamObserver * @return $subscribeAllReturn */ - def apply(observer: Observer[T]): Subscription = subscribe(observer) + def apply(subscriber: Subscriber[T]): Subscription = subscribe(subscriber) /** * $subscribeCallbacksMainNoNotifications @@ -163,60 +197,42 @@ trait Observable[+T] } /** - * $subscribeCallbacksMainWithNotifications - * - * @param onNext $subscribeCallbacksParamOnNext - * @param onError $subscribeCallbacksParamOnError - * @param onCompleted $subscribeCallbacksParamOnComplete - * @param scheduler $subscribeCallbacksParamScheduler - * @return $subscribeAllReturn - */ - def subscribe(onNext: T => Unit, onError: Throwable => Unit, onCompleted: () => Unit, scheduler: Scheduler): Subscription = { - asJavaObservable.subscribe(scalaFunction1ProducingUnitToAction1(onNext), - scalaFunction1ProducingUnitToAction1(onError), - scalaFunction0ProducingUnitToAction0(onCompleted), - scheduler) - } - - /** - * $subscribeCallbacksMainWithNotifications + * Returns a pair of a start function and an [[rx.lang.scala.Observable]] that upon calling the start function causes the source Observable to + * push results into the specified subject. * - * @param onNext $subscribeCallbacksParamOnNext - * @param onError $subscribeCallbacksParamOnError - * @param scheduler $subscribeCallbacksParamScheduler - * @return $subscribeAllReturn + * @param subject + * the `rx.lang.scala.subjects.Subject` to push source items into + * @return a pair of a start function and an [[rx.lang.scala.Observable]] such that when the start function + * is called, the Observable starts to push results into the specified Subject */ - def subscribe(onNext: T => Unit, onError: Throwable => Unit, scheduler: Scheduler): Subscription = { - asJavaObservable.subscribe( - scalaFunction1ProducingUnitToAction1(onNext), - scalaFunction1ProducingUnitToAction1(onError), - scheduler) + def multicast[R >: T](subject: rx.lang.scala.Subject[R]): ConnectableObservable[R] = { + val s: rx.subjects.Subject[_ >: T, _<: R] = subject.asJavaSubject + new ConnectableObservable[R](asJavaObservable.multicast(s)) } /** - * $subscribeCallbacksMainNoNotifications + * Returns an Observable that emits items produced by multicasting the source Observable within a selector function. * - * @param onNext $subscribeCallbacksParamOnNext - * @param scheduler $subscribeCallbacksParamScheduler - * @return $subscribeAllReturn + * @param subjectFactory the `Subject` factory + * @param selector the selector function, which can use the multicasted source Observable subject to the policies + * enforced by the created `Subject` + * @return an Observable that emits the items produced by multicasting the source Observable within a selector function */ - def subscribe(onNext: T => Unit, scheduler: Scheduler): Subscription = { - asJavaObservable.subscribe(scalaFunction1ProducingUnitToAction1(onNext), scheduler) + def multicast[R >: T, U](subjectFactory: () => rx.lang.scala.Subject[R], selector: Observable[R] => Observable[U]): Observable[U] = { + val subjectFactoryJava: Func0[rx.subjects.Subject[_ >: T, _ <: R]] = () => subjectFactory().asJavaSubject + val selectorJava: Func1[rx.Observable[R], rx.Observable[U]] = + (jo: rx.Observable[R]) => selector(toScalaObservable[R](jo)).asJavaObservable.asInstanceOf[rx.Observable[U]] + toScalaObservable[U](asJavaObservable.multicast[R, U](subjectFactoryJava, selectorJava)) } /** - * Returns a pair of a start function and an [[rx.lang.scala.Observable]] that upon calling the start function causes the source Observable to - * push results into the specified subject. + * Returns an Observable that first emits the items emitted by `this`, and then `elem`. * - * @param subject - * the `rx.lang.scala.subjects.Subject` to push source items into - * @return a pair of a start function and an [[rx.lang.scala.Observable]] such that when the start function - * is called, the Observable starts to push results into the specified Subject + * @param elem the item to be appended + * @return an Observable that first emits the items emitted by `this`, and then `elem`. */ - def multicast[R >: T](subject: rx.lang.scala.Subject[R]): (() => Subscription, Observable[R]) = { - val s: rx.subjects.Subject[_ >: T, _<: R] = subject.asJavaSubject - val javaCO: rx.observables.ConnectableObservable[R] = asJavaObservable.multicast(s) - (() => javaCO.connect(), toScalaObservable(javaCO)) + def :+[U >: T](elem: U): Observable[U] = { + this ++ Observable.items(elem) } /** @@ -236,6 +252,19 @@ trait Observable[+T] toScalaObservable(rx.Observable.concat(o1, o2)) } + /** + * Returns an Observable that emits a specified item before it begins to emit items emitted by the source Observable. + *

+ * + * + * @param elem the item to emit + * @return an Observable that emits the specified item before it begins to emit items emitted by the source Observable + */ + def +:[U >: T](elem: U): Observable[U] = { + val thisJava = this.asJavaObservable.asInstanceOf[rx.Observable[U]] + toScalaObservable(thisJava.startWith(elem)) + } + /** * Returns an Observable that emits the items emitted by several Observables, one after the * other. @@ -261,13 +290,13 @@ trait Observable[+T] * * A well-behaved Observable does not interleave its invocations of the [[rx.lang.scala.Observer.onNext onNext]], [[rx.lang.scala.Observer.onCompleted onCompleted]], and [[rx.lang.scala.Observer.onError onError]] methods of * its [[rx.lang.scala.Observer]]s; it invokes `onCompleted` or `onError` only once; and it never invokes `onNext` after invoking either `onCompleted` or `onError`. - * `synchronize` enforces this, and the Observable it returns invokes `onNext` and `onCompleted` or `onError` synchronously. + * [[Observable.serialize serialize]] enforces this, and the Observable it returns invokes `onNext` and `onCompleted` or `onError` synchronously. * * @return an Observable that is a chronologically well-behaved version of the source * Observable, and that synchronously notifies its [[rx.lang.scala.Observer]]s */ - def synchronize: Observable[T] = { - toScalaObservable[T](asJavaObservable.synchronize) + def serialize: Observable[T] = { + toScalaObservable[T](asJavaObservable.serialize) } /** @@ -304,7 +333,7 @@ trait Observable[+T] * is the minumum of the number of `onNext` invocations of `this` and `that`. */ def zip[U](that: Observable[U]): Observable[(T, U)] = { - zip(that, (t: T, u: U) => (t, u)) + zipWith(that, (t: T, u: U) => (t, u)) } /** @@ -312,10 +341,8 @@ trait Observable[+T] * corresponding elements using the selector function. * The number of `onNext` invocations of the resulting `Observable[(T, U)]` * is the minumum of the number of `onNext` invocations of `this` and `that`. - * - * Note that this function is private because Scala collections don't have such a function. */ - private def zip[U, R](that: Observable[U], selector: (T,U) => R): Observable[R] = { + def zipWith[U, R](that: Observable[U], selector: (T,U) => R): Observable[R] = { toScalaObservable[R](rx.Observable.zip[T, U, R](this.asJavaObservable, that.asJavaObservable, selector)) } @@ -326,8 +353,7 @@ trait Observable[+T] * their index. Indices start at 0. */ def zipWithIndex: Observable[(T, Int)] = { - var n = 0; - this.map(x => { val result = (x,n); n += 1; result }) + zip((0 until Int.MaxValue).toObservable) } /** @@ -345,18 +371,17 @@ trait Observable[+T] * An [[rx.lang.scala.Observable]] which produces connected non-overlapping buffers, which are emitted * when the current [[rx.lang.scala.Observable]] created with the function argument produces an object. */ - def buffer[Closing](closings: () => Observable[_ <: Closing]) : Observable[Seq[T]] = { - val f: Func0[_ <: rx.Observable[_ <: Closing]] = closings().asJavaObservable - val jObs: rx.Observable[_ <: java.util.List[_]] = asJavaObservable.buffer[Closing](f) + def buffer(closings: () => Observable[Any]) : Observable[Seq[T]] = { + val f: Func0[_ <: rx.Observable[_ <: Any]] = closings().asJavaObservable + val jObs: rx.Observable[_ <: java.util.List[_]] = asJavaObservable.buffer[Any](f) Observable.jObsOfListToScObsOfSeq(jObs.asInstanceOf[rx.Observable[_ <: java.util.List[T]]]) } /** * Creates an Observable which produces buffers of collected values. * * This Observable produces buffers. Buffers are created when the specified `openings` - * Observable produces an object. Additionally the function argument - * is used to create an Observable which produces [[rx.lang.scala.util.Closing]] objects. When this - * Observable produces such an object, the associated buffer is emitted. + * Observable produces an object. That object is used to construct an Observable to emit buffers, feeding it into `closings` function. + * Buffers are emitted when the created Observable produces an object. * * @param openings * The [[rx.lang.scala.Observable]] which, when it produces an object, will cause @@ -540,6 +565,43 @@ trait Observable[+T] Observable.jObsOfListToScObsOfSeq(oJava.asInstanceOf[rx.Observable[_ <: java.util.List[T]]]) } + /** + * Returns an Observable that emits non-overlapping buffered items from the source Observable each time the + * specified boundary Observable emits an item. + *

+ * + *

+ * Completion of either the source or the boundary Observable causes the returned Observable to emit the + * latest buffer and complete. + * + * @param boundary the boundary Observable + * @return an Observable that emits buffered items from the source Observable when the boundary Observable + * emits an item + */ + def buffer(boundary: Observable[Any]): Observable[Seq[T]] = { + val thisJava = this.asJavaObservable.asInstanceOf[rx.Observable[T]] + toScalaObservable(thisJava.buffer(boundary.asJavaObservable)).map(_.asScala) + } + + /** + * Returns an Observable that emits non-overlapping buffered items from the source Observable each time the + * specified boundary Observable emits an item. + *

+ * + *

+ * Completion of either the source or the boundary Observable causes the returned Observable to emit the + * latest buffer and complete. + * + * @param boundary the boundary Observable + * @param initialCapacity the initial capacity of each buffer chunk + * @return an Observable that emits buffered items from the source Observable when the boundary Observable + * emits an item + */ + def buffer(boundary: Observable[Any], initialCapacity: Int): Observable[Seq[T]] = { + val thisJava = this.asJavaObservable.asInstanceOf[rx.Observable[T]] + toScalaObservable(thisJava.buffer(boundary.asJavaObservable, initialCapacity)).map(_.asScala) + } + /** * Creates an Observable which produces windows of collected values. This Observable produces connected * non-overlapping windows. The current window is emitted and replaced with a new window when the @@ -566,10 +628,9 @@ trait Observable[+T] } /** - * Creates an Observable which produces windows of collected values. This Observable produces windows. - * Chunks are created when the specified `openings` Observable produces an object. - * Additionally the `closings` argument is used to create an Observable which produces [[rx.lang.scala.util.Closing]] objects. - * When this Observable produces such an object, the associated window is emitted. + * Creates an Observable which produces windows of collected values. Chunks are created when the specified `openings` + * Observable produces an object. That object is used to construct an Observable to emit windows, feeding it into `closings` function. + * Windows are emitted when the created Observable produces an object. * * @param openings * The [[rx.lang.scala.Observable]] which when it produces an object, will cause @@ -1032,9 +1093,281 @@ trait Observable[+T] * @return a pair of a start function and an [[rx.lang.scala.Observable]] such that when the start function * is called, the Observable starts to emit items to its [[rx.lang.scala.Observer]]s */ - def replay: (() => Subscription, Observable[T]) = { - val javaCO = asJavaObservable.replay() - (() => javaCO.connect(), toScalaObservable[T](javaCO)) + def replay: ConnectableObservable[T] = { + new ConnectableObservable[T](asJavaObservable.replay()) + } + + /** + * Returns an Observable that emits items that are the results of invoking a specified selector on the items + * emitted by a `ConnectableObservable` that shares a single subscription to the source Observable. + *

+ * + * + * @param selector the selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the Observable + * @return an Observable that emits items that are the results of invoking the selector on a `ConnectableObservable` + * that shares a single subscription to the source Observable + */ + def replay[U >: T, R](selector: Observable[U] => Observable[R]): Observable[R] = { + val thisJava = this.asJavaObservable.asInstanceOf[rx.Observable[U]] + val fJava: Func1[rx.Observable[U], rx.Observable[R]] = + (jo: rx.Observable[U]) => selector(toScalaObservable[U](jo)).asJavaObservable.asInstanceOf[rx.Observable[R]] + toScalaObservable[R](thisJava.replay(fJava)) + } + + /** + * Returns an Observable that emits items that are the results of invoking a specified selector on items + * emitted by a `ConnectableObservable` that shares a single subscription to the source Observable, + * replaying `bufferSize` notifications. + *

+ * + * + * @param selector the selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the Observable + * @param bufferSize the buffer size that limits the number of items the connectable observable can replay + * @return an Observable that emits items that are the results of invoking the selector on items emitted by + * a `ConnectableObservable` that shares a single subscription to the source Observable replaying + * no more than `bufferSize` items + */ + def replay[U >: T, R](selector: Observable[U] => Observable[R], bufferSize: Int): Observable[R] = { + val thisJava = this.asJavaObservable.asInstanceOf[rx.Observable[U]] + val fJava: Func1[rx.Observable[U], rx.Observable[R]] = + (jo: rx.Observable[U]) => selector(toScalaObservable[U](jo)).asJavaObservable.asInstanceOf[rx.Observable[R]] + toScalaObservable[R](thisJava.replay(fJava, bufferSize)) + } + + /** + * Returns an Observable that emits items that are the results of invoking a specified selector on items + * emitted by a `ConnectableObservable` that shares a single subscription to the source Observable, + * replaying no more than `bufferSize` items that were emitted within a specified time window. + *

+ * + * + * @param selector a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the Observable + * @param bufferSize the buffer size that limits the number of items the connectable observable can replay + * @param time the duration of the window in which the replayed items must have been emitted + * @return an Observable that emits items that are the results of invoking the selector on items emitted by + * a `ConnectableObservable` that shares a single subscription to the source Observable, and + * replays no more than `bufferSize` items that were emitted within the window defined by `time` + */ + def replay[U >: T, R](selector: Observable[U] => Observable[R], bufferSize: Int, time: Duration): Observable[R] = { + val thisJava = this.asJavaObservable.asInstanceOf[rx.Observable[U]] + val fJava: Func1[rx.Observable[U], rx.Observable[R]] = + (jo: rx.Observable[U]) => selector(toScalaObservable[U](jo)).asJavaObservable.asInstanceOf[rx.Observable[R]] + toScalaObservable[R](thisJava.replay(fJava, bufferSize, time.length, time.unit)) + } + + /** + * Returns an Observable that emits items that are the results of invoking a specified selector on items + * emitted by a `ConnectableObservable` that shares a single subscription to the source Observable, + * replaying no more than `bufferSize` items that were emitted within a specified time window. + *

+ * + * + * @param selector a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the Observable + * @param bufferSize the buffer size that limits the number of items the connectable observable can replay + * @param time the duration of the window in which the replayed items must have been emitted + * @param scheduler the Scheduler that is the time source for the window + * @return an Observable that emits items that are the results of invoking the selector on items emitted by + * a `ConnectableObservable` that shares a single subscription to the source Observable, and + * replays no more than `bufferSize` items that were emitted within the window defined by `time` + * @throws IllegalArgumentException if `bufferSize` is less than zero + */ + def replay[U >: T, R](selector: Observable[U] => Observable[R], bufferSize: Int, time: Duration, scheduler: Scheduler): Observable[R] = { + val thisJava = this.asJavaObservable.asInstanceOf[rx.Observable[U]] + val fJava: Func1[rx.Observable[U], rx.Observable[R]] = + (jo: rx.Observable[U]) => selector(toScalaObservable[U](jo)).asJavaObservable.asInstanceOf[rx.Observable[R]] + toScalaObservable[R](thisJava.replay(fJava, bufferSize, time.length, time.unit, scheduler)) + } + + /** + * Returns an Observable that emits items that are the results of invoking a specified selector on items + * emitted by a `ConnectableObservable` that shares a single subscription to the source Observable, + * replaying a maximum of `bufferSize` items. + *

+ * + * + * @param selector a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the Observable + * @param bufferSize the buffer size that limits the number of items the connectable observable can replay + * @param scheduler the Scheduler on which the replay is observed + * @return an Observable that emits items that are the results of invoking the selector on items emitted by + * a `ConnectableObservable` that shares a single subscription to the source Observable, + * replaying no more than `bufferSize` notifications + */ + def replay[U >: T, R](selector: Observable[U] => Observable[R], bufferSize: Int, scheduler: Scheduler): Observable[R] = { + val thisJava = this.asJavaObservable.asInstanceOf[rx.Observable[U]] + val fJava: Func1[rx.Observable[U], rx.Observable[R]] = + (jo: rx.Observable[U]) => selector(toScalaObservable[U](jo)).asJavaObservable.asInstanceOf[rx.Observable[R]] + toScalaObservable[R](thisJava.replay(fJava, bufferSize, scheduler)) + } + + /** + * Returns an Observable that emits items that are the results of invoking a specified selector on items + * emitted by a `ConnectableObservable` that shares a single subscription to the source Observable, + * replaying all items that were emitted within a specified time window. + *

+ * + * + * @param selector a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the Observable + * @param time the duration of the window in which the replayed items must have been emitted + * @return an Observable that emits items that are the results of invoking the selector on items emitted by + * a `ConnectableObservable` that shares a single subscription to the source Observable, + * replaying all items that were emitted within the window defined by `time` + */ + def replay[U >: T, R](selector: Observable[U] => Observable[R], time: Duration, scheduler: Scheduler): Observable[R] = { + val thisJava = this.asJavaObservable.asInstanceOf[rx.Observable[U]] + val fJava: Func1[rx.Observable[U], rx.Observable[R]] = + (jo: rx.Observable[U]) => selector(toScalaObservable[U](jo)).asJavaObservable.asInstanceOf[rx.Observable[R]] + toScalaObservable[R](thisJava.replay(fJava, time.length, time.unit, scheduler)) + } + + /** + * Returns an Observable that emits items that are the results of invoking a specified selector on items + * emitted by a `ConnectableObservable` that shares a single subscription to the source Observable. + *

+ * + * + * @param selector a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the Observable + * @param scheduler the Scheduler where the replay is observed + * @return an Observable that emits items that are the results of invoking the selector on items emitted by + * a `ConnectableObservable` that shares a single subscription to the source Observable, + * replaying all items + */ + def replay[U >: T, R](selector: Observable[U] => Observable[R], scheduler: Scheduler): Observable[R] = { + val thisJava = this.asJavaObservable.asInstanceOf[rx.Observable[U]] + val fJava: Func1[rx.Observable[U], rx.Observable[R]] = + (jo: rx.Observable[U]) => selector(toScalaObservable[U](jo)).asJavaObservable.asInstanceOf[rx.Observable[R]] + toScalaObservable[R](thisJava.replay(fJava, scheduler)) + } + + /** + * Returns a `ConnectableObservable` that shares a single subscription to the source Observable and + * replays at most `bufferSize` items that were emitted during a specified time window. + *

+ * + * + * @param bufferSize the buffer size that limits the number of items that can be replayed + * @param time the duration of the window in which the replayed items must have been emitted + * @return a `ConnectableObservable` that shares a single subscription to the source Observable and + * replays at most `bufferSize` items that were emitted during the window defined by `time` + */ + def replay(bufferSize: Int, time: Duration): ConnectableObservable[T] = { + new ConnectableObservable[T](asJavaObservable.replay(bufferSize, time.length, time.unit)) + } + + /** + * Returns a `ConnectableObservable` that shares a single subscription to the source Observable and + * that replays a maximum of `bufferSize` items that are emitted within a specified time window. + *

+ * + * + * @param bufferSize the buffer size that limits the number of items that can be replayed + * @param time the duration of the window in which the replayed items must have been emitted + * @param scheduler the scheduler that is used as a time source for the window + * @return a `ConnectableObservable` that shares a single subscription to the source Observable and + * replays at most `bufferSize` items that were emitted during the window defined by `time`` + * @throws IllegalArgumentException if `bufferSize` is less than zero + */ + def replay(bufferSize: Int, time: Duration, scheduler: Scheduler): ConnectableObservable[T] = { + new ConnectableObservable[T](asJavaObservable.replay(bufferSize, time.length, time.unit, scheduler)) + } + + /** + * Returns an Observable that emits items that are the results of invoking a specified selector on items + * emitted by a `ConnectableObservable` that shares a single subscription to the source Observable, + * replaying all items that were emitted within a specified time window. + *

+ * + * + * @param selector a selector function, which can use the multicasted sequence as many times as needed, without + * causing multiple subscriptions to the Observable + * @param time the duration of the window in which the replayed items must have been emitted + * @return an Observable that emits items that are the results of invoking the selector on items emitted by + * a `ConnectableObservable` that shares a single subscription to the source Observable, + * replaying all items that were emitted within the window defined by `time`` + */ + def replay[U >: T, R](selector: Observable[U] => Observable[R], time: Duration): Observable[R] = { + val thisJava = this.asJavaObservable.asInstanceOf[rx.Observable[U]] + val fJava: Func1[rx.Observable[U], rx.Observable[R]] = + (jo: rx.Observable[U]) => selector(toScalaObservable[U](jo)).asJavaObservable.asInstanceOf[rx.Observable[R]] + toScalaObservable[R](thisJava.replay(fJava, time.length, time.unit)) + } + + /** + * Returns a `ConnectableObservable` that shares a single subscription to the source Observable that + * replays at most `bufferSize` items emitted by that Observable. + *

+ * + * + * @param bufferSize the buffer size that limits the number of items that can be replayed + * @return a `ConnectableObservable` that shares a single subscription to the source Observable and + * replays at most `bufferSize` items emitted by that Observable + */ + def replay(bufferSize: Int): ConnectableObservable[T] = { + new ConnectableObservable[T](asJavaObservable.replay(bufferSize)) + } + + /** + * Returns a `ConnectableObservable` that shares a single subscription to the source Observable and + * replays at most `bufferSize` items emitted by that Observable. + *

+ * + * + * @param bufferSize the buffer size that limits the number of items that can be replayed + * @param scheduler the scheduler on which the Observers will observe the emitted items + * @return a `ConnectableObservable` that shares a single subscription to the source Observable and + * replays at most `bufferSize` items that were emitted by the Observable + */ + def replay(bufferSize: Int, scheduler: Scheduler): ConnectableObservable[T] = { + new ConnectableObservable[T](asJavaObservable.replay(bufferSize, scheduler)) + } + + /** + * Returns a `ConnectableObservable` that shares a single subscription to the source Observable and + * replays all items emitted by that Observable within a specified time window. + *

+ * + * + * @param time the duration of the window in which the replayed items must have been emitted + * @return a `ConnectableObservable` that shares a single subscription to the source Observable and + * replays the items that were emitted during the window defined by `time` + */ + def replay(time: Duration): ConnectableObservable[T] = { + new ConnectableObservable[T](asJavaObservable.replay(time.length, time.unit)) + } + + /** + * Returns a `ConnectableObservable` that shares a single subscription to the source Observable and + * replays all items emitted by that Observable within a specified time window. + *

+ * + * + * @param time the duration of the window in which the replayed items must have been emitted + * @param scheduler the Scheduler that is the time source for the window + * @return a `ConnectableObservable` that shares a single subscription to the source Observable and + * replays the items that were emitted during the window defined by `time` + */ + def replay(time: Duration, scheduler: Scheduler): ConnectableObservable[T] = { + new ConnectableObservable[T](asJavaObservable.replay(time.length, time.unit, scheduler)) + } + + /** + * Returns a `ConnectableObservable` that shares a single subscription to the source Observable that + * will replay all of its items and notifications to any future `Observer` on the given `Scheduler`. + *

+ * + * + * @param scheduler the Scheduler on which the Observers will observe the emitted items + * @return a `ConnectableObservable` that shares a single subscription to the source Observable that + * will replay all of its items and notifications to any future `bserver` on the given `Scheduler` + */ + def replay(scheduler: Scheduler): ConnectableObservable[T] = { + new ConnectableObservable[T](asJavaObservable.replay(scheduler)) } /** @@ -1061,6 +1394,22 @@ trait Observable[+T] toScalaObservable[T](asJavaObservable.cache()) } + /** + * Returns an Observable that emits a Boolean that indicates whether the source Observable emitted a + * specified item. + * + * Note: this method uses `==` to compare elements. It's a bit different from RxJava which uses `Object.equals`. + *

+ * + * + *@param elem the item to search for in the emissions from the source Observable + * @return an Observable that emits `true` if the specified item is emitted by the source Observable, + * or `false` if the source Observable completes without emitting that item + */ + def contains[U >: T](elem: U): Observable[Boolean] = { + exists(_ == elem) + } + /** * Returns a a pair of a start function and an [[rx.lang.scala.Observable]], which waits until the start function is called before it begins emitting * items to those [[rx.lang.scala.Observer]]s that have subscribed to it. @@ -1073,6 +1422,60 @@ trait Observable[+T] new ConnectableObservable[T](asJavaObservable.publish()) } + + /** + * Returns an Observable that emits `initialValue` followed by the items emitted by a `ConnectableObservable` that shares a single subscription to the source Observable. + *

+ * + * + * @param initialValue the initial value to be emitted by the resulting Observable + * @return a `ConnectableObservable` that shares a single subscription to the underlying Observable and starts with `initialValue` + */ + def publish[U >: T](initialValue: U): ConnectableObservable[U] = { + val thisJava = this.asJavaObservable.asInstanceOf[rx.Observable[U]] + new ConnectableObservable[U](thisJava.publish(initialValue)) + } + + /** + * Returns an Observable that emits the results of invoking a specified selector on items emitted by a `ConnectableObservable` + * that shares a single subscription to the underlying sequence. + *

+ * + * + * @param selector a function that can use the multicasted source sequence as many times as needed, without + * causing multiple subscriptions to the source sequence. Subscribers to the given source will + * receive all notifications of the source from the time of the subscription forward. + * @return an Observable that emits the results of invoking the selector on the items emitted by a `ConnectableObservable` + * that shares a single subscription to the underlying sequence + */ + def publish[U >: T, R](selector: Observable[U] => Observable[R]): Observable[R] = { + val thisJava = this.asJavaObservable.asInstanceOf[rx.Observable[U]] + val fJava: Func1[rx.Observable[U], rx.Observable[R]] = + (jo: rx.Observable[U]) => selector(toScalaObservable[U](jo)).asJavaObservable.asInstanceOf[rx.Observable[R]] + toScalaObservable[R](thisJava.publish(fJava)) + } + + /** + * Returns an Observable that emits `initialValue` followed by the results of invoking a specified + * selector on items emitted by a `ConnectableObservable` that shares a single subscription to the + * source Observable. + *

+ * + * + * @param selector a function that can use the multicasted source sequence as many times as needed, without + * causing multiple subscriptions to the source Observable. Subscribers to the source will + * receive all notifications of the source from the time of the subscription forward + * @param initialValue the initial value of the underlying `BehaviorSubject` + * @return an Observable that emits `initialValue` followed by the results of invoking the selector + * on a `ConnectableObservable` that shares a single subscription to the underlying Observable + */ + def publish[U >: T, R](selector: Observable[U] => Observable[R], initialValue: U): Observable[R] = { + val thisJava = this.asJavaObservable.asInstanceOf[rx.Observable[U]] + val fJava: Func1[rx.Observable[U], rx.Observable[R]] = + (jo: rx.Observable[U]) => selector(toScalaObservable[U](jo)).asJavaObservable.asInstanceOf[rx.Observable[R]] + toScalaObservable[R](thisJava.publish(fJava, initialValue)) + } + // TODO add Scala-like aggregate function /** @@ -1132,6 +1535,21 @@ trait Observable[+T] toScalaObservable[T](asJavaObservable.sample(duration.length, duration.unit, scheduler)) } + /** + * Return an Observable that emits the results of sampling the items emitted by the source Observable + * whenever the specified sampler Observable emits an item or completes. + * + * + * + * @param sampler + * the Observable to use for sampling the source Observable + * @return an Observable that emits the results of sampling the items emitted by this Observable whenever + * the sampler Observable emits an item or completes + */ + def sample(sampler: Observable[Any]): Observable[T] = { + toScalaObservable[T](asJavaObservable.sample(sampler)) + } + /** * Returns an Observable that applies a function of your choosing to the first item emitted by a * source Observable, then feeds the result of that function along with the second item emitted @@ -1216,6 +1634,35 @@ trait Observable[+T] toScalaObservable[T](asJavaObservable.skip(n)) } + /** + * Returns an Observable that drops values emitted by the source Observable before a specified time window + * elapses. + * + * + * + * @param time the length of the time window to drop + * @return an Observable that drops values emitted by the source Observable before the time window defined + * by `time` elapses and emits the remainder + */ + def drop(time: Duration): Observable[T] = { + toScalaObservable(asJavaObservable.skip(time.length, time.unit)) + } + + /** + * Returns an Observable that drops values emitted by the source Observable before a specified time window + * elapses. + * + * + * + * @param time the length of the time window to drop + * @param scheduler the `Scheduler` on which the timed wait happens + * @return an Observable that drops values emitted by the source Observable before the time window defined + * by `time` elapses and emits the remainder + */ + def drop(time: Duration, scheduler: Scheduler): Observable[T] = { + toScalaObservable(asJavaObservable.skip(time.length, time.unit, scheduler)) + } + /** * Returns an Observable that bypasses all items from the source Observable as long as the specified * condition holds true. Emits all further source items as soon as the condition becomes false. @@ -1231,6 +1678,74 @@ trait Observable[+T] toScalaObservable(asJavaObservable.skipWhile(predicate)) } + /** + * Returns an Observable that drops a specified number of items from the end of the sequence emitted by the + * source Observable. + *

+ * + *

+ * This Observer accumulates a queue long enough to store the first `n` items. As more items are + * received, items are taken from the front of the queue and emitted by the returned Observable. This causes + * such items to be delayed. + * + * @param n number of items to drop from the end of the source sequence + * @return an Observable that emits the items emitted by the source Observable except for the dropped ones + * at the end + * @throws IndexOutOfBoundsException if `n` is less than zero + */ + def dropRight(n: Int): Observable[T] = { + toScalaObservable(asJavaObservable.skipLast(n)) + } + + /** + * Returns an Observable that drops items emitted by the source Observable during a specified time window + * before the source completes. + *

+ * + * + * Note: this action will cache the latest items arriving in the specified time window. + * + * @param time the length of the time window + * @return an Observable that drops those items emitted by the source Observable in a time window before the + * source completes defined by `time` + */ + def dropRight(time: Duration): Observable[T] = { + toScalaObservable(asJavaObservable.skipLast(time.length, time.unit)) + } + + /** + * Returns an Observable that drops items emitted by the source Observable during a specified time window + * (defined on a specified scheduler) before the source completes. + *

+ * + * + * Note: this action will cache the latest items arriving in the specified time window. + * + * @param time the length of the time window + * @param scheduler the scheduler used as the time source + * @return an Observable that drops those items emitted by the source Observable in a time window before the + * source completes defined by `time` and `scheduler` + */ + def dropRight(time: Duration, scheduler: Scheduler): Observable[T] = { + toScalaObservable(asJavaObservable.skipLast(time.length, time.unit, scheduler)) + } + + /** + * Returns an Observable that skips items emitted by the source Observable until a second Observable emits an item. + *

+ * + * + * @param other the second Observable that has to emit an item before the source Observable's elements begin + * to be mirrored by the resulting Observable + * @return an Observable that skips items from the source Observable until the second Observable emits an + * item, then emits the remaining items + * @see RxJava Wiki: skipUntil() + * @see MSDN: Observable.SkipUntil + */ + def dropUntil[E](other: Observable[E]): Observable[T] = { + toScalaObservable[T](asJavaObservable.skipUntil(other)) + } + /** * Returns an Observable that emits only the first `num` items emitted by the source * Observable. @@ -1251,6 +1766,33 @@ trait Observable[+T] toScalaObservable[T](asJavaObservable.take(n)) } + /** + * Returns an Observable that emits those items emitted by source Observable before a specified time runs out. + *

+ * + * + * @param time the length of the time window + * @return an Observable that emits those items emitted by the source Observable before the time runs out + */ + def take(time: Duration): Observable[T] = { + toScalaObservable[T](asJavaObservable.take(time.length, time.unit)) + } + + /** + * Returns an Observable that emits those items emitted by source Observable before a specified time (on + * specified Scheduler) runs out + *

+ * + * + * @param time the length of the time window + * @param scheduler the Scheduler used for time source + * @return an Observable that emits those items emitted by the source Observable before the time runs out, + * according to the specified Scheduler + */ + def take(time: Duration, scheduler: Scheduler) { + toScalaObservable[T](asJavaObservable.take(time.length, time.unit, scheduler.asJavaScheduler)) + } + /** * Returns an Observable that emits items emitted by the source Observable so long as a * specified condition is true. @@ -1283,6 +1825,72 @@ trait Observable[+T] toScalaObservable[T](asJavaObservable.takeLast(count)) } + /** + * Return an Observable that emits the items from the source Observable that were emitted in a specified + * window of `time` before the Observable completed. + *

+ * + * + * @param time the length of the time window + * @return an Observable that emits the items from the source Observable that were emitted in the window of + * time before the Observable completed specified by `time` + */ + def takeRight(time: Duration): Observable[T] = { + toScalaObservable[T](asJavaObservable.takeLast(time.length, time.unit)) + } + + /** + * Return an Observable that emits the items from the source Observable that were emitted in a specified + * window of `time` before the Observable completed, where the timing information is provided by a specified + * Scheduler. + *

+ * + * + * @param time the length of the time window + * @param scheduler the Scheduler that provides the timestamps for the Observed items + * @return an Observable that emits the items from the source Observable that were emitted in the window of + * time before the Observable completed specified by `time`, where the timing information is + * provided by `scheduler` + */ + def takeRight(time: Duration, scheduler: Scheduler): Observable[T] = { + toScalaObservable[T](asJavaObservable.takeLast(time.length, time.unit, scheduler.asJavaScheduler)) + } + + /** + * Return an Observable that emits at most a specified number of items from the source Observable that were + * emitted in a specified window of time before the Observable completed. + *

+ * + * + * @param count the maximum number of items to emit + * @param time the length of the time window + * @return an Observable that emits at most `count` items from the source Observable that were emitted + * in a specified window of time before the Observable completed + * @throws IllegalArgumentException if `count` is less than zero + */ + def takeRight(count: Int, time: Duration): Observable[T] = { + toScalaObservable[T](asJavaObservable.takeLast(count, time.length, time.unit)) + } + + /** + * Return an Observable that emits at most a specified number of items from the source Observable that were + * emitted in a specified window of `time` before the Observable completed, where the timing information is + * provided by a given Scheduler. + *

+ * + * + * @param count the maximum number of items to emit + * @param time the length of the time window + * @param scheduler the Scheduler that provides the timestamps for the observed items + * @return an Observable that emits at most `count` items from the source Observable that were emitted + * in a specified window of time before the Observable completed, where the timing information is + * provided by the given `scheduler` + * @throws IllegalArgumentException if `count` is less than zero + */ + def takeRight(count: Int, time: Duration, scheduler: Scheduler): Observable[T] = { + toScalaObservable[T](asJavaObservable.takeLast(count, time.length, time.unit, scheduler.asJavaScheduler)) + } + /** * Returns an Observable that emits the items from the source Observable only until the * `other` Observable emits an item. @@ -1934,36 +2542,85 @@ trait Observable[+T] def headOrElse[U >: T](default: => U): Observable[U] = firstOrElse(default) /** - * Returns an Observable that emits only the very first item emitted by the source Observable. - * This is just a shorthand for `take(1)`. - * + * Returns an Observable that emits only the very first item emitted by the source Observable, or raises an + * `NoSuchElementException` if the source Observable is empty. + *

* - * - * @return an Observable that emits only the very first item from the source, or none if the - * source Observable completes without emitting a single item. + * + * @return an Observable that emits only the very first item emitted by the source Observable, or raises an + * `NoSuchElementException` if the source Observable is empty + * @see RxJava Wiki: first() + * @see "MSDN: Observable.firstAsync()" */ - def first: Observable[T] = take(1) + def first: Observable[T] = { + toScalaObservable[T](asJavaObservable.first) + } - /* - - TODO once https://github.com/Netflix/RxJava/issues/417 is fixed, we can add head and tail methods - /** - * emits NoSuchElementException("head of empty Observable") if empty + * Returns an Observable that emits only the very first item emitted by the source Observable, or raises an + * `NoSuchElementException` if the source Observable is empty. + *

+ * + * + * @return an Observable that emits only the very first item emitted by the source Observable, or raises an + * `NoSuchElementException` if the source Observable is empty + * @see RxJava Wiki: first() + * @see "MSDN: Observable.firstAsync()" + * @see [[Observable.first]] + */ + def head: Observable[T] = first + + /** + * Returns an Observable that emits all items except the first one, or raises an `UnsupportedOperationException` + * if the source Observable is empty. + * + * @return an Observable that emits all items except the first one, or raises an `UnsupportedOperationException` + * if the source Observable is empty. + */ + def tail: Observable[T] = { + lift { + (subscriber: Subscriber[T]) => { + var isFirst = true + Subscriber[T]( + subscriber, + (v: T) => if(isFirst) isFirst = false else subscriber.onNext(v), + e => subscriber.onError(e), + () => if(isFirst) subscriber.onError(new UnsupportedOperationException("tail of empty Observable")) else subscriber.onCompleted + ) + } + } + } + + /** + * Returns an Observable that emits the last item emitted by the source Observable or notifies observers of + * an `NoSuchElementException` if the source Observable is empty. + * + * + * + * @return an Observable that emits the last item from the source Observable or notifies observers of an + * error + * @see RxJava Wiki: last() + * @see "MSDN: Observable.lastAsync()" */ - def head: Observable[T] = { - this.take(1).fold[Option[T]](None)((v: Option[T], e: T) => Some(e)).map({ - case Some(element) => element - case None => throw new NoSuchElementException("head of empty Observable") - }) + def last: Observable[T] = { + toScalaObservable[T](asJavaObservable.last) } - + /** - * emits an UnsupportedOperationException("tail of empty list") if empty + * If the source Observable completes after emitting a single item, return an Observable that emits that + * item. If the source Observable emits more than one item or no items, throw an `NoSuchElementException`. + * + * + * + * @return an Observable that emits the single item emitted by the source Observable + * @throws NoSuchElementException + * if the source emits more than one item or no items + * @see RxJava Wiki: single() + * @see "MSDN: Observable.singleAsync()" */ - def tail: Observable[T] = ??? - - */ + def single: Observable[T] = { + toScalaObservable[T](asJavaObservable.single) + } /** * Returns an Observable that forwards all sequentially distinct items emitted from the source Observable. @@ -2076,6 +2733,68 @@ trait Observable[+T] toScalaObservable[T](asJavaObservable.retry()) } + /** + * Returns an Observable that repeats the sequence of items emitted by the source Observable indefinitely. + *

+ * + * + * @return an Observable that emits the items emitted by the source Observable repeatedly and in sequence + * @see RxJava Wiki: repeat() + * @see MSDN: Observable.Repeat + */ + def repeat: Observable[T] = { + toScalaObservable[T](asJavaObservable.repeat()) + } + + /** + * Returns an Observable that repeats the sequence of items emitted by the source Observable indefinitely, + * on a particular Scheduler. + *

+ * + * + * @param scheduler the Scheduler to emit the items on + * @return an Observable that emits the items emitted by the source Observable repeatedly and in sequence + * @see RxJava Wiki: repeat() + * @see MSDN: Observable.Repeat + */ + def repeat(scheduler: Scheduler): Observable[T] = { + toScalaObservable[T](asJavaObservable.repeat(scheduler)) + } + + /** + * Returns an Observable that repeats the sequence of items emitted by the source Observable at most `count` times. + *

+ * + * + * @param count the number of times the source Observable items are repeated, + * a count of 0 will yield an empty sequence + * @return an Observable that repeats the sequence of items emitted by the source Observable at most `count` times + * @throws IllegalArgumentException if `count` is less than zero + * @see RxJava Wiki: repeat() + * @see MSDN: Observable.Repeat + */ + def repeat(count: Long): Observable[T] = { + toScalaObservable[T](asJavaObservable.repeat(count)) + } + + /** + * Returns an Observable that repeats the sequence of items emitted by the source Observable + * at most `count` times, on a particular Scheduler. + *

+ * + * + * @param count the number of times the source Observable items are repeated, + * a count of 0 will yield an empty sequence + * @param scheduler the `Scheduler` to emit the items on + * @return an Observable that repeats the sequence of items emitted by the source Observable at most `count` times + * on a particular Scheduler + * @see RxJava Wiki: repeat() + * @see MSDN: Observable.Repeat + */ + def repeat(count: Long, scheduler: Scheduler): Observable[T] = { + toScalaObservable[T](asJavaObservable.repeat(count, scheduler)) + } + /** * Converts an Observable into a [[rx.lang.scala.observables.BlockingObservable]] (an Observable with blocking * operators). @@ -2088,8 +2807,7 @@ trait Observable[+T] /** * Perform work in parallel by sharding an `Observable[T]` on a - * [[rx.lang.scala.concurrency.Schedulers.threadPoolForComputation computation]] - * [[rx.lang.scala.Scheduler]] and return an `Observable[R]` with the output. + * [[rx.lang.scala.schedulers.ComputationScheduler]] and return an `Observable[R]` with the output. * * @param f * a function that applies Observable operators to `Observable[T]` in parallel and returns an `Observable[R]` @@ -2221,6 +2939,319 @@ trait Observable[+T] def doOnEach(onNext: T => Unit, onError: Throwable => Unit, onCompleted: () => Unit): Observable[T] = { toScalaObservable[T](asJavaObservable.doOnEach(Observer(onNext, onError,onCompleted))) } + + /** + * Modifies an Observable so that it invokes an action when it calls `onCompleted` or `onError`. + *

+ * + *

+ * This differs from `finallyDo` in that this happens **before** `onCompleted/onError` are emitted. + * + * @param onTerminate the action to invoke when the source Observable calls `onCompleted` or `onError` + * @return the source Observable with the side-effecting behavior applied + * @see RxJava Wiki: doOnTerminate() + * @see MSDN: Observable.Do + */ + def doOnTerminate(onTerminate: () => Unit): Observable[T] = { + toScalaObservable[T](asJavaObservable.doOnTerminate(onTerminate)) + } + + /** + * Given two Observables, mirror the one that first emits an item. + * + * + * + * @param that + * an Observable competing to react first + * @return an Observable that emits the same sequence of items as whichever of `this` or `that` first emitted an item. + */ + def amb[U >: T](that: Observable[U]): Observable[U] = { + val thisJava: rx.Observable[_ <: U] = this.asJavaObservable + val thatJava: rx.Observable[_ <: U] = that.asJavaObservable + toScalaObservable[U](rx.Observable.amb(thisJava, thatJava)) + } + + /** + * Returns an Observable that emits the items emitted by the source Observable shifted forward in time by a + * specified delay. Error notifications from the source Observable are not delayed. + * + * + * + * @param delay the delay to shift the source by + * @return the source Observable shifted in time by the specified delay + */ + def delay(delay: Duration): Observable[T] = { + toScalaObservable[T](asJavaObservable.delay(delay.length, delay.unit)) + } + + /** + * Returns an Observable that emits the items emitted by the source Observable shifted forward in time by a + * specified delay. Error notifications from the source Observable are not delayed. + * + * + * + * @param delay the delay to shift the source by + * @param scheduler the Scheduler to use for delaying + * @return the source Observable shifted in time by the specified delay + */ + def delay(delay: Duration, scheduler: Scheduler): Observable[T] = { + toScalaObservable[T](asJavaObservable.delay(delay.length, delay.unit, scheduler)) + } + + /** + * Returns an Observable that delays the emissions of the source Observable via another Observable on a + * per-item basis. + *

+ * + *

+ * Note: the resulting Observable will immediately propagate any `onError` notification + * from the source Observable. + * + * @param itemDelay a function that returns an Observable for each item emitted by the source Observable, which is + * then used to delay the emission of that item by the resulting Observable until the Observable + * returned from `itemDelay` emits an item + * @return an Observable that delays the emissions of the source Observable via another Observable on a per-item basis + */ + def delay(itemDelay: T => Observable[Any]): Observable[T] = { + val itemDelayJava = new Func1[T, rx.Observable[Any]] { + override def call(t: T): rx.Observable[Any] = + itemDelay(t).asJavaObservable.asInstanceOf[rx.Observable[Any]] + } + toScalaObservable[T](asJavaObservable.delay[Any](itemDelayJava)) + } + + /** + * Returns an Observable that delays the subscription to and emissions from the souce Observable via another + * Observable on a per-item basis. + *

+ * + *

+ * Note: the resulting Observable will immediately propagate any `onError` notification + * from the source Observable. + * + * @param subscriptionDelay a function that returns an Observable that triggers the subscription to the source Observable + * once it emits any item + * @param itemDelay a function that returns an Observable for each item emitted by the source Observable, which is + * then used to delay the emission of that item by the resulting Observable until the Observable + * returned from `itemDelay` emits an item + * @return an Observable that delays the subscription and emissions of the source Observable via another + * Observable on a per-item basis + */ + def delay(subscriptionDelay: () => Observable[Any], itemDelay: T => Observable[Any]): Observable[T] = { + val subscriptionDelayJava = new Func0[rx.Observable[Any]] { + override def call(): rx.Observable[Any] = + subscriptionDelay().asJavaObservable.asInstanceOf[rx.Observable[Any]] + } + val itemDelayJava = new Func1[T, rx.Observable[Any]] { + override def call(t: T): rx.Observable[Any] = + itemDelay(t).asJavaObservable.asInstanceOf[rx.Observable[Any]] + } + toScalaObservable[T](asJavaObservable.delay[Any, Any](subscriptionDelayJava, itemDelayJava)) + } + + /** + * Return an Observable that delays the subscription to the source Observable by a given amount of time. + * + * + * + * @param delay the time to delay the subscription + * @return an Observable that delays the subscription to the source Observable by the given amount + */ + def delaySubscription(delay: Duration): Observable[T] = { + toScalaObservable[T](asJavaObservable.delaySubscription(delay.length, delay.unit)) + } + + /** + * Return an Observable that delays the subscription to the source Observable by a given amount of time, + * both waiting and subscribing on a given Scheduler. + * + * + * + * @param delay the time to delay the subscription + * @param scheduler the Scheduler on which the waiting and subscription will happen + * @return an Observable that delays the subscription to the source Observable by a given + * amount, waiting and subscribing on the given Scheduler + */ + def delaySubscription(delay: Duration, scheduler: Scheduler): Observable[T] = { + toScalaObservable[T](asJavaObservable.delaySubscription(delay.length, delay.unit, scheduler)) + } + + /** + * Returns an Observable that emits the single item at a specified index in a sequence of emissions from a + * source Observbable. + * + * + * + * @param index + * the zero-based index of the item to retrieve + * @return an Observable that emits a single item: the item at the specified position in the sequence of + * those emitted by the source Observable + * @throws IndexOutOfBoundsException + * if index is greater than or equal to the number of items emitted by the source + * Observable, or index is less than 0 + */ + def elementAt(index: Int): Observable[T] = { + toScalaObservable[T](asJavaObservable.elementAt(index)) + } + + /** + * Returns an Observable that emits the item found at a specified index in a sequence of emissions from a + * source Observable, or a default item if that index is out of range. + * + * + * + * @param index + * the zero-based index of the item to retrieve + * @param default + * the default item + * @return an Observable that emits the item at the specified position in the sequence emitted by the source + * Observable, or the default item if that index is outside the bounds of the source sequence + * @throws IndexOutOfBoundsException + * if {@code index} is less than 0 + */ + def elementAtOrDefault[U >: T](index: Int, default: U): Observable[U] = { + val thisJava = asJavaObservable.asInstanceOf[rx.Observable[U]] + toScalaObservable[U](thisJava.elementAtOrDefault(index, default)) + } + + /** + * Return an Observable that emits a single Map containing all items emitted by the source Observable, + * mapped by the keys returned by a specified {@code keySelector} function. + *

+ * + *

+ * If more than one source item maps to the same key, the Map will contain the latest of those items. + * + * @param keySelector + * the function that extracts the key from a source item to be used in the Map + * @return an Observable that emits a single item: a Map containing the mapped items from the source + * Observable + */ + def toMap[K] (keySelector: T => K): Observable[Map[K, T]]= { + val thisJava = asJavaObservable.asInstanceOf[rx.Observable[T]] + val o: rx.Observable[util.Map[K, T]] = thisJava.toMap[K](keySelector) + toScalaObservable[util.Map[K,T]](o).map(m => m.toMap) + } + + /** + * Return an Observable that emits a single Map containing values corresponding to items emitted by the + * source Observable, mapped by the keys returned by a specified {@code keySelector} function. + *

+ * + *

+ * If more than one source item maps to the same key, the Map will contain a single entry that + * corresponds to the latest of those items. + * + * @param keySelector + * the function that extracts the key from a source item to be used in the Map + * @param valueSelector + * the function that extracts the value from a source item to be used in the Map + * @return an Observable that emits a single item: a HashMap containing the mapped items from the source + * Observable + */ + def toMap[K, V] (keySelector: T => K, valueSelector: T => V) : Observable[Map[K, V]] = { + val thisJava = asJavaObservable.asInstanceOf[rx.Observable[T]] + val o: rx.Observable[util.Map[K, V]] = thisJava.toMap[K, V](keySelector, valueSelector) + toScalaObservable[util.Map[K, V]](o).map(m => m.toMap) + } + + /** + * Return an Observable that emits a single Map, returned by a specified {@code mapFactory} function, that + * contains keys and values extracted from the items emitted by the source Observable. + *

+ * + * + * @param keySelector + * the function that extracts the key from a source item to be used in the Map + * @param valueSelector + * the function that extracts the value from the source items to be used as value in the Map + * @param mapFactory + * the function that returns a Map instance to be used + * @return an Observable that emits a single item: a Map that contains the mapped items emitted by the + * source Observable + */ + def toMap[K, V] (keySelector: T => K, valueSelector: T => V, mapFactory: () => Map[K, V]): Observable[Map[K, V]] = { + val thisJava = asJavaObservable.asInstanceOf[rx.Observable[T]] + val o: rx.Observable[util.Map[K, V]] = thisJava.toMap[K, V](keySelector, valueSelector) + toScalaObservable[util.Map[K, V]](o).map(m => mapFactory() ++ m.toMap) + } + + /** + * Returns an Observable that emits a Boolean value that indicates whether `this` and `that` Observable sequences are the + * same by comparing the items emitted by each Observable pairwise. + *

+ * + * + * Note: this method uses `==` to compare elements. It's a bit different from RxJava which uses `Object.equals`. + * + * @param that the Observable to compare + * @return an Observable that emits a `Boolean` value that indicates whether the two sequences are the same + */ + def sequenceEqual[U >: T](that: Observable[U]): Observable[Boolean] = { + sequenceEqual(that, (_1: U, _2: U) => _1 == _2) + } + + /** + * Returns an Observable that emits a Boolean value that indicates whether `this` and `that` Observable sequences are the + * same by comparing the items emitted by each Observable pairwise based on the results of a specified `equality` function. + *

+ * + * + * @param that the Observable to compare + * @param equality a function used to compare items emitted by each Observable + * @return an Observable that emits a `Boolean` value that indicates whether the two sequences are the same based on the `equality` function. + */ + def sequenceEqual[U >: T](that: Observable[U], equality: (U, U) => Boolean): Observable[Boolean] = { + val thisJava: rx.Observable[_ <: U] = this.asJavaObservable + val thatJava: rx.Observable[_ <: U] = that.asJavaObservable + val equalityJava: Func2[_ >: U, _ >: U, java.lang.Boolean] = equality + toScalaObservable[java.lang.Boolean](rx.Observable.sequenceEqual[U](thisJava, thatJava, equalityJava)).map(_.booleanValue) + } + + /** + * Returns an Observable that emits records of the time interval between consecutive items emitted by the + * source Obsegrvable. + *

+ * + * + * @return an Observable that emits time interval information items + */ + def timeInterval: Observable[(Duration, T)] = { + toScalaObservable(asJavaObservable.timeInterval()) + .map(inv => (Duration(inv.getIntervalInMilliseconds, MILLISECONDS), inv.getValue)) + } + + /** + * Returns an Observable that emits records of the time interval between consecutive items emitted by the + * source Observable, where this interval is computed on a specified Scheduler. + *

+ * + * + * @param scheduler the [[Scheduler]] used to compute time intervals + * @return an Observable that emits time interval information items + */ + def timeInterval(scheduler: Scheduler): Observable[(Duration, T)] = { + toScalaObservable(asJavaObservable.timeInterval(scheduler.asJavaScheduler)) + .map(inv => (Duration(inv.getIntervalInMilliseconds, MILLISECONDS), inv.getValue)) + } + + /** + * Lift a function to the current Observable and return a new Observable that when subscribed to will pass + * the values of the current Observable through the function. + *

+ * In other words, this allows chaining Observers together on an Observable for acting on the values within + * the Observable. + * {{{ + * observable.map(...).filter(...).take(5).lift(new ObserverA()).lift(new ObserverB(...)).subscribe() + * }}} + * + * @param operator + * @return an Observable that emits values that are the result of applying the bind function to the values + * of the current Observable + */ + def lift[R](operator: Subscriber[R] => Subscriber[T]): Observable[R] = { + toScalaObservable(asJavaObservable.lift(toJavaOperator[T, R](operator))) + } } /** @@ -2257,9 +3288,6 @@ object Observable { * should invoke the Observer's [[rx.lang.scala.Observer.onNext onNext]], [[rx.lang.scala.Observer.onError onError]], and [[rx.lang.scala.Observer.onCompleted onCompleted]] methods * appropriately. * - * A well-formed Observable must invoke either the Observer's `onCompleted` method - * exactly once or its `onError` method exactly once. - * * See Rx Design Guidelines (PDF) * for detailed information. * @@ -2273,6 +3301,7 @@ object Observable { * @return * an Observable that, when an [[rx.lang.scala.Observer]] subscribes to it, will execute the given function. */ + @deprecated("Use `apply[T](Subscriber[T] => Unit)` instead", "0.17.0") def create[T](func: Observer[T] => Subscription): Observable[T] = { toScalaObservable[T](rx.Observable.create(new OnSubscribeFunc[T] { def onSubscribe(t1: rx.Observer[_ >: T]): rx.Subscription = { @@ -2281,6 +3310,50 @@ object Observable { })) } + /* + Note: It's dangerous to have two overloads where one takes an `Observer[T] => Subscription` + function and the other takes a `Subscriber[T] => Unit` function, because expressions like + `o => Subscription{}` have both of these types. + So we call the old create method "create", and the new create method "apply". + Try it out yourself here: + def foo[T]: Unit = { + val fMeant: Observer[T] => Subscription = o => Subscription{} + val fWrong: Subscriber[T] => Unit = o => Subscription{} + } + */ + + /** + * Returns an Observable that will execute the specified function when someone subscribes to it. + * + * + * + * Write the function you pass so that it behaves as an Observable: It should invoke the + * Subscriber's `onNext`, `onError`, and `onCompleted` methods appropriately. + * + * You can `add` custom [[Subscription]]s to [[Subscriber]]. These [[Subscription]]s will be called + *

    + *
  • when someone calls `unsubscribe`.
  • + *
  • after `onCompleted` or `onError`.
  • + *
+ * + * See Rx Design Guidelines (PDF) for detailed + * information. + * + * See `RxScalaDemo.createExampleGood` + * and `RxScalaDemo.createExampleGood2`. + * + * @param T + * the type of the items that this Observable emits + * @param f + * a function that accepts a `Subscriber[T]`, and invokes its `onNext`, + * `onError`, and `onCompleted` methods as appropriate + * @return an Observable that, when someone subscribes to it, will execute the specified + * function + */ + def apply[T](f: Subscriber[T] => Unit): Observable[T] = { + toScalaObservable(rx.Observable.create(f)) + } + /** * Returns an Observable that invokes an [[rx.lang.scala.Observer]]'s [[rx.lang.scala.Observer.onError onError]] * method when the Observer subscribes to it. @@ -2555,6 +3628,28 @@ object Observable { toScalaObservable[java.lang.Long](rx.Observable.timer(initialDelay.toNanos, period.toNanos, duration.NANOSECONDS, scheduler)).map(_.longValue()) } + /** + * Constructs an Observable that creates a dependent resource object. + *

+ * + * + * @param resourceFactory the factory function to create a resource object that depends on the Observable + * @param observableFactory the factory function to obtain an Observable + * @return the Observable whose lifetime controls the lifetime of the dependent resource object + */ + def using[T, Resource <: Subscription](resourceFactory: () => Resource, observableFactory: Resource => Observable[T]): Observable[T] = { + class ResourceSubscription(val resource: Resource) extends rx.Subscription { + def unsubscribe = resource.unsubscribe + + def isUnsubscribed: Boolean = resource.isUnsubscribed + } + + toScalaObservable(rx.Observable.using[T, ResourceSubscription]( + () => new ResourceSubscription(resourceFactory()), + (s: ResourceSubscription) => observableFactory(s.resource).asJavaObservable + )) + } + } diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observer.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observer.scala index 6e956dd1a1..e7e9ad5bf1 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observer.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Observer.scala @@ -56,7 +56,7 @@ trait Observer[-T] { } -object Observer { +object Observer extends ObserverFactoryMethods[Observer] { /** * Scala calls XXX; Java receives XXX. @@ -72,17 +72,25 @@ object Observer { } - def apply[T]( ): Observer[T] = apply[T]((v:T)=>(), (e: Throwable)=>(), ()=>()) - def apply[T](onNext: T=>Unit ): Observer[T] = apply[T](onNext, (e: Throwable)=>(), ()=>()) - def apply[T](onNext: T=>Unit, onError: Throwable=>Unit ): Observer[T] = apply[T](onNext, onError, ()=>()) - def apply[T](onNext: T=>Unit, onCompleted: ()=>Unit): Observer[T] = apply[T](onNext, (e: Throwable)=>(), onCompleted) - def apply[T](onNext: T=>Unit, onError: Throwable=>Unit, onCompleted: ()=>Unit): Observer[T] = { - val n = onNext; val e = onError; val c = onCompleted - // Java calls XXX; Scala receives XXX. - Observer(new rx.Observer[T] { - override def onNext(value: T): Unit = n(value) - override def onError(error: Throwable): Unit = e(error) - override def onCompleted(): Unit = c() - }) + def apply[T](onNext: T => Unit, onError: Throwable => Unit, onCompleted: () => Unit): Observer[T] = { + val n = onNext; val e = onError; val c = onCompleted + // Java calls XXX; Scala receives XXX. + Observer(new rx.Observer[T] { + override def onNext(value: T): Unit = n(value) + override def onError(error: Throwable): Unit = e(error) + override def onCompleted(): Unit = c() + }) } -} \ No newline at end of file +} + + +private [scala] trait ObserverFactoryMethods[P[_] <: Observer[_]] { + + def apply[T](onNext: T => Unit, onError: Throwable => Unit, onCompleted: () => Unit): P[T] + + def apply[T]( ): P[T] = apply[T]((v:T)=>(), (e: Throwable)=>(), ()=>()) + def apply[T](onNext: T=>Unit ): P[T] = apply[T](onNext, (e: Throwable)=>(), ()=>()) + def apply[T](onNext: T=>Unit, onError: Throwable=>Unit ): P[T] = apply[T](onNext, onError, ()=>()) + def apply[T](onNext: T=>Unit, onCompleted: ()=>Unit): P[T] = apply[T](onNext, (e: Throwable)=>(), onCompleted) +} + diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Scheduler.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Scheduler.scala index 1bfbb69c4e..eee12bfc0a 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Scheduler.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Scheduler.scala @@ -16,7 +16,7 @@ package rx.lang.scala import scala.concurrent.duration.Duration -import rx.functions.Action1 +import rx.functions.Action0 import rx.lang.scala.schedulers._ import scala.concurrent.duration import rx.lang.scala.JavaConversions._ @@ -31,11 +31,11 @@ trait Scheduler { /** * Parallelism available to a Scheduler. * - * This defaults to {@code Runtime.getRuntime().availableProcessors()} but can be overridden for use cases such as scheduling work on a computer cluster. + * This defaults to `Runtime.getRuntime().availableProcessors()` but can be overridden for use cases such as scheduling work on a computer cluster. * * @return the scheduler's available degree of parallelism. */ - def degreeOfParallelism: Int = asJavaScheduler.degreeOfParallelism + def parallelism: Int = asJavaScheduler.parallelism() /** * @return the scheduler's notion of current absolute time in milliseconds. @@ -43,74 +43,80 @@ trait Scheduler { def now: Long = this.asJavaScheduler.now() /** - * Schedules a cancelable action to be executed. + * Retrieve or create a new [[rx.lang.scala.Worker]] that represents serial execution of actions. + *

+ * When work is completed it should be unsubscribed using [[rx.lang.scala.Worker unsubscribe]]. + *

+ * Work on a [[rx.lang.scala.Worker]] is guaranteed to be sequential. * - * @param action Action to schedule. - * @return a subscription to be able to unsubscribe from action. + * @return Inner representing a serial queue of actions to be executed */ - def schedule(action: Inner => Unit): Subscription = this.asJavaScheduler.schedule(action) + def createWorker: Worker = this.asJavaScheduler.createWorker() - /** - * Schedules a cancelable action to be executed periodically. - * This default implementation schedules recursively and waits for actions to complete (instead of potentially executing - * long-running actions concurrently). Each scheduler that can do periodic scheduling in a better way should override this. - * - * @param action - * The action to execute periodically. - * @param initialDelay - * Time to wait before executing the action for the first time. - * @param period - * The time interval to wait each time in between executing the action. - * @return A subscription to be able to unsubscribe from action. - */ - def schedulePeriodically(action: Inner => Unit, initialDelay: Duration, period: Duration): Subscription = - this.asJavaScheduler.schedulePeriodically ( - new Action1[rx.Scheduler.Inner] { - override def call(inner: rx.Scheduler.Inner): Unit = action(javaInnerToScalaInner(inner)) - }, - initialDelay.toNanos, - period.toNanos, - duration.NANOSECONDS - ) - - def scheduleRec(work: (=>Unit)=>Unit): Subscription = { - Subscription(asJavaScheduler.schedule(new Action1[rx.Scheduler.Inner] { - override def call(inner: rx.Scheduler.Inner): Unit = work{ inner.schedule(this) } - })) - } } -object Inner { - def apply(inner: rx.Scheduler.Inner): Inner = new Inner { private[scala] val asJavaInner = inner } +object Worker { + def apply(worker: rx.Scheduler.Worker): Worker = new Worker { private[scala] val asJavaWorker = worker } } -trait Inner extends Subscription { - private [scala] val asJavaInner: rx.Scheduler.Inner +trait Worker extends Subscription { + private [scala] val asJavaWorker: rx.Scheduler.Worker /** - * Schedules a cancelable action to be executed in delayTime. + * Schedules an Action for execution at some point in the future. + * + * @param action the Action to schedule + * @param delay time to wait before executing the action + * @return a subscription to be able to unsubscribe the action (unschedule it if not executed) */ - def schedule(action: Inner => Unit, delayTime: Duration): Unit = - this.asJavaInner.schedule( - new Action1[rx.Scheduler.Inner] { - override def call(inner: rx.Scheduler.Inner): Unit = action(javaInnerToScalaInner(inner)) + def schedule(delay: Duration)(action: => Unit): Subscription = { + this.asJavaWorker.schedule( + new Action0 { + override def call(): Unit = action }, - delayTime.length, - delayTime.unit) + delay.length, + delay.unit) + } + + /** + * Schedules an Action for execution. + * + * @param action the Action to schedule + * @return a subscription to be able to unsubscribe the action (unschedule it if not executed) + */ + def schedule(action: => Unit): Subscription = { + this.asJavaWorker.schedule( + new Action0 { + override def call(): Unit = action + } + ) + } /** - * Schedules a cancelable action to be executed immediately. + * Schedules a cancelable action to be executed periodically. This default implementation schedules + * recursively and waits for actions to complete (instead of potentially executing long-running actions + * concurrently). Each scheduler that can do periodic scheduling in a better way should override this. + * + * @param action the Action to execute periodically + * @param initialDelay time to wait before executing the action for the first time + * @param period the time interval to wait each time in between executing the action + * @return a subscription to be able to unsubscribe the action (unschedule it if not executed) */ - def schedule(action: Inner=>Unit): Unit = this.asJavaInner.schedule( - new Action1[rx.Scheduler.Inner]{ - override def call(inner: rx.Scheduler.Inner): Unit = action(javaInnerToScalaInner(inner)) - } - ) + def schedulePeriodically(initialDelay: Duration, period: Duration)(action: => Unit): Subscription = { + this.asJavaWorker.schedulePeriodically( + new Action0 { + override def call(): Unit = action + }, + initialDelay.toNanos, + period.toNanos, + duration.NANOSECONDS + ) + } /** * @return the scheduler's notion of current absolute time in milliseconds. */ - def now: Long = this.asJavaInner.now() + def now: Long = this.asJavaWorker.now() } diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Subscriber.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Subscriber.scala new file mode 100644 index 0000000000..2b5d714076 --- /dev/null +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/Subscriber.scala @@ -0,0 +1,64 @@ +package rx.lang.scala + +trait Subscriber[-T] extends Observer[T] with Subscription { + + self => + + private [scala] val asJavaSubscriber: rx.Subscriber[_ >: T] = new rx.Subscriber[T] { + def onNext(value: T): Unit = self.onNext(value) + def onError(error: Throwable): Unit = self.onError(error) + def onCompleted(): Unit = self.onCompleted() + } + + private [scala] override val asJavaObserver: rx.Observer[_ >: T] = asJavaSubscriber + private [scala] override val asJavaSubscription: rx.Subscription = asJavaSubscriber + + /** + * Used to register an unsubscribe callback. + */ + final def add(s: Subscription): Unit = { + asJavaSubscriber.add(s.asJavaSubscription) + } + + override final def unsubscribe(): Unit = { + asJavaSubscriber.unsubscribe() + } + + override final def isUnsubscribed: Boolean = { + asJavaSubscriber.isUnsubscribed() + } + +} + +object Subscriber extends ObserverFactoryMethods[Subscriber] { + + private[scala] def apply[T](subscriber: rx.Subscriber[T]): Subscriber[T] = new Subscriber[T] { + override val asJavaSubscriber = subscriber + override val asJavaObserver: rx.Observer[_ >: T] = asJavaSubscriber + override val asJavaSubscription: rx.Subscription = asJavaSubscriber + + override def onNext(value: T): Unit = asJavaSubscriber.onNext(value) + override def onError(error: Throwable): Unit = asJavaSubscriber.onError(error) + override def onCompleted(): Unit = asJavaSubscriber.onCompleted() + } + + def apply[T](onNext: T => Unit, onError: Throwable => Unit, onCompleted: () => Unit): Subscriber[T] = { + val n = onNext; val e = onError; val c = onCompleted + // Java calls XXX; Scala receives XXX. + Subscriber(new rx.Subscriber[T] { + override def onNext(value: T): Unit = n(value) + override def onError(error: Throwable): Unit = e(error) + override def onCompleted(): Unit = c() + }) + } + + def apply[T](subscriber: Subscriber[_], onNext: T => Unit, onError: Throwable => Unit, onCompleted: () => Unit): Subscriber[T] = { + val n = onNext; val e = onError; val c = onCompleted + // Java calls XXX; Scala receives XXX. + Subscriber(new rx.Subscriber[T](subscriber.asJavaSubscriber) { + override def onNext(value: T): Unit = n(value) + override def onError(error: Throwable): Unit = e(error) + override def onCompleted(): Unit = c() + }) + } +} diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/observables/BlockingObservable.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/observables/BlockingObservable.scala index c1ce913095..4ae6a54ce7 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/observables/BlockingObservable.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/observables/BlockingObservable.scala @@ -53,6 +53,49 @@ class BlockingObservable[+T] private[scala] (val asJava: rx.observables.Blocking new WithFilter[T](p, asJava) } + /** + * Returns the last item emitted by a specified [[Observable]], or + * throws `NoSuchElementException` if it emits no items. + * + * + * + * @return the last item emitted by the source [[Observable]] + * @throws NoSuchElementException + * if source contains no elements + * @see RxJava Wiki: last() + * @see MSDN: Observable.Last + */ + def last : T = { + asJava.last : T + } + + /** + * Returns the first item emitted by a specified [[Observable]], or + * `NoSuchElementException` if source contains no elements. + * + * @return the first item emitted by the source [[Observable]] + * @throws NoSuchElementException + * if source contains no elements + * @see RxJava Wiki: first() + * @see MSDN: Observable.First + */ + def first : T = { + asJava.first : T + } + + /** + * Returns the first item emitted by a specified [[Observable]], or + * `NoSuchElementException` if source contains no elements. + * + * @return the first item emitted by the source [[Observable]] + * @throws NoSuchElementException + * if source contains no elements + * @see RxJava Wiki: first() + * @see MSDN: Observable.First + * @see [[BlockingObservable.first]] + */ + def head : T = first + // last -> use toIterable.last // lastOrDefault -> use toIterable.lastOption // first -> use toIterable.head diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/ComputationScheduler.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/ComputationScheduler.scala index 608d390c64..80621db933 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/ComputationScheduler.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/ComputationScheduler.scala @@ -5,13 +5,13 @@ import rx.lang.scala.Scheduler object ComputationScheduler { /** - * {@link Scheduler} intended for computational work. + * [[rx.lang.scala.Scheduler]] intended for computational work. *

* This can be used for event-loops, processing callbacks and other computational work. *

- * Do not perform IO-bound work on this scheduler. Use {@link IOScheduler()} instead. + * Do not perform IO-bound work on this scheduler. Use [[rx.lang.scala.schedulers.IOScheduler]] instead. * - * @return { @link Scheduler} for computation-bound work. + * @return [[rx.lang.scala.Scheduler]] for computation-bound work. */ def apply(): ComputationScheduler = { new ComputationScheduler(rx.schedulers.Schedulers.computation()) diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/ExecutorScheduler.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/ExecutorScheduler.scala deleted file mode 100644 index 0ffe30b2a2..0000000000 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/ExecutorScheduler.scala +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright 2013 Netflix, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package rx.lang.scala.schedulers - -import java.util.concurrent.{ScheduledExecutorService, Executor} -import rx.lang.scala.Scheduler - -object ExecutorScheduler { - - /** - * Returns a [[rx.lang.scala.Scheduler]] that queues work on an `java.util.concurrent.Executor`. - * - * Note that this does not support scheduled actions with a delay. - */ - def apply(executor: Executor): ExecutorScheduler = { - new ExecutorScheduler(rx.schedulers.Schedulers.executor(executor)) - } - - def apply(executor: ScheduledExecutorService): ExecutorScheduler = { - new ExecutorScheduler(rx.schedulers.Schedulers.executor(executor)) - } -} - -class ExecutorScheduler private[scala] (val asJavaScheduler: rx.Scheduler) - extends Scheduler {} - - - diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/IOScheduler.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/IOScheduler.scala index b7453016cf..339d82fd6a 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/IOScheduler.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/IOScheduler.scala @@ -19,15 +19,15 @@ import rx.lang.scala.Scheduler object IOScheduler { /** - * {@link Scheduler} intended for IO-bound work. + * [[rx.lang.scala.Scheduler]] intended for IO-bound work. *

- * The implementation is backed by an {@link Executor} thread-pool that will grow as needed. + * The implementation is backed by an `Executor` thread-pool that will grow as needed. *

* This can be used for asynchronously performing blocking IO. *

- * Do not perform computational work on this scheduler. Use {@link ComputationScheduler()} instead. + * Do not perform computational work on this scheduler. Use [[rx.lang.scala.schedulers.ComputationScheduler]] instead. * - * @return { @link ExecutorScheduler} for IO-bound work. + * @return [[rx.lang.scala.Scheduler]] for IO-bound work */ def apply(): IOScheduler = { new IOScheduler(rx.schedulers.Schedulers.io) diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/NewThreadScheduler.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/NewThreadScheduler.scala index e24450f89a..09b0f30c08 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/NewThreadScheduler.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/NewThreadScheduler.scala @@ -20,7 +20,7 @@ import rx.lang.scala.Scheduler object NewThreadScheduler { /** - * Returns a [[rx.lang.scala.Scheduler]] that creates a new {@link Thread} for each unit of work. + * Returns a [[rx.lang.scala.Scheduler]] that creates a new `java.lang.Thread` for each unit of work. */ def apply(): NewThreadScheduler = { new NewThreadScheduler(rx.schedulers.Schedulers.newThread()) diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/TrampolineScheduler.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/TrampolineScheduler.scala index 0ff50c1770..e2cdc49e7d 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/TrampolineScheduler.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/schedulers/TrampolineScheduler.scala @@ -4,7 +4,7 @@ import rx.lang.scala.Scheduler object TrampolineScheduler { /** - * {@link Scheduler} that queues work on the current thread to be executed after the current work completes. + * [[rx.lang.scala.Scheduler]] that queues work on the current thread to be executed after the current work completes. */ def apply(): TrampolineScheduler = { new TrampolineScheduler(rx.schedulers.Schedulers.trampoline()) diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/subjects/PublishSubject.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/subjects/PublishSubject.scala index 129717d8cc..577f6693ea 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/subjects/PublishSubject.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/subjects/PublishSubject.scala @@ -17,7 +17,7 @@ package rx.lang.scala.subjects import rx.lang.scala.Subject -private [scala] object PublishSubject { +object PublishSubject { def apply[T](): PublishSubject[T] = new PublishSubject[T](rx.subjects.PublishSubject.create[T]()) } diff --git a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/subscriptions/CompositeSubscription.scala b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/subscriptions/CompositeSubscription.scala index abe2e721c4..dcd0ca5c39 100644 --- a/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/subscriptions/CompositeSubscription.scala +++ b/language-adaptors/rxjava-scala/src/main/scala/rx/lang/scala/subscriptions/CompositeSubscription.scala @@ -50,9 +50,9 @@ class CompositeSubscription private[scala] (override val asJavaSubscription: rx. /** * Adds a subscription to the group, - * or unsubscribes immediately is the [[rx.subscriptions.CompositeSubscription]] is unsubscribed. + * or unsubscribes immediately is the [[rx.lang.scala.subscriptions.CompositeSubscription]] is unsubscribed. * @param subscription the subscription to be added. - * @return the [[rx.subscriptions.CompositeSubscription]] itself. + * @return the [[rx.lang.scala.subscriptions.CompositeSubscription]] itself. */ def +=(subscription: Subscription): this.type = { asJavaSubscription.add(subscription.asJavaSubscription) @@ -62,7 +62,7 @@ class CompositeSubscription private[scala] (override val asJavaSubscription: rx. /** * Removes and unsubscribes a subscription to the group, * @param subscription the subscription be removed. - * @return the [[rx.subscriptions.CompositeSubscription]] itself. + * @return the [[rx.lang.scala.subscriptions.CompositeSubscription]] itself. */ def -=(subscription: Subscription): this.type = { asJavaSubscription.remove(subscription.asJavaSubscription) diff --git a/language-adaptors/rxjava-scala/src/test/scala/rx/lang/scala/CompletenessTest.scala b/language-adaptors/rxjava-scala/src/test/scala/rx/lang/scala/CompletenessTest.scala index d4778d7d5e..cd5c6a1d9a 100644 --- a/language-adaptors/rxjava-scala/src/test/scala/rx/lang/scala/CompletenessTest.scala +++ b/language-adaptors/rxjava-scala/src/test/scala/rx/lang/scala/CompletenessTest.scala @@ -45,6 +45,7 @@ class CompletenessTest extends JUnitSuite { val commentForFirstWithPredicate = "[use `.filter(condition).first`]" val fromFuture = "[TODO: Decide how Scala Futures should relate to Observables. Should there be a " + "common base interface for Future and Observable? And should Futures also have an unsubscribe method?]" + val commentForTakeLastBuffer = "[use `takeRight(...).toSeq`]" /** * Maps each method from the Java Observable to its corresponding method in the Scala Observable @@ -71,38 +72,83 @@ class CompletenessTest extends JUnitSuite { "all(Func1[_ >: T, Boolean])" -> "forall(T => Boolean)", "buffer(Long, Long, TimeUnit)" -> "buffer(Duration, Duration)", "buffer(Long, Long, TimeUnit, Scheduler)" -> "buffer(Duration, Duration, Scheduler)", + "buffer(Func0[_ <: Observable[_ <: TClosing]])" -> "buffer(() => Observable[Any])", + "buffer(Observable[B])" -> "buffer(Observable[Any])", + "buffer(Observable[B], Int)" -> "buffer(Observable[Any], Int)", + "buffer(Observable[_ <: TOpening], Func1[_ >: TOpening, _ <: Observable[_ <: TClosing]])" -> "buffer(Observable[Opening], Opening => Observable[Any])", + "contains(Any)" -> "contains(U)", "count()" -> "length", + "delay(Func0[_ <: Observable[U]], Func1[_ >: T, _ <: Observable[V]])" -> "delay(() => Observable[Any], T => Observable[Any])", + "delay(Func1[_ >: T, _ <: Observable[U]])" -> "delay(T => Observable[Any])", "dematerialize()" -> "dematerialize(<:<[Observable[T], Observable[Notification[U]]])", - "elementAt(Int)" -> "[use `.drop(index).first`]", - "elementAtOrDefault(Int, T)" -> "[use `.drop(index).firstOrElse(default)`]", + "elementAtOrDefault(Int, T)" -> "elementAtOrDefault(Int, U)", "first(Func1[_ >: T, Boolean])" -> commentForFirstWithPredicate, "firstOrDefault(T)" -> "firstOrElse(=> U)", - "firstOrDefault(Func1[_ >: T, Boolean], T)" -> "[use `.filter(condition).firstOrElse(default)`]", + "firstOrDefault(T, Func1[_ >: T, Boolean])" -> "[use `.filter(condition).firstOrElse(default)`]", "groupBy(Func1[_ >: T, _ <: K], Func1[_ >: T, _ <: R])" -> "[use `groupBy` and `map`]", - "mapMany(Func1[_ >: T, _ <: Observable[_ <: R]])" -> "flatMap(T => Observable[R])", + "groupByUntil(Func1[_ >: T, _ <: TKey], Func1[_ >: GroupedObservable[TKey, T], _ <: Observable[_ <: TDuration]])" -> "groupByUntil(T => K, (K, Observable[T]) => Observable[Any])", + "lift(Operator[_ <: R, _ >: T])" -> "lift(Subscriber[R] => Subscriber[T])", "mapWithIndex(Func2[_ >: T, Integer, _ <: R])" -> "[combine `zipWithIndex` with `map` or with a for comprehension]", + "multicast(Subject[_ >: T, _ <: R])" -> "multicast(Subject[R])", + "multicast(Func0[_ <: Subject[_ >: T, _ <: TIntermediate]], Func1[_ >: Observable[TIntermediate], _ <: Observable[TResult]])" -> "multicast(() => Subject[R], Observable[R] => Observable[U])", "onErrorResumeNext(Func1[Throwable, _ <: Observable[_ <: T]])" -> "onErrorResumeNext(Throwable => Observable[U])", "onErrorResumeNext(Observable[_ <: T])" -> "onErrorResumeNext(Observable[U])", "onErrorReturn(Func1[Throwable, _ <: T])" -> "onErrorReturn(Throwable => U)", "onExceptionResumeNext(Observable[_ <: T])" -> "onExceptionResumeNext(Observable[U])", "parallel(Func1[Observable[T], Observable[R]])" -> "parallel(Observable[T] => Observable[R])", "parallel(Func1[Observable[T], Observable[R]], Scheduler)" -> "parallel(Observable[T] => Observable[R], Scheduler)", + "publish(T)" -> "publish(U)", + "publish(Func1[_ >: Observable[T], _ <: Observable[R]])" -> "publish(Observable[U] => Observable[R])", + "publish(Func1[_ >: Observable[T], _ <: Observable[R]], T)" -> "publish(Observable[U] => Observable[R], U)", "reduce(Func2[T, T, T])" -> "reduce((U, U) => U)", "reduce(R, Func2[R, _ >: T, R])" -> "foldLeft(R)((R, T) => R)", + "replay(Func1[_ >: Observable[T], _ <: Observable[R]])" -> "replay(Observable[U] => Observable[R])", + "replay(Func1[_ >: Observable[T], _ <: Observable[R]], Int)" -> "replay(Observable[U] => Observable[R], Int)", + "replay(Func1[_ >: Observable[T], _ <: Observable[R]], Int, Long, TimeUnit)" -> "replay(Observable[U] => Observable[R], Int, Duration)", + "replay(Func1[_ >: Observable[T], _ <: Observable[R]], Int, Long, TimeUnit, Scheduler)" -> "replay(Observable[U] => Observable[R], Int, Duration, Scheduler)", + "replay(Func1[_ >: Observable[T], _ <: Observable[R]], Int, Scheduler)" -> "replay(Observable[U] => Observable[R], Int, Scheduler)", + "replay(Func1[_ >: Observable[T], _ <: Observable[R]], Long, TimeUnit)" -> "replay(Observable[U] => Observable[R], Duration)", + "replay(Func1[_ >: Observable[T], _ <: Observable[R]], Long, TimeUnit, Scheduler)" -> "replay(Observable[U] => Observable[R], Duration, Scheduler)", + "replay(Func1[_ >: Observable[T], _ <: Observable[R]], Scheduler)" -> "replay(Observable[U] => Observable[R], Scheduler)", "scan(Func2[T, T, T])" -> unnecessary, "scan(R, Func2[R, _ >: T, R])" -> "scan(R)((R, T) => R)", "skip(Int)" -> "drop(Int)", + "skip(Long, TimeUnit)" -> "drop(Duration)", + "skip(Long, TimeUnit, Scheduler)" -> "drop(Duration, Scheduler)", "skipWhile(Func1[_ >: T, Boolean])" -> "dropWhile(T => Boolean)", "skipWhileWithIndex(Func2[_ >: T, Integer, Boolean])" -> unnecessary, - "startWith(Iterable[T])" -> "[unnecessary because we can just use `++` instead]", - "takeFirst()" -> "first", - "takeFirst(Func1[_ >: T, Boolean])" -> commentForFirstWithPredicate, + "skipUntil(Observable[U])" -> "dropUntil(Observable[E])", + "startWith(T)" -> "[use `item +: o`]", + "startWith(Array[T])" -> "[use `Observable.items(items) ++ o`]", + "startWith(Array[T], Scheduler)" -> "[use `Observable.items(items).subscribeOn(scheduler) ++ o`]", + "startWith(Iterable[T])" -> "[use `Observable.from(iterable) ++ o`]", + "startWith(Iterable[T], Scheduler)" -> "[use `Observable.from(iterable).subscribeOn(scheduler) ++ o`]", + "startWith(Observable[T])" -> "[use `++`]", + "skipLast(Int)" -> "dropRight(Int)", + "skipLast(Long, TimeUnit)" -> "dropRight(Duration)", + "skipLast(Long, TimeUnit, Scheduler)" -> "dropRight(Duration, Scheduler)", + "subscribe()" -> "subscribe()", + "takeFirst(Func1[_ >: T, Boolean])" -> "[use `filter(condition).take(1)`]", "takeLast(Int)" -> "takeRight(Int)", + "takeLast(Long, TimeUnit)" -> "takeRight(Duration)", + "takeLast(Long, TimeUnit, Scheduler)" -> "takeRight(Duration, Scheduler)", + "takeLast(Int, Long, TimeUnit)" -> "takeRight(Int, Duration)", + "takeLast(Int, Long, TimeUnit, Scheduler)" -> "takeRight(Int, Duration, Scheduler)", + "takeLastBuffer(Int)" -> commentForTakeLastBuffer, + "takeLastBuffer(Int, Long, TimeUnit)" -> commentForTakeLastBuffer, + "takeLastBuffer(Int, Long, TimeUnit, Scheduler)" -> commentForTakeLastBuffer, + "takeLastBuffer(Long, TimeUnit)" -> commentForTakeLastBuffer, + "takeLastBuffer(Long, TimeUnit, Scheduler)" -> commentForTakeLastBuffer, "takeWhileWithIndex(Func2[_ >: T, _ >: Integer, Boolean])" -> "[use `.zipWithIndex.takeWhile{case (elem, index) => condition}.map(_._1)`]", + "timeout(Func0[_ <: Observable[U]], Func1[_ >: T, _ <: Observable[V]], Observable[_ <: T])" -> "timeout(() => Observable[U], T => Observable[V], Observable[O])", + "timeout(Func1[_ >: T, _ <: Observable[V]], Observable[_ <: T])" -> "timeout(() => Observable[U], T => Observable[V])", + "timeout(Long, TimeUnit, Observable[_ <: T])" -> "timeout(Duration, Observable[U])", + "timeout(Long, TimeUnit, Observable[_ <: T], Scheduler)" -> "timeout(Duration, Observable[U], Scheduler)", + "timer(Long, Long, TimeUnit)" -> "timer(Duration, Duration)", + "timer(Long, Long, TimeUnit, Scheduler)" -> "timer(Duration, Duration, Scheduler)", "toList()" -> "toSeq", "toSortedList()" -> "[Sorting is already done in Scala's collection library, use `.toSeq.map(_.sorted)`]", "toSortedList(Func2[_ >: T, _ >: T, Integer])" -> "[Sorting is already done in Scala's collection library, use `.toSeq.map(_.sortWith(f))`]", - "where(Func1[_ >: T, Boolean])" -> "filter(T => Boolean)", "window(Long, Long, TimeUnit)" -> "window(Duration, Duration)", "window(Long, Long, TimeUnit, Scheduler)" -> "window(Duration, Duration, Scheduler)", @@ -111,44 +157,44 @@ class CompletenessTest extends JUnitSuite { "averageDoubles(Observable[Double])" -> averageProblem, "averageFloats(Observable[Float])" -> averageProblem, "averageLongs(Observable[Long])" -> averageProblem, - "create(OnSubscribeFunc[T])" -> "apply(Observer[T] => Subscription)", + "create(OnSubscribeFunc[T])" -> "create(Observer[T] => Subscription)", + "create(OnSubscribe[T])" -> "apply(Subscriber[T] => Unit)", "combineLatest(Observable[_ <: T1], Observable[_ <: T2], Func2[_ >: T1, _ >: T2, _ <: R])" -> "combineLatest(Observable[U])", "concat(Observable[_ <: Observable[_ <: T]])" -> "concat(<:<[Observable[T], Observable[Observable[U]]])", "defer(Func0[_ <: Observable[_ <: T]])" -> "defer(=> Observable[T])", - "empty()" -> "apply(T*)", - "error(Throwable)" -> "apply(Throwable)", - "from(Array[T])" -> "apply(T*)", - "from(Iterable[_ <: T])" -> "apply(T*)", + "from(Array[T])" -> "[use `items(T*)`]", + "from(Iterable[_ <: T])" -> "from(Iterable[T])", "from(Future[_ <: T])" -> fromFuture, "from(Future[_ <: T], Long, TimeUnit)" -> fromFuture, "from(Future[_ <: T], Scheduler)" -> fromFuture, - "just(T)" -> "apply(T*)", + "just(T)" -> "[use `items(T*)`]", + "just(T, Scheduler)" -> "[use `items(T*).subscribeOn(scheduler)`]", "merge(Observable[_ <: T], Observable[_ <: T])" -> "merge(Observable[U])", "merge(Observable[_ <: Observable[_ <: T]])" -> "flatten(<:<[Observable[T], Observable[Observable[U]]])", "mergeDelayError(Observable[_ <: T], Observable[_ <: T])" -> "mergeDelayError(Observable[U])", "mergeDelayError(Observable[_ <: Observable[_ <: T]])" -> "flattenDelayError(<:<[Observable[T], Observable[Observable[U]]])", - "range(Int, Int)" -> "apply(Range)", - "sequenceEqual(Observable[_ <: T], Observable[_ <: T])" -> "[use `(first zip second) map (p => p._1 == p._2)`]", - "sequenceEqual(Observable[_ <: T], Observable[_ <: T], Func2[_ >: T, _ >: T, Boolean])" -> "[use `(first zip second) map (p => equality(p._1, p._2))`]", + "sequenceEqual(Observable[_ <: T], Observable[_ <: T])" -> "sequenceEqual(Observable[U])", + "sequenceEqual(Observable[_ <: T], Observable[_ <: T], Func2[_ >: T, _ >: T, Boolean])" -> "sequenceEqual(Observable[U], (U, U) => Boolean)", + "range(Int, Int)" -> "[use `(start until (start + count)).toObservable` instead of `range(start, count)`]", + "range(Int, Int, Scheduler)" -> "[use `(start until (start + count)).toObservable.subscribeOn(scheduler)` instead of `range(start, count, scheduler)`]`]", "sum(Observable[Integer])" -> "sum(Numeric[U])", "sumDoubles(Observable[Double])" -> "sum(Numeric[U])", "sumFloats(Observable[Float])" -> "sum(Numeric[U])", "sumLongs(Observable[Long])" -> "sum(Numeric[U])", - "synchronize(Observable[T])" -> "synchronize", "switchDo(Observable[_ <: Observable[_ <: T]])" -> deprecated, "switchOnNext(Observable[_ <: Observable[_ <: T]])" -> "switch(<:<[Observable[T], Observable[Observable[U]]])", "zip(Observable[_ <: T1], Observable[_ <: T2], Func2[_ >: T1, _ >: T2, _ <: R])" -> "[use instance method `zip` and `map`]", "zip(Observable[_ <: Observable[_]], FuncN[_ <: R])" -> "[use `zip` in companion object and `map`]", "zip(Iterable[_ <: Observable[_]], FuncN[_ <: R])" -> "[use `zip` in companion object and `map`]" - ) ++ List.iterate("T", 9)(s => s + ", T").map( + ) ++ List.iterate("T, T", 8)(s => s + ", T").map( // all 9 overloads of startWith: - "startWith(" + _ + ")" -> "[unnecessary because we can just use `++` instead]" + "startWith(" + _ + ")" -> "[use `Observable.items(...) ++ o`]" ).toMap ++ List.iterate("Observable[_ <: T]", 9)(s => s + ", Observable[_ <: T]").map( // concat 2-9 "concat(" + _ + ")" -> "[unnecessary because we can use `++` instead or `Observable(o1, o2, ...).concat`]" ).drop(1).toMap ++ List.iterate("T", 10)(s => s + ", T").map( // all 10 overloads of from: - "from(" + _ + ")" -> "apply(T*)" + "from(" + _ + ")" -> "[use `items(T*)`]" ).toMap ++ (3 to 9).map(i => { // zip3-9: val obsArgs = (1 to i).map(j => s"Observable[_ <: T$j], ").mkString("") @@ -190,6 +236,8 @@ class CompletenessTest extends JUnitSuite { // TODO how can we filter out instance methods which were put into companion because // of extends AnyVal in a way which does not depend on implementation-chosen name '$extension'? .filter(! _.contains("$extension")) + // `access$000` is public. How to distinguish it from others without hard-code? + .filter(! _.contains("access$000")) } // also applicable for Java types @@ -347,7 +395,10 @@ class CompletenessTest extends JUnitSuite { def escape(s: String) = s.replaceAllLiterally("[", "<").replaceAllLiterally("]", ">") println(""" -## Comparison of Scala Observable and Java Observable +--- +layout: comparison +title: Comparison of Scala Observable and Java Observable +--- Note: * This table contains both static methods and instance methods. diff --git a/language-adaptors/rxjava-scala/src/test/scala/rx/lang/scala/ObservableTest.scala b/language-adaptors/rxjava-scala/src/test/scala/rx/lang/scala/ObservableTest.scala index 397907a6cb..d8ef40590e 100644 --- a/language-adaptors/rxjava-scala/src/test/scala/rx/lang/scala/ObservableTest.scala +++ b/language-adaptors/rxjava-scala/src/test/scala/rx/lang/scala/ObservableTest.scala @@ -146,16 +146,34 @@ class ObservableTests extends JUnitSuite { assertEquals(l2, l1) } - /* - @Test def testHead() { - val observer = mock(classOf[Observer[Int]]) - val o = Observable().head - val sub = o.subscribe(observer) - - verify(observer, never).onNext(any(classOf[Int])) - verify(observer, never).onCompleted() - verify(observer, times(1)).onError(any(classOf[NoSuchElementException])) - } - */ + @Test def testHead() { + val o: Observable[String] = List("alice", "bob", "carol").toObservable.head + assertEquals(List("alice"), o.toBlockingObservable.toList) + } + + @Test(expected = classOf[NoSuchElementException]) + def testHeadWithEmptyObservable() { + val o: Observable[String] = List[String]().toObservable.head + o.toBlockingObservable.toList + } + + @Test def testTail() { + val o: Observable[String] = List("alice", "bob", "carol").toObservable.tail + assertEquals(List("bob", "carol"), o.toBlockingObservable.toList) + assertEquals(List("bob", "carol"), o.toBlockingObservable.toList) + } + + @Test(expected = classOf[UnsupportedOperationException]) + def testTailWithEmptyObservable() { + val o: Observable[String] = List[String]().toObservable.tail + o.toBlockingObservable.toList + } + + @Test + def testZipWithIndex() { + val o = List("alice", "bob", "carol").toObservable.zipWithIndex.map(_._2) + assertEquals(List(0, 1, 2), o.toBlockingObservable.toList) + assertEquals(List(0, 1, 2), o.toBlockingObservable.toList) + } } diff --git a/language-adaptors/rxjava-scala/src/test/scala/rx/lang/scala/SubscriberTests.scala b/language-adaptors/rxjava-scala/src/test/scala/rx/lang/scala/SubscriberTests.scala new file mode 100644 index 0000000000..d1c03227fe --- /dev/null +++ b/language-adaptors/rxjava-scala/src/test/scala/rx/lang/scala/SubscriberTests.scala @@ -0,0 +1,45 @@ +/** + * Copyright 2013 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package rx.lang.scala + +import org.junit.Test +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.scalatest.junit.JUnitSuite + +class SubscriberTests extends JUnitSuite { + + @Test def testIssue1173() { + // https://github.com/Netflix/RxJava/issues/1173 + val subscriber = Subscriber((n: Int) => println(n)) + assertNotNull(subscriber.asJavaObserver) + assertNotNull(subscriber.asJavaSubscription) + assertNotNull(subscriber.asJavaSubscriber) + } + + @Test def testUnsubscribeForSubscriber() { + var innerSubscriber: Subscriber[Int] = null + val o = Observable[Int](subscriber => { + Observable[Int](subscriber => { + innerSubscriber = subscriber + }).subscribe(subscriber) + }) + o.subscribe().unsubscribe() + // If we unsubscribe outside, the inner Subscriber should also be unsubscribed + assertTrue(innerSubscriber.isUnsubscribed) + } + +} diff --git a/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle b/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle new file mode 100644 index 0000000000..03513b751f --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples-build-wrapper/build.gradle @@ -0,0 +1,11 @@ +tasks.build.doLast { + def androidHome = System.getenv("ANDROID_HOME") + if (project.hasProperty('buildAndroidSamples') && !androidHome.isEmpty()) { + println("Android SDK detected at $androidHome, running samples build") + project.exec { + workingDir '../rxjava-android-samples' + + commandLine "./gradlew", "clean", "packageDebug" + } + } +} \ No newline at end of file diff --git a/rxjava-contrib/rxjava-android-samples/.gitignore b/rxjava-contrib/rxjava-android-samples/.gitignore new file mode 100644 index 0000000000..d6bfc95b18 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/.gitignore @@ -0,0 +1,4 @@ +.gradle +/local.properties +/.idea/workspace.xml +.DS_Store diff --git a/rxjava-contrib/rxjava-android-samples/build.gradle b/rxjava-contrib/rxjava-android-samples/build.gradle new file mode 100644 index 0000000000..0ba4fcdb79 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/build.gradle @@ -0,0 +1,17 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:0.9.+' + } +} + +allprojects { + repositories { + mavenLocal() + mavenCentral() + } +} diff --git a/rxjava-contrib/rxjava-android-samples/gradle.properties b/rxjava-contrib/rxjava-android-samples/gradle.properties new file mode 100644 index 0000000000..5d08ba75bb --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/gradle.properties @@ -0,0 +1,18 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Settings specified in this file will override any Gradle settings +# configured through the IDE. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true \ No newline at end of file diff --git a/rxjava-contrib/rxjava-android-samples/gradle/wrapper/gradle-wrapper.properties b/rxjava-contrib/rxjava-android-samples/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..acc117c91f --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Apr 05 11:18:06 CEST 2014 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://services.gradle.org/distributions/gradle-1.11-all.zip diff --git a/rxjava-contrib/rxjava-android-samples/gradlew b/rxjava-contrib/rxjava-android-samples/gradlew new file mode 100755 index 0000000000..91a7e269e1 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/rxjava-contrib/rxjava-android-samples/gradlew.bat b/rxjava-contrib/rxjava-android-samples/gradlew.bat new file mode 100644 index 0000000000..8a0b282aa6 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/rxjava-contrib/rxjava-android-samples/samples/.gitignore b/rxjava-contrib/rxjava-android-samples/samples/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/.gitignore @@ -0,0 +1 @@ +/build diff --git a/rxjava-contrib/rxjava-android-samples/samples/build.gradle b/rxjava-contrib/rxjava-android-samples/samples/build.gradle new file mode 100644 index 0000000000..9a080128a8 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'android' + +android { + compileSdkVersion 19 + buildToolsVersion "19.0.0" + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 19 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + runProguard false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } +} + +// make sure we always compile against the latest version of RxJava +def rootProjectProperties = new Properties() +file("../../../gradle.properties").withReader { reader -> + rootProjectProperties.load(reader) + properties.putAll(rootProjectProperties) +} + +dependencies { + def rxjVersion = rootProjectProperties.get("version") + compile "com.netflix.rxjava:rxjava-android:$rxjVersion" + compile fileTree(dir: 'libs', include: ['*.jar']) +} diff --git a/rxjava-contrib/rxjava-android-samples/samples/proguard-rules.txt b/rxjava-contrib/rxjava-android-samples/samples/proguard-rules.txt new file mode 100644 index 0000000000..c423b03cb2 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/proguard-rules.txt @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/opt/android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} \ No newline at end of file diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..96b3fcb7d0 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/AndroidManifest.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListFragmentActivity.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListFragmentActivity.java new file mode 100644 index 0000000000..446bffa55d --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListFragmentActivity.java @@ -0,0 +1,75 @@ +package com.netflix.rxjava.android.samples; + +import android.app.Activity; +import android.app.ListFragment; +import android.os.Bundle; +import android.widget.ArrayAdapter; + +import rx.Observable; +import rx.Subscriber; + +import static rx.android.schedulers.AndroidSchedulers.mainThread; + +/** + * Problem: + * You have an asynchronous sequence that emits items to be displayed in a list. You want the data + * to survive rotation changes. + *

+ * Solution: + * Combine {@link android.app.Fragment#setRetainInstance(boolean)} in a ListFragment with + * {@link rx.android.schedulers.AndroidSchedulers#mainThread()} and an {@link rx.Observable.Operator} + * that binds to the list adapter. + */ +public class ListFragmentActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTitle("Lists"); + setContentView(R.layout.list_fragment_activity); + } + + @SuppressWarnings("ConstantConditions") + public static class RetainedListFragment extends ListFragment { + + private ArrayAdapter adapter; + + public RetainedListFragment() { + setRetainInstance(true); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + adapter = new ArrayAdapter(getActivity(), android.R.layout.simple_list_item_1); + setListAdapter(adapter); + SampleObservables.numberStrings(1, 20, 250) + .observeOn(mainThread()) + .lift(new BindAdapter()) + .subscribe(); + } + + private final class BindAdapter implements Observable.Operator { + @Override + public Subscriber call(Subscriber subscriber) { + return new Subscriber() { + @Override + public void onCompleted() { + adapter.notifyDataSetChanged(); + } + + @Override + public void onError(Throwable throwable) { + + } + + @Override + public void onNext(String strings) { + adapter.add(strings); + } + }; + } + } + } +} diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListenInOutActivity.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListenInOutActivity.java new file mode 100644 index 0000000000..893124b2f2 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListenInOutActivity.java @@ -0,0 +1,87 @@ +package com.netflix.rxjava.android.samples; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.ToggleButton; + +import rx.Observable; +import rx.Observer; +import rx.Subscription; +import rx.observables.ConnectableObservable; + +import static rx.android.observables.AndroidObservable.bindActivity; + +/** + * Activity that binds to a counting sequence and is able to listen in and out to that + * sequence by pressing a toggle button. The button disables itself once the sequence + * finishes. + */ +public class ListenInOutActivity extends Activity implements Observer { + + private Observable source; + private Subscription subscription; + private TextView textView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.listen_in_out_activity); + + textView = (TextView) findViewById(android.R.id.text1); + + // in a production app, you would use dependency injection, fragments, or other + // means to preserve the observable, but this will suffice here + source = (Observable) getLastNonConfigurationInstance(); + if (source == null) { + source = SampleObservables.numberStrings(1, 100, 200).publish(); + ((ConnectableObservable) source).connect(); + } + + subscribeToSequence(); + } + + private void subscribeToSequence() { + subscription = bindActivity(this, source).subscribe(this); + } + + @Override + public Object onRetainNonConfigurationInstance() { + return source; + } + + @Override + protected void onDestroy() { + subscription.unsubscribe(); + super.onDestroy(); + } + + @Override + public void onCompleted() { + TextView button = (TextView) findViewById(R.id.toggle_button); + button.setText("Completed"); + button.setEnabled(false); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + Toast.makeText(this, "Error: " + e, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onNext(String s) { + textView.setText(s); + } + + public void onSequenceToggleClicked(View view) { + if (((ToggleButton) view).isChecked()) { + subscription.unsubscribe(); + } else { + subscribeToSequence(); + } + } +} diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListeningFragmentActivity.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListeningFragmentActivity.java new file mode 100644 index 0000000000..966568fd15 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/ListeningFragmentActivity.java @@ -0,0 +1,96 @@ +package com.netflix.rxjava.android.samples; + +import android.app.Activity; +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import rx.Subscriber; +import rx.Subscription; +import rx.observables.ConnectableObservable; +import rx.subscriptions.Subscriptions; + +import static rx.android.observables.AndroidObservable.bindFragment; + +/** + * Problem: + * You have a background sequence which keeps emitting items (either a limited or unlimited number) + * and your UI component should be able to "listen in" to the sequence, i.e. it's okay to miss + * in-flight items when going e.g. through a screen rotation or being otherwise detached from the + * screen for a limited period of time. (Another example is a "page out" in a fragment ViewPager.) + *

+ * This is useful if you need behavior that mimics event buses. Think of a publishing + * Observable as a channel or queue on an event bus. + *

+ * Solution: + * Combine {@link android.app.Fragment#setRetainInstance(boolean)} with + * {@link rx.android.schedulers.AndroidSchedulers#mainThread()} and {@link rx.Observable#publish()} + */ +public class ListeningFragmentActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.listening_fragment_activity); + } + + @SuppressWarnings("ConstantConditions") + public static class ListeningFragment extends Fragment { + + private ConnectableObservable strings; + private Subscription subscription = Subscriptions.empty(); + + public ListeningFragment() { + setRetainInstance(true); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + strings = SampleObservables.numberStrings(1, 50, 250).publish(); + strings.connect(); // trigger the sequence + } + + @Override + public void onDestroyView() { + subscription.unsubscribe(); // stop listening + super.onDestroyView(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.retained_fragment, container, false); + } + + @Override + public void onViewCreated(final View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + final TextView textView = (TextView) view.findViewById(android.R.id.text1); + + // re-connect to sequence + subscription = bindFragment(this, strings).subscribe(new Subscriber() { + + @Override + public void onCompleted() { + Toast.makeText(getActivity(), "Done!", Toast.LENGTH_SHORT).show(); + } + + @Override + public void onError(Throwable throwable) { + + } + + @Override + public void onNext(String s) { + textView.setText(s); + } + }); + } + } +} diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragmentActivity.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragmentActivity.java new file mode 100644 index 0000000000..b598fe5c72 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/RetainedFragmentActivity.java @@ -0,0 +1,107 @@ +package com.netflix.rxjava.android.samples; + +import android.app.Activity; +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.TextView; + +import org.json.JSONException; +import org.json.JSONObject; + +import rx.Observable; +import rx.Subscription; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.subscriptions.Subscriptions; + +import static rx.android.observables.AndroidObservable.bindFragment; + +/** + * Problem: + * You have a data source (where that data is potentially expensive to obtain), and you want to + * emit this data into a fragment. However, you want to gracefully deal with rotation changes and + * not lose any data already emitted. + *

+ * Solution: + * Combine {@link android.app.Fragment#setRetainInstance(boolean)} with + * {@link rx.android.schedulers.AndroidSchedulers#mainThread()} and {@link rx.Observable#cache()} + */ +public class RetainedFragmentActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setTitle("Fake API call"); + setContentView(R.layout.retained_fragment_activity); + } + + @SuppressWarnings("ConstantConditions") + public static class RetainedFragment extends Fragment { + + // in a production app, you don't want to have JSON parser code in your fragment, + // but we'll simplify a little here + private static final Func1 PARSE_JSON = new Func1() { + @Override + public String call(String json) { + try { + JSONObject jsonObject = new JSONObject(json); + return String.valueOf(jsonObject.getInt("result")); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + }; + + private Observable strings; + private Subscription subscription = Subscriptions.empty(); + + public RetainedFragment() { + setRetainInstance(true); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // simulate fetching a JSON document with a latency of 2 seconds + // in retained fragments, it's sufficient to bind the fragment in onCreate, since + // Android takes care of detaching the Activity for us, and holding a reference for + // the duration of the observable does not harm. + strings = bindFragment(this, SampleObservables.fakeApiCall(2000).map(PARSE_JSON).cache()); + } + + @Override + public void onDestroyView() { + subscription.unsubscribe(); + super.onDestroyView(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + getActivity().setProgressBarIndeterminateVisibility(true); + return inflater.inflate(R.layout.retained_fragment, container, false); + } + + @Override + public void onViewCreated(final View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + final TextView textView = (TextView) view.findViewById(android.R.id.text1); + + // (re-)subscribe to the sequence, which either emits the cached result or simply re- + // attaches the subscriber to wait for it to arrive + subscription = strings.subscribe(new Action1() { + @Override + public void call(String result) { + textView.setText(result); + getActivity().setProgressBarIndeterminateVisibility(false); + } + }); + } + } +} diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleObservables.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleObservables.java new file mode 100644 index 0000000000..7a17bedad5 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SampleObservables.java @@ -0,0 +1,42 @@ +package com.netflix.rxjava.android.samples; + +import android.os.SystemClock; + +import rx.Observable; +import rx.Subscriber; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.schedulers.Schedulers; + +public class SampleObservables { + + /** + * Emits numbers as strings, where these numbers a generated on a background thread. + */ + public static Observable numberStrings(int from, int to, final long delay) { + return Observable.range(from, to).map(new Func1() { + @Override + public String call(Integer integer) { + return integer.toString(); + } + }).doOnNext(new Action1() { + @Override + public void call(String s) { + SystemClock.sleep(delay); + } + }).subscribeOn(Schedulers.newThread()); + } + + public static Observable fakeApiCall(final long delay) { + return Observable.create(new Observable.OnSubscribe() { + @Override + public void call(Subscriber subscriber) { + // simulate I/O latency + SystemClock.sleep(delay); + final String fakeJson = "{\"result\": 42}"; + subscriber.onNext(fakeJson); + subscriber.onCompleted(); + } + }).subscribeOn(Schedulers.io()); + } +} diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SamplesApplication.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SamplesApplication.java new file mode 100644 index 0000000000..e91e7d543e --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/SamplesApplication.java @@ -0,0 +1,13 @@ +package com.netflix.rxjava.android.samples; + +import android.app.Application; +import android.os.StrictMode; + +public class SamplesApplication extends Application { + + @Override + public void onCreate() { + super.onCreate(); + StrictMode.enableDefaults(); + } +} diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/UIBindingActivity.java b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/UIBindingActivity.java new file mode 100644 index 0000000000..8d4508846c --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/java/com/netflix/rxjava/android/samples/UIBindingActivity.java @@ -0,0 +1,150 @@ +package com.netflix.rxjava.android.samples; + +import android.app.Activity; +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.Button; +import android.widget.TextView; +import org.json.JSONException; +import org.json.JSONObject; +import rx.Observable; +import rx.Subscription; +import rx.android.schedulers.AndroidSchedulers; +import rx.functions.Action1; +import rx.functions.Func1; +import rx.subscriptions.Subscriptions; + +/** + * Problem: + * You have a data source (where that data is potentially expensive to obtain), and you want to + * emit this data into a fragment. However, you want to gracefully deal with rotation changes and + * not lose any data already emitted. + *

+ * You also want your UI to update accordingly to the data being emitted. + * + * @author zsiegel (zsiegel87@gmail.com) + */ +public class UIBindingActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setTitle("UIBinding"); + setContentView(R.layout.ui_binding_activity); + } + + @SuppressWarnings("ConstantConditions") + public static class RetainedBindingFragment extends Fragment { + + private Button startButton; + + private Observable request; + private Subscription requestSubscription = Subscriptions.empty(); + + // in a production app, you don't want to have JSON parser code in your fragment, + // but we'll simplify a little here + private static final Func1 PARSE_JSON = new Func1() { + @Override + public String call(String json) { + try { + JSONObject jsonObject = new JSONObject(json); + return String.valueOf(jsonObject.getInt("result")); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + }; + + public RetainedBindingFragment() { + setRetainInstance(true); + } + + /** + * We un-subscribe whenever we are paused + */ + @Override + public void onPause() { + requestSubscription.unsubscribe(); + super.onPause(); + } + + /** + * We re-subscribe whenever we are resumed + */ + @Override + public void onResume() { + super.onResume(); + subscribe(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.retained_fragment_v2, container, false); + } + + @Override + public void onViewCreated(final View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + final TextView textView = (TextView) getView().findViewById(android.R.id.text1); + + startButton = (Button) view.findViewById(R.id.button); + startButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + textView.setText(""); + start(); + } + }); + } + + private void start() { + + request = SampleObservables + .fakeApiCall(5000) + .map(PARSE_JSON) + .observeOn(AndroidSchedulers.mainThread()) + .cache(); + + subscribe(); + } + + /** + * We subscribe/re-subscribe here + */ + private void subscribe() { + if (request != null) { + + final TextView textView = (TextView) getView().findViewById(android.R.id.text1); + + requestSubscription = request.map(new Func1() { + + //Consume the data that comes back and then signal that we are done + @Override + public Boolean call(String s) { + textView.setText(s); + return true; + } + }).startWith(false) //Before we receive data our request is not complete + .subscribe(new Action1() { + + //We update the UI based on the state of the request + @Override + public void call(Boolean completed) { + setRequestInProgress(completed); + } + }); + } + } + + private void setRequestInProgress(boolean completed) { + getActivity().setProgressBarIndeterminateVisibility(!completed); + startButton.setEnabled(completed); + } + } +} diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-hdpi/ic_launcher.png b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000000..96a442e5b8 Binary files /dev/null and b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-mdpi/ic_launcher.png b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000000..359047dfa4 Binary files /dev/null and b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-xhdpi/ic_launcher.png b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..71c6d760f0 Binary files /dev/null and b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-xxhdpi/ic_launcher.png b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..4df1894644 Binary files /dev/null and b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/list_fragment_activity.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/list_fragment_activity.xml new file mode 100644 index 0000000000..f741ee62dc --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/list_fragment_activity.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/listen_in_out_activity.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/listen_in_out_activity.xml new file mode 100644 index 0000000000..bc2c1e1a99 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/listen_in_out_activity.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/listening_fragment_activity.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/listening_fragment_activity.xml new file mode 100644 index 0000000000..ecfc325d20 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/listening_fragment_activity.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment.xml new file mode 100644 index 0000000000..0fc012b481 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment_activity.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment_activity.xml new file mode 100644 index 0000000000..e5eb51fb86 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment_activity.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment_v2.xml b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment_v2.xml new file mode 100644 index 0000000000..aa5e2792c7 --- /dev/null +++ b/rxjava-contrib/rxjava-android-samples/samples/src/main/res/layout/retained_fragment_v2.xml @@ -0,0 +1,19 @@ + + + + + + +