├── gradle.properties ├── settings.gradle ├── .travis.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── CONTRIBUTING.md ├── src ├── main │ ├── java │ │ └── rx │ │ │ └── lang │ │ │ └── clojure │ │ │ └── interop │ │ │ └── DummyObservable.java │ └── clojure │ │ └── rx │ │ └── lang │ │ └── clojure │ │ ├── future.clj │ │ ├── blocking.clj │ │ ├── interop.clj │ │ ├── chunk.clj │ │ ├── realized.clj │ │ ├── graph.clj │ │ └── core.clj ├── test │ └── clojure │ │ └── rx │ │ └── lang │ │ └── clojure │ │ ├── future_test.clj │ │ ├── blocking_test.clj │ │ ├── chunk_test.clj │ │ ├── graph_test.clj │ │ ├── interop_test.clj │ │ ├── realized_test.clj │ │ └── core_test.clj └── examples │ └── clojure │ └── rx │ └── lang │ └── clojure │ └── examples │ ├── http_examples.clj │ ├── video_example.clj │ └── rx_examples.clj ├── gradlew.bat ├── gradlew ├── README.md └── LICENSE /gradle.properties: -------------------------------------------------------------------------------- 1 | version=1.0.0-RC1-SNAPSHOT 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='rxclojure' 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - oraclejdk7 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveX/RxClojure/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Sep 02 17:31:29 PDT 2014 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-1.12-bin.zip 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | 27 | # OS generated files # 28 | ###################### 29 | .DS_Store* 30 | ehthumbs.db 31 | Icon? 32 | Thumbs.db 33 | 34 | # Editor Files # 35 | ################ 36 | *~ 37 | *.swp 38 | 39 | # Gradle Files # 40 | ################ 41 | .gradle 42 | .gradletasknamecache 43 | .m2 44 | 45 | # Build output directies 46 | target/ 47 | build/ 48 | 49 | # IntelliJ specific files/directories 50 | out 51 | .idea 52 | *.ipr 53 | *.iws 54 | *.iml 55 | atlassian-ide-plugin.xml 56 | 57 | # Eclipse specific files/directories 58 | .classpath 59 | .project 60 | .settings 61 | .metadata 62 | bin/ 63 | 64 | # NetBeans specific files/directories 65 | .nbattrs 66 | /.nb-gradle/profiles/private/ 67 | .nb-gradle-properties 68 | 69 | # Scala build 70 | *.cache 71 | /.nb-gradle/private/ 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to RxJava 2 | 3 | If you would like to contribute code you can do so through GitHub by forking the repository and sending a pull request (on a branch other than `master` or `gh-pages`). 4 | 5 | When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. 6 | 7 | ## License 8 | 9 | By contributing your code, you agree to license your contribution under the terms of the APLv2: https://github.com/ReactiveX/RxJava/blob/master/LICENSE 10 | 11 | All files are released with the Apache 2.0 license. 12 | 13 | If you are adding a new file it should have a header like this: 14 | 15 | ``` 16 | /** 17 | * Copyright 2014 Netflix, Inc. 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | */ 31 | ``` 32 | -------------------------------------------------------------------------------- /src/main/java/rx/lang/clojure/interop/DummyObservable.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package rx.lang.clojure.interop; 17 | 18 | // Dummy class with some overloads to make sure that type hinting works 19 | // correctly with the fn and action macros. Used only for testing. 20 | public class DummyObservable { 21 | 22 | public String call(Object f) { 23 | return "Object"; 24 | } 25 | 26 | public String call(rx.functions.Func1 f) { 27 | return "rx.functions.Func1"; 28 | } 29 | 30 | public String call(rx.functions.Func2 f) { 31 | return "rx.functions.Func2"; 32 | } 33 | 34 | public String call(rx.functions.Action1 f) { 35 | return "rx.functions.Action1"; 36 | } 37 | 38 | public String call(rx.functions.Action2 f) { 39 | return "rx.functions.Action2"; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/clojure/rx/lang/clojure/future_test.clj: -------------------------------------------------------------------------------- 1 | (ns rx.lang.clojure.future-test 2 | (:require [rx.lang.clojure.core :as rx] 3 | [rx.lang.clojure.blocking :as b] 4 | [rx.lang.clojure.future :as f]) 5 | (:require [clojure.test :refer [deftest testing is]])) 6 | 7 | (deftest test-future-generator 8 | (is (not= [(.getId (Thread/currentThread))] 9 | (b/into [] 10 | (f/future-generator* future-call 11 | #(rx/on-next % (.getId (Thread/currentThread)))))))) 12 | 13 | (deftest test-future 14 | (is (= [15] (b/into [] (f/future* future-call + 1 2 3 4 5))))) 15 | 16 | (deftest test-future-exception 17 | (is (= "Caught: boo" 18 | (->> (f/future* future-call #(throw (java.io.FileNotFoundException. "boo"))) 19 | (rx/catch java.io.FileNotFoundException e 20 | (rx/return (str "Caught: " (.getMessage e)))) 21 | (b/single))))) 22 | 23 | (deftest test-future-cancel 24 | (let [exited? (atom nil) 25 | o (f/future* future-call 26 | (fn [] (Thread/sleep 1000) 27 | (reset! exited? true) 28 | "WAT")) 29 | result (->> o 30 | (rx/take 0) 31 | (b/into []))] 32 | (Thread/sleep 2000) 33 | (is (= [nil []] 34 | [@exited? result])))) 35 | 36 | (deftest test-future-generator-cancel 37 | (let [exited? (atom nil) 38 | o (f/future-generator* future-call 39 | (fn [o] 40 | (rx/on-next o "FIRST") 41 | (Thread/sleep 1000) 42 | (reset! exited? true))) 43 | result (->> o 44 | (rx/take 1) 45 | (b/into []))] 46 | (Thread/sleep 2000) 47 | (is (= [nil ["FIRST"]] 48 | [@exited? result])))) 49 | 50 | (deftest test-future-generator-exception 51 | (let [e (java.io.FileNotFoundException. "snake")] 52 | (is (= [1 2 e] 53 | (->> (f/future-generator* 54 | future-call 55 | (fn [o] 56 | (rx/on-next o 1) 57 | (rx/on-next o 2) 58 | (throw e))) 59 | (rx/catch java.io.FileNotFoundException e 60 | (rx/return e)) 61 | (b/into [])))))) 62 | -------------------------------------------------------------------------------- /src/test/clojure/rx/lang/clojure/blocking_test.clj: -------------------------------------------------------------------------------- 1 | (ns rx.lang.clojure.blocking-test 2 | (:require [rx.lang.clojure.blocking :as b] 3 | [rx.lang.clojure.core :as rx] 4 | [clojure.test :refer [deftest testing is]])) 5 | 6 | (deftest test-->blocking 7 | (testing "returns a BlockingObservable from an Observable" 8 | (is (instance? rx.observables.BlockingObservable (b/->blocking (rx/return 0))))) 9 | 10 | (testing "is idempotent" 11 | (is (instance? rx.observables.BlockingObservable (b/->blocking (b/->blocking (rx/return 0))))))) 12 | 13 | 14 | (deftest test-o->seq 15 | (is (= [1 2 3] (b/o->seq (rx/seq->o [1 2 3]))))) 16 | 17 | (deftest test-first 18 | (testing "returns first element of observable" 19 | (is (= 1 (b/first (rx/seq->o [1 2 3]))))) 20 | (testing "returns nil for empty observable" 21 | (is (nil? (b/first (rx/empty))))) 22 | (testing "rethrows errors" 23 | (is (thrown? java.io.FileNotFoundException 24 | (b/first (rx/throw (java.io.FileNotFoundException. "boo"))))))) 25 | 26 | (deftest test-last 27 | (testing "returns last element of observable" 28 | (is (= 3 (b/last (rx/seq->o [1 2 3]))))) 29 | (testing "returns nil for empty observable" 30 | (is (nil? (b/last (rx/empty))))) 31 | (testing "rethrows errors" 32 | (is (thrown? java.io.FileNotFoundException 33 | (b/last (rx/throw (java.io.FileNotFoundException. "boo"))))))) 34 | 35 | (deftest test-single 36 | (testing "returns one element" 37 | (is (= 1 (b/single (rx/return 1))))) 38 | (testing "throw if empty" 39 | (is (thrown? java.util.NoSuchElementException (b/single (rx/empty))))) 40 | (testing "throw if many" 41 | (is (thrown? java.lang.IllegalArgumentException (b/single (rx/seq->o [1 2]))))) 42 | (testing "rethrows errors" 43 | (is (thrown? java.io.FileNotFoundException 44 | (b/single (rx/throw (java.io.FileNotFoundException. "boo"))))))) 45 | 46 | (deftest test-into 47 | (is (= [1 2 3] 48 | (b/into [1] (rx/seq->o [2 3])))) 49 | (testing "rethrows errors" 50 | (is (thrown? java.io.FileNotFoundException 51 | (b/into #{} (rx/throw (java.io.FileNotFoundException. "boo"))))))) 52 | 53 | (deftest test-doseq 54 | (is (= (range 3) 55 | (let [capture (atom [])] 56 | (b/doseq [{:keys [value]} (rx/seq->o (map #(hash-map :value %) (range 3)))] 57 | (println value) 58 | (swap! capture conj value)) 59 | @capture))) 60 | 61 | (testing "rethrows errors" 62 | (is (thrown? java.io.FileNotFoundException 63 | (b/doseq [i (rx/seq->o (range 3))] 64 | (throw (java.io.FileNotFoundException. "boo"))))))) 65 | 66 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /src/main/clojure/rx/lang/clojure/future.clj: -------------------------------------------------------------------------------- 1 | (ns rx.lang.clojure.future 2 | "Functions and macros for making rx-ified futures. That is, run some code in some 3 | other thread and return an Observable of its result. 4 | " 5 | (:require [rx.lang.clojure.interop :as iop] 6 | [rx.lang.clojure.core :as rx])) 7 | 8 | (def ^:private -ns- *ns*) 9 | (set! *warn-on-reflection* true) 10 | 11 | (defn future* 12 | "Exerimental/Possibly a bad idea 13 | 14 | Execute (f & args) in a separate thread and pass the result to onNext. 15 | If an exception is thrown, onError is called with the exception. 16 | 17 | runner is a function that takes a no-arg function argument and returns a future 18 | representing the execution of that function. 19 | 20 | Returns an Observable. If the subscriber unsubscribes, the future will be canceled 21 | with clojure.core/future-cancel 22 | 23 | Examples: 24 | 25 | (subscribe (rx/future future-call 26 | #(slurp \"input.txt\")) 27 | (fn [v] (println \"Got: \" v))) 28 | ; eventually outputs content of input.txt 29 | " 30 | [runner f & args] 31 | {:pre [(ifn? runner) (ifn? f)]} 32 | (rx/observable* (fn [^rx.Subscriber observer] 33 | (let [wrapped (-> #(rx/on-next % (apply f args)) 34 | rx/wrap-on-completed 35 | rx/wrap-on-error) 36 | fu (runner #(wrapped observer))] 37 | (.add observer 38 | (rx/subscription #(future-cancel fu))))))) 39 | 40 | (defn future-generator* 41 | "Exerimental/Possibly a bad idea 42 | 43 | Same as rx/generator* except f is invoked in a separate thread. 44 | 45 | runner is a function that takes a no-arg function argument and returns a future 46 | representing the execution of that function. 47 | 48 | Returns an Observable. If the subscriber unsubscribes, the future will be canceled 49 | with clojure.core/future-cancel 50 | 51 | Example: 52 | 53 | (future-generator* future-call 54 | (fn [o] 55 | (rx/on-next o 1) 56 | (Thread/sleep 1000) 57 | (rx/on-next o 2))) 58 | 59 | See: 60 | rx.lang.clojure.core/generator* 61 | " 62 | [runner f & args] 63 | {:pre [(ifn? runner) (ifn? f)]} 64 | (rx/observable* (fn [^rx.Subscriber observer] 65 | (let [wrapped (-> (fn [o] 66 | (apply f o args)) 67 | rx/wrap-on-completed 68 | rx/wrap-on-error) 69 | fu (runner #(wrapped observer))] 70 | (.add observer 71 | (rx/subscription #(future-cancel fu))))))) 72 | -------------------------------------------------------------------------------- /src/test/clojure/rx/lang/clojure/chunk_test.clj: -------------------------------------------------------------------------------- 1 | (ns rx.lang.clojure.chunk-test 2 | (:require [rx.lang.clojure.chunk :as rx-chunk] 3 | [rx.lang.clojure.core :as rx] 4 | [rx.lang.clojure.future :as rx-future] 5 | [rx.lang.clojure.blocking :as rx-blocking] 6 | [clojure.test :refer [deftest testing is]])) 7 | 8 | 9 | (deftest test-chunk 10 | (let [n 20 11 | chunk-size 10 12 | factory (rx-future/future-generator* 13 | future-call 14 | (fn[o] 15 | (doseq [i (range n)] 16 | (Thread/sleep (rand-int 50)) 17 | (rx/on-next o (rx-future/future* 18 | future-call 19 | #(let [t (rand-int 500)] 20 | (Thread/sleep t) 21 | i))))))] 22 | (is (= (range n) 23 | (sort (rx-blocking/into [] 24 | (rx-chunk/chunk chunk-size {:debug true} factory))))))) 25 | 26 | (deftest test-chunk-with-error 27 | (testing "error from source is propagated" 28 | (let [n 20 29 | chunk-size 4 30 | factory (rx-future/future-generator* 31 | future-call 32 | (fn [o] 33 | (doseq [i (range n)] 34 | (Thread/sleep (rand-int 50)) 35 | (rx/on-next o (rx-future/future* 36 | future-call 37 | #(let [t (rand-int 1000)] 38 | (Thread/sleep t) 39 | i)))) 40 | (throw (IllegalArgumentException. "hi"))))] 41 | (is (thrown-with-msg? IllegalArgumentException #"hi" 42 | (rx-blocking/into [] 43 | (rx-chunk/chunk chunk-size {:debug true} factory)))))) 44 | 45 | (testing "error from single observable is propagated" 46 | (let [n 20 47 | chunk-size 4 48 | factory (rx-future/future-generator* 49 | future-call 50 | (fn [o] 51 | (doseq [i (range n)] 52 | (Thread/sleep (rand-int 50)) 53 | (rx/on-next o (rx-future/future* 54 | future-call 55 | #(let [t (rand-int 1000)] 56 | (throw (IllegalArgumentException. "byebye")) 57 | (Thread/sleep t) 58 | i))))))] 59 | (is (thrown? rx.exceptions.CompositeException 60 | (rx-blocking/into [] 61 | (rx-chunk/chunk chunk-size 62 | {:debug true 63 | :delay-error? true } 64 | factory))))))) 65 | 66 | -------------------------------------------------------------------------------- /src/examples/clojure/rx/lang/clojure/examples/http_examples.clj: -------------------------------------------------------------------------------- 1 | ; 2 | ; Copyright 2013 Netflix, Inc. 3 | ; 4 | ; Licensed under the Apache License, Version 2.0 (the "License"); 5 | ; you may not use this file except in compliance with the License. 6 | ; You may obtain a copy of the License at 7 | ; 8 | ; http://www.apache.org/licenses/LICENSE-2.0 9 | ; 10 | ; Unless required by applicable law or agreed to in writing, software 11 | ; distributed under the License is distributed on an "AS IS" BASIS, 12 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ; See the License for the specific language governing permissions and 14 | ; limitations under the License. 15 | ; 16 | (ns rx.lang.clojure.examples.http-examples 17 | (:require [rx.lang.clojure.interop :as rx] 18 | [clj-http.client :as http]) 19 | (:import rx.Observable rx.subscriptions.Subscriptions)) 20 | 21 | ; NOTE on naming conventions. I'm using camelCase names (against clojure convention) 22 | ; in this file as I'm purposefully keeping functions and methods across 23 | ; different language implementations in-sync for easy comparison. 24 | 25 | (defn fetchWikipediaArticleAsynchronously [wikipediaArticleNames] 26 | "Fetch a list of Wikipedia articles asynchronously. 27 | 28 | return Observable of HTML" 29 | (Observable/create 30 | (rx/action [observer] 31 | (let [f (future 32 | (doseq [articleName wikipediaArticleNames] 33 | (-> observer (.onNext (http/get (str "http://en.wikipedia.org/wiki/" articleName))))) 34 | ; after sending response to onnext we complete the sequence 35 | (-> observer .onCompleted))] 36 | ; a subscription that cancels the future if unsubscribed 37 | (.add observer (Subscriptions/create (rx/action [] (future-cancel f)))))))) 38 | 39 | ; To see output 40 | (comment 41 | (-> (fetchWikipediaArticleAsynchronously ["Tiger" "Elephant"]) 42 | (.subscribe (rx/action [v] (println "--- Article ---\n" (subs (:body v) 0 125) "..."))))) 43 | 44 | 45 | ; -------------------------------------------------- 46 | ; Error Handling 47 | ; -------------------------------------------------- 48 | 49 | (defn fetchWikipediaArticleAsynchronouslyWithErrorHandling [wikipediaArticleNames] 50 | "Fetch a list of Wikipedia articles asynchronously 51 | with proper error handling. 52 | 53 | return Observable of HTML" 54 | (Observable/create 55 | (rx/action [observer] 56 | (let [f (future 57 | (try 58 | (doseq [articleName wikipediaArticleNames] 59 | (-> observer (.onNext (http/get (str "http://en.wikipedia.org/wiki/" articleName))))) 60 | ;(catch Exception e (prn "exception"))) 61 | (catch Exception e (-> observer (.onError e)))) 62 | ; after sending response to onNext we complete the sequence 63 | (-> observer .onCompleted))] 64 | ; a subscription that cancels the future if unsubscribed 65 | (.add observer (Subscriptions/create (rx/action [] (future-cancel f)))))))) 66 | 67 | ; To see output 68 | (comment 69 | (-> (fetchWikipediaArticleAsynchronouslyWithErrorHandling ["Tiger" "NonExistentTitle" "Elephant"]) 70 | (.subscribe (rx/action [v] (println "--- Article ---\n" (subs (:body v) 0 125) "...")) 71 | (rx/action [e] (println "--- Error ---\n" (.getMessage e)))))) 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/main/clojure/rx/lang/clojure/blocking.clj: -------------------------------------------------------------------------------- 1 | (ns rx.lang.clojure.blocking 2 | "Blocking operators and functions. These should never be used in 3 | production code except at the end of an async chain to convert from 4 | rx land back to sync land. For example, to produce a servlet response. 5 | 6 | If you use these, you're a bad person. 7 | " 8 | (:refer-clojure :exclude [first into doseq last]) 9 | (:require [rx.lang.clojure.interop :as iop] [rx.lang.clojure.core :as rx]) 10 | (:import [rx Observable] 11 | [rx.observables BlockingObservable])) 12 | 13 | (def ^:private -ns- *ns*) 14 | (set! *warn-on-reflection* true) 15 | 16 | (defmacro ^:private with-ex-unwrap 17 | "The blocking ops wrap errors stuff in RuntimeException because of stupid Java. 18 | This tries to unwrap them so callers get the exceptions they expect." 19 | [& body] 20 | `(try 21 | ~@body 22 | (catch RuntimeException e# 23 | (throw (or 24 | (and (identical? RuntimeException (class e#)) 25 | (.getCause e#)) 26 | e#))))) 27 | 28 | (defn ^BlockingObservable ->blocking 29 | "Convert an Observable to a BlockingObservable. 30 | 31 | If o is already a BlockingObservable it's returned unchanged. 32 | " 33 | [o] 34 | (if (instance? BlockingObservable o) 35 | o 36 | (BlockingObservable/from ^Observable o))) 37 | 38 | (defn o->seq 39 | "Returns a lazy sequence of the items emitted by o 40 | 41 | See: 42 | rx.observables.BlockingObservable/getIterator 43 | rx.lang.clojure.core/seq->o 44 | " 45 | [o] 46 | (-> (->blocking o) 47 | (.getIterator) 48 | (iterator-seq))) 49 | 50 | (defn first 51 | "*Blocks* and waits for the first value emitted by the given observable. 52 | 53 | If the Observable is empty, returns nil 54 | 55 | If an error is produced it is thrown. 56 | 57 | See: 58 | clojure.core/first 59 | rx/first 60 | rx.observables.BlockingObservable/first 61 | " 62 | [observable] 63 | (with-ex-unwrap 64 | (.firstOrDefault (->blocking observable) nil))) 65 | 66 | (defn last 67 | "*Blocks* and waits for the last value emitted by the given observable. 68 | 69 | If the Observable is empty, returns nil 70 | 71 | If an error is produced it is thrown. 72 | 73 | See: 74 | clojure.core/last 75 | rx/last 76 | rx.observable.BlockingObservable/last 77 | " 78 | [observable] 79 | (with-ex-unwrap 80 | (.lastOrDefault (->blocking observable) nil))) 81 | 82 | (defn single 83 | "*Blocks* and waits for the first value emitted by the given observable. 84 | 85 | An error is thrown if zero or more then one value is produced. 86 | " 87 | [observable] 88 | (with-ex-unwrap 89 | (.single (->blocking observable)))) 90 | 91 | (defn into 92 | "*Blocks* and pours the elements emitted by the given observables into 93 | to. 94 | 95 | If an error is produced it is thrown. 96 | 97 | See: 98 | clojure.core/into 99 | rx/into 100 | " 101 | [to from-observable] 102 | (with-ex-unwrap 103 | (clojure.core/into to (o->seq from-observable)))) 104 | 105 | (defn doseq* 106 | "*Blocks* and executes (f x) for each x emitted by xs 107 | 108 | Returns nil. 109 | 110 | See: 111 | doseq 112 | clojure.core/doseq 113 | " 114 | [xs f] 115 | (with-ex-unwrap 116 | (-> (->blocking xs) 117 | (.forEach (rx.lang.clojure.interop/action* f))))) 118 | 119 | (defmacro doseq 120 | "Like clojure.core/doseq except iterates over an observable in a blocking manner. 121 | 122 | Unlike clojure.core/doseq, only supports a single binding 123 | 124 | Returns nil. 125 | 126 | Example: 127 | 128 | (rx-blocking/doseq [{:keys [name]} users-observable] 129 | (println \"User:\" name)) 130 | 131 | See: 132 | doseq* 133 | clojure.core/doseq 134 | " 135 | [bindings & body] 136 | (when (not= (count bindings) 2) 137 | (throw (IllegalArgumentException. (str "sorry, rx/doseq only supports one binding")))) 138 | (let [[k v] bindings] 139 | `(doseq* ~v (fn [~k] ~@body)))) 140 | 141 | -------------------------------------------------------------------------------- /src/main/clojure/rx/lang/clojure/interop.clj: -------------------------------------------------------------------------------- 1 | (ns rx.lang.clojure.interop 2 | "Functions an macros for instantiating rx Func* and Action* interfaces." 3 | (:refer-clojure :exclude [fn])) 4 | 5 | (def ^:private -ns- *ns*) 6 | (set! *warn-on-reflection* true) 7 | 8 | (defmacro ^:private reify-callable 9 | "Reify a bunch of Callable-like interfaces 10 | 11 | prefix fully qualified interface name. numbers will be appended 12 | arities vector of desired arities 13 | f the function to execute 14 | 15 | " 16 | [prefix arities f] 17 | (let [f-name (gensym "rc")] 18 | `(let [~f-name ~f] 19 | (reify 20 | ; OnSubscribe is just an Action1, so add it to the list of implemented interfaces 21 | ; so an action can be used with Observable/create 22 | ~@(if (and (= prefix "rx.functions.Action") 23 | (some #{1} arities)) 24 | `(rx.Observable$OnSubscribe)) 25 | 26 | ~@(mapcat (clojure.core/fn [n] 27 | (let [ifc-sym (symbol (str prefix n)) 28 | arg-syms (map #(symbol (str "v" %)) (range n))] 29 | `(~ifc-sym 30 | (~'call ~(vec (cons 'this arg-syms)) 31 | ~(cons f-name arg-syms))))) 32 | arities) )))) 33 | 34 | (defn fn* 35 | "Given function f, returns an object that implements rx.functions.Func0-9 36 | by delegating the call() method to the given function. 37 | 38 | If the f has the wrong arity, an ArityException will be thrown at runtime. 39 | 40 | This will also implement rx.Observable$OnSubscribeFunc.onSubscribe for use with 41 | Observable/create. In this case, the function must take an Observable as its single 42 | argument and return a subscription object. 43 | 44 | Example: 45 | 46 | (.reduce my-numbers (rx/fn* +)) 47 | 48 | See: 49 | http://netflix.github.io/RxJava/javadoc/rx/functions/Func0.html 50 | " 51 | [f] 52 | (reify-callable "rx.functions.Func" [0 1 2 3 4 5 6 7 8 9] f)) 53 | 54 | (defn fnN* 55 | "Given function f, returns an object that implements rx.functions.FuncN 56 | by delegating to the given function. 57 | 58 | Unfortunately, this can't be included in fn* because of ambiguities between 59 | the single arg call() method and the var args call method. 60 | 61 | See: 62 | http://netflix.github.io/RxJava/javadoc/rx/functions/FuncN.html 63 | " 64 | [f] 65 | (reify rx.functions.FuncN 66 | (call [this objects] 67 | (apply f objects)))) 68 | 69 | (defmacro fn 70 | "Like clojure.core/fn, but returns the appropriate rx.functions.Func* 71 | interface. 72 | 73 | Example: 74 | 75 | (.map my-observable (rx/fn [a] (* 2 a))) 76 | 77 | or, to create an Observable: 78 | 79 | (Observable/create (rx/fn [observer] 80 | (.onNext observer 10) 81 | (.onCompleted observer) 82 | (Subscriptions/empty))) 83 | 84 | See: 85 | rx.lang.clojure.interop/fn* 86 | " 87 | [& fn-form] 88 | ; preserve metadata so type hints work 89 | ; have to qualify fn*. Otherwise bad things happen with the fn* special form in clojure 90 | (with-meta `(rx.lang.clojure.interop/fn* (clojure.core/fn ~@fn-form)) 91 | (meta &form))) 92 | 93 | (defn action* 94 | "Given function f, returns an object that implements rx.functions.Action0-3 95 | by delegating to the given function. Also implements rx.Observable$OnSubscribe which 96 | is just an Action1. 97 | 98 | Example: 99 | 100 | (.subscribe my-observable (rx/action* println)) 101 | 102 | See: 103 | http://netflix.github.io/RxJava/javadoc/rx/functions/Action0.html 104 | " 105 | [f] 106 | (reify-callable "rx.functions.Action" [0 1 2 3] f)) 107 | 108 | (defmacro action 109 | "Like clojure.core/fn, but returns the appropriate rx.functions.Action* 110 | interface. 111 | 112 | Example: 113 | 114 | (.finallyDo my-observable (rx/action [] (println \"Finally!\"))) 115 | 116 | " 117 | [& fn-form] 118 | ; preserve metadata so type hints work 119 | (with-meta `(action* (clojure.core/fn ~@fn-form)) 120 | (meta &form))) 121 | 122 | ;################################################################################ 123 | -------------------------------------------------------------------------------- /src/main/clojure/rx/lang/clojure/chunk.clj: -------------------------------------------------------------------------------- 1 | (ns rx.lang.clojure.chunk 2 | (:refer-clojure :exclude [chunk]) 3 | (:require [rx.lang.clojure.core :as rx])) 4 | 5 | (def ^:private -ns- *ns*) 6 | (set! *warn-on-reflection* true) 7 | 8 | (defn chunk 9 | "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION 10 | 11 | TODO RxJava's much bigger since this was written. Is there something built in? 12 | 13 | Same as rx.Observable.merge(Observable>) but the input Observables 14 | are \"chunked\" so that at most chunk-size of them are \"in flight\" at any given 15 | time. 16 | 17 | The order of the input Observables is not preserved. 18 | 19 | The main purpose here is to allow a large number of Hystrix observables to 20 | be processed in a controlled way so that the Hystrix execution queues aren't 21 | overwhelmed. 22 | 23 | Example: 24 | 25 | (->> users 26 | (rx/map #(-> (GetUserCommand. %) .toObservable)) 27 | (chunk 10)) 28 | 29 | See: 30 | http://netflix.github.io/RxJava/javadoc/rx/Observable.html#merge(rx.Observable) 31 | http://netflix.github.io/RxJava/javadoc/rx/Observable.html#mergeDelayError(rx.Observable) 32 | " 33 | ([chunk-size observable-source] (chunk chunk-size {} observable-source)) 34 | ([chunk-size options observable-source] 35 | (let [new-state-atom #(atom {:in-flight #{} ; observables currently in-flight 36 | :buffered [] ; observables waiting to be emitted 37 | :complete false ; true if observable-source is complete 38 | :observer % }) ; the observer 39 | ps #(do (printf "%s/%d/%d%n" 40 | (:complete %) 41 | (-> % :buffered count) 42 | (-> % :in-flight count)) 43 | (flush)) 44 | 45 | ; Given the current state, returns [action new-state]. action is the 46 | ; next Observable or Throwable to emit, or :complete if we're done. 47 | next-state (fn [{:keys [complete buffered in-flight] :as old}] 48 | (cond 49 | (empty? buffered) [complete old] 50 | 51 | (< (count in-flight) chunk-size) (let [next-o (first buffered)] 52 | [next-o 53 | (-> old 54 | (update-in [:buffered] next) 55 | (update-in [:in-flight] conj next-o))]) 56 | 57 | :else [nil old])) 58 | 59 | ; Advance the state, performing side-effects as necessary 60 | advance! (fn advance! [state-atom] 61 | (let [old-state @state-atom 62 | [action new-state] (next-state old-state)] 63 | (if (compare-and-set! state-atom old-state new-state) 64 | (let [observer (:observer new-state)] 65 | (if (:debug options) (ps new-state)) 66 | (cond 67 | (= :complete action) 68 | (rx/on-completed observer) 69 | 70 | (instance? Throwable action) 71 | (rx/on-error observer action) 72 | 73 | (instance? rx.Observable action) 74 | (rx/on-next observer 75 | (.finallyDo ^rx.Observable action 76 | (reify rx.functions.Action0 77 | (call [this] 78 | (swap! state-atom update-in [:in-flight] disj action) 79 | (advance! state-atom))))))) 80 | (recur state-atom)))) 81 | 82 | subscribe (fn [state-atom] 83 | (rx/subscribe observable-source 84 | (fn [o] 85 | (swap! state-atom update-in [:buffered] conj o) 86 | (advance! state-atom)) 87 | 88 | (fn [e] 89 | (swap! state-atom assoc :complete e) 90 | (advance! state-atom)) 91 | 92 | (fn [] 93 | (swap! state-atom assoc :complete :complete) 94 | (advance! state-atom)))) 95 | observable (rx/observable* (fn [observer] 96 | (subscribe (new-state-atom observer)))) ] 97 | (if (:delay-error? options) 98 | (rx.Observable/mergeDelayError observable) 99 | (rx.Observable/merge observable))))) 100 | 101 | -------------------------------------------------------------------------------- /src/main/clojure/rx/lang/clojure/realized.clj: -------------------------------------------------------------------------------- 1 | (ns rx.lang.clojure.realized 2 | (:require [rx.lang.clojure.interop :as iop])) 3 | 4 | (def ^:private -ns- *ns*) 5 | (set! *warn-on-reflection* true) 6 | 7 | (defrecord ^:private PostProc [o f]) 8 | 9 | (defn all 10 | "Tell realized map to capture all output of the observable, not just the last one" 11 | [o] 12 | (->PostProc o identity)) 13 | 14 | (defn only 15 | "Tell realized map to capture the only value emitted by the observable. 16 | If there are 0 or more than one values, an IllegalStateException is thrown 17 | which should propagate to onError. 18 | 19 | This is the default mode of realized-map and let-realized. 20 | " 21 | [o] 22 | (->PostProc o (fn [values] 23 | (condp = (count values) 24 | 1 (first values) 25 | (throw (IllegalStateException. "Observable did not produce exactly one value")))))) 26 | 27 | (defn ^:private ->post-proc 28 | [v] 29 | (cond 30 | (instance? rx.Observable v) (only v) 31 | (instance? PostProc v) v 32 | (vector? v) (->PostProc (first v) 33 | (apply comp (reverse (next v)))) 34 | :else (->post-proc (rx.Observable/just v)))) 35 | 36 | (defn realized-map 37 | "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION 38 | 39 | See let-realized. 40 | 41 | Given a map from key to observable, returns an observable that emits a single 42 | map from the same keys to the values emitted by their corresponding observable. 43 | 44 | keyvals is a list of key/value pairs where key is a key in the emitted map and val 45 | can be one of the following: 46 | 47 | rx.Observable The only value of the emitted sequence is bound to the key. This is the 48 | default since this is often a singleton response from a request. If the 49 | Observable produces 0 or more than 1 values, an IllegalStateException is 50 | produced. 51 | 52 | vector The first element of the vector must be an Observable. Remaining elements 53 | are functions applied in sequence to the list of values emitted by the 54 | observable. For example [my-observable first] will result in a single 55 | value in the emitted map rather than a vector of values. 56 | 57 | other The value is placed in the emitted map as is 58 | 59 | Note the observable can also be wrapped with realized/all to get the full list rather than 60 | just the last value. 61 | 62 | The purpose of this is to simplify the messy pattern of mapping observables to 63 | single key maps, merging and then folding all the separate maps together. So code 64 | like this: 65 | 66 | ; TODO update 67 | (->> (rx/merge (->> (user-info-o user-id) 68 | (rx/map (fn [u] {:user u}))) 69 | (->> (user-likes-o user-id) 70 | (rx/map (fn [u] {:likes u})))) 71 | (rx/reduce merge {})) 72 | 73 | becomes: 74 | 75 | (realized-map :user (user-info-o user-id) 76 | :likes (user-likes-o user-id)) 77 | 78 | See: 79 | let-realized 80 | " 81 | [& keyvals] 82 | (let [o (->> keyvals 83 | (partition 2) 84 | ; generate a sequence of observables 85 | (map (fn [[k v]] 86 | (let [{:keys [^rx.Observable o f]} (->post-proc v)] 87 | ; pour the observable into a single list and apply post-proc func to it 88 | (-> o 89 | .toList 90 | (.map (iop/fn [list] {k (f list)})))))))] 91 | 92 | (-> ^Iterable o 93 | (rx.Observable/merge) ; funnel all the observables into a single sequence 94 | (.reduce {} (iop/fn* merge))))) ; do the map merge dance 95 | 96 | (defn ^rx.Observable realized-map* 97 | "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION 98 | 99 | Same as realized-map, but takes a map argument rather than key-value pairs." 100 | [map-description] 101 | (apply realized-map (apply concat map-description))) 102 | 103 | (defmacro let-realized 104 | "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION 105 | 106 | 'let' version of realized map. 107 | 108 | (let-realized [a (make-observable)] 109 | (* 2 a)) 110 | 111 | is equivalent to: 112 | 113 | (->> (realized-map :a (make-observable)) 114 | (map (fn [{:keys [a]}] (* 2 a)))) 115 | 116 | That is, it eliminates the repition of the map keys when you want to do something 117 | with the final result. 118 | 119 | Evaluates to an Observable that emits the value of the let body. 120 | 121 | See: 122 | rx.lang.clojure.realized/realized-map 123 | rx.lang.clojure.realized/all 124 | " 125 | [bindings & body] 126 | (let [b-parts (partition 2 bindings) 127 | b-map (->> b-parts 128 | (map (fn [[k v]] 129 | [(keyword (name k)) v])) 130 | (into {})) 131 | b-names (mapv first b-parts)] 132 | `(.map (realized-map* ~b-map) 133 | (iop/fn [{:keys ~b-names}] ~@body)))) 134 | 135 | -------------------------------------------------------------------------------- /src/test/clojure/rx/lang/clojure/graph_test.clj: -------------------------------------------------------------------------------- 1 | (ns rx.lang.clojure.graph-test 2 | (:require [rx.lang.clojure.graph :as graph] 3 | [rx.lang.clojure.core :as rx] 4 | [rx.lang.clojure.future :as rx-future] 5 | [rx.lang.clojure.blocking :as rx-blocking] 6 | [clojure.test :refer [deftest testing is]])) 7 | 8 | (deftest test-let-o* 9 | (testing "throws on cycle" 10 | (is (thrown-with-msg? IllegalArgumentException #"Cycle found" 11 | (graph/let-o* {:a {:deps [:a]}})))) 12 | 13 | (testing "throws on unknown" 14 | (is (thrown-with-msg? IllegalArgumentException #"Unknown node" 15 | (graph/let-o* {:a {:deps [:b]}})))) 16 | 17 | (testing "it works in a simple case" 18 | (let [d {:a {:deps [] 19 | :factory (fn [_] (rx/seq->o [1 2 3 4 5]))} 20 | :b {:deps [:a] 21 | :factory (fn [{:keys [a]}] (rx/map #(* % %) a)) } 22 | :c {:deps [:a :b] 23 | :factory (fn [{:keys [a b]}] (rx/map #(+ %1 %2) a b)) } 24 | :d {:deps [:c :b] 25 | :factory (fn [{:keys [c b]}] (rx/map #(+ %1 %2) c b)) } 26 | } 27 | f (graph/let-o* d) ] 28 | (println f) 29 | ; (n^2 + n) + n^2 30 | (is (= [3 10 21 36 55] 31 | (rx-blocking/into [] (:d f))))))) 32 | 33 | (deftest test-let-o 34 | (testing "it works" 35 | (let [f (graph/let-o [?a (rx/seq->o [1 2 3]) 36 | ?b (rx/seq->o [4 5 6])] 37 | (rx/map + ?a ?b))] 38 | (is (= [5 7 9] 39 | (rx-blocking/into [] f))))) 40 | 41 | (testing "it still works" 42 | (is (= {:a 99 :b 100 :z "hi"} 43 | (rx-blocking/single 44 | (-> (let [z (rx/return "hi")] ; an observable from "somewhere else" 45 | (graph/let-o 46 | [?a (rx-future/future* future-call #(do (Thread/sleep 50) 99)) 47 | ?b (rx-future/future* future-call #(do (Thread/sleep 500) 100)) 48 | ?c (rx/map #(hash-map :a %1 :b %2 :z %3) ?a ?b ?z) 49 | ?z z] 50 | (rx/reduce merge {} ?c))))))))) 51 | 52 | (deftest test-complicated-graph 53 | ; These funcs model network requests for various stuff. They all return observable. 54 | (let [request-vhs (fn [] 55 | (rx-future/future-generator* 56 | future-call 57 | (fn [o] 58 | (Thread/sleep 50) 59 | (doseq [i (range 3)] 60 | (rx/on-next o {:id i}))))) 61 | request-user (fn [id] 62 | (rx-future/future* 63 | future-call 64 | #(do (Thread/sleep (rand-int 250)) 65 | {:id id 66 | :name (str "friend" id) }))) 67 | request-ab (fn [u] 68 | (rx-future/future* 69 | future-call 70 | #(do (Thread/sleep (rand-int 250)) 71 | {:user-id (:id u) 72 | :cell (* 2 (:id u))}))) 73 | 74 | request-video-md (fn [v] 75 | (rx/return {:video v 76 | :title (str "title" (:id v)) })) 77 | 78 | ; Now we can stitch all these requests together into an rx graph to 79 | ; produce a response. 80 | o (graph/let-o [?user-info (rx-future/future* 81 | future-call 82 | #(do (Thread/sleep 20) 83 | {:name "Bob" 84 | :id 12345 85 | :friend-ids [1 2 3] })) 86 | 87 | ?friends (->> ?user-info 88 | (rx/mapcat (fn [ui] 89 | (rx/mapcat request-user 90 | (rx/seq->o (:friend-ids ui)))))) 91 | 92 | ?ab (->> (rx/concat ?user-info ?friends) 93 | (rx/mapcat request-ab)) 94 | 95 | ?ab-lookup (->> ?ab 96 | (rx/map (juxt :user-id #(dissoc % :user-id))) 97 | (rx/into {})) 98 | 99 | ?vhs (request-vhs) 100 | 101 | 102 | ?metadata (->> ?vhs 103 | (rx/mapcat request-video-md))] 104 | (rx/map (fn [u m f ab-lookup] 105 | {:user (dissoc u :friend-ids) 106 | :videos m 107 | :friends (sort-by :id f) 108 | :ab ab-lookup}) 109 | ?user-info 110 | (rx/into [] ?metadata) 111 | (rx/into [] ?friends) 112 | ?ab-lookup))] 113 | 114 | (is (= {:user {:name "Bob" :id 12345} 115 | :videos [{:video {:id 0} :title "title0"} 116 | {:video {:id 1} :title "title1"} 117 | {:video {:id 2} :title "title2"}] 118 | :friends [{:name "friend1" :id 1}{:name "friend2" :id 2}{:name "friend3" :id 3}] 119 | :ab {12345 {:cell 24690} 1 {:cell 2} 2 {:cell 4} 3 {:cell 6}} } 120 | (rx-blocking/single o))))) 121 | 122 | 123 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /src/test/clojure/rx/lang/clojure/interop_test.clj: -------------------------------------------------------------------------------- 1 | (ns rx.lang.clojure.interop-test 2 | (:require [rx.lang.clojure.interop :as rx] 3 | [clojure.test :refer [deftest testing is]]) 4 | (:import [rx Observable] 5 | [rx.observables BlockingObservable] 6 | [rx.lang.clojure.interop DummyObservable])) 7 | 8 | (deftest test-fn* 9 | (testing "implements Func0-9" 10 | (let [f (rx/fn* vector)] 11 | (is (instance? rx.functions.Func0 f)) 12 | (is (instance? rx.functions.Func1 f)) 13 | (is (instance? rx.functions.Func2 f)) 14 | (is (instance? rx.functions.Func3 f)) 15 | (is (instance? rx.functions.Func4 f)) 16 | (is (instance? rx.functions.Func5 f)) 17 | (is (instance? rx.functions.Func6 f)) 18 | (is (instance? rx.functions.Func7 f)) 19 | (is (instance? rx.functions.Func8 f)) 20 | (is (instance? rx.functions.Func9 f)) 21 | (is (= [] (.call f))) 22 | (is (= [1] (.call f 1))) 23 | (is (= [1 2] (.call f 1 2))) 24 | (is (= [1 2 3] (.call f 1 2 3))) 25 | (is (= [1 2 3 4] (.call f 1 2 3 4))) 26 | (is (= [1 2 3 4 5] (.call f 1 2 3 4 5))) 27 | (is (= [1 2 3 4 5 6] (.call f 1 2 3 4 5 6))) 28 | (is (= [1 2 3 4 5 6 7] (.call f 1 2 3 4 5 6 7))) 29 | (is (= [1 2 3 4 5 6 7 8] (.call f 1 2 3 4 5 6 7 8))) 30 | (is (= [1 2 3 4 5 6 7 8 9] (.call f 1 2 3 4 5 6 7 8 9))))) 31 | 32 | (let [dummy (DummyObservable.)] 33 | (testing "preserves metadata applied to form" 34 | ; No type hint, picks Object overload 35 | (is (= "Object" 36 | (.call dummy (rx/fn* +)))) 37 | (is (= "rx.functions.Func1" 38 | (.call dummy 39 | ^rx.functions.Func1 (rx/fn* +)))) 40 | (is (= "rx.functions.Func2" 41 | (.call dummy 42 | ^rx.functions.Func2 (rx/fn* *))))))) 43 | 44 | (deftest test-fn 45 | (testing "makes appropriate Func*" 46 | (let [f (rx/fn [a b c] (println "test-fn") (+ a b c))] 47 | (is (= 6 (.call f 1 2 3))))) 48 | 49 | (let [dummy (DummyObservable.)] 50 | (testing "preserves metadata applied to form" 51 | ; No type hint, picks Object overload 52 | (is (= "Object" 53 | (.call dummy 54 | (rx/fn [a] a)))) 55 | (is (= "rx.functions.Func1" 56 | (.call dummy 57 | ^rx.functions.Func1 (rx/fn [a] a)))) 58 | (is (= "rx.functions.Func2" 59 | (.call dummy 60 | ^rx.functions.Func2 (rx/fn [a b] (* a b)))))))) 61 | 62 | 63 | (deftest test-fnN* 64 | (testing "implements FuncN" 65 | (is (= (vec (range 99)) 66 | (.call (rx/fnN* vector) (into-array Object (range 99))))))) 67 | 68 | (deftest test-action* 69 | (testing "implements Action0-3" 70 | (let [calls (atom []) 71 | a (rx/action* #(swap! calls conj (vec %&)))] 72 | (is (instance? rx.Observable$OnSubscribe a)) 73 | (is (instance? rx.functions.Action0 a)) 74 | (is (instance? rx.functions.Action1 a)) 75 | (is (instance? rx.functions.Action2 a)) 76 | (is (instance? rx.functions.Action3 a)) 77 | (.call a) 78 | (.call a 1) 79 | (.call a 1 2) 80 | (.call a 1 2 3) 81 | (is (= [[] [1] [1 2] [1 2 3]])))) 82 | (let [dummy (DummyObservable.)] 83 | (testing "preserves metadata applied to form" 84 | ; no meta, picks Object overload 85 | (is (= "Object" 86 | (.call dummy 87 | (rx/action* println)))) 88 | (is (= "rx.functions.Action1" 89 | (.call dummy 90 | ^rx.functions.Action1 (rx/action* println)))) 91 | (is (= "rx.functions.Action2" 92 | (.call dummy 93 | ^rx.functions.Action2 (rx/action* prn))))))) 94 | 95 | (deftest test-action 96 | (testing "makes appropriate Action*" 97 | (let [called (atom nil) 98 | a (rx/action [a b] (reset! called [a b]))] 99 | (.call a 9 10) 100 | (is (= [9 10] @called)))) 101 | 102 | (let [dummy (DummyObservable.)] 103 | (testing "preserves metadata applied to form" 104 | ; no meta, picks Object overload 105 | (is (= "Object" 106 | (.call dummy 107 | (rx/action [a] a)))) 108 | (is (= "rx.functions.Action1" 109 | (.call dummy 110 | ^rx.functions.Action1 (rx/action [a] a)))) 111 | (is (= "rx.functions.Action2" 112 | (.call dummy 113 | ^rx.functions.Action2 (rx/action [a b] (* a b)))))))) 114 | 115 | (deftest test-basic-usage 116 | 117 | (testing "can create an observable with new-style action" 118 | (is (= 99 119 | (-> (Observable/create (rx/action [^rx.Subscriber s] 120 | (when-not (.isUnsubscribed s) 121 | (.onNext s 99)) 122 | (.onCompleted s))) 123 | BlockingObservable/from 124 | .single)))) 125 | (testing "can pass rx/fn to map and friends" 126 | (is (= (+ 1 4 9) 127 | (-> (Observable/from [1 2 3]) 128 | (.map (rx/fn [v] (* v v))) 129 | (.reduce (rx/fn* +)) 130 | BlockingObservable/from 131 | .single)))) 132 | 133 | (testing "can pass rx/action to subscribe and friends" 134 | (let [finally-called (atom nil) 135 | complete-called (promise) 136 | result (atom []) 137 | o (-> (Observable/from ["4" "5" "6"]) 138 | (.map (rx/fn* #(Long/parseLong %))) 139 | (.finallyDo (rx/action [] 140 | (reset! finally-called true))) 141 | (.reduce (rx/fn [a v] (* a v))) 142 | (.subscribe (rx/action [v] (swap! result conj v)) 143 | (rx/action [e]) 144 | (rx/action [] (deliver complete-called true)))) ] 145 | (is (= true @complete-called)) 146 | (is (= true @finally-called)) 147 | (is (= [(* 4 5 6)] @result))))) 148 | 149 | ;################################################################################ 150 | -------------------------------------------------------------------------------- /src/test/clojure/rx/lang/clojure/realized_test.clj: -------------------------------------------------------------------------------- 1 | (ns rx.lang.clojure.realized-test 2 | (:require [rx.lang.clojure.realized :as r] 3 | [rx.lang.clojure.core :as rx] 4 | [rx.lang.clojure.future :as rx-future] 5 | [rx.lang.clojure.blocking :as rx-blocking] 6 | [clojure.test :refer [deftest testing is]])) 7 | 8 | 9 | 10 | (deftest test-realized-map 11 | (testing "Turns map of observables into observable of map" 12 | (let [o (r/realized-map :a (r/all (rx/seq->o [1 2 3])) 13 | :a2 (rx/seq->o [99 100 101]) 14 | :b (rx/return "hi") 15 | :c [(->> [1 2 3] 16 | rx/seq->o 17 | (rx/map #(* % %))) 18 | next] 19 | :d (rx/return "just one") 20 | :e "just a value") 21 | result (rx-blocking/single o)] 22 | (is (= {:a [1 2 3] 23 | :a2 101 24 | :b "hi" 25 | :c [4 9] 26 | :d "just one" 27 | :e "just a value" } 28 | result))))) 29 | 30 | (deftest test-realized-map 31 | (testing "works like realized-map, but takes a map instead of key/value pairs" 32 | (is (= {:a [1 2] 33 | :b 500 } 34 | (->> {:a (r/all (rx/seq->o [1 2])) 35 | :b 500 } 36 | r/realized-map* 37 | rx-blocking/single))))) 38 | 39 | (deftest test-let-realized 40 | (is (= {:a* 2 41 | :b* 500 42 | :c* 1000 } 43 | (->> (r/let-realized [a [(rx/seq->o [1 2]) last] 44 | b 500 45 | c (rx/return 1000) ] 46 | {:a* a 47 | :b* b 48 | :c* c }) 49 | rx-blocking/single)))) 50 | 51 | (deftest test-only 52 | (testing "raises IllegalStateException if sequence is empty" 53 | (is (thrown-with-msg? IllegalStateException #"did not produce" 54 | (->> (r/let-realized [a (rx/seq->o [1 2])] 55 | {:a a}) 56 | rx-blocking/single))) 57 | ; Just to be sure, make sure it goes through onError. 58 | (let [values (atom []) 59 | errors (atom [])] 60 | (rx/subscribe (r/let-realized [a (rx/seq->o [1 2])] 61 | {:a a}) 62 | #(swap! values conj %) 63 | #(swap! errors conj %)) 64 | (is (empty? @values)) 65 | (is (= 1 (count @errors))) 66 | (let [[e] @errors] 67 | (is (instance? IllegalStateException e)))))) 68 | 69 | (deftest test-all 70 | (testing "collects all values from an observable" 71 | (is (= [1 2 3] 72 | (->> (r/let-realized [a (r/all (rx/seq->o [1 2 3]))] 73 | a) 74 | rx-blocking/single))))) 75 | 76 | ; Playing with some expressing some of the video stuff with this. 77 | (comment 78 | (->> (get-list-of-lists user-id) 79 | (rx/mapcat (fn [list] 80 | (->> (video-list->videos list) 81 | (rx/take 10)))) 82 | (rx/mapcat (fn [video] 83 | (->> (r/let-realized [md (video->metadata video) 84 | bm (video->bookmark video) 85 | rt (video->rating video user-id)] 86 | {:id (:id video) 87 | :title (:title md) 88 | :length (:duration md) 89 | :bookmark bm 90 | :rating {:actual (:actual-star-rating rt) 91 | :average (:average-star-rating rt) 92 | :predicted (:predicted-star-rating rt) } }))))) 93 | 94 | (->> (get-list-of-lists user-id) 95 | (rx/mapcat (fn [list] 96 | (->> (video-list->videos list) 97 | (rx/take 10)))) 98 | (rx/mapcat (fn [video] 99 | (->> (r/realized-map :md (video->metadata video) 100 | :bm (video->bookmark video) 101 | :rt (video->rating video user-id)) 102 | (rx/map (fn [{:keys [md bm rt]}] 103 | {:id (:id video) 104 | :title (:title md) 105 | :length (:duration md) 106 | :bookmark bm 107 | :rating {:actual (:actual-star-rating rt) 108 | :average (:average-star-rating rt) 109 | :predicted (:predicted-star-rating rt) } })))))) 110 | 111 | (->> (get-list-of-lists user-id) 112 | (rx/mapcat (fn [list] 113 | (->> (video-list->videos list) 114 | (rx/take 10)))) 115 | (rx/mapcat (fn [video] 116 | (->> (r/realized-map :id (:id video) 117 | :md [(video->metadata video) 118 | first 119 | #(select-keys % [:title :duration])] 120 | :bookmark (video->bookmark video) 121 | :rating [(video->rating video user-id) 122 | first 123 | #(hash-map :actual (:actual-star-rating %) 124 | :average (:average-star-rating %) 125 | :predicted (:predicted-star-rating %))]) 126 | (rx/map (fn [m] 127 | (-> m 128 | (merge (:md m)) 129 | (dissoc :md))))))))) 130 | 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Clojure bindings for RxJava. 2 | 3 | # Binaries 4 | 5 | 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). 6 | 7 | Example for Leiningen: 8 | 9 | ```clojure 10 | [io.reactivex/rxclojure "x.y.z"] 11 | ``` 12 | 13 | and for Gradle: 14 | 15 | ```groovy 16 | compile 'io.reactivex:rxclojure:x.y.z' 17 | ``` 18 | 19 | and for Maven: 20 | 21 | ```xml 22 | 23 | io.reactivex 24 | rxclojure 25 | x.y.z 26 | 27 | ``` 28 | 29 | and for Ivy: 30 | 31 | ```xml 32 | 33 | ``` 34 | 35 | # Clojure Bindings 36 | This library provides convenient, idiomatic Clojure bindings for RxJava. 37 | 38 | 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: 39 | 40 | * 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 41 | * Operators take normal Clojure functions as arguments, bypassing need for the interop described below 42 | * Predicates accomodate Clojure's notion of truth 43 | * Operators are generally names as they would be in `clojure.core` rather than the Rx names 44 | 45 | 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. 46 | 47 | ## Basic Usage 48 | Most functionality resides in the `rx.lang.clojure.core` namespace and for the most part looks like normal Clojure sequence manipulation: 49 | 50 | ```clojure 51 | (require '[rx.lang.clojure.core :as rx]) 52 | 53 | (->> my-observable 54 | (rx/map (comp clojure.string/lower-case :first-name)) 55 | (rx/map clojure.string/lower-case) 56 | (rx/filter #{"bob"}) 57 | (rx/distinct) 58 | (rx/into [])) 59 | ;=> An Observable that emits a single vector of names 60 | ``` 61 | 62 | Blocking operators, which are useful for testing, but should otherwise be avoided, reside in `rx.lang.clojure.blocking`. For example: 63 | 64 | ```clojure 65 | (require '[rx.lang.clojure.blocking :as rxb]) 66 | 67 | (rxb/doseq [{:keys [first-name]} users-observable] 68 | (println "Hey," first-name)) 69 | ;=> nil 70 | ``` 71 | 72 | ## Open Issues 73 | 74 | * The missing stuff mentioned below 75 | * `group-by` val-fn variant isn't implemented in RxJava 76 | * 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. 77 | 78 | ## What's Missing 79 | 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: 80 | 81 | * 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()`. 82 | * Most everything involving schedulers 83 | * Most everything involving time 84 | * `Observable.window` and `Observable.buffer`. Who knows which parts of these beasts to wrap? 85 | 86 | Of course, contributions that cover these cases are welcome. 87 | 88 | # Low-level Interop 89 | 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. 90 | 91 | ## Basic Usage 92 | 93 | ### Requiring the interop namespace 94 | The first thing to do is to require the namespace: 95 | 96 | ```clojure 97 | (ns my.namespace 98 | (:require [rx.lang.clojure.interop :as rx]) 99 | (:import [rx Observable])) 100 | ``` 101 | 102 | or, at the REPL: 103 | 104 | ```clojure 105 | (require '[rx.lang.clojure.interop :as rx]) 106 | ``` 107 | 108 | ### Using rx/fn 109 | 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`: 110 | 111 | ```clojure 112 | (-> my-observable 113 | (.map (rx/fn [v] (* 2 v)))) 114 | ``` 115 | 116 | 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`: 117 | 118 | ```clojure 119 | (-> my-numbers 120 | (.reduce (rx/fn* +))) 121 | ``` 122 | 123 | ### Using rx/action 124 | 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: 125 | 126 | ```clojure 127 | (-> my-observable 128 | (.map (rx/fn* transform-data)) 129 | (.finallyDo (rx/action [] (println "Finished transform"))) 130 | (.subscribe (rx/action [v] (println "Got value" v)) 131 | (rx/action [e] (println "Get error" e)) 132 | (rx/action [] (println "Sequence complete")))) 133 | ``` 134 | 135 | ### Using Observable/create 136 | 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: 137 | 138 | ```clojure 139 | ; A simple observable that emits 0..9 taking unsubscribe into account 140 | (Observable/create (rx/action [^rx.Subscriber s] 141 | (loop [i 0] 142 | (when (and (< i 10) (.isUnsubscribed s)) 143 | (.onNext s i) 144 | (recur (inc i)))) 145 | (.onCompleted s))) 146 | ``` 147 | 148 | ## Gotchas 149 | Here are a few things to keep in mind when using this interop: 150 | 151 | * Keep in mind the (mostly empty) distinction between `Func` and `Action` and which is used in which contexts 152 | * 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. 153 | * 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`. 154 | 155 | -------------------------------------------------------------------------------- /src/main/clojure/rx/lang/clojure/graph.clj: -------------------------------------------------------------------------------- 1 | (ns rx.lang.clojure.graph 2 | "This is an implementation namespace. Don't use it directly. Use the symbols 3 | in rx.lang.clojure.core 4 | " 5 | (:require [clojure.set :as set])) 6 | 7 | (def ^:private -ns- *ns*) 8 | (set! *warn-on-reflection* true) 9 | 10 | (defn ^:private ->let-o*-observable 11 | [^rx.Observable o n name] 12 | (if (= n 1) 13 | o 14 | ; TODO This is a shortcut. We know the expected number of subscriptions so 15 | ; we only need to cache values until we get the nth subscription at which 16 | ; point, it just becomes a pass through. I haven't found a cache/replay-ish 17 | ; operator that gives this level of control over the cached values 18 | (.cache o))) 19 | 20 | (defn let-o* 21 | "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION 22 | 23 | Given a graph description, returns an observable that emits a single 24 | map of observables all hooked up and ready for subscription. 25 | 26 | A graph is a map from name to a map with keys: 27 | 28 | :deps A vector of dependency names 29 | :factory A function that takes a map from name to Observable 30 | for the names in :deps and returns an Observable 31 | 32 | Returns a map from name to Observable. Additionally, there will be a 33 | ::non-terminals key in the map with a vector of non-terminal names. 34 | 35 | See: 36 | let-o 37 | " 38 | [description] 39 | (let [in-dep-counts (->> description 40 | vals 41 | (mapcat :deps) 42 | frequencies) 43 | terminals (set/difference (set (keys description)) (set (keys in-dep-counts))) 44 | non-terminals (vec (keys in-dep-counts)) 45 | 46 | resolve-node (fn resolve-node [state {:keys [id deps factory] :as node}] 47 | (let [existing (state id)] 48 | (cond 49 | ; It's already resolving up the stack. We've hit a cycle. 50 | (= ::resolving existing) (throw (IllegalArgumentException. (format "Cycle found at '%s'" id))) 51 | 52 | ; It's already resolved. Done. 53 | (not (nil? existing)) state 54 | 55 | :else 56 | ; recursively resolve dependencies 57 | (let [new-state (reduce (fn [s dep] 58 | (if-let [dep-node (description dep)] 59 | (resolve-node s (assoc dep-node :id dep)) 60 | (throw (IllegalArgumentException. (format "Unknown node '%s' referenced from '%s'" dep id))))) 61 | (assoc state id ::resolving) 62 | deps) 63 | ; execute the factory function and wrap it in an observable that delays dependencies 64 | o (-> (select-keys new-state deps) 65 | factory 66 | (->let-o*-observable (in-dep-counts id 1) id))] 67 | ; return the updated state with the resolved node 68 | (assoc new-state id o)))))] 69 | ; resolve the graph and build the result map 70 | (-> (reduce (fn [s [id node]] 71 | (resolve-node s (assoc node :id id))) 72 | {} 73 | description) 74 | (select-keys terminals) 75 | (assoc ::non-terminals non-terminals)))) 76 | 77 | (defmacro let-o 78 | "EXTREMELY EXPERIMENTAL AND SUBJECT TO CHANGE OR DELETION 79 | 80 | Similar to clojure.core/let, but bindings are Observables and the result of the body 81 | must be an Observable. Binding names must start with ?. Binding order doesn't matter 82 | and any binding is visible to all other expressions as long as no cycles are produced 83 | in the resulting Observable expression. 84 | 85 | The key difference here is that the macro can identify the dependencies between Observables 86 | and correctly connect them, protecting from variations in subscribe behavior as well as 87 | the idiosyncracies of setting up multiple subscriptions to Observables. 88 | 89 | This is only very useful for constructing graphs of Observables where you'd usually have 90 | to fiddle around with publish, connect, replay and all that stuff. If you have a linear 91 | sequence of operators, just chain them together. 92 | 93 | Current limitations: 94 | 95 | * All Observables are cache()'d so watch out for large sequences. This will be 96 | fixed eventually. 97 | * let-o cannot be nested. Some deep-walking macro-magic will be required for this. 98 | 99 | Example: 100 | 101 | ; Note that both ?c and ?d depend on ?b and the result Observable depends on 102 | ; ?c and ?d. 103 | (let-o [?a (rx/return 99) 104 | ?b (... some observable network request ...) 105 | ?c (rx/map vector ?a ?b) 106 | ?d (rx/map ... ?b)] 107 | (rx/map vector ?c ?d)) 108 | 109 | See: 110 | let-o* 111 | " 112 | [bindings & result-body] 113 | (let [sym->dep-sym (fn [s] 114 | (when (and (symbol? s) 115 | (not (namespace s)) 116 | (.startsWith (name s) "?")) 117 | s)) 118 | body->dep-syms (fn [body] 119 | (->> body 120 | flatten 121 | (keep sym->dep-sym) 122 | distinct 123 | vec)) 124 | ->node-map (fn [[id & body]] 125 | (let [dep-syms (body->dep-syms body) 126 | dep-keys (->> dep-syms (map (comp keyword name)) vec)] 127 | [(keyword (name id)) {:deps dep-keys 128 | :factory `(fn [{:keys ~dep-syms}] ~@body) }])) 129 | node-map (let [base-map (->> bindings 130 | (partition 2) 131 | (map ->node-map) 132 | (into {})) 133 | result-dep-syms (body->dep-syms result-body)] 134 | (assoc base-map 135 | :rx.lang.clojure.core/result 136 | {:deps (mapv keyword result-dep-syms) 137 | :factory `(fn [{:keys ~result-dep-syms}] ~@result-body) }))] 138 | `(->> ~node-map 139 | let-o* 140 | :rx.lang.clojure.core/result))) 141 | 142 | -------------------------------------------------------------------------------- /src/examples/clojure/rx/lang/clojure/examples/video_example.clj: -------------------------------------------------------------------------------- 1 | ; 2 | ; Copyright 2013 Netflix, Inc. 3 | ; 4 | ; Licensed under the Apache License, Version 2.0 (the "License"); 5 | ; you may not use this file except in compliance with the License. 6 | ; You may obtain a copy of the License at 7 | ; 8 | ; http://www.apache.org/licenses/LICENSE-2.0 9 | ; 10 | ; Unless required by applicable law or agreed to in writing, software 11 | ; distributed under the License is distributed on an "AS IS" BASIS, 12 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ; See the License for the specific language governing permissions and 14 | ; limitations under the License. 15 | ; 16 | (ns rx.lang.clojure.examples.video-example 17 | (:require [rx.lang.clojure.interop :as rx]) 18 | (:import [rx Observable Observer Subscription] 19 | rx.subscriptions.Subscriptions)) 20 | 21 | ; Adapted from language-adaptors/rxjava-groovy/src/examples/groovy/rx/lang/groovy/examples/VideoExample.groovy 22 | 23 | (declare get-video-grid-for-display) 24 | (declare get-list-of-lists) 25 | (declare video-list) 26 | (declare video-list->videos) 27 | (declare video->metadata) 28 | (declare video->bookmark) 29 | (declare video->rating) 30 | 31 | ; just use a simple lock to keep multi-threaded output from being interleaved 32 | (def print-lock (Object.)) 33 | 34 | (defn example1 35 | [on-complete] 36 | (println "Starting example 1") 37 | ; this will print the dictionary for each video and is a good representation of 38 | ; how progressive rendering could work 39 | (println "---- sequence of video dictionaries ----") 40 | (-> (get-video-grid-for-display 1) 41 | (.subscribe (rx/action [v] (locking print-lock (println v))) 42 | (rx/action [v] (locking print-lock (println "Error: " v))) 43 | (rx/action [] 44 | (println "Finished example 1") 45 | (on-complete))))) 46 | 47 | (defn example2 48 | [on-complete] 49 | (println "Starting example 2") 50 | ; onNext will be called once with a list and demonstrates how a sequence can be combined 51 | ; for document style responses (most webservices) 52 | (-> (get-video-grid-for-display 1) 53 | .toList 54 | (.subscribe (rx/action [v] (println "\n ---- single list of video dictionaries ----\n" v)) 55 | (rx/action [v] (println "Error: " v)) 56 | (rx/action [] 57 | (println "Finished Example 2") 58 | (println "Exiting") 59 | (on-complete))))) 60 | 61 | (defn -main 62 | [& args] 63 | ; Run example1 followed by example2, then exit 64 | (example1 (fn [] (example2 #(System/exit 0))))) 65 | 66 | (defn ^Observable get-video-grid-for-display 67 | " 68 | Demonstrate how Rx is used to compose Observables together such as 69 | how a web service would to generate a JSON response. 70 | 71 | The simulated methods for the metadata represent different services 72 | that are often backed by network calls. 73 | 74 | This will return a sequence of maps like this: 75 | 76 | {:id 1000, :title video-1000-title, :length 5428, :bookmark 0, 77 | :rating {:actual 4 :average 3 :predicted 0}} 78 | " 79 | [user-id] 80 | (-> (get-list-of-lists user-id) 81 | (.mapMany (rx/fn [list] 82 | ; for each VideoList we want to fetch the videos 83 | (-> (video-list->videos list) 84 | (.take 10) ; we only want the first 10 of each list 85 | (.mapMany (rx/fn [video] 86 | ; for each video we want to fetch metadata 87 | (let [m (-> (video->metadata video) 88 | (.map (rx/fn [md] 89 | ; transform to the data and format we want 90 | {:title (:title md) 91 | :length (:duration md) }))) 92 | b (-> (video->bookmark video user-id) 93 | (.map (rx/fn [position] 94 | {:bookmark position}))) 95 | r (-> (video->rating video user-id) 96 | (.map (rx/fn [rating] 97 | {:rating {:actual (:actual-star-rating rating) 98 | :average (:average-star-rating rating) 99 | :predicted (:predicted-star-rating rating) }})))] 100 | ; join these together into a single, merged map for each video 101 | (Observable/zip m b r (rx/fn [m b r] 102 | (merge {:id video} m b r))))))))))) 103 | 104 | 105 | ; A little helper to make the future-based observables a little less verbose 106 | ; this has possibilities ... 107 | (defn- ^Observable future-observable 108 | "Returns an observable that executes (f observer) in a future, returning a 109 | subscription that will cancel the future." 110 | [f] 111 | (Observable/create (rx/action [^rx.Subscriber s] 112 | (println "Starting f") 113 | (let [f (future (f s))] 114 | (.add s (Subscriptions/create (rx/action [] (future-cancel f)))))))) 115 | 116 | (defn ^Observable get-list-of-lists 117 | " 118 | Retrieve a list of lists of videos (grid). 119 | 120 | Observable is the \"push\" equivalent to List 121 | " 122 | [user-id] 123 | (future-observable (fn [^rx.Subscriber s] 124 | (Thread/sleep 180) 125 | (dotimes [i 15] 126 | (.onNext s (video-list i))) 127 | (.onCompleted s)))) 128 | 129 | 130 | (comment (-> (get-list-of-lists 7777) 131 | .toList 132 | .toBlockingObservable 133 | .single)) 134 | 135 | (defn video-list 136 | [position] 137 | {:position position 138 | :name (str "ListName-" position) }) 139 | 140 | (defn ^Observable video-list->videos 141 | [{:keys [position] :as video-list}] 142 | (Observable/create (rx/action [^rx.Subscriber s] 143 | (dotimes [i 50] 144 | (.onNext s (+ (* position 1000) i))) 145 | (.onCompleted s)))) 146 | 147 | (comment (-> (video-list->videos (video-list 2)) 148 | .toList 149 | .toBlockingObservable 150 | .single)) 151 | 152 | (defn ^Observable video->metadata 153 | [video-id] 154 | (Observable/create (rx/action [^rx.Subscriber s] 155 | (.onNext s {:title (str "video-" video-id "-title") 156 | :actors ["actor1" "actor2"] 157 | :duration 5428 }) 158 | (.onCompleted s)))) 159 | 160 | (comment (-> (video->metadata 10) 161 | .toList 162 | .toBlockingObservable 163 | .single)) 164 | 165 | (defn ^Observable video->bookmark 166 | [video-id user-id] 167 | (future-observable (fn [^Observer observer] 168 | (Thread/sleep 4) 169 | (println "onNext") 170 | (.onNext observer (if (> (rand-int 6) 1) 0 (rand-int 4000))) 171 | (println "onComplete") 172 | (.onCompleted observer)))) 173 | 174 | (comment (-> (video->bookmark 112345 99999) 175 | .toList 176 | .toBlockingObservable 177 | .single)) 178 | 179 | (defn ^Observable video->rating 180 | [video-id user-id] 181 | (future-observable (fn [^Observer observer] 182 | (Thread/sleep 10) 183 | (.onNext observer {:video-id video-id 184 | :user-id user-id 185 | :predicted-star-rating (rand-int 5) 186 | :average-star-rating (rand-int 5) 187 | :actual-star-rating (rand-int 5) }) 188 | (.onCompleted observer)))) 189 | 190 | (comment (-> (video->rating 234345 8888) 191 | .toList 192 | .toBlockingObservable 193 | .single)) 194 | 195 | -------------------------------------------------------------------------------- /src/examples/clojure/rx/lang/clojure/examples/rx_examples.clj: -------------------------------------------------------------------------------- 1 | ; 2 | ; Copyright 2013 Netflix, Inc. 3 | ; 4 | ; Licensed under the Apache License, Version 2.0 (the "License"); 5 | ; you may not use this file except in compliance with the License. 6 | ; You may obtain a copy of the License at 7 | ; 8 | ; http://www.apache.org/licenses/LICENSE-2.0 9 | ; 10 | ; Unless required by applicable law or agreed to in writing, software 11 | ; distributed under the License is distributed on an "AS IS" BASIS, 12 | ; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | ; See the License for the specific language governing permissions and 14 | ; limitations under the License. 15 | ; 16 | (ns rx.lang.clojure.examples.rx-examples 17 | (:require [rx.lang.clojure.interop :as rx]) 18 | (:import rx.Observable rx.subscriptions.Subscriptions)) 19 | 20 | ; NOTE on naming conventions. I'm using camelCase names (against clojure convention) 21 | ; in this file as I'm purposefully keeping functions and methods across 22 | ; different language implementations in-sync for easy comparison. 23 | 24 | ; -------------------------------------------------- 25 | ; Hello World! 26 | ; -------------------------------------------------- 27 | 28 | (defn hello 29 | [& args] 30 | (-> 31 | ; type hint required due to `Observable/from` overloading 32 | (Observable/from ^java.lang.Iterable args) 33 | (.subscribe (rx/action [v] (println (str "Hello " v "!")))))) 34 | 35 | ; To see output 36 | (comment 37 | (hello "Ben" "George")) 38 | 39 | ; -------------------------------------------------- 40 | ; Create Observable from Existing Data 41 | ; -------------------------------------------------- 42 | 43 | 44 | (defn existingDataFromNumbersUsingFrom [] 45 | (Observable/from [1 2 3 4 5 6])) 46 | 47 | (defn existingDataFromObjectsUsingFrom [] 48 | (Observable/from ["a" "b" "c"])) 49 | 50 | (defn existingDataFromListUsingFrom [] 51 | (let [list [5, 6, 7, 8]] 52 | (Observable/from list))) 53 | 54 | (defn existingDataWithJust [] 55 | (Observable/just "one object")) 56 | 57 | ; -------------------------------------------------- 58 | ; Custom Observable 59 | ; -------------------------------------------------- 60 | 61 | (defn customObservable 62 | "This example shows a custom Observable. Note the 63 | .isUnsubscribed check so that it can be stopped early. 64 | 65 | returns Observable" 66 | [] 67 | (Observable/create 68 | (rx/action [^rx.Subscriber s] 69 | (loop [x (range 50)] 70 | (when (and (not (.isUnsubscribed s)) x) 71 | ; TODO 72 | (println "HERE " (.isUnsubscribed s) (first x)) 73 | (-> s (.onNext (str "value_" (first x)))) 74 | (recur (next x)))) 75 | ; after sending all values we complete the sequence 76 | (-> s .onCompleted)))) 77 | 78 | ; To see output 79 | (comment 80 | (.subscribe (customObservable) (rx/action* println))) 81 | 82 | ; -------------------------------------------------- 83 | ; Composition - Simple 84 | ; -------------------------------------------------- 85 | 86 | (defn simpleComposition 87 | "Calls 'customObservable' and defines 88 | a chain of operators to apply to the callback sequence." 89 | [] 90 | (-> 91 | (customObservable) 92 | (.skip 10) 93 | (.take 5) 94 | (.map (rx/fn [v] (str v "_transformed"))) 95 | (.subscribe (rx/action [v] (println "onNext =>" v))))) 96 | 97 | ; To see output 98 | (comment 99 | (simpleComposition)) 100 | 101 | 102 | ; -------------------------------------------------- 103 | ; Composition - Multiple async calls combined 104 | ; -------------------------------------------------- 105 | 106 | (defn getUser 107 | "Asynchronously fetch user data 108 | 109 | return Observable" 110 | [userId] 111 | (Observable/create 112 | (rx/action [^rx.Subscriber s] 113 | (let [f (future 114 | (try 115 | ; simulate fetching user data via network service call with latency 116 | (Thread/sleep 60) 117 | (-> s (.onNext {:user-id userId 118 | :name "Sam Harris" 119 | :preferred-language (if (= 0 (rand-int 2)) "en-us" "es-us") })) 120 | (-> s .onCompleted) 121 | (catch Exception e 122 | (-> s (.onError e))))) ] 123 | ; a subscription that cancels the future if unsubscribed 124 | (.add s (Subscriptions/create (rx/action [] (future-cancel f)))))))) 125 | 126 | (defn getVideoBookmark 127 | "Asynchronously fetch bookmark for video 128 | 129 | return Observable" 130 | [userId, videoId] 131 | (Observable/create 132 | (rx/action [^rx.Subscriber s] 133 | (let [f (future 134 | (try 135 | ; simulate fetching user data via network service call with latency 136 | (Thread/sleep 20) 137 | (-> s (.onNext {:video-id videoId 138 | ; 50/50 chance of giving back position 0 or 0-2500 139 | :position (if (= 0 (rand-int 2)) 0 (rand-int 2500))})) 140 | (-> s .onCompleted) 141 | (catch Exception e 142 | (-> s (.onError e)))))] 143 | ; a subscription that cancels the future if unsubscribed 144 | (.add s (Subscriptions/create (rx/action [] (future-cancel f)))))))) 145 | 146 | (defn getVideoMetadata 147 | "Asynchronously fetch movie metadata for a given language 148 | return Observable" 149 | [videoId, preferredLanguage] 150 | (Observable/create 151 | (rx/action [^rx.Subscriber s] 152 | (let [f (future 153 | (println "getVideoMetadata " videoId) 154 | (try 155 | ; simulate fetching video data via network service call with latency 156 | (Thread/sleep 50) 157 | ; contrived metadata for en-us or es-us 158 | (if (= "en-us" preferredLanguage) 159 | (-> s (.onNext {:video-id videoId 160 | :title "House of Cards: Episode 1" 161 | :director "David Fincher" 162 | :duration 3365}))) 163 | (if (= "es-us" preferredLanguage) 164 | (-> s (.onNext {:video-id videoId 165 | :title "Cámara de Tarjetas: Episodio 1" 166 | :director "David Fincher" 167 | :duration 3365}))) 168 | (-> s .onCompleted) 169 | (catch Exception e 170 | (-> s (.onError e))))) ] 171 | ; a subscription that cancels the future if unsubscribed 172 | (.add s (Subscriptions/create (rx/action [] (future-cancel f)))))))) 173 | 174 | 175 | (defn getVideoForUser [userId videoId] 176 | "Get video metadata for a given userId 177 | - video metadata 178 | - video bookmark position 179 | - user data 180 | return Observable" 181 | (let [user-observable (-> (getUser userId) 182 | (.map (rx/fn [user] 183 | {:user-name (:name user) 184 | :language (:preferred-language user)}))) 185 | bookmark-observable (-> (getVideoBookmark userId videoId) 186 | (.map (rx/fn [bookmark] {:viewed-position (:position bookmark)}))) 187 | ; getVideoMetadata requires :language from user-observable so nest inside map function 188 | video-metadata-observable (-> user-observable 189 | (.mapMany 190 | ; fetch metadata after a response from user-observable is received 191 | (rx/fn [user-map] 192 | (getVideoMetadata videoId (:language user-map)))))] 193 | ; now combine 3 async sequences using zip 194 | (-> (Observable/zip bookmark-observable video-metadata-observable user-observable 195 | (rx/fn [bookmark-map metadata-map user-map] 196 | {:bookmark-map bookmark-map 197 | :metadata-map metadata-map 198 | :user-map user-map})) 199 | ; and transform into a single response object 200 | (.map (rx/fn [data] 201 | {:video-id videoId 202 | :video-metadata (:metadata-map data) 203 | :user-id userId 204 | :language (:language (:user-map data)) 205 | :bookmark (:viewed-position (:bookmark-map data)) }))))) 206 | 207 | ; To see output like this: 208 | ; {:video-id 78965, :video-metadata {:video-id 78965, :title Cámara de Tarjetas: Episodio 1, 209 | ; :director David Fincher, :duration 3365}, :user-id 12345, :language es-us, :bookmark 0} 210 | ; 211 | (comment 212 | (-> (getVideoForUser 12345 78965) 213 | (.toBlockingObservable) 214 | .single)) 215 | 216 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2012 Netflix, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/test/clojure/rx/lang/clojure/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns rx.lang.clojure.core-test 2 | (:require [rx.lang.clojure.core :as rx] 3 | [rx.lang.clojure.blocking :as b] 4 | [rx.lang.clojure.future :as f] 5 | [clojure.test :refer [deftest is testing are]])) 6 | 7 | (deftest test-observable? 8 | (is (rx/observable? (rx/return 99))) 9 | (is (not (rx/observable? "I'm not an observable")))) 10 | 11 | (deftest test-on-next 12 | (testing "calls onNext" 13 | (let [called (atom []) 14 | o (reify rx.Observer (onNext [this value] (swap! called conj value)))] 15 | (is (identical? o (rx/on-next o 1))) 16 | (is (= [1] @called))))) 17 | 18 | (deftest test-on-completed 19 | (testing "calls onCompleted" 20 | (let [called (atom 0) 21 | o (reify rx.Observer (onCompleted [this] (swap! called inc)))] 22 | (is (identical? o (rx/on-completed o))) 23 | (is (= 1 @called))))) 24 | 25 | (deftest test-on-error 26 | (testing "calls onError" 27 | (let [called (atom []) 28 | e (java.io.FileNotFoundException. "yum") 29 | o (reify rx.Observer (onError [this e] (swap! called conj e)))] 30 | (is (identical? o (rx/on-error o e))) 31 | (is (= [e] @called))))) 32 | 33 | (deftest test-catch-error-value 34 | (testing "if no exception, returns body" 35 | (let [o (reify rx.Observer)] 36 | (is (= 3 (rx/catch-error-value o 99 37 | (+ 1 2)))))) 38 | 39 | (testing "exceptions call onError on observable and inject value in exception" 40 | (let [called (atom []) 41 | e (java.io.FileNotFoundException. "boo") 42 | o (reify rx.Observer 43 | (onError [this e] 44 | (swap! called conj e))) 45 | result (rx/catch-error-value o 100 46 | (throw e)) 47 | cause (.getCause e)] 48 | (is (identical? e result)) 49 | (is (= [e] @called)) 50 | (when (is (instance? rx.exceptions.OnErrorThrowable$OnNextValue cause)) 51 | (is (= 100 (.getValue cause))))))) 52 | 53 | (deftest test-subscribe 54 | (testing "subscribe overload with only onNext" 55 | (let [o (rx/return 1) 56 | called (atom nil)] 57 | (rx/subscribe o (fn [v] (swap! called (fn [_] v)))) 58 | (is (= 1 @called))))) 59 | 60 | (deftest test-fn->predicate 61 | (are [f arg result] (= result (.call (rx/fn->predicate f) arg)) 62 | identity nil false 63 | identity false false 64 | identity 1 true 65 | identity "true" true 66 | identity true true)) 67 | 68 | (deftest test-subscription 69 | (let [called (atom 0) 70 | s (rx/subscription #(swap! called inc))] 71 | (is (identical? s (rx/unsubscribe s))) 72 | (is (= 1 @called)))) 73 | 74 | (deftest test-unsubscribed? 75 | (let [s (rx/subscription #())] 76 | (is (not (rx/unsubscribed? s))) 77 | (rx/unsubscribe s) 78 | (is (rx/unsubscribed? s)))) 79 | 80 | 81 | (deftest test-observable* 82 | (let [o (rx/observable* (fn [s] 83 | (rx/on-next s 0) 84 | (rx/on-next s 1) 85 | (when-not (rx/unsubscribed? s) (rx/on-next s 2)) 86 | (rx/on-completed s)))] 87 | (is (= [0 1 2] (b/into [] o))))) 88 | 89 | (deftest test-operator* 90 | (let [o (rx/operator* #(rx/subscriber % 91 | (fn [o v] 92 | (if (even? v) 93 | (rx/on-next o v))))) 94 | result (->> (rx/seq->o [1 2 3 4 5]) 95 | (rx/lift o) 96 | (b/into []))] 97 | (is (= [2 4] result)))) 98 | 99 | (deftest test-serialize 100 | ; I'm going to believe serialize works and just exercise it 101 | ; here for sanity. 102 | (is (= [1 2 3] 103 | (->> [1 2 3] 104 | (rx/seq->o) 105 | (rx/serialize) 106 | (b/into []))))) 107 | 108 | (let [expected-result [[1 3 5] [2 4 6]] 109 | sleepy-o #(f/future-generator* 110 | future-call 111 | (fn [o] 112 | (doseq [x %] 113 | (Thread/sleep 10) 114 | (rx/on-next o x)))) 115 | make-inputs (fn [] (mapv sleepy-o expected-result)) 116 | make-output (fn [r] [(keep #{1 3 5} r) 117 | (keep #{2 4 6} r)])] 118 | (deftest test-merge* 119 | (is (= expected-result 120 | (->> (make-inputs) 121 | (rx/seq->o) 122 | (rx/merge*) 123 | (b/into []) 124 | (make-output))))) 125 | (deftest test-merge 126 | (is (= expected-result 127 | (->> (make-inputs) 128 | (apply rx/merge) 129 | (b/into []) 130 | (make-output))))) 131 | (deftest test-merge-delay-error* 132 | (is (= expected-result 133 | (->> (make-inputs) 134 | (rx/seq->o) 135 | (rx/merge-delay-error*) 136 | (b/into []) 137 | (make-output))))) 138 | (deftest test-merge-delay-error 139 | (is (= expected-result 140 | (->> (make-inputs) 141 | (apply rx/merge-delay-error) 142 | (b/into []) 143 | (make-output)))))) 144 | 145 | (deftest test-generator 146 | (testing "calls on-completed automatically" 147 | (let [o (rx/generator [o]) 148 | called (atom nil)] 149 | (rx/subscribe o (fn [v]) (fn [_]) #(reset! called "YES")) 150 | (is (= "YES" @called)))) 151 | 152 | (testing "exceptions automatically go to on-error" 153 | (let [expected (IllegalArgumentException. "hi") 154 | actual (atom nil)] 155 | (rx/subscribe (rx/generator [o] (throw expected)) 156 | #() 157 | #(reset! actual %)) 158 | (is (identical? expected @actual))))) 159 | 160 | (deftest test-seq->o 161 | (is (= [] (b/into [] (rx/seq->o [])))) 162 | (is (= [] (b/into [] (rx/seq->o nil)))) 163 | (is (= [\a \b \c] (b/into [] (rx/seq->o "abc")))) 164 | (is (= [0 1 2 3] (b/first (rx/into [] (rx/seq->o (range 4)))))) 165 | (is (= #{0 1 2 3} (b/first (rx/into #{} (rx/seq->o (range 4)))))) 166 | (is (= {:a 1 :b 2 :c 3} (b/first (rx/into {} (rx/seq->o [[:a 1] [:b 2] [:c 3]])))))) 167 | 168 | (deftest test-return 169 | (is (= [0] (b/into [] (rx/return 0))))) 170 | 171 | (deftest test-cache 172 | (let [value (atom 0) 173 | o (->> 174 | (rx/return 0) 175 | (rx/map (fn [x] (swap! value inc))) 176 | (rx/cache))] 177 | (is (= 1 (b/single o))) 178 | (is (= 1 @value)) 179 | (is (= 1 (b/single o))) 180 | (is (= 1 @value)) 181 | (is (= 1 (b/single o))))) 182 | 183 | (deftest test-cons 184 | (is (= [1] (b/into [] (rx/cons 1 (rx/empty))))) 185 | (is (= [1 2 3 4] (b/into [] (rx/cons 1 (rx/seq->o [2 3 4])))))) 186 | 187 | (deftest test-concat 188 | (is (= [:q :r] 189 | (b/into [] (rx/concat (rx/seq->o [:q :r]))))) 190 | (is (= [:q :r 1 2 3] 191 | (b/into [] (rx/concat (rx/seq->o [:q :r]) 192 | (rx/seq->o [1 2 3])))))) 193 | 194 | (deftest test-concat* 195 | (is (= [:q :r] 196 | (b/into [] (rx/concat* (rx/return (rx/seq->o [:q :r])))))) 197 | (is (= [:q :r 1 2 3] 198 | (b/into [] (rx/concat* (rx/seq->o [(rx/seq->o [:q :r]) 199 | (rx/seq->o [1 2 3])])))))) 200 | 201 | (deftest test-count 202 | (are [xs] (= (count xs) (->> xs (rx/seq->o) (rx/count) (b/single))) 203 | [] 204 | [1] 205 | [5 6 7] 206 | (range 10000))) 207 | 208 | (deftest test-cycle 209 | (is (= [1 2 3 1 2 3 1 2 3 1 2] 210 | (->> [1 2 3] 211 | (rx/seq->o) 212 | (rx/cycle) 213 | (rx/take 11) 214 | (b/into []))))) 215 | 216 | (deftest test-distinct 217 | (let [input [{:a 1} {:a 1} {:b 1} {"a" (int 1)} {:a (int 1)}]] 218 | (is (= (distinct input) 219 | (->> input 220 | (rx/seq->o) 221 | (rx/distinct) 222 | (b/into []))))) 223 | (let [input [{:name "Bob" :x 2} {:name "Jim" :x 99} {:name "Bob" :x 3}]] 224 | (is (= [{:name "Bob" :x 2} {:name "Jim" :x 99}] 225 | (->> input 226 | (rx/seq->o) 227 | (rx/distinct :name) 228 | (b/into [])))))) 229 | 230 | (deftest test-do 231 | (testing "calls a function with each element" 232 | (let [collected (atom [])] 233 | (is (= [1 2 3] 234 | (->> (rx/seq->o [1 2 3]) 235 | (rx/do (fn [v] 236 | (swap! collected conj (* 2 v)))) 237 | (rx/do (partial println "GOT")) 238 | (b/into [])))) 239 | (is (= [2 4 6] @collected)))) 240 | (testing "ends sequence with onError if action code throws an exception" 241 | (let [collected (atom []) 242 | o (->> (rx/seq->o [1 2 3]) 243 | (rx/do (fn [v] 244 | (if (= v 2) 245 | (throw (IllegalStateException. (str "blah" v))) 246 | (swap! collected conj (* 99 v))))))] 247 | (is (thrown-with-msg? IllegalStateException #"blah2" 248 | (b/into [] o))) 249 | (is (= [99] @collected))))) 250 | 251 | (deftest test-drop-while 252 | (is (= (into [] (drop-while even? [2 4 6 8 1 2 3])) 253 | (b/into [] (rx/drop-while even? (rx/seq->o [2 4 6 8 1 2 3]))))) 254 | (is (= (into [] (drop-while even? [2 4 6 8 1 2 3])) 255 | (b/into [] (rx/drop-while even? (rx/seq->o [2 4 6 8 1 2 3])))))) 256 | 257 | (deftest test-every? 258 | (are [xs p result] (= result (->> xs (rx/seq->o) (rx/every? p) (b/single))) 259 | [2 4 6 8] even? true 260 | [2 4 3 8] even? false 261 | [1 2 3 4] #{1 2 3 4} true 262 | [1 2 3 4] #{1 3 4} false)) 263 | 264 | (deftest test-filter 265 | (is (= (into [] (->> [:a :b :c :d :e :f :G :e] 266 | (filter #{:b :e :G}))) 267 | (b/into [] (->> (rx/seq->o [:a :b :c :d :e :f :G :e]) 268 | (rx/filter #{:b :e :G})))))) 269 | 270 | (deftest test-first 271 | (is (= [3] 272 | (b/into [] (rx/first (rx/seq->o [3 4 5]))))) 273 | (is (= [] 274 | (b/into [] (rx/first (rx/empty)))))) 275 | 276 | (deftest test-group-by 277 | (let [xs [{:k :a :v 1} {:k :b :v 2} {:k :a :v 3} {:k :c :v 4}]] 278 | (testing "with just a key-fn" 279 | (is (= [[:a {:k :a :v 1}] 280 | [:a {:k :a :v 3}] 281 | [:b {:k :b :v 2}] 282 | [:c {:k :c :v 4}]] 283 | (->> xs 284 | (rx/seq->o) 285 | (rx/group-by :k) 286 | (rx/mapcat (fn [[k vo :as me]] 287 | (is (instance? clojure.lang.MapEntry me)) 288 | (rx/map #(vector k %) vo))) 289 | (b/into []))))) 290 | 291 | ; TODO reinstate once this is implemented 292 | ; see https://github.com/Netflix/RxJava/commit/02ccc4d727a9297f14219549208757c6e0efce2a 293 | #_(testing "with a val-fn" 294 | (is (= [[:a 1] 295 | [:b 2] 296 | [:a 3] 297 | [:c 4]] 298 | (->> xs 299 | (rx/seq->o) 300 | (rx/group-by :k :v) 301 | (rx/mapcat (fn [[k vo :as me]] 302 | (is (instance? clojure.lang.MapEntry me)) 303 | (rx/map #(vector k %) vo))) 304 | (b/into []))))))) 305 | 306 | (deftest test-interleave 307 | (are [inputs] (= (apply interleave inputs) 308 | (->> (apply rx/interleave (map rx/seq->o inputs)) 309 | (b/into []))) 310 | [[] []] 311 | [[] [1]] 312 | [(range 5) (range 10) (range 10) (range 3)] 313 | [(range 50) (range 10)] 314 | [(range 5) (range 10 60) (range 10) (range 50)]) 315 | 316 | ; one-arg case, not supported by clojure.core/interleave 317 | (is (= (range 10) 318 | (->> (rx/interleave (rx/seq->o (range 10))) 319 | (b/into []))))) 320 | 321 | (deftest test-interleave* 322 | (are [inputs] (= (apply interleave inputs) 323 | (->> (rx/interleave* (->> inputs 324 | (map rx/seq->o) 325 | (rx/seq->o))) 326 | (b/into []))) 327 | [[] []] 328 | [[] [1]] 329 | [(range 5) (range 10) (range 10) (range 3)] 330 | [(range 50) (range 10)] 331 | [(range 5) (range 10 60) (range 10) (range 50)])) 332 | 333 | (deftest test-interpose 334 | (is (= (interpose \, [1 2 3]) 335 | (b/into [] (rx/interpose \, (rx/seq->o [1 2 3])))))) 336 | 337 | (deftest test-into 338 | (are [input to] (= (into to input) 339 | (b/single (rx/into to (rx/seq->o input)))) 340 | [6 7 8] [9 10 [11]] 341 | #{} [1 2 3 2 4 5] 342 | {} [[1 2] [3 2] [4 5]] 343 | {} [] 344 | '() (range 50))) 345 | 346 | (deftest test-iterate 347 | (are [f x n] (= (->> (iterate f x) (take n)) 348 | (->> (rx/iterate f x) (rx/take n) (b/into []))) 349 | inc 0 10 350 | dec 20 100 351 | #(conj % (count %)) [] 5 352 | #(cons (count %) % ) nil 5)) 353 | 354 | (deftest test-keep 355 | (is (= (into [] (keep identity [true true false])) 356 | (b/into [] (rx/keep identity (rx/seq->o [true true false]))))) 357 | 358 | (is (= (into [] (keep #(if (even? %) (* 2 %)) (range 9))) 359 | (b/into [] (rx/keep #(if (even? %) (* 2 %)) (rx/seq->o (range 9))))))) 360 | 361 | (deftest test-keep-indexed 362 | (is (= (into [] (keep-indexed (fn [i v] 363 | (if (even? i) v)) 364 | [true true false])) 365 | (b/into [] (rx/keep-indexed (fn [i v] 366 | (if (even? i) v)) 367 | (rx/seq->o [true true false])))))) 368 | 369 | (deftest test-map 370 | (is (= (into {} (map (juxt identity name) 371 | [:q :r :s :t :u])) 372 | (b/into {} (rx/map (juxt identity name) 373 | (rx/seq->o [:q :r :s :t :u]))))) 374 | (is (= (into [] (map vector 375 | [:q :r :s :t :u] 376 | (range 10) 377 | ["a" "b" "c" "d" "e"] )) 378 | (b/into [] (rx/map vector 379 | (rx/seq->o [:q :r :s :t :u]) 380 | (rx/seq->o (range 10) ) 381 | (rx/seq->o ["a" "b" "c" "d" "e"] ))))) 382 | ; check > 4 arg case 383 | (is (= (into [] (map vector 384 | [:q :r :s :t :u] 385 | [:q :r :s :t :u] 386 | [:q :r :s :t :u] 387 | (range 10) 388 | (range 10) 389 | (range 10) 390 | ["a" "b" "c" "d" "e"] 391 | ["a" "b" "c" "d" "e"] 392 | ["a" "b" "c" "d" "e"])) 393 | (b/into [] (rx/map vector 394 | (rx/seq->o [:q :r :s :t :u]) 395 | (rx/seq->o [:q :r :s :t :u]) 396 | (rx/seq->o [:q :r :s :t :u]) 397 | (rx/seq->o (range 10)) 398 | (rx/seq->o (range 10)) 399 | (rx/seq->o (range 10)) 400 | (rx/seq->o ["a" "b" "c" "d" "e"]) 401 | (rx/seq->o ["a" "b" "c" "d" "e"]) 402 | (rx/seq->o ["a" "b" "c" "d" "e"])))))) 403 | 404 | (deftest test-map* 405 | (is (= [[1 2 3 4 5 6 7 8]] 406 | (b/into [] (rx/map* vector 407 | (rx/seq->o [(rx/seq->o [1]) 408 | (rx/seq->o [2]) 409 | (rx/seq->o [3]) 410 | (rx/seq->o [4]) 411 | (rx/seq->o [5]) 412 | (rx/seq->o [6]) 413 | (rx/seq->o [7]) 414 | (rx/seq->o [8])])))))) 415 | (deftest test-map-indexed 416 | (is (= (map-indexed vector [:a :b :c]) 417 | (b/into [] (rx/map-indexed vector (rx/seq->o [:a :b :c]))))) 418 | (testing "exceptions from fn have error value injected" 419 | (try 420 | (->> (rx/seq->o [:a :b :c]) 421 | (rx/map-indexed (fn [i v] 422 | (if (= 1 i) 423 | (throw (java.io.FileNotFoundException. "blah"))) 424 | v)) 425 | (b/into [])) 426 | (catch java.io.FileNotFoundException e 427 | (is (= :b (-> e .getCause .getValue))))))) 428 | 429 | (deftest test-mapcat* 430 | (let [f (fn [a b c d e] 431 | [(+ a b) (+ c d) e])] 432 | (is (= (->> (range 5) 433 | (map (fn [_] (range 5))) 434 | (apply mapcat f)) 435 | (->> (range 5) 436 | (map (fn [_] (rx/seq->o (range 5)))) 437 | (rx/seq->o) 438 | (rx/mapcat* (fn [& args] (rx/seq->o (apply f args)))) 439 | (b/into [])))))) 440 | 441 | (deftest test-mapcat 442 | (let [f (fn [v] [v (* v v)]) 443 | xs (range 10)] 444 | (is (= (mapcat f xs) 445 | (b/into [] (rx/mapcat (comp rx/seq->o f) (rx/seq->o xs)))))) 446 | 447 | (let [f (fn [a b] [a b (* a b)]) 448 | as (range 10) 449 | bs (range 15)] 450 | (is (= (mapcat f as bs) 451 | (b/into [] (rx/mapcat (comp rx/seq->o f) 452 | (rx/seq->o as) 453 | (rx/seq->o bs))))))) 454 | 455 | (deftest test-flatmap 456 | (let [f (fn [v] [v (* v v)]) 457 | xs (range 10)] 458 | (is (= (mapcat f xs) 459 | (b/into [] (rx/flatmap (comp rx/seq->o f) (rx/seq->o xs)))))) 460 | 461 | ; group-by is a good way to test merge behavior without truly async code 462 | ; here the :a and :b observables are interleaved when merged 463 | (let [xs [{:k :a :v 1} {:k :b :v 2} {:k :a :v 3} {:k :c :v 4}]] 464 | (is (= [[:a {:k :a :v 1}] 465 | [:b {:k :b :v 2}] 466 | [:a {:k :a :v 3}] 467 | [:c {:k :c :v 4}]] 468 | (->> xs 469 | (rx/seq->o) 470 | (rx/group-by :k) 471 | (rx/flatmap (fn [[k vo :as me]] 472 | (is (instance? clojure.lang.MapEntry me)) 473 | (rx/map #(vector k %) vo))) 474 | (b/into []))))) 475 | 476 | ; still looking for a simple demo of merging for the multi-arg case 477 | ; Here, because ys is "inline", the interleaving is removed. sigh. 478 | (let [xs [{:k :a :v 1} {:k :b :v 2} {:k :a :v 3} {:k :c :v 4}] 479 | ys [:ay :by :cy]] 480 | (is (= [[:a {:k :a :v 1} :ay] 481 | [:a {:k :a :v 3} :ay] 482 | [:b {:k :b :v 2} :by] 483 | [:c {:k :c :v 4} :cy]] 484 | (->> (rx/flatmap (fn [[k vo :as me] y] 485 | (is (instance? clojure.lang.MapEntry me)) 486 | (rx/map #(vector k % y) vo)) 487 | (->> xs 488 | rx/seq->o 489 | (rx/group-by :k)) 490 | (rx/seq->o ys)) 491 | (b/into [])))))) 492 | 493 | (deftest test-next 494 | (let [in [:q :r :s :t :u]] 495 | (is (= (next in) (b/into [] (rx/next (rx/seq->o in))))))) 496 | 497 | (deftest test-nth 498 | (is (= [:a] 499 | (b/into [] (rx/nth (rx/seq->o [:s :b :a :c]) 2)))) 500 | (is (= [:fallback] 501 | (b/into [] (rx/nth (rx/seq->o [:s :b :a :c]) 25 :fallback))))) 502 | 503 | (deftest test-rest 504 | (let [in [:q :r :s :t :u]] 505 | (is (= (rest in) (b/into [] (rx/rest (rx/seq->o in))))))) 506 | 507 | (deftest test-partition-all 508 | (are [input-size part-size step] (= (->> (range input-size) 509 | (partition-all part-size step)) 510 | (->> (range input-size) 511 | (rx/seq->o) 512 | (rx/partition-all part-size step) 513 | (rx/map #(rx/into [] %)) 514 | (rx/concat*) 515 | (b/into []))) 516 | 0 1 1 517 | 10 2 2 518 | 10 3 2 519 | 15 30 4) 520 | 521 | (are [input-size part-size] (= (->> (range input-size) 522 | (partition-all part-size)) 523 | (->> (range input-size) 524 | (rx/seq->o) 525 | (rx/partition-all part-size) 526 | (rx/map #(rx/into [] %)) 527 | (rx/concat*) 528 | (b/into []))) 529 | 0 1 530 | 10 2 531 | 10 3 532 | 15 30)) 533 | 534 | (deftest test-range 535 | (are [start end step] (= (range start end step) 536 | (->> (rx/range start end step) (b/into []))) 537 | 0 10 2 538 | 0 -100 -1 539 | 5 100 9) 540 | 541 | (are [start end] (= (range start end) 542 | (->> (rx/range start end) (b/into []))) 543 | 0 10 544 | 0 -100 545 | 5 100) 546 | 547 | (are [start] (= (->> (range start) (take 100)) 548 | (->> (rx/range start) (rx/take 100) (b/into []))) 549 | 50 550 | 0 551 | 5 552 | -20) 553 | (is (= (->> (range) (take 500)) 554 | (->> (rx/range) (rx/take 500) (b/into []))))) 555 | 556 | (deftest test-reduce 557 | (is (= (reduce + 0 (range 4)) 558 | (b/first (rx/reduce + 0 (rx/seq->o (range 4))))))) 559 | 560 | (deftest test-reductions 561 | (is (= (into [] (reductions + 0 (range 4))) 562 | (b/into [] (rx/reductions + 0 (rx/seq->o (range 4))))))) 563 | 564 | (deftest test-some 565 | (is (= [:r] (b/into [] (rx/some #{:r :s :t} (rx/seq->o [:q :v :r]))))) 566 | (is (= [] (b/into [] (rx/some #{:r :s :t} (rx/seq->o [:q :v])))))) 567 | 568 | (deftest test-sort 569 | (are [in cmp] (= (if cmp 570 | (sort cmp in) 571 | (sort in)) 572 | (->> in 573 | (rx/seq->o) 574 | (#(if cmp (rx/sort cmp %) (rx/sort %))) 575 | (b/into []))) 576 | [] nil 577 | [] (comp - compare) 578 | [3 1 2] nil 579 | [1 2 3] nil 580 | [1 2 3] (comp - compare) 581 | [2 1 3] (comp - compare))) 582 | 583 | (deftest test-sort-by 584 | (are [rin cmp] (let [in (map #(hash-map :foo %) rin)] 585 | (= (if cmp 586 | (sort-by :foo cmp in) 587 | (sort-by :foo in)) 588 | (->> in 589 | (rx/seq->o) 590 | (#(if cmp (rx/sort-by :foo cmp %) (rx/sort-by :foo %))) 591 | (b/into [])))) 592 | [] nil 593 | [] (comp - compare) 594 | [3 1 2] nil 595 | [1 2 3] nil 596 | [1 2 3] (comp - compare) 597 | [2 1 3] (comp - compare))) 598 | 599 | 600 | (deftest test-split-with 601 | (is (= (split-with (partial >= 3) (range 6)) 602 | (->> (rx/seq->o (range 6)) 603 | (rx/split-with (partial >= 3)) 604 | b/first 605 | (map (partial b/into [])))))) 606 | 607 | (deftest test-take-while 608 | (is (= (into [] (take-while even? [2 4 6 8 1 2 3])) 609 | (b/into [] (rx/take-while even? (rx/seq->o [2 4 6 8 1 2 3])))))) 610 | 611 | (deftest test-throw 612 | (let [expected (IllegalArgumentException. "HI") 613 | called (atom nil)] 614 | (rx/subscribe (rx/throw expected) 615 | (fn [_]) 616 | (fn [e] (reset! called expected)) 617 | (fn [_])) 618 | (is (identical? expected @called)))) 619 | 620 | (deftest test-catch* 621 | (testing "Is just a passthrough if there's no error" 622 | (is (= [1 2 3] 623 | (->> (rx/seq->o [1 2 3]) 624 | (rx/catch* Exception (fn [e] (throw "OH NO"))) 625 | (b/into []))))) 626 | 627 | (testing "Can catch a particular exception type and continue with an observable" 628 | (is (= [1 2 4 5 6 "foo"] 629 | (->> (rx/generator [o] 630 | (rx/on-next o 1) 631 | (rx/on-next o 2) 632 | (rx/on-error o (IllegalStateException. "foo"))) 633 | (rx/catch* IllegalStateException 634 | (fn [e] 635 | (rx/seq->o [4 5 6 (.getMessage e)]))) 636 | (b/into []))))) 637 | 638 | (testing "if exception isn't matched, it's passed to on-error" 639 | (let [expected (IllegalArgumentException. "HI") 640 | called (atom nil)] 641 | (rx/subscribe (->> (rx/generator [o] 642 | (rx/on-next o 1) 643 | (rx/on-next o 2) 644 | (rx/on-error o expected)) 645 | (rx/catch* IllegalStateException (fn [e] 646 | (rx/return "WAT?")))) 647 | (fn [_]) 648 | (fn [e] (reset! called expected)) 649 | (fn [_])) 650 | (is (identical? expected @called)))) 651 | 652 | (testing "if p returns Throwable, that's passed as e" 653 | (let [cause (IllegalArgumentException. "HI") 654 | wrapper (java.util.concurrent.ExecutionException. cause)] 655 | (is (= [cause] 656 | (->> (rx/throw wrapper) 657 | (rx/catch #(.getCause %) e 658 | (rx/return e)) 659 | (b/into []))))))) 660 | 661 | 662 | (deftest test-finally 663 | (testing "Supports a finally clause" 664 | (testing "called on completed" 665 | (let [completed (atom nil) 666 | called (atom nil)] 667 | (rx/subscribe (->> (rx/seq->o [1 2 3]) 668 | (rx/finally* (fn [] (reset! called (str "got it"))))) 669 | (fn [_]) 670 | (fn [_] (throw (IllegalStateException. "WAT"))) 671 | (fn [] (reset! completed "DONE"))) 672 | (is (= "got it" @called)) 673 | (is (= "DONE" @completed)))) 674 | 675 | (testing "called on error" 676 | (let [expected (IllegalStateException. "expected") 677 | completed (atom nil) 678 | called (atom nil)] 679 | (rx/subscribe (->> (rx/generator [o] 680 | (rx/on-next o 1) 681 | (rx/on-next o 2) 682 | (rx/on-error o expected)) 683 | (rx/finally 684 | (reset! called "got it"))) 685 | (fn [_]) 686 | (fn [e] (reset! completed e)) 687 | (fn [] (throw (IllegalStateException. "WAT")))) 688 | (is (= "got it" @called)) 689 | (is (identical? expected @completed)))))) 690 | 691 | 692 | ;################################################################################ 693 | 694 | (deftest test-graph-imports 695 | (is (= 99 696 | (-> {:a {:deps [] :factory (fn [_] (rx/return 99))}} 697 | rx/let-o* 698 | :a 699 | b/single))) 700 | (is (= 100 701 | (b/single (rx/let-o [?a (rx/return 100)] 702 | ?a))))) 703 | 704 | ;################################################################################ 705 | 706 | (deftest test-realized-imports 707 | (is (= {:a 1 :b 2} 708 | (->> (rx/let-realized [a (rx/return 1) 709 | b (rx/return 2)] 710 | {:a a :b b}) 711 | b/single)))) 712 | 713 | 714 | -------------------------------------------------------------------------------- /src/main/clojure/rx/lang/clojure/core.clj: -------------------------------------------------------------------------------- 1 | (ns rx.lang.clojure.core 2 | (:refer-clojure :exclude [concat cons count cycle 3 | distinct do drop drop-while 4 | empty every? 5 | filter first future flatmap 6 | group-by 7 | interleave interpose into iterate 8 | keep keep-indexed 9 | map mapcat map-indexed 10 | merge next nth partition-all 11 | range reduce reductions 12 | rest seq some sort sort-by split-with 13 | take take-while throw]) 14 | (:require [rx.lang.clojure.interop :as iop] 15 | [rx.lang.clojure.graph :as graph] 16 | [rx.lang.clojure.realized :as realized]) 17 | (:import [rx 18 | Observable 19 | Observer Observable$Operator Observable$OnSubscribe 20 | Subscriber Subscription] 21 | [rx.observables 22 | BlockingObservable 23 | GroupedObservable] 24 | [rx.subscriptions Subscriptions] 25 | [rx.functions Action0 Action1 Func0 Func1 Func2])) 26 | 27 | (set! *warn-on-reflection* true) 28 | 29 | (declare concat* concat map* map map-indexed reduce take take-while) 30 | 31 | (defn ^Func1 fn->predicate 32 | "Turn f into a predicate that returns true/false like Rx predicates should" 33 | [f] 34 | (iop/fn* (comp boolean f))) 35 | 36 | ;################################################################################ 37 | 38 | (defn observable? 39 | "Returns true if o is an rx.Observable" 40 | [o] 41 | (instance? Observable o)) 42 | 43 | ;################################################################################ 44 | 45 | (defn on-next 46 | "Call onNext on the given observer and return o." 47 | [^Observer o value] 48 | (.onNext o value) 49 | o) 50 | 51 | (defn on-completed 52 | "Call onCompleted on the given observer and return o." 53 | [^Observer o] 54 | (.onCompleted o) 55 | o) 56 | 57 | (defn on-error 58 | "Call onError on the given observer and return o." 59 | [^Observer o e] 60 | (.onError o e) 61 | o) 62 | 63 | (defmacro catch-error-value 64 | "Experimental 65 | 66 | TODO: Better name, better abstraction. 67 | 68 | Evaluate body and return its value. If an exception e is thrown, inject the 69 | given value into the exception's cause and call (on-error error-observer e), 70 | returning e. 71 | 72 | This is meant to facilitate implementing Observers that call user-supplied code 73 | safely. The general pattern is something like: 74 | 75 | (fn [o v] 76 | (rx/catch-error-value o v 77 | (rx/on-next o (some-func v)))) 78 | 79 | If (some-func v) throws an exception, it is caught, v is injected into the 80 | exception's cause (with OnErrorThrowable/addValueAsLastCause) and 81 | (rx/on-error o e) is invoked. 82 | 83 | See: 84 | rx.exceptions.OnErrorThrowable/addValueAsLastCause 85 | " 86 | [error-observer value & body] 87 | `(try 88 | ~@body 89 | (catch Throwable e# 90 | (on-error ~error-observer 91 | (rx.exceptions.OnErrorThrowable/addValueAsLastCause e# ~value)) 92 | e#))) 93 | 94 | ;################################################################################ 95 | ; Tools for creating new operators and observables 96 | 97 | (declare unsubscribed?) 98 | 99 | (defn ^Subscriber subscriber 100 | "Experimental, subject to change or deletion." 101 | ([o on-next-action] (subscriber o on-next-action nil nil)) 102 | ([o on-next-action on-error-action] (subscriber o on-next-action on-error-action nil)) 103 | ([^Subscriber o on-next-action on-error-action on-completed-action] 104 | (proxy [Subscriber] [o] 105 | (onCompleted [] 106 | (if on-completed-action 107 | (on-completed-action o) 108 | (on-completed o))) 109 | (onError [e] 110 | (if on-error-action 111 | (on-error-action o e) 112 | (on-error o e))) 113 | (onNext [t] 114 | (if on-next-action 115 | (on-next-action o t) 116 | (on-next o t)))))) 117 | 118 | (defn ^Subscription subscription 119 | "Create a new subscription that calls the given no-arg handler function when 120 | unsubscribe is called 121 | 122 | See: 123 | rx.subscriptions.Subscriptions/create 124 | " 125 | [handler] 126 | (Subscriptions/create ^Action0 (iop/action* handler))) 127 | 128 | (defn ^Observable$Operator operator* 129 | "Experimental, subject to change or deletion. 130 | 131 | Returns a new implementation of rx.Observable$Operator that calls the given 132 | function with a rx.Subscriber. The function should return a rx.Subscriber. 133 | 134 | See: 135 | lift 136 | rx.Observable$Operator 137 | " 138 | [f] 139 | {:pre [(fn? f)]} 140 | (reify Observable$Operator 141 | (call [this o] 142 | (f o)))) 143 | 144 | (defn ^Observable observable* 145 | "Create an Observable from the given function. 146 | 147 | When subscribed to, (f subscriber) is called at which point, f can start emitting values, etc. 148 | The passed subscriber is of type rx.Subscriber. 149 | 150 | See: 151 | rx.Subscriber 152 | rx.Observable/create 153 | " 154 | [f] 155 | (Observable/create ^Observable$OnSubscribe (iop/action* f))) 156 | 157 | (defn wrap-on-completed 158 | "Wrap handler with code that automaticaly calls rx.Observable.onCompleted." 159 | [handler] 160 | (fn [^Observer observer] 161 | (handler observer) 162 | (when-not (unsubscribed? observer) 163 | (.onCompleted observer)))) 164 | 165 | (defn wrap-on-error 166 | "Wrap handler with code that automaticaly calls (on-error) if an exception is thrown" 167 | [handler] 168 | (fn [^Observer observer] 169 | (try 170 | (handler observer) 171 | (catch Throwable e 172 | (when-not (unsubscribed? observer) 173 | (.onError observer e)))))) 174 | 175 | (defn lift 176 | "Lift the Operator op over the given Observable xs 177 | 178 | Example: 179 | 180 | (->> my-observable 181 | (rx/lift (rx/operator ...)) 182 | ...) 183 | 184 | See: 185 | rx.Observable/lift 186 | operator 187 | " 188 | [^Observable$Operator op ^Observable xs] 189 | (.lift xs op)) 190 | 191 | ;################################################################################ 192 | 193 | (defn ^Subscription subscribe 194 | "Subscribe to the given observable. 195 | 196 | on-X-action is a normal clojure function. 197 | 198 | See: 199 | rx.Observable/subscribe 200 | " 201 | 202 | ([^Observable o on-next-action] 203 | (.subscribe o 204 | ^Action1 (iop/action* on-next-action))) 205 | 206 | ([^Observable o on-next-action on-error-action] 207 | (.subscribe o 208 | ^Action1 (iop/action* on-next-action) 209 | ^Action1 (iop/action* on-error-action))) 210 | 211 | ([^Observable o on-next-action on-error-action on-completed-action] 212 | (.subscribe o 213 | ^Action1 (iop/action* on-next-action) 214 | ^Action1 (iop/action* on-error-action) 215 | ^Action0 (iop/action* on-completed-action)))) 216 | 217 | (defn unsubscribe 218 | "Unsubscribe from Subscription s and return it." 219 | [^Subscription s] 220 | (.unsubscribe s) 221 | s) 222 | 223 | (defn subscribe-on 224 | "Cause subscriptions to the given observable to happen on the given scheduler. 225 | 226 | Returns a new Observable. 227 | 228 | See: 229 | rx.Observable/subscribeOn 230 | " 231 | [^rx.Scheduler s ^Observable xs] 232 | (.subscribeOn xs s)) 233 | 234 | (defn unsubscribe-on 235 | "Cause unsubscriptions from the given observable to happen on the given scheduler. 236 | 237 | Returns a new Observable. 238 | 239 | See: 240 | rx.Observable/unsubscribeOn 241 | " 242 | [^rx.Scheduler s ^Observable xs] 243 | (.unsubscribeOn xs s)) 244 | 245 | (defn unsubscribed? 246 | "Returns true if the given Subscription (or Subscriber) is unsubscribed. 247 | 248 | See: 249 | rx.Observable/create 250 | observable* 251 | " 252 | [^Subscription s] 253 | (.isUnsubscribed s)) 254 | 255 | ;################################################################################ 256 | ; Functions for creating Observables 257 | 258 | (defn ^Observable never 259 | "Returns an Observable that never emits any values and never completes. 260 | 261 | See: 262 | rx.Observable/never 263 | " 264 | [] 265 | (Observable/never)) 266 | 267 | (defn ^Observable empty 268 | "Returns an Observable that completes immediately without emitting any values. 269 | 270 | See: 271 | rx.Observable/empty 272 | " 273 | [] 274 | (Observable/empty)) 275 | 276 | (defn ^Observable return 277 | "Returns an observable that emits a single value. 278 | 279 | See: 280 | rx.Observable/just 281 | " 282 | [value] 283 | (Observable/just value)) 284 | 285 | (defn ^Observable seq->o 286 | "Make an observable out of some seq-able thing. The rx equivalent of clojure.core/seq." 287 | [xs] 288 | (if-let [s (clojure.core/seq xs)] 289 | (Observable/from ^Iterable s) 290 | (empty))) 291 | 292 | ;################################################################################ 293 | ; Operators 294 | 295 | (defn serialize 296 | "Serialize execution. 297 | 298 | See: 299 | rx.Observable/serialize 300 | " 301 | ([^Observable xs] 302 | (.serialize xs))) 303 | 304 | (defn merge* 305 | "Merge an Observable of Observables into a single Observable 306 | 307 | If you want clojure.core/merge, it's just this: 308 | 309 | (rx/reduce clojure.core/merge {} maps) 310 | 311 | See: 312 | merge 313 | merge-delay-error* 314 | rx.Observable/merge 315 | " 316 | [^Observable xs] 317 | (Observable/merge xs)) 318 | 319 | (defn ^Observable merge 320 | "Merge one or more Observables into a single observable. 321 | 322 | If you want clojure.core/merge, it's just this: 323 | 324 | (rx/reduce clojure.core/merge {} maps) 325 | 326 | See: 327 | merge* 328 | merge-delay-error 329 | rx.Observable/merge 330 | " 331 | [& os] 332 | (merge* (seq->o os))) 333 | 334 | (defn ^Observable merge-delay-error* 335 | "Same as merge*, but all values are emitted before errors are propagated" 336 | [^Observable xs] 337 | (Observable/mergeDelayError xs)) 338 | 339 | (defn ^Observable merge-delay-error 340 | "Same as merge, but all values are emitted before errors are propagated" 341 | [& os] 342 | (merge-delay-error* (seq->o os))) 343 | 344 | (defn cache 345 | "caches the observable value so that multiple subscribers don't re-evaluate it. 346 | 347 | See: 348 | rx.Observable/cache" 349 | [^Observable xs] 350 | (.cache xs)) 351 | 352 | (defn cons 353 | "cons x to the beginning of xs" 354 | [x xs] 355 | (concat (return x) xs)) 356 | 357 | (defn ^Observable concat 358 | "Concatenate the given Observables one after the another. 359 | 360 | Note that xs is separate Observables which are concatentated. To concatenate an 361 | Observable of Observables, use concat* 362 | 363 | See: 364 | rx.Observable/concat 365 | concat* 366 | " 367 | [& xs] 368 | (Observable/concat (seq->o xs))) 369 | 370 | (defn ^Observable concat* 371 | "Concatenate the given Observable of Observables one after another. 372 | 373 | See: 374 | rx.Observable/concat 375 | concat 376 | " 377 | [^Observable os] 378 | (Observable/concat os)) 379 | 380 | (defn count 381 | "Returns an Observable that emits the number of items is xs as a long. 382 | 383 | See: 384 | rx.Observable/countLong 385 | " 386 | [^Observable xs] 387 | (.countLong xs)) 388 | 389 | (defn cycle 390 | "Returns an Observable that emits the items of xs repeatedly, forever. 391 | 392 | TODO: Other sigs. 393 | 394 | See: 395 | rx.Observable/repeat 396 | clojure.core/cycle 397 | " 398 | [^Observable xs] 399 | (.repeat xs)) 400 | 401 | (defn distinct 402 | "Returns an Observable of the elements of Observable xs with duplicates 403 | removed. key-fn, if provided, is a one arg function that determines the 404 | key used to determined duplicates. key-fn defaults to identity. 405 | 406 | This implementation doesn't use rx.Observable/distinct because it doesn't 407 | honor Clojure's equality semantics. 408 | 409 | See: 410 | clojure.core/distinct 411 | " 412 | ([xs] (distinct identity xs)) 413 | ([key-fn ^Observable xs] 414 | (let [op (operator* (fn [o] 415 | (let [seen (atom #{})] 416 | (subscriber o 417 | (fn [o v] 418 | (let [key (key-fn v)] 419 | (when-not (contains? @seen key) 420 | (swap! seen conj key) 421 | (on-next o v))))))))] 422 | (lift op xs)))) 423 | 424 | (defn ^Observable do 425 | "Returns a new Observable that, for each x in Observable xs, executes (do-fn x), 426 | presumably for its side effects, and then passes x along unchanged. 427 | 428 | If do-fn throws an exception, that exception is emitted via onError and the sequence 429 | is finished. 430 | 431 | Example: 432 | 433 | (->> (rx/seq->o [1 2 3]) 434 | (rx/do println) 435 | ...) 436 | 437 | Will print 1, 2, 3. 438 | 439 | See: 440 | rx.Observable/doOnNext 441 | " 442 | [do-fn ^Observable xs] 443 | (.doOnNext xs (iop/action* do-fn))) 444 | 445 | (defn ^Observable drop 446 | [n ^Observable xs] 447 | (.skip xs n)) 448 | 449 | (defn ^Observable drop-while 450 | [p ^Observable xs] 451 | (.skipWhile xs (fn->predicate p))) 452 | 453 | (defn ^Observable every? 454 | "Returns an Observable that emits a single true value if (p x) is true for 455 | all x in xs. Otherwise emits false. 456 | 457 | See: 458 | clojure.core/every? 459 | rx.Observable/all 460 | " 461 | [p ^Observable xs] 462 | (.all xs (fn->predicate p))) 463 | 464 | (defn ^Observable filter 465 | [p ^Observable xs] 466 | (.filter xs (fn->predicate p))) 467 | 468 | (defn ^Observable first 469 | "Returns an Observable that emits the first item emitted by xs, or an 470 | empty Observable if xs is empty. 471 | 472 | See: 473 | rx.Observable/take(1) 474 | " 475 | [^Observable xs] 476 | (.take xs 1)) 477 | 478 | (defn ^Observable group-by 479 | "Returns an Observable of clojure.lang.MapEntry where the key is the result of 480 | (key-fn x) and the val is an Observable of x for each key. 481 | 482 | This returns a clojure.lang.MapEntry rather than rx.observables.GroupedObservable 483 | for some vague consistency with clojure.core/group-by and so that clojure.core/key, 484 | clojure.core/val and destructuring will work as expected. 485 | 486 | See: 487 | clojure.core/group-by 488 | rx.Observable/groupBy 489 | rx.observables.GroupedObservable 490 | " 491 | ([key-fn ^Observable xs] 492 | (->> (.groupBy xs (iop/fn* key-fn)) 493 | (map (fn [^GroupedObservable go] 494 | (clojure.lang.MapEntry. (.getKey go) go)))))) 495 | 496 | (defn interleave* 497 | "Returns an Observable of the first item in each Observable emitted by observables, then 498 | the second etc. 499 | 500 | observables is an Observable of Observables 501 | 502 | See: 503 | interleave 504 | clojure.core/interleave 505 | " 506 | [observables] 507 | (->> (map* #(seq->o %&) observables) 508 | (concat*))) 509 | 510 | (defn interleave 511 | "Returns an Observable of the first item in each Observable, then the second etc. 512 | 513 | Each argument is an individual Observable 514 | 515 | See: 516 | observable* 517 | clojure.core/interleave 518 | " 519 | [o1 & observables] 520 | (->> (apply map #(seq->o %&) o1 observables) 521 | (concat*))) 522 | 523 | (defn interpose 524 | "Returns an Observable of the elements of xs separated by sep 525 | 526 | See: 527 | clojure.core/interpose 528 | " 529 | [sep xs] 530 | (let [op (operator* (fn [o] 531 | (let [first? (atom true)] 532 | (subscriber o (fn [o v] 533 | (if-not (compare-and-set! first? true false) 534 | (on-next o sep)) 535 | (on-next o v))))))] 536 | (lift op xs))) 537 | 538 | (defn into 539 | "Returns an observable that emits a single value which is all of the 540 | values of from-observable conjoined onto to 541 | 542 | See: 543 | clojure.core/into 544 | rx.Observable/toList 545 | " 546 | [to ^Observable from] 547 | ; clojure.core/into uses transients if to is IEditableCollection 548 | ; I don't think we have any guarantee that all on-next calls will be on the 549 | ; same thread, so we can't do that here. 550 | (reduce conj to from)) 551 | 552 | (defn iterate 553 | "Returns an Observable of x, (f x), (f (f x)) etc. f must be free of side-effects 554 | 555 | See: 556 | clojure.core/iterate 557 | " 558 | [f x] 559 | (observable* (fn [s] 560 | (loop [x x] 561 | (when-not (unsubscribed? s) 562 | (on-next s x) 563 | (recur (f x))))))) 564 | 565 | (defn keep 566 | [f xs] 567 | (filter (complement nil?) (map f xs))) 568 | 569 | (defn keep-indexed 570 | [f xs] 571 | (filter (complement nil?) (map-indexed f xs))) 572 | 573 | (defn ^Observable map* 574 | "Map a function over an Observable of Observables. 575 | 576 | Each item from the first emitted Observable is the first arg, each 577 | item from the second emitted Observable is the second arg, and so on. 578 | 579 | See: 580 | map 581 | clojure.core/map 582 | rx.Observable/zip 583 | " 584 | [f ^Observable observable] 585 | (Observable/zip observable 586 | ^rx.functions.FuncN (iop/fnN* f))) 587 | 588 | (defn ^Observable map 589 | "Map a function over one or more observable sequences. 590 | 591 | Each item from the first Observable is the first arg, each item 592 | from the second Observable is the second arg, and so on. 593 | 594 | See: 595 | clojure.core/map 596 | rx.Observable/zip 597 | " 598 | [f & observables] 599 | (Observable/zip ^Iterable observables 600 | ^rx.functions.FuncN (iop/fnN* f))) 601 | 602 | (defn ^Observable mapcat* 603 | "Same as multi-arg mapcat, but input is an Observable of Observables. 604 | 605 | See: 606 | mapcat 607 | clojure.core/mapcat 608 | " 609 | [f ^Observable xs] 610 | (->> xs 611 | (map* f) 612 | (concat*))) 613 | 614 | (defn ^Observable mapcat 615 | "Returns an observable which, for each value x in xs, calls (f x), which must 616 | return an Observable. The resulting observables are concatentated together 617 | into one observable. 618 | 619 | WARNING: This operator, like clojure.core/mapcat, preserves ordering of the 620 | generated Observables. In an asynchronous context, this may cause unintended 621 | blocking. Try flatmap instead. 622 | 623 | If multiple Observables are given, the arguments to f are the first item from 624 | each observable, then the second item, etc. 625 | 626 | See: 627 | clojure.core/mapcat 628 | flatmap 629 | rx.Observable/concatMap 630 | " 631 | [f & xs] 632 | (if (clojure.core/next xs) 633 | (mapcat* f (seq->o xs)) 634 | ; use built-in flatMap for single-arg case 635 | (.concatMap ^Observable (clojure.core/first xs) (iop/fn* f)))) 636 | 637 | (defn ^Observable flatmap* 638 | "Same as multi-arg flatmap, but input is an Observable of Observables. 639 | 640 | See: 641 | flatmap 642 | " 643 | [f ^Observable xs] 644 | (->> xs 645 | (map* f) 646 | (merge*))) 647 | 648 | (defn ^Observable flatmap 649 | "Like mapcat, but the Observables produced by f are merged rather than concatenated. 650 | This behavior is preferable in asynchronous contexts where order is not important. 651 | 652 | See: 653 | mapcat 654 | rx.Observable/flatMap 655 | " 656 | [f & xs] 657 | (if (clojure.core/next xs) 658 | (flatmap* f (seq->o xs)) 659 | (.flatMap ^Observable (clojure.core/first xs) (iop/fn* f)))) 660 | 661 | (defn map-indexed 662 | "Returns an observable that invokes (f index value) for each value of the input 663 | observable. index starts at 0. 664 | 665 | See: 666 | clojure.core/map-indexed 667 | " 668 | [f xs] 669 | (let [op (operator* (fn [o] 670 | (let [n (atom -1)] 671 | (subscriber o 672 | (fn [o v] 673 | (catch-error-value o v 674 | (on-next o (f (swap! n inc) v))))))))] 675 | (lift op xs))) 676 | 677 | (def next 678 | "Returns an observable that emits all but the first element of the input observable. 679 | 680 | See: 681 | clojure.core/next 682 | " 683 | (partial drop 1)) 684 | 685 | (defn nth 686 | "Returns an Observable that emits the value at the index in the given 687 | Observable. nth throws an IndexOutOfBoundsException unless not-found 688 | is supplied. 689 | 690 | Note that the Observable is the *first* arg! 691 | " 692 | ([^Observable xs index] 693 | (.elementAt xs index)) 694 | ([^Observable xs index not-found] 695 | (.elementAtOrDefault xs index not-found))) 696 | 697 | (defn ^Observable partition-all 698 | "Returns an Observable of Observables of n items each, at offsets step 699 | apart. If step is not supplied, defaults to n, i.e. the partitions 700 | do not overlap. May include partitions with fewer than n items at the end. 701 | 702 | See: 703 | clojure.core/partition-all 704 | rx.Observable/window 705 | " 706 | ([n ^Observable xs] (.window xs (int n))) 707 | ([n step ^Observable xs] (.window xs (int n) (int step)))) 708 | 709 | (defn range 710 | "Returns an Observable nums from start (inclusive) to end 711 | (exclusive), by step, where start defaults to 0, step to 1, and end 712 | to infinity. 713 | 714 | Note: this is not implemented on rx.Observable/range 715 | 716 | See: 717 | clojure.core/range 718 | " 719 | ([] (range 0 Double/POSITIVE_INFINITY 1)) 720 | ([end] (range 0 end 1)) 721 | ([start end] (range start end 1)) 722 | ([start end step] 723 | (observable* (fn [s] 724 | (let [comp (if (pos? step) < >)] 725 | (loop [i start] 726 | (if-not (unsubscribed? s) 727 | (if (comp i end) 728 | (do 729 | (on-next s i) 730 | (recur (+ i step))) 731 | (on-completed s))))))))) 732 | 733 | (defn ^Observable reduce 734 | ([f ^Observable xs] (.reduce xs (iop/fn* f))) 735 | ([f val ^Observable xs] (.reduce xs val (iop/fn* f)))) 736 | 737 | (defn ^Observable reductions 738 | ([f ^Observable xs] (.scan xs (iop/fn* f))) 739 | ([f val ^Observable xs] (.scan xs val (iop/fn* f)))) 740 | 741 | (def rest 742 | "Same as rx/next" 743 | next) 744 | 745 | (defn some 746 | "Returns an observable that emits the first logical true value of (pred x) for 747 | any x in xs, else completes immediately. 748 | 749 | See: 750 | clojure.core/some 751 | " 752 | [p ^Observable xs] 753 | (->> xs 754 | (map p) 755 | (filter identity) 756 | first)) 757 | 758 | (defn ^:private sorted-list-by 759 | ([keyfn coll] (sorted-list-by keyfn clojure.core/compare coll)) 760 | ([keyfn comp ^Observable coll] 761 | (.toSortedList coll (iop/fn [a b] 762 | ; force to int so rxjava doesn't have a fit 763 | (int (comp (keyfn a) (keyfn b))))))) 764 | 765 | (defn sort 766 | "Returns an observable that emits the items in xs, where the sort order is 767 | determined by comparing items. If no comparator is supplied, uses compare. 768 | comparator must implement java.util.Comparator. 769 | 770 | See: 771 | clojure.core/sort 772 | " 773 | ([xs] 774 | (sort clojure.core/compare xs)) 775 | ([comp xs] 776 | (->> xs 777 | (sorted-list-by identity comp) 778 | (mapcat seq->o)))) 779 | 780 | (defn sort-by 781 | "Returns an observable that emits the items in xs, where the sort order is 782 | determined by comparing (keyfn item). If no comparator is supplied, uses 783 | compare. comparator must implement java.util.Comparator. 784 | 785 | See: 786 | clojure.core/sort-by 787 | " 788 | ([keyfn xs] 789 | (sort-by keyfn clojure.core/compare xs)) 790 | ([keyfn comp ^Observable xs] 791 | (->> xs 792 | (sorted-list-by keyfn comp) 793 | (mapcat seq->o)))) 794 | 795 | (defn split-with 796 | "Returns an observable that emits a pair of observables 797 | 798 | [(take-while p xs) (drop-while p xs)] 799 | 800 | See: 801 | rx.lang.clojure/take-while 802 | rx.lang.clojure/drop-while 803 | clojure.core/split-with 804 | " 805 | [p xs] 806 | (return [(take-while p xs) (drop-while p xs)])) 807 | 808 | (defn ^Observable take 809 | "Returns an observable that emits the first n elements of xs. 810 | 811 | See: 812 | clojure.core/take 813 | " 814 | [n ^Observable xs] 815 | {:pre [(>= n 0)]} 816 | (.take xs n)) 817 | 818 | (defn take-while 819 | "Returns an Observable that emits xs until the first x such that 820 | (p x) is falsey. 821 | 822 | See: 823 | clojure.core/take-while 824 | rx.Observable/takeWhile 825 | " 826 | [p ^Observable xs] 827 | (.takeWhile xs (fn->predicate p))) 828 | 829 | ;################################################################################; 830 | 831 | (defn throw 832 | "Returns an Observable the simply emits the given exception with on-error 833 | 834 | See: 835 | rx.Observable/error 836 | " 837 | [^Throwable e] 838 | (Observable/error e)) 839 | 840 | (defn catch* 841 | "Returns an observable that, when Observable o triggers an error, e, continues with 842 | Observable returned by (f e) if (p e) is true. If (p e) returns a Throwable 843 | that value is passed as e. 844 | 845 | If p is a class object, a normal instance? check is performed rather than calling it 846 | as a function. If the value returned by (p e) is not true, the error is propagated. 847 | 848 | Examples: 849 | 850 | (->> my-observable 851 | 852 | ; On IllegalArgumentException, just emit 1 853 | (catch* IllegalArgumentException 854 | (fn [e] (rx/return 1))) 855 | 856 | ; If exception message contains \"WAT\", emit [\\W \\A \\T] 857 | (catch* (fn [e] (-> e .getMessage (.contains \"WAT\"))) 858 | (fn [e] (rx/seq->o [\\W \\A \\T])))) 859 | 860 | See: 861 | rx.Observable/onErrorResumeNext 862 | http://netflix.github.io/RxJava/javadoc/rx/Observable.html#onErrorResumeNext(rx.functions.Func1) 863 | " 864 | [p f ^Observable o] 865 | (let [p (if (class? p) 866 | (fn [e] (.isInstance ^Class p e)) 867 | p)] 868 | (.onErrorResumeNext o 869 | ^Func1 (iop/fn [e] 870 | (if-let [maybe-e (p e)] 871 | (f (if (instance? Throwable maybe-e) 872 | maybe-e 873 | e)) 874 | (rx.lang.clojure.core/throw e)))))) 875 | 876 | (defmacro catch 877 | "Macro version of catch*. 878 | 879 | The body of the catch is wrapped in an implicit (do). It must evaluate to an Observable. 880 | 881 | Note that the source observable is the last argument so this works with ->> but may look 882 | slightly odd when used standalone. 883 | 884 | Example: 885 | 886 | (->> my-observable 887 | ; just emit 0 on IllegalArgumentException 888 | (catch IllegalArgumentException e 889 | (rx/return 0)) 890 | 891 | (catch DependencyException e 892 | (if (.isMinor e) 893 | (rx/return 0) 894 | (rx/throw (WebException. 503))))) 895 | 896 | See: 897 | catch* 898 | " 899 | {:arglists '([p binding & body observable])} 900 | [p binding & body] 901 | (let [o (last body) 902 | body (butlast body)] 903 | `(catch* ~p 904 | (fn [~binding] ~@body) 905 | ~o))) 906 | 907 | (defn finally* 908 | "Returns an Observable that, as a side-effect, executes (f) when the given 909 | Observable completes regardless of success or failure. 910 | 911 | Example: 912 | 913 | (->> my-observable 914 | (finally* (fn [] (println \"Done\")))) 915 | 916 | " 917 | [f ^Observable o] 918 | (.finallyDo o ^Action0 (iop/action* f))) 919 | 920 | (defmacro finally 921 | "Macro version of finally*. 922 | 923 | Note that the source observable is the last argument so this works with ->> but may look 924 | slightly odd when used standalone. 925 | 926 | Example: 927 | 928 | (->> my-observable 929 | (finally (println \"Done\"))) 930 | 931 | See: 932 | finally* 933 | " 934 | {:arglists '([& body observable])} 935 | [& body] 936 | (let [o (last body) 937 | body (butlast body)] 938 | `(finally* (fn [] ~@body) ~o))) 939 | 940 | ;################################################################################; 941 | 942 | (defn generator* 943 | "Creates an observable that calls (f observer & args) which should emit values 944 | with (rx/on-next observer value). 945 | 946 | Automatically calls on-completed on return, or on-error if any exception is thrown. 947 | 948 | f should exit early if (rx/unsubscribed? observable) returns true 949 | 950 | Examples: 951 | 952 | ; An observable that emits just 99 953 | (rx/generator* on-next 99) 954 | " 955 | [f & args] 956 | (observable* (-> #(apply f % args) 957 | wrap-on-completed 958 | wrap-on-error))) 959 | 960 | (defmacro generator 961 | "Create an observable that executes body which should emit values with 962 | (rx/on-next observer value) where observer comes from bindings. 963 | 964 | Automatically calls on-completed on return, or on-error if any exception is thrown. 965 | 966 | The body should exit early if (rx/unsubscribed? observable) returns true 967 | 968 | Examples: 969 | 970 | ; make an observer that emits [0 1 2 3 4] 971 | (generator [observer] 972 | (dotimes [i 5] 973 | (on-next observer i))) 974 | 975 | " 976 | [bindings & body] 977 | `(generator* (fn ~bindings ~@body))) 978 | 979 | ;################################################################################; 980 | 981 | ; Import public graph symbols here. I want them in this namespace, but implementing 982 | ; them here with all the clojure.core symbols excluded is a pain. 983 | (intern *ns* (with-meta 'let-o* (meta #'graph/let-o*)) @#'graph/let-o*) 984 | (intern *ns* (with-meta 'let-o (meta #'graph/let-o)) @#'graph/let-o) 985 | 986 | ;################################################################################; 987 | 988 | ; Import some public realized symbols here. I want them in this namespace, but implementing 989 | ; them here with all the clojure.core symbols excluded is a pain. 990 | (intern *ns* (with-meta 'let-realized (meta #'realized/let-realized)) @#'realized/let-realized) 991 | 992 | --------------------------------------------------------------------------------