├── .formatter.exs ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── config └── config.exs ├── lib ├── exmqttc.ex ├── exmqttc_callback.ex └── exmqttc_test_client.ex ├── mix.exs ├── mix.lock └── test ├── exmqttc_regex_test.exs ├── exmqttc_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | /docs 13 | 14 | # Ignore .fetch files in case you like to edit your project deps locally. 15 | /.fetch 16 | 17 | # If the VM crashes, it generates a dump, let's ignore it too. 18 | erl_crash.dump 19 | 20 | # Also ignore archive artifacts (built via "mix archive.build"). 21 | *.ez 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | language: elixir 4 | addons: 5 | apt: 6 | sources: 7 | - sourceline: "ppa:mosquitto-dev/mosquitto-ppa" 8 | packages: 9 | - mosquitto 10 | elixir: 11 | - 1.6.3 12 | otp_release: 13 | - 20.2 14 | script: 15 | - mix test 16 | - mix credo 17 | - mix inch.report 18 | cache: 19 | directories: 20 | - deps 21 | - _build 22 | notifications: 23 | email: 24 | - tim@buchwaldt.ws 25 | deploy: 26 | provider: script 27 | script: >- 28 | mix deps.get && 29 | mix hex.config username "$HEX_USERNAME" && 30 | (mix hex.config encrypted_key "$HEX_ENCRYPTED_KEY" > /dev/null 2>&1) && 31 | (echo "$HEX_PASSPHRASE"\\nY | mix hex.publish) && 32 | mix clean && 33 | mix deps.clean --all 34 | on: 35 | tags: true 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Tim Buchwaldt 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Exmqttc [![Deps Status](https://beta.hexfaktor.org/badge/all/github/timbuchwaldt/exmqttc.svg)](https://beta.hexfaktor.org/github/timbuchwaldt/exmqttc) [![Build Status](https://travis-ci.org/timbuchwaldt/exmqttc.svg?branch=master)](https://travis-ci.org/timbuchwaldt/exmqttc) [![Inline docs](http://inch-ci.org/github/timbuchwaldt/exmqttc.svg?branch=master)](http://inch-ci.org/github/timbuchwaldt/exmqttc) 2 | 3 | Elixir wrapper for the emqttc library. 4 | 5 | emqttc must currently be installed manually as it is not available on hex (yet). 6 | 7 | ## Installation 8 | 9 | The package can be installed by adding `exmqttc` and `emqttc` to your list of dependencies in `mix.exs`: 10 | 11 | ```elixir 12 | def deps do 13 | [{:exmqttc, "~> 0.5"}, {:emqttc, github: "emqtt/emqttc"}] 14 | end 15 | ``` 16 | 17 | 18 | ## Usage 19 | 20 | Create a callback module: 21 | ```elixir 22 | defmodule MyClient do 23 | require Logger 24 | use Exmqttc.Callback 25 | 26 | def init(_params) do 27 | {:ok, []} 28 | end 29 | 30 | def handle_connect(state) do 31 | Logger.debug "Connected" 32 | {:ok, state} 33 | end 34 | 35 | def handle_disconnect(state) do 36 | Logger.debug "Disconnected" 37 | {:ok, state} 38 | end 39 | 40 | def handle_publish(topic, payload, state) do 41 | Logger.debug "Message received on topic #{topic} with payload #{payload}" 42 | {:ok, state} 43 | end 44 | end 45 | ``` 46 | 47 | You can keep your own state and return it just like with `:gen_server`. 48 | 49 | Start the MQTT connection process by calling `start_link/3`: 50 | ```elixir 51 | {:ok, pid} = Exmqttc.start_link(MyClient, [], [host: '127.0.0.1'], params) 52 | ``` 53 | The third argument is a list of emqtt connection options, supporting the following options 54 | 55 | - `host`: Connection host, charlist, default: `'localhost'` 56 | - `port`: Connection port, integer, default 1883 57 | - `client_id`: Binary ID for client, automatically set to UUID if not specified 58 | - `clean_sess`: MQTT cleanSession flag. `true` disables persistent sessions on the server 59 | - `keepalive`: Keepalive timer, integer 60 | - `username`: Login username, binary 61 | - `password`: Login password, binary 62 | - `will`: Last will, keywordlist, sample: `[qos: 1, retain: false, topic: "WillTopic", payload: "I died"]` 63 | - `connack_timeout`: Timeout for connack package, integer, default 60 64 | - `puback_timeout`: Timeout for puback package, integer, default 8 65 | - `suback_timeout`: Timeout for suback package, integer, default 4 66 | - `ssl`: List of ssl options 67 | - `auto_resub`: Automatically resubscribe to topics, boolean, default: `false` 68 | - `reconnect`: Automatically reconnect on lost connection, integer (), default `false` 69 | 70 | Params are passed to the init function of your callback module. 71 | 72 | You can publish messages to the given PID: 73 | 74 | ```elixir 75 | Exmqttc.publish(pid, "test", "foo") 76 | ``` 77 | 78 | `publish/4` also supports passing in QOS and retain options: 79 | ```elixir 80 | Exmqttc.publish(pid, "test", "foo", qos: 2, retain: true) 81 | ``` 82 | 83 | Further docs can be found at [https://hexdocs.pm/exmqttc](https://hexdocs.pm/exmqttc). 84 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :exmqttc, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:exmqttc, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | config :lager, 24 | handlers: [ 25 | {:lager_console_backend, :error} 26 | ] 27 | 28 | # It is also possible to import configuration files, relative to this 29 | # directory. For example, you can emulate configuration per environment 30 | # by uncommenting the line below and defining dev.exs, test.exs and such. 31 | # Configuration from the imported file will override the ones defined 32 | # here (which is why it is important to import them last). 33 | # 34 | # import_config "#{Mix.env}.exs" 35 | -------------------------------------------------------------------------------- /lib/exmqttc.ex: -------------------------------------------------------------------------------- 1 | defmodule Exmqttc do 2 | use GenServer 3 | 4 | @moduledoc """ 5 | `Exmqttc` provides a connection to a MQTT server based on [emqttc](https://github.com/emqtt/emqttc) 6 | """ 7 | 8 | @typedoc """ 9 | A PID like type 10 | """ 11 | @type pidlike :: pid() | port() | atom() | {atom(), node()} 12 | 13 | @typedoc """ 14 | A QoS level 15 | """ 16 | @type qos :: :qos0 | :qos1 | :qos2 17 | 18 | @typedoc """ 19 | A single topic, a list of topics or a list of tuples of topic and QoS level 20 | """ 21 | @type topics :: String.t() | [String.t()] | [{String.t(), qos}] 22 | 23 | # API 24 | 25 | @doc """ 26 | Start the Exmqttc client. `callback_module` is used for callbacks and should implement the `Exmqttc.Callback` behaviour. 27 | `opts` are passed directly to GenServer. 28 | `mqtt_opts` are reformatted so all options can be passed in as a Keyworld list. 29 | Params are passed to your callbacks init function. 30 | 31 | `mqtt_opts` supports the following options: 32 | - `host`: Connection host, charlist, default: `'localhost'` 33 | - `port`: Connection port, integer, default 1883 34 | - `client_id`: Binary ID for client, automatically set to UUID if not specified 35 | - `clean_sess`: MQTT cleanSession flag. `true` disables persistent sessions on the server 36 | - `keepalive`: Keepalive timer, integer 37 | - `username`: Login username, binary 38 | - `password`: Login password, binary 39 | - `will`: Last will, keywordlist, sample: `[qos: 1, retain: false, topic: "WillTopic", payload: "I died"]` 40 | - `connack_timeout`: Timeout for connack package, integer, default 60 41 | - `puback_timeout`: Timeout for puback package, integer, default 8 42 | - `suback_timeout`: Timeout for suback package, integer, default 4 43 | - `ssl`: List of ssl options 44 | - `auto_resub`: Automatically resubscribe to topics, boolean, default: `false` 45 | - `reconnect`: Automatically reconnect on lost connection, integer (), default `false` 46 | 47 | """ 48 | def start_link(callback_module, opts \\ [], mqtt_opts \\ [], params \\ []) do 49 | # default client_id to new uuidv4 50 | GenServer.start_link(__MODULE__, [callback_module, mqtt_opts, params], opts) 51 | end 52 | 53 | @doc """ 54 | Subscribe to the given topic(s) given as `topics` with a given `qos`. 55 | """ 56 | @spec subscribe(pidlike, topics, qos) :: :ok 57 | def subscribe(pid, topics, qos \\ :qos0) do 58 | GenServer.call(pid, {:subscribe_topics, topics, qos}) 59 | end 60 | 61 | @doc """ 62 | Subscribe to the given topics while blocking until the subscribtion has been confirmed by the server. 63 | """ 64 | @spec sync_subscribe(pid, topics) :: :ok 65 | def sync_subscribe(pid, topics) do 66 | GenServer.call(pid, {:sync_subscribe_topics, topics}) 67 | end 68 | 69 | @doc """ 70 | Unsubscribe from the given topic(s) given as `topics`. 71 | """ 72 | @spec unsubscribe(pidlike, topics) :: :ok 73 | def unsubscribe(pid, topics) do 74 | GenServer.call(pid, {:unsubscribe_topics, topics}) 75 | end 76 | 77 | @doc """ 78 | Publish a message to MQTT. 79 | `opts` is a keywordlist and supports `:retain` with a boolean and `:qos` with an integer from 1 to 3 80 | """ 81 | @spec publish(pid, binary, binary, list) :: :ok 82 | def publish(pid, topic, payload, opts \\ []) do 83 | GenServer.call(pid, {:publish_message, topic, payload, opts}) 84 | end 85 | 86 | @doc """ 87 | Publish a message to MQTT synchronously. 88 | `opts` is a keywordlist and supports `:retain` with a boolean and `:qos` with an integer from 1 to 3 89 | """ 90 | @spec sync_publish(pid, binary, binary, list) :: :ok 91 | def sync_publish(pid, topic, payload, opts \\ []) do 92 | GenServer.call(pid, {:sync_publish_message, topic, payload, opts}) 93 | end 94 | 95 | @doc """ 96 | Disconnect socket from MQTT server 97 | """ 98 | @spec disconnect(pid) :: :ok 99 | def disconnect(pid) do 100 | GenServer.call(pid, :disconnect) 101 | end 102 | 103 | # GenServer callbacks 104 | def init([callback_module, opts, params]) do 105 | # start callback handler 106 | {:ok, callback_pid} = Exmqttc.Callback.start_link(callback_module, params) 107 | 108 | {:ok, mqtt_pid} = 109 | opts 110 | |> Keyword.put_new_lazy(:client_id, fn -> UUID.uuid4() end) 111 | |> map_options 112 | |> :emqttc.start_link() 113 | 114 | {:ok, {mqtt_pid, callback_pid}} 115 | end 116 | 117 | def handle_call({:sync_subscribe_topics, topics}, _from, {mqtt_pid, callback_pid}) do 118 | res = :emqttc.sync_subscribe(mqtt_pid, topics) 119 | {:reply, res, {mqtt_pid, callback_pid}} 120 | end 121 | 122 | def handle_call({:sync_publish_message, topic, payload, opts}, _from, {mqtt_pid, callback_pid}) do 123 | res = :emqttc.sync_publish(mqtt_pid, topic, payload, opts) 124 | {:reply, res, {mqtt_pid, callback_pid}} 125 | end 126 | 127 | def handle_call({:subscribe_topics, topics, qos}, _from, {mqtt_pid, callback_pid}) do 128 | :ok = :emqttc.subscribe(mqtt_pid, topics, qos) 129 | {:reply, :ok, {mqtt_pid, callback_pid}} 130 | end 131 | 132 | def handle_call({:unsubscribe_topics, topics}, _from, {mqtt_pid, callback_pid}) do 133 | :ok = :emqttc.unsubscribe(mqtt_pid, topics) 134 | {:reply, :ok, {mqtt_pid, callback_pid}} 135 | end 136 | 137 | def handle_call({:publish_message, topic, payload, opts}, _from, {mqtt_pid, callback_pid}) do 138 | :emqttc.publish(mqtt_pid, topic, payload, opts) 139 | {:reply, :ok, {mqtt_pid, callback_pid}} 140 | end 141 | 142 | def handle_call(:disconnect, _from, {mqtt_pid, callback_pid}) do 143 | :emqttc.disconnect(mqtt_pid) 144 | {:reply, :ok, {mqtt_pid, callback_pid}} 145 | end 146 | 147 | def handle_call(message, _from, state = {_mqtt_pid, callback_pid}) do 148 | reply = GenServer.call(callback_pid, message) 149 | {:reply, reply, state} 150 | end 151 | 152 | def handle_cast(message, state = {_mqtt_pid, callback_pid}) do 153 | GenServer.cast(callback_pid, message) 154 | {:noreply, state} 155 | end 156 | 157 | # emqttc messages 158 | 159 | def handle_info({:mqttc, _pid, :connected}, {mqtt_pid, callback_pid}) do 160 | GenServer.cast(callback_pid, :connect) 161 | {:noreply, {mqtt_pid, callback_pid}} 162 | end 163 | 164 | def handle_info({:mqttc, _pid, :disconnected}, {mqtt_pid, callback_pid}) do 165 | GenServer.cast(callback_pid, :disconnect) 166 | {:noreply, {mqtt_pid, callback_pid}} 167 | end 168 | 169 | def handle_info({:publish, topic, message}, {mqtt_pid, callback_pid}) do 170 | GenServer.cast(callback_pid, {:publish, topic, message}) 171 | {:noreply, {mqtt_pid, callback_pid}} 172 | end 173 | 174 | def handle_info(message, state = {_mqtt_pid, callback_pid}) do 175 | send(callback_pid, message) 176 | {:noreply, state} 177 | end 178 | 179 | # helpers 180 | 181 | defp map_options(input) do 182 | merged_defaults = Keyword.merge([logger: :error], input) 183 | 184 | Enum.map(merged_defaults, fn {key, value} -> 185 | if value == true do 186 | key 187 | else 188 | {key, value} 189 | end 190 | end) 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /lib/exmqttc_callback.ex: -------------------------------------------------------------------------------- 1 | defmodule Exmqttc.Callback do 2 | require Logger 3 | 4 | @moduledoc """ 5 | Behaviour module for Exmqttc callbacks 6 | """ 7 | use GenServer 8 | 9 | @doc """ 10 | Initializing the callback module, returned data is passed in as state on the next call. Params are taken from Exmqttc.start_link 11 | """ 12 | @callback init(params :: any) :: {:ok, state :: any()} 13 | 14 | @doc """ 15 | Called once a connection has been established. 16 | """ 17 | @callback handle_connect(state :: any()) :: {:ok, state :: any()} 18 | 19 | @doc """ 20 | Called on disconnection from the broker. 21 | """ 22 | @callback handle_disconnect(state :: any()) :: {:ok, state :: any()} 23 | 24 | @doc """ 25 | Called upon reception of a MQTT message, passes in topic and message. 26 | """ 27 | @callback handle_publish(topic :: String.t(), message :: String.t(), state :: any()) :: 28 | {:ok, state :: any()} 29 | 30 | @doc """ 31 | Called if the connection process or the callback handler process receive unknown `handle_call/3` calls. 32 | """ 33 | @callback handle_call(message :: term(), from :: {pid(), atom()}, state :: term()) :: 34 | {:ok, state :: term()} 35 | 36 | @doc """ 37 | Called if the connection process or the callback handler process receive unknown `handle_cast/2` calls. 38 | """ 39 | @callback handle_cast(message :: term(), state :: term()) :: 40 | {:ok, state :: term()} :: {:ok, state :: term()} 41 | 42 | @doc """ 43 | Called if the connection process or the callback handler process receive unknown `handle_info/2` calls, and by extend also unknown Elixir messages. 44 | """ 45 | @callback handle_info(message :: term(), state :: term()) :: 46 | {:ok, state :: term()} :: {:ok, state :: term()} 47 | 48 | @doc false 49 | defmacro __using__(_opts) do 50 | quote location: :keep do 51 | @behaviour Exmqttc.Callback 52 | def init(_params) do 53 | {:ok, []} 54 | end 55 | 56 | def handle_call(:test, _from, state) do 57 | {:ok, state} 58 | end 59 | 60 | def handle_cast(:test, state) do 61 | {:ok, state} 62 | end 63 | 64 | def handle_info(:test, state) do 65 | {:ok, state} 66 | end 67 | 68 | defoverridable init: 1, handle_call: 3, handle_info: 2, handle_cast: 2 69 | end 70 | end 71 | 72 | @doc false 73 | def start_link(module, params) do 74 | GenServer.start_link(__MODULE__, {module, self(), params}) 75 | end 76 | 77 | @doc false 78 | def init({cb, connection_pid, params}) do 79 | {:ok, state} = cb.init(params) 80 | {:ok, %{cb: cb, state: state, connection_pid: connection_pid}} 81 | end 82 | 83 | @doc false 84 | def handle_cast(:connect, %{cb: cb, state: state, connection_pid: connection_pid}) do 85 | {:ok, new_state} = cb.handle_connect(state) 86 | {:noreply, %{cb: cb, state: new_state, connection_pid: connection_pid}} 87 | end 88 | 89 | @doc false 90 | def handle_cast(:disconnect, %{cb: cb, state: state, connection_pid: connection_pid}) do 91 | {:ok, new_state} = cb.handle_disconnect(state) 92 | {:noreply, %{cb: cb, state: new_state, connection_pid: connection_pid}} 93 | end 94 | 95 | @doc false 96 | def handle_cast({:publish, topic, message}, %{ 97 | cb: cb, 98 | state: state, 99 | connection_pid: connection_pid 100 | }) do 101 | case cb.handle_publish(topic, message, state) do 102 | {:ok, new_state} -> 103 | {:noreply, %{cb: cb, state: new_state, connection_pid: connection_pid}} 104 | 105 | {:reply, reply_topic, reply_message, new_state} -> 106 | Exmqttc.publish(connection_pid, reply_topic, reply_message) 107 | {:noreply, %{cb: cb, state: new_state, connection_pid: connection_pid}} 108 | end 109 | end 110 | 111 | # Pass unknown casts through 112 | @doc false 113 | def handle_cast(message, %{cb: cb, state: state, connection_pid: connection_pid}) do 114 | {:ok, new_state} = cb.handle_cast(message, state) 115 | {:noreply, %{cb: cb, state: new_state, connection_pid: connection_pid}} 116 | end 117 | 118 | # Pass unknown calls through 119 | @doc false 120 | def handle_call(message, from, %{cb: cb, state: state, connection_pid: connection_pid}) do 121 | {:ok, new_state} = cb.handle_call(message, from, state) 122 | {:reply, :ok, %{cb: cb, state: new_state, connection_pid: connection_pid}} 123 | end 124 | 125 | # Pass unknown infos through 126 | @doc false 127 | def handle_info(message, %{cb: cb, state: state, connection_pid: connection_pid}) do 128 | {:ok, new_state} = cb.handle_info(message, state) 129 | {:noreply, %{cb: cb, state: new_state, connection_pid: connection_pid}} 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/exmqttc_test_client.ex: -------------------------------------------------------------------------------- 1 | defmodule Exmqttc.Testclient do 2 | @moduledoc false 3 | use Exmqttc.Callback 4 | 5 | def init(_params) do 6 | {:ok, []} 7 | end 8 | 9 | def handle_connect(state) do 10 | send(:testclient, :connected) 11 | {:ok, state} 12 | end 13 | 14 | def handle_disconnect(state) do 15 | send(:testclient, :disconnected) 16 | {:ok, state} 17 | end 18 | 19 | def handle_publish("reply_topic", payload, state) do 20 | send(:testclient, {:publish, "reply_topic", payload}) 21 | {:reply, "foobar", "testmessage", state} 22 | end 23 | 24 | def handle_publish(topic, payload, state) do 25 | send(:testclient, {:publish, topic, payload}) 26 | {:ok, state} 27 | end 28 | 29 | def handle_call(:test, _from, state) do 30 | send(:testclient, :test_call) 31 | {:ok, state} 32 | end 33 | 34 | def handle_cast(:test, state) do 35 | send(:testclient, :test_cast) 36 | {:ok, state} 37 | end 38 | 39 | def handle_info(:test, state) do 40 | send(:testclient, :test_info) 41 | {:ok, state} 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Exmqttc.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :exmqttc, 7 | version: "0.6.1", 8 | elixir: "~> 1.7", 9 | build_embedded: Mix.env() == :prod, 10 | start_permanent: Mix.env() == :prod, 11 | deps: deps(), 12 | package: package(), 13 | docs: [main: "readme.html#usage", extras: ["README.md"]] 14 | ] 15 | end 16 | 17 | def application do 18 | [extra_applications: [:logger]] 19 | end 20 | 21 | def package do 22 | %{ 23 | name: :exmqttc, 24 | files: ["lib", "mix.exs", "README*", "LICENSE*"], 25 | maintainers: ["Tim Buchwaldt"], 26 | description: 27 | "Elixir wrapper for the emqttc library. Some of the features: Reconnection, offline queueing, gen_* like callback APIs, QoS 0-2.", 28 | licenses: ["MIT"], 29 | links: %{"GitHub" => "https://github.com/timbuchwaldt/exmqttc"} 30 | } 31 | end 32 | 33 | defp deps do 34 | [ 35 | {:emqttc, github: "emqtt/emqttc", only: [:dev, :test]}, 36 | {:elixir_uuid, "~> 1.2"}, 37 | {:ex_doc, ">= 0.0.0", only: :dev}, 38 | {:dialyxir, "~> 0.5", only: :dev, runtime: false}, 39 | {:credo, "~> 0.10", only: [:dev, :test], runtime: false}, 40 | {:inch_ex, "~> 1.0", only: :dev} 41 | ] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 3 | "credo": {:hex, :credo, "0.10.2", "03ad3a1eff79a16664ed42fc2975b5e5d0ce243d69318060c626c34720a49512", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, 5 | "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, 6 | "elixir_uuid": {:hex, :elixir_uuid, "1.2.0", "ff26e938f95830b1db152cb6e594d711c10c02c6391236900ddd070a6b01271d", [:mix], [], "hexpm"}, 7 | "emqttc": {:git, "https://github.com/emqtt/emqttc.git", "9be166ff1c7deadb2763b5a27c1bba3c635fcee9", []}, 8 | "eqc_ex": {:hex, :eqc_ex, "1.4.2", "c89322cf8fbd4f9ddcb18141fb162a871afd357c55c8c0198441ce95ffe2e105", [:mix], []}, 9 | "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 10 | "gen_logger": {:git, "https://github.com/emqtt/gen_logger.git", "e3c9c60b153642193319c793501b10c263d76477", [branch: "master"]}, 11 | "goldrush": {:git, "https://github.com/basho/goldrush.git", "8f1b715d36b650ec1e1f5612c00e28af6ab0de82", [tag: "0.1.9"]}, 12 | "inch_ex": {:hex, :inch_ex, "1.0.1", "1f0af1a83cec8e56f6fc91738a09c838e858db3d78ef5f2ec040fe4d5a62dabf", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 13 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 14 | "lager": {:git, "https://github.com/basho/lager.git", "81eaef0ce98fdbf64ab95665e3bc2ec4b24c7dac", [branch: "master"]}, 15 | "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 16 | "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 17 | "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"}, 18 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, 19 | "qmttc": {:git, "https://github.com/emqtt/emqttc.git", "d4cbc38808fec0bda30be245268e876283c0e49f", []}, 20 | "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm"}, 21 | } 22 | -------------------------------------------------------------------------------- /test/exmqttc_regex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExmqttcRegexTest do 2 | use ExUnit.Case, async: false 3 | 4 | test "parsing a simple topic to regex" do 5 | topic = "test/123" 6 | {:ok, regex} = Exmqttc.TopicParser.compile(topic) 7 | assert Regex.match?(regex, topic) 8 | end 9 | 10 | test "parsing a + placeholder topic to regex" do 11 | topic = "test/+" 12 | {:ok, regex} = Exmqttc.TopicParser.compile(topic) 13 | assert Regex.match?(regex, "test/123") 14 | refute Regex.match?(regex, "test/123/321") 15 | end 16 | 17 | test "parsing a # placeholder topic to regex" do 18 | topic = "test/#" 19 | {:ok, regex} = Exmqttc.TopicParser.compile(topic) 20 | assert Regex.match?(regex, "test/123") 21 | assert Regex.match?(regex, "test/123/321") 22 | end 23 | end 24 | 25 | defmodule Exmqttc.TopicParser do 26 | def tokenize(topic) do 27 | String.split(topic, "/") 28 | end 29 | 30 | def compile(topic) do 31 | regex = 32 | topic 33 | |> tokenize 34 | |> Enum.map(&replace/1) 35 | |> Enum.join("/") 36 | 37 | Regex.compile("^#{regex}$") 38 | end 39 | 40 | def replace("+" <> _data) do 41 | "[a-zA-Z0-9_-]+" 42 | end 43 | 44 | def replace("#") do 45 | ".*" 46 | end 47 | 48 | def replace(input) do 49 | input 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/exmqttc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExmqttcTest do 2 | use ExUnit.Case, async: false 3 | doctest Exmqttc 4 | 5 | setup do 6 | Process.register(self(), :testclient) 7 | :ok 8 | end 9 | 10 | test "connecting with minimal options" do 11 | {:ok, pid} = Exmqttc.start_link(Exmqttc.Testclient, [], host: '127.0.0.1') 12 | assert_receive :connected, 250 13 | Exmqttc.disconnect(pid) 14 | end 15 | 16 | test "connecting with registered names" do 17 | {:ok, pid} = Exmqttc.start_link(Exmqttc.Testclient, [name: :my_client], host: '127.0.0.1') 18 | assert_receive :connected, 250 19 | Exmqttc.disconnect(pid) 20 | end 21 | 22 | test "connecting with enhanced options" do 23 | {:ok, pid} = 24 | Exmqttc.start_link( 25 | Exmqttc.Testclient, 26 | [name: :my_client_2], 27 | keepalive: 30, 28 | host: '127.0.0.1' 29 | ) 30 | 31 | assert_receive :connected, 250 32 | Exmqttc.disconnect(pid) 33 | end 34 | 35 | test "subscribing and sending" do 36 | {:ok, pid} = 37 | Exmqttc.start_link( 38 | Exmqttc.Testclient, 39 | [name: :my_client_3], 40 | keepalive: 30, 41 | host: '127.0.0.1' 42 | ) 43 | 44 | assert_receive :connected, 250 45 | 46 | Exmqttc.subscribe(:my_client_3, "test") 47 | Exmqttc.publish(:my_client_3, "test", "foo") 48 | assert_receive {:publish, "test", "foo"}, 250 49 | Exmqttc.disconnect(pid) 50 | end 51 | 52 | test "synchronous subscribing and sending" do 53 | {:ok, pid} = 54 | Exmqttc.start_link( 55 | Exmqttc.Testclient, 56 | [name: :my_client_4], 57 | keepalive: 30, 58 | host: '127.0.0.1' 59 | ) 60 | 61 | assert_receive :connected, 250 62 | 63 | Exmqttc.sync_subscribe(:my_client_4, "test2") 64 | Exmqttc.sync_publish(:my_client_4, "test2", "foo") 65 | assert_receive {:publish, "test2", "foo"}, 250 66 | Exmqttc.disconnect(pid) 67 | end 68 | 69 | test "unsubscribing and sending" do 70 | {:ok, pid} = 71 | Exmqttc.start_link( 72 | Exmqttc.Testclient, 73 | [name: :my_client_3], 74 | keepalive: 30, 75 | host: '127.0.0.1' 76 | ) 77 | 78 | assert_receive :connected, 250 79 | 80 | Exmqttc.subscribe(:my_client_3, "test") 81 | Exmqttc.publish(:my_client_3, "test", "foo") 82 | assert_receive {:publish, "test", "foo"}, 250 83 | 84 | Exmqttc.unsubscribe(:my_client_3, "test") 85 | Exmqttc.publish(:my_client_3, "test", "foo") 86 | refute_receive {:publish, "test", "foo"}, 250 87 | Exmqttc.disconnect(pid) 88 | end 89 | 90 | test "replying in the callback module" do 91 | {:ok, pid} = 92 | Exmqttc.start_link( 93 | Exmqttc.Testclient, 94 | [name: :my_client_6], 95 | keepalive: 30, 96 | host: '127.0.0.1' 97 | ) 98 | 99 | assert_receive :connected, 250 100 | 101 | Exmqttc.subscribe(:my_client_6, "reply_topic") 102 | Exmqttc.subscribe(:my_client_6, "foobar") 103 | Exmqttc.publish(:my_client_6, "reply_topic", "foo") 104 | assert_receive {:publish, "reply_topic", "foo"}, 250 105 | assert_receive {:publish, "foobar", "testmessage"}, 250 106 | Exmqttc.disconnect(pid) 107 | end 108 | 109 | test "passing through messages" do 110 | {:ok, pid} = 111 | Exmqttc.start_link( 112 | Exmqttc.Testclient, 113 | [name: :my_client_7], 114 | keepalive: 30, 115 | host: '127.0.0.1' 116 | ) 117 | 118 | assert_receive :connected, 250 119 | send(:my_client_7, :test) 120 | assert_receive :test_info, 250 121 | Exmqttc.disconnect(pid) 122 | end 123 | 124 | test "passing through calls" do 125 | {:ok, pid} = 126 | Exmqttc.start_link( 127 | Exmqttc.Testclient, 128 | [name: :my_client_8], 129 | keepalive: 30, 130 | host: '127.0.0.1' 131 | ) 132 | 133 | assert_receive :connected, 250 134 | GenServer.call(:my_client_8, :test) 135 | assert_receive :test_call, 250 136 | Exmqttc.disconnect(pid) 137 | end 138 | 139 | test "passing through casts" do 140 | {:ok, pid} = 141 | Exmqttc.start_link( 142 | Exmqttc.Testclient, 143 | [name: :my_client_9], 144 | keepalive: 30, 145 | host: '127.0.0.1' 146 | ) 147 | 148 | assert_receive :connected, 250 149 | GenServer.cast(:my_client_9, :test) 150 | assert_receive :test_cast, 250 151 | Exmqttc.disconnect(pid) 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------