├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── project.clj ├── src └── clojure_erlastic │ └── core.clj └── test └── clojure_erlastic └── core_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | pom.xml.asc 3 | /lib/ 4 | /classes/ 5 | /target/ 6 | /checkouts/ 7 | .lein-deps-sum 8 | .lein-repl-history 9 | .lein-plugins/ 10 | .lein-failures 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.3.0 4 | 5 | * Enhancements 6 | * add `config` option to every functions 7 | * handle config for elixir or erlang conventions (string and nil) 8 | * 3 modes available for string detection : respectively to erlang/elixir, 9 | decode all list/binary as string, never decode as string, or autodetect 10 | with a test on a configurable number of first elem/bytes. 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Arnaud Wetzel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | clojure-erlastic 2 | ================ 3 | 4 | ![Clojars Project](http://clojars.org/clojure-erlastic/latest-version.svg) 5 | 6 | Micro lib making use of erlang JInterface lib to decode and encode Binary 7 | Erlang Term and simple erlang port interface with core.async channel. So you 8 | can communicate with erlang coroutine with clojure abstraction 9 | 10 | Designed to be used (but not necessarily) with 11 | [https://github.com/awetzel/exos](https://github.com/awetzel/exos). 12 | 13 | Last version of JInterface (from erlang 17.0) is taken from google scalaris 14 | maven repo. 15 | 16 | ## Usage 17 | 18 | `port-connection` creates two channels that you can use to 19 | communicate respectively in and out with the calling erlang port. 20 | The objects you put or receive throught these channels are encoded 21 | and decoded into erlang binary term following these rules : 22 | 23 | - erlang atom is clojure keyword 24 | - erlang list is clojure list 25 | - erlang tuple is clojure vector 26 | - erlang binary is clojure bytes[] 27 | - erlang integer is clojure long 28 | - erlang float is clojure double 29 | - erlang map is clojure map (thanks to erlang 17.0) 30 | - clojure set is erlang list 31 | 32 | Conversion of nil and string are configurable : every functions 33 | `port-connection`, `decode`, `encode`, `run-server` can take an optional 34 | `config` argument : a map defining 3 configs `:convention`, `:str-detect`, `:str-autodetect-len`. 35 | 36 | - if `(= :convention :elixir)` then : 37 | - clojure nil is erlang `nil` atom, so elixir `nil` 38 | - clojure string is encoded into erlang utf8 binary 39 | - erlang binaries are decoded into clojure string : 40 | - always if `(= :str-detect :all)` 41 | - never if `(= :str-detect :none)` 42 | - if the "str-autodetect-len" first bytes are printable when `(= :str-detect :auto)` 43 | - if `(= :convention :erlang)` then : 44 | - clojure nil is erlang `undefined` 45 | - clojure string is encoded into erlang integer list 46 | - erlang lists are decoded into clojure string : 47 | - always if `(= :str-detect :all)` 48 | - never if `(= :str-detect :none)` 49 | - if the "str-autodetect-len" first elems are printable when `(= :str-detect :auto)` 50 | 51 | - default config is Elixir convention with no str detection. 52 | 53 | For instance, here is a simple echo server : 54 | 55 | ```clojure 56 | (let [[in out] (clojure-erlastic.core/port-connection)] 57 | (! out ( mkdir calculator; cd calculator 66 | 67 | > vim project.clj 68 | 69 | ```clojure 70 | (defproject calculator "0.0.1" 71 | :dependencies [[clojure-erlastic "0.1.4"] 72 | [org.clojure/core.match "0.2.1"]]) 73 | ``` 74 | 75 | > lein uberjar 76 | 77 | Then create your clojure server as a simple script 78 | 79 | > vim calculator.clj 80 | 81 | ```clojure 82 | (require '[clojure.core.async :as async :refer [! ! out num) (recur num))))))) 93 | ``` 94 | 95 | Finally launch the clojure server as a port, do not forget the `:binary` and `{:packet,4}` options, mandatory, then convert sent and received terms with `:erlang.binary_to_term` and `:erlang.term_to_binary`. 96 | 97 | > vim calculator.exs 98 | 99 | ```elixir 100 | defmodule CljPort do 101 | def start, do: 102 | Port.open({:spawn,'java -cp target/calculator-0.0.1-standalone.jar clojure.main calculator.clj'},[:binary, packet: 4]) 103 | def psend(port,data), do: 104 | send(port,{self,{:command,:erlang.term_to_binary(data)}}) 105 | def preceive(port), do: 106 | receive(do: ({^port,{:data,b}}->:erlang.binary_to_term(b))) 107 | end 108 | port = CljPort.start 109 | CljPort.psend(port, {:add,3}) 110 | CljPort.psend(port, {:rem,2}) 111 | CljPort.psend(port, {:add,5}) 112 | CljPort.psend(port, :get) 113 | 6 = CljPort.preceive(port) 114 | ``` 115 | 116 | > elixir calculator.exs 117 | 118 | ## OTP integration ## 119 | 120 | If you want to integrate your clojure server in your OTP application, use the 121 | `priv` directory which is copied 'as is'. 122 | 123 | ```bash 124 | mix new myapp ; cd myapp 125 | mkdir -p priv/calculator 126 | vim priv/calculator/project.clj # define dependencies 127 | vim priv/calculator/calculator.clj # write your server 128 | cd priv/calculator ; lein uberjar ; cd ../../ # build the jar 129 | ``` 130 | 131 | Then use `"#{:code.priv_dir(:myapp)}/calculator"` to find correct path in your app. 132 | 133 | To easily use your clojure server, link the opened port in a GenServer, to 134 | ensure that if java crash, then the genserver crash and can be restarted by its 135 | supervisor. 136 | 137 | > vim lib/calculator.ex 138 | 139 | ```elixir 140 | defmodule Calculator do 141 | use GenServer 142 | def start_link, do: GenServer.start_link(__MODULE__, nil, name: __MODULE__) 143 | def init(nil) do 144 | Process.flag(:trap_exit, true) 145 | cd = "#{:code.priv_dir(:myapp)}/calculator" 146 | cmd = "java -cp 'target/*' clojure.main calculator.clj" 147 | {:ok,Port.open({:spawn,'#{cmd}'},[:binary, packet: 4, cd: cd])} 148 | end 149 | def handle_info({:EXIT,port,_},port), do: exit(:port_terminated) 150 | 151 | def handle_cast(term,port) do 152 | send(port,{self,{:command,:erlang.term_to_binary(term)}}) 153 | {:noreply,port} 154 | end 155 | 156 | def handle_call(term,_,port) do 157 | send(port,{self,{:command,:erlang.term_to_binary(term)}}) 158 | result = receive do {^port,{:data,b}}->:erlang.binary_to_term(b) end 159 | {:reply,result,port} 160 | end 161 | end 162 | ``` 163 | 164 | Then create the OTP application and its root supervisor launching `Calculator`. 165 | 166 | > vim mix.exs 167 | 168 | ```elixir 169 | def application do 170 | [mod: { Myapp, [] }, 171 | applications: []] 172 | end 173 | ``` 174 | 175 | > vim lib/myapp.ex 176 | 177 | ```elixir 178 | defmodule Myapp do 179 | use Application 180 | def start(_type, _args), do: Myapp.Sup.start_link 181 | 182 | defmodule Sup do 183 | use Supervisor 184 | def start_link, do: :supervisor.start_link(__MODULE__,nil) 185 | def init(nil), do: 186 | supervise([worker(Calculator,[])], strategy: :one_for_one) 187 | end 188 | end 189 | ``` 190 | 191 | Then you can launch and test your application in the shell : 192 | 193 | ``` 194 | iex -S mix 195 | iex(1)> GenServer.call Calculator,:get 196 | 0 197 | iex(2)> GenServer.cast Calculator,{:add, 3} 198 | :ok 199 | iex(3)> GenServer.cast Calculator,{:add, 3} 200 | :ok 201 | iex(4)> GenServer.cast Calculator,{:add, 3} 202 | :ok 203 | iex(5)> GenServer.cast Calculator,{:add, 3} 204 | :ok 205 | iex(6)> GenServer.call Calculator,:get 206 | 12 207 | ``` 208 | ## Handle exit 209 | 210 | The channels are closed when the launching erlang application dies, so you just 211 | have to test if `(! ! ! in b))))) 124 | (catch Exception e (do 125 | (log "receive error : " (type e) " " (.getMessage e)) 126 | (close! out) (close! in))))) 127 | (go ;; term sender coroutine 128 | (loop [] 129 | (when-let [term (! out (res 1)) (recur (res 2))) 151 | :noreply (recur (res 1)) 152 | :stop (res 1))) 153 | :normal))))))) 154 | -------------------------------------------------------------------------------- /test/clojure_erlastic/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-erlastic.core-test 2 | (:require clojure-erlastic.core [clojure.test :refer :all])) 3 | 4 | (deftest encoding-test 5 | (let [conf {:str-detect :none :convention :elixir :str-autodetect-len 10} 6 | encode (fn [obj] (clojure-erlastic.core/encode obj conf)) 7 | decode (fn [obj] (clojure-erlastic.core/decode obj conf))] 8 | (testing "Keyword encoding" 9 | (is (= (-> :toto encode decode) :toto))) 10 | (testing "Map encoding" 11 | (is (= (-> {:a :b :c :d} encode decode) {:a :b :c :d}))) 12 | (testing "Vector encoding" 13 | (is (= (-> [:a :b :c :d] encode decode) [:a :b :c :d]))) 14 | (testing "List encoding" 15 | (is (= (-> '(:a :b :c :d) encode decode) '(:a :b :c :d)))) 16 | (testing "Set encoding : not bijective, set is list" 17 | (is (= (-> #{:a :b :c :d} encode decode set) #{:a :b :c :d}))) 18 | (testing "Float encoding" 19 | (is (= (-> 4.3 encode decode) 4.3))) 20 | (testing "Bool encoding" 21 | (is (= (-> true encode decode) true))) 22 | (testing "Binary encoding" 23 | (is (= (-> (byte-array (repeat 4 0)) encode decode seq) (seq (byte-array (repeat 4 0)))))) 24 | (testing "Nil encoding" 25 | (is (= (-> nil encode decode) nil))) 26 | (testing "Char encoding" 27 | (is (= (-> \c encode decode char) \c))) 28 | (testing "Elixir convention : string is binary" 29 | (is (= (-> "toto" encode decode String.) "toto"))) 30 | (testing "Elixir convention : string codec not reflexive" 31 | (is (not= (-> "toto" encode decode) "toto"))) 32 | (testing "Elixir convention : nil is :nil" 33 | (is (= (-> nil encode .atomValue) "nil"))))) 34 | 35 | (deftest elixir-str-detect 36 | (let [conf {:str-detect :auto :convention :elixir :str-autodetect-len 10} 37 | encode (fn [obj] (clojure-erlastic.core/encode obj conf)) 38 | decode (fn [obj] (clojure-erlastic.core/decode obj conf))] 39 | (testing "Elixir : string is binary and reflexive codec" 40 | (is (= (-> "€é;-[" encode decode) "€é;-["))) 41 | (testing "Elixir : works if char split by detect len" 42 | (is (= (-> "€€€€" encode decode) "€€€€"))) 43 | (testing "Elixir: no string when broken utf8" 44 | (is (not= (type (decode (byte-array '(116 111 226 130)))) java.lang.String))))) 45 | 46 | (deftest erlang-str-detect 47 | (let [conf {:str-detect :auto :convention :erlang :str-autodetect-len 10} 48 | encode (fn [obj] (clojure-erlastic.core/encode obj conf)) 49 | decode (fn [obj] (clojure-erlastic.core/decode obj conf))] 50 | (testing "erlang : string encode to list" 51 | (is (= (-> "€é;-[" encode (.elements) first .longValue) 8364))) 52 | (testing "erlang : string reflexive codec" 53 | (is (= (-> "€é;-[" encode decode) "€é;-["))) 54 | (testing "erlang : works if char split by detect len" 55 | (is (= (-> "€€€€" encode decode) "€€€€"))) 56 | (testing "Elixir: no string when broken utf8" 57 | (is (not= (type (decode (byte-array '(116 111 226 130)))) java.lang.String))))) 58 | --------------------------------------------------------------------------------