Exampple

Build Status Coverage Status License: LGPL 2.1 Hex

eXaMPPle is a XMPP framework to build components using a router, controllers and an easy way to generate stanzas. It also has facilities to perform functional and system tests.

Installation

You can install the application for your project in the following way:

def deps do
[
{:exampple, github: "altenwald/exampple"}
]
end

Getting started

To use Exampple you only need to add the following information for the config/config.exs file:

config :myapp,
router: Myapp.Router
config :myapp, Exampple.Component,
domain: "mycomponent.example.com",
host: "localhost",
password: "guest",
ping: 30_000,
port: 5252,
set_from: true,
trimmed: true,
auto_connect: true

The configuration for you XMPP Server should accept the connection for a component in the port 5252, for the domain mycomponent.example.com (so, it's suppose your XMPP server is handling the example.com domain), both installed in the same machine.

After that, is a good idea to have inside of the supervisor the example server, usually it should be in your lib/myapp/application.ex file:

children = [
{Exampple.Component, [otp_app: :myapp]}
]

And a new module should be created, as mention the first part of the configuration, to define the router, in this example, something like lib/myapp/router.ex:

defmodule Myapp.Router do
use Exampple.Router
iq "jabber:iq" do
get "roster", Myapp.Xmpp.RosterController, :get
end
fallback Myapp.Xmpp.ErrorController, :error
end

This is a very small example with only two controllers. The construction tries to match as much as possible with the data provided in the incoming stanza:

And that's choosing a module and a function to be called. Which we call a controller.

The last file you need to create is the controller lib/myapp/xmpp/roster_controller.ex:

defmodule Myapp.Xmpp.RosterController do
use Exampple.Component
def get(conn, query) do
conn
|> iq_resp(query)
|> send()
end
end

Note that send/1 is performing the sent of the stanza and it's not based on the return like other frameworks like Phoenix. You can perform as many sent as you need.

This way we have a perfect echo. You can process the data coming in query and then perform a better output. Also, you can generate an error like the fallback we develop under lib/myapp/xmpp/error_controller.ex:

defmodule Myapp.Xmpp.ErrorController do
use Exampple.Component
def error(conn, _query) do
conn
|> iq_error("feature-not-implemented")
|> send()
end
end

Configuration

As we saw above, the configuration is split in two pars, the configuration of the router to be localized by Exampple inside of your project, and the configuration for the connection to the XMPP server.

The router localization is configured with these lines:

config :myapp,
router: Myapp.Router

Of course, you have to create the Myapp.Router module changing Myapp for the real name of your project or the base namespace you are using. We will see how to create the router in the Router section.

About the connection, the configuration is as follows:

config :myapp, Exampple.Component,
domain: "mycomponent.example.com",
host: "localhost",
password: "guest",
ping: 30_000,
port: 5252,
set_from: true,
trimmed: true,
auto_connect: true

The possible configuration entries are:

Choose your XMPP Server

Note that you only need a server which is supporting the XEP-0114. At the moment we can see there is available these servers:

The configuration for the server depends on each one, you can go to their respective websites and search the configuration for the components module.

Mix tasks

When you are creating a project using Exampple you will have available two new commands for mix:

$ mix xmpp.routes
iq get urn:xmpp:ping Myapp.Xmpp.PingController ping
iq get urn:xmpp:mam:2 Myapp.Xmpp.ArchivingController get
$ mix xmpp.namespaces
urn:xmpp:ping
urn:xmpp:mam:2

Routing

The router was created inspired by the router from Phoenix Framework. The router let us configure how the system handles the stanzas. Based on different matches:

With these we can route the stanzas to a specific module and function: the controllers. For example, if we have this routing file:

defmodule Myapp.Router do
use Exampple.Router
iq "urn:xmpp" do
get "ping", Myapp.Xmpp.PingController, :ping
get "mam:2", Myapp.Xmpp.ArchivingController, :get
end
iq "http://jabber.org/" do
join_with "/"
get "disco#info", Myapp.Xmpp.DiscoController, :info
get "disco#items", Myapp.Xmpp.DiscoController, :items
end
fallback Myapp.Xmpp.ErrorController, :error
end

The whole flow is as follows:

+-----------+ +----------+ +----------+ +----------+ +------------+
| | | | | | | | | |
+-->+ Component +-->+ Router +-->+ Task +-->+ CRoute +-->+ Controller |
| | | | | | | | | |
+-----------+ +----------+ +----------+ +----------+ +------------+

These elements are:

Matching Routes

In the configuration for the router we can specify different kind of routes. For example:

defmodule Myapp.Router do
use Exampple.Router
iq "urn:xmpp" do
get "ping", Myapp.Xmpp.PingController, :ping
end
presence do
available Myapp.Xmpp.PresenceController, :available
unavailable Myapp.Xmpp.PresenceController, :unavailable
end
message do
normal Myapp.Xmpp.MessageController, :normal
groupchat Myapp.Xmpp.GroupchatController, :message
end
end

The namespace is defined in two parts, in the stanza type we can set the base (e.g. in the first block defining "urn:xmpp") and in the type sentence, inside of the stanza block where we can see the last part (e.g. in the first block we can see inside "ping"). Both parts are merged using the connector which is by default :. If we need to change to another connector, like /, we can use inside of the stanza block:

join_with "/"

In addition, the namespace is optional, we can set the base for the namespace in the message, presence or iq main sections and then specify the completion for the namespace inside of the specific type. The way to perform the match is:

<iq type='get'>
<query xmlns='urn:xmpp:ping'/>
</iq>

This is the minimum message which is going to match with the first entry for the router which we declared above. This is going to parse the stanza to generate an Exampple.Router.Conn struct and then using the query in Exampple.Xml.Xmlel format the controller we implement as Myapp.Xmpp.PingController is going to be called using the function ping/2.

Fallback

When there is no match we can declare a special fallback:

defmodule Myapp.Router do
use Exampple.Router
iq "urn:xmpp:" do
get "ping", Myapp.Xmpp.PingController, :ping
end
fallback Myapp.Xmpp.ErrorController, :error
end

This is defining only the module and the function which will be called to handle the unknown stanza.

Discovery

To let us implement XEP-0030 in an easy way, we can use the following configuration inside of our router module:

defmodule Myapp.Router do
use Exampple.Router
discovery()
iq "urn:xmpp:" do
get "ping", Myapp.Xmpp.PingController, :ping
end
end

This is including a new namespace (keep in mind this one is not shown using mix xmpp.routes or mix xmpp.namespaces). You can send to the component:

<iq type='get'
from='user@example.com/res'
to='component.example.com'
id='info1'>
<query xmlns='http://jabber.org/protocol/disco#info'/>
</iq>

And the response keeping in mind the previous example should be:

<iq type='result'
from='component.example.com'
to='user@example.com/res'
id='info1'>
<query xmlns='http://jabber.org/protocol/disco#info'>
<feature var='urn:xmpp:ping'/>
</query>
</iq>

Inside of the discovery macro we can add also the identity for the component:

defmodule Myapp.Router do
use Exampple.Router
discovery do
identity category: "component", type: "generic", name: "myapp"
end
iq "urn:xmpp:" do
get "ping", Myapp.Xmpp.PingController, :ping
end
end

About the information you can configure for identity you can see the available categories. We are going to list here the categories and inside of the their possible types:

You can provide as name the name of the component or whatever which could means the mission of the component to be clear for the rest of the clients, server and components.

Envelope

Because we can configure XMPP to delegate using XEP-0355, we could configure to receive in a transparent way the incoming messages inside of their envelope and reply them just as if we were inside of the XMPP Server replying directly to the user or component asking.

The configuration is like this:

defmodule Myapp.Router do
use Exampple.Router
envelope "urn:xmpp:delegation:1"
iq "urn:xmpp:" do
get "ping", Myapp.Xmpp.PingController, :ping
end
end

Using this code we say to the router we are going to implement as wrapper the namespaces urn:xmpp:delegation:1 and the urn:xmpp:forward:0 implicitly because is in use by the XEP-0355. Everything regarding the envelope is configured inside of the connection variable passed to the controlled so, every response we perform using that connection will be using the same envelop to send it via the XMPP Server.

Including other Routers

It is possible to include other routers. This could be made to include other controllers and routes from a dependency or in order to split the router in different applications (umbrella) inside of our project.

This could be performed as:

defmodule MyMainApp.Router do
use Exampple.Router
includes MySubApp1.Router
includes MySubApp2.Router
end

This way the MyMainApp.Router will have the content (routes and namespaces) from the other routes.

Note that the information regarding discovery is copied only for namespaces, the identity, category and other information is not copied and should be defined.

Controllers

The controllers are the place where we are going to implement all of these functions we indicate during the routing writing process. For example:

defmodule Myapp.Router do
use Exampple.Router
iq "urn:xmpp:" do
get "ping", Myapp.Xmpp.PingController, :ping
end
end

For this router configuration we have to implement our Myapp.Xmpp.PingController module where should appear a function called ping accepting two parameters:

The usual implementation for the ping:

defmodule Myapp.Xmpp.PingController do
use Exampple.Component
def ping(conn, query) do
conn
|> iq_resp(query)
|> send()
end
end

The action performed by iq_resp/2 over the conn is creating the response and putting it inside of the connection to be in use by the following send/1 function. The second parameter of the iq_resp/2 let us to include the new payload for the result.

For example, if we are implementing a request and we want to send back the information retrieved, we could to use the XML format to write the response:

def get(conn, query) do
payload = ~x[
<name>Exampple</name>
<vsn>#{Application.spec(:exampple)[:vsn]}</vsn>
]
conn
|> iq_resp([payload])
|> send()
end

Note that the ~x sigil is provided by the Exampple.Xml.Xmlel module.

We hav different functions to use to generate responses:

You can check the module to get even more functionalities regarding stanzas.

Tracing

At the moment we have the possibility to see in the logs (info and error) the amount of time each stanza is taking. To get this information we have to configure properly the output for logger:

config :logger, :console,
format: "$time $metadata[$level] $levelpad$message\n",
metadata: [:ellapsed_time, :stanza_id, :stanza_type, :type]

The provided metadata available is:

The output will be in the form:

00:20:42.717 ellapsed_time=1ms stanza_type=iq type=set [info] success

The time could appear in milliseconds (ms) if the amount is less than 1 second or in seconds (s) otherwise.

Testing

Finally, but maybe the most important topic, we have facilities to perform the testing part of our component. Thanks to Exampply.DummyTcp we can easily use the following macros to test our systems.

The definition of the test should be:

use Exampple.ConnCase

This macro let us to include and configure the basics to run all of the necessary tests for us and provide us more macros for assertion (see below).

Configuration

You will need to create a special block to configure the component, as we saw in the very beginning the block is as follows:

config :myapp, Exampple.Component,
domain: "mycomponent.example.com",
host: "localhost",
password: "guest",
ping: 30_000,
port: 5252,
set_from: true,
trimmed: true,
auto_connect: true,
tcp_handler: Exampple.DummyTcp

As you can see, we configured our own tcp_handler. This let us not only test controlling what we are sending but also this let you to change the way the communication with the component is made using a different transport.

Setup

The setup phase is adding the start of the DummyTcp subscription and the start of the Component machine. DummyTcp is simulating a handshake for us so, it should be properly configured to directly start using it.

Assertions

The new assertions are the following ones:

component_received ~x[
<iq type='get'>
<query xmlns='urn:xmpp:ping'/>
</iq>
]
assert_stanza_received ~x[
<iq type='result'/>
]
assert_stanza_receive ~x[
<iq type='result'/>
]

Collaboration

You can help us to improve and grow this library giving us suggestions using the issues from github, reporting bugs opening an issue or providing a pull request if you want to give us an improvement or a bugfix.

Enjoy!