Introduction

The following documentation will give you an overview of matador. It will show you all main components of the library so that you can start building your own application with it.

matador consists of four main parts: A container for any kind of objects, a sql query class providing a fluent interface, an ORM layer and on top a simple web server.

Object Store

The object store is the central element. Once it is configured with an object hierarchy, one can insert, update or delete any of objects of the introduced types. Create a view to access all objects of a specific type or of a specific base type. Once the view is created all objects can be iterated or filtered via a simple expression. As a special feature a transaction mechanism is also integrated.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  #include <matador/utils/access.hpp>
  
  #include <matador/object/object_store.hpp>

  struct person
  {
    unsigned long id{};
    std::string name;

    person() = default;
    explicit person(const std::string &n) : name(n) {}

    template < typename Operator >
    void process(Operator &op) {
      namespace field = matador::access;
      field::primary_key(op, "id", id);
      field::attribute(op, "name", name, 255);
    }
  };

  int main()
  {
    matador::object_store store;
    // attach class to object store
    store.attach<person>("person");

    // insert object
    store.insert<person>("georg");

    // create view
    matador::object_view<person> view(store);
  }

SQL Query

With the query class at hand one can write simple sql statements, prepare and execute.

1
2
3
4
5
6
7
8
9
10
11
  matador::connection conn("sqlite://db.sqlite");
  matador::query<person> q("person");

  // select all person with name 'jane' or 'tarzan' from database
  auto res = q.select()
              .where("name"_col == "jane" || "name"_col == "tarzan")
              .execute(conn);

  for (const auto &p : res) {
    std::cout << "my name is " << p->name << "\n";
  }

ORM Layer

On top of the object store and the query interface comes the persistence layer. It represents the ORM mechanism provided by the library. Once a persistence object is created one can again configure the object hierarchy in the same way it is done with the object store.

1
2
matador::persistence p("sqlite://db.sqlite");
p.attach<person>("person");

After the persistance layer is configured all the database tables can be created

1
p.create();

Now you can create a session and insert, update or delete entities.

1
2
3
4
5
6
7
8
9
10
11
session s(p);

auto jane = s.insert<person>("jane");

// set janes real age
jane.modify()->age = 35;
s.flush();

// bye bye jane
s.remove(jane)
s.flush();

Once you have data in your database you can load it this way:

1
2
3
  session s(p);

  s.load();

HTTP Server

With the web server it is possible to write a web application and serve the data as REST Api or as full HTML pages. To make life a bit easier a template engine based on the Django Template language was also added.

1
2
3
4
5
6
7
8
9
10
11
  http::server server(8081);
  // add routing middleware
  server.add_routing_middleware();

  server.on_get("/", [](const http::request &req) {
    return http::response::ok(
      "hello world",
      http::mime_types::TYPE_TEXT_PLAIN
    );
  });
  server.run();

Installation

There are installation packages for Linux and Windows available.

Linux

Windows

Objects

All kind of object up from POD can be attached to the object store. It can be a simple struct with publiv members or a complex class hierarchy with bases classes and virtual methods.

The only thing that must exist is a process method. This method takes as parameter an operator.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct person
{
  unsigned long id{};
  std::string name;
  matador::date birthday;

  template < typename Operator >
  void process(Operator &op)
  {
    namespace field = matador::access;
    field::primary_key(op, "id", id);
    field::attribute(op, "name", name, 255);
    field::attribute(op, "birthday", birthday);
  }
};

Provide such a method for all objects to be attached to the object store and you’re done.

Within the namespace matador::access one can specify what type of column is added to the entity. The following types are available (called via function):

Primary Key

Primary key is added via primary_key function. With this function the added column will have the constraint PRIMARY KEY. Currently two types of primary keys are supported: Integral type and varchar e.g. string with size (leading to db type VARCHAR)

1
2
3
4
5
// add primary key where field member is an integral type
matador::access::primary_key(op, "id", id);
// add primary key where field member is of type string and
// size is the size of the string. This leads to a VARCHAR(size) database column type.
matador::access::primary_key(op, "name", name, 255);

The process method for integral types looks like this:

1
2
3
4
5
6
7
8
9
10
struct identifier_type_integral
{
  unsigned long id{};

  template < typename Operator >
  void process(Operator &op)
  {
    matador::access::primary_key(op, "id", id);
  }
};

Below an example of a string primary key. Note that the size parameter is mandatory.

1
2
3
4
5
6
7
8
9
10
struct identifier_type_string
{
  std::string id{};

  template < typename Operator >
  void process(Operator &op)
  {
    matador::access::primary_key(op, "id", id, 255);
  }
};

Attributes

The attribute(...) function takes the following arguments:

attribute(<Operator>, <field name>, <field member>, <field attributes>)

While the first three parameters are the usual suspects operator, field name and field member, the fourth is of type field_attributes and consists of a size value and field constraints. The size is only relevant for type std::string and const char* and leads when greater than zero to a db field type of VARCHAR. The field constraints handle the well known constraints

Note: In a future release field attributes will also support INDEX and DEFAULT.

To allow an easy instantiation within the call to matador::access::attribute the class has a couple of constructors allowing to write the following:

1
2
3
4
5
6
// string column with size of 255: VARCHAR(255)
matador::access::attribute(op, "name", name, 255);
// column with NOT NULL constraint
matador::access::attribute(op, "value", value, constraints::NOT_NULL);
// string column with size of 255 and unique constraint:
matador::access::attribute(op, "name", name, {255, constraints::UNIQUE});

Relations

To add a foreign key relationship matador provides the functions has_one(), belongs_to() and has_many(). Object relations are supported by using object_ptr<Type>.

The syntax of has_one() and belongs_to() looks as follows:

1
2
matador::access::has_one(<operator>, <field name>, <field member>, <cascade>);
matador::access::belongs_to(<operator>, <field name>, <field member>, <cascade>);

The first parameter is the operator in charge. The second parameter is the name of the foreign table. The third is the relation object member. With the fourth parameter in the process method adjusts the cascading command behavior when an insert, update, delete and select operation is executed on the containing object. matador::cascade_type::ALL means that all operations are also passed to the foreign object (e.g. when containing object is deleted the foreign is deleted as well).

And the syntax of has_many() looks this way. There are two variants of the has_many() function:

1
2
3
4
// This function creates the name of the relation table columns on its own
matador::access::has_many(<Operator>, <relation table name>, <container member>, <cascade>);
// This function takes the given names as the relation table column names
matador::access::has_many(<Operator>, <relation table name>, <container member>, <left column name>, <right column name>, <cascade>);

The first parameter is the operator in charge. The second parameter is the name of the foreign table. The third is the relation object member. With the fourth parameter in the process method adjusts the cascading command behavior when an insert, update, delete and select operation is executed on the collection. matador::cascade_type::ALL means that all operations are also passed to the collection. The optional fifth and sixth parameter defining the names of the relation table columns. If not set matador decide the names.

Has One Relation

has_one() acts like a simple foreign key relation or as the counterpart for a belongs to relation. It creates a column for a foreign key constraint. Therefor the entity struct must contain a member of type matador::object_ptr<ForeignType>.

1
2
3
4
5
6
7
8
9
10
11
struct foreign_key
{
  matador::object_ptr<T> has_one_;         // has one relationship

  template < typename Operator >
  void process(Operator &op)
  {
    namespace field = matador::access;
    field::has_one(op, "has_one_table", has_one_, cascade_type::ALL);
  }
};

Belongs To Relation

belongs_to() acts like the name indicates as a belongs to relation ship. It needs as a counterpart a has_one or a has_many relationship (described below). It creates a column for a foreign key constraint. Therefor the entity struct must contain a member of type matador::object_ptr<ForeignType>.

1
2
3
4
5
6
7
8
9
10
11
struct foreign_key
{
  matador::object_ptr<T> belongs_to_;      // belongs to relationship

  template < typename Operator >
  void process(Operator &op)
  {
    namespace field = matador::access;
    field::belongs_to(op, "belongs_to_table", belongs_to_, cascade_type::ALL);
  }
};

Has Many Relation

has_many() acts as the many side of a belongs_to() or another has_many() relation. If the other side is the latter one a relation table is created automatically. Collections are supported by container<Type, ContainerType>. By now container supports internal two container types std::vector (default) and std::list.

1
2
3
4
5
6
7
8
9
10
11
struct relations
{
  matador::container<T> collection_; // a collection with objects

  template < typename Operator >
  void process(Operator &op)
  {
    namespace field = matador::access;
    field::has_many(op, "collection", collection_, "local_id", "foreign_id", cascade_type::NONE);
  }
};

container<Type> describes a collection leading to a relation table/object between the containing object and the foreign object. If the foreign object is of a simple builtin type (e.g. int or matador::date) the value itself is stored in the relation table.

Attributes

The following types are supported and can be used within the matador::access::attribute() functions:

Numbers

All arithmetic number types are supported (char, short, int, long, long long, unsigned char, unsigned short, unsigned int, unsigned long, unsigned long long, float, double and bool).

The attribute() function takes the operator, the name of the attribute, attribute itself and an optional field_attribute parameter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct numbers
{
  char char_ = 'c';
  short short_ = -127;
  int int_ = -65000;
  long long_ = -128000;
  long long long64_ = -32128000;
  unsigned char unsigned_char_ = 'u';
  unsigned short unsigned_short_ = 128;
  unsigned int unsigned_int_ = 65000;
  unsigned long unsigned_long_ = 128000;
  unsigned long long unsigned_long64_ = 32128000;
  bool bool_ = true;
  float float_ = 3.1415f;
  double double_ = 1.1414;

  template < typename Operator >
  void process(Operator &op)
  {
    namespace field = matador::access;
    field::attribute(op, "val_char", char_);
    field::attribute(op, "val_short", short_);
    field::attribute(op, "val_int", int_);
    field::attribute(op, "val_long", long_);
    field::attribute(op, "val_long64", long64_);
    field::attribute(op, "val_unsigned_chr", unsigned_char_);
    field::attribute(op, "val_unsigned_short", unsigned_short_);
    field::attribute(op, "val_unsigned_int", unsigned_int_);
    field::attribute(op, "val_unsigned_long", unsigned_long_);
    field::attribute(op, "val_unsigned_long64", unsigned_long64_);
    field::attribute(op, "val_bool", bool_);
    field::attribute(op, "val_float", float_);
    field::attribute(op, "val_double", double_);
  }
};

Strings

There are three types of supported strings (std::string and const char*). A varchar type can be represented by a string combined with a length in its attribute() function. See example below

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct strings
{
  enum { CSTR_LEN=255 };

  char cstr_[CSTR_LEN];
  std::string string_ = "world";
  std::string varchar_ = "earth";

  template < typename Operator >
  void process(Operator &op)
  {
    matador::access::attribute(op, "val_cstr", cstr_, (std::size_t)CSTR_LEN);
    matador::access::attribute(op, "val_string", string_);
    matador::access::attribute(op, "val_varchar", varchar_, 255); // varchar of length 255
  }
};

Time and date

matador comes with its own time and date classes. The attribute() interface is also straight forward.

1
2
3
4
5
6
7
8
9
10
11
12
struct time_and_date
{
  matador::date date_;
  matador::time time_;

  template < typename Operator >
  void process(Operator &op)
  {
    matador::access::attribute(op, "val_date", date_);
    matador::access::attribute(op, "val_time", time_);
  }
};

Relations

When it comes to object relations you can use one to one, one to many and many to many relations in a straight forward way. Therefor object store provides easy wrapper for these relations

OneToOne Relations

In this example we have two object types an address class and a person class. The address class acts as the belongs_to part of the relation. So we add a matador::object_ptr<person> to our address class. Don’t forget to add the appropriate belongs_to() to the process() method.

For the person in address we add the matador::cascade_type::NONE. That means if the address is removed the person won’t be removed.

1
2
3
4
5
6
7
8
9
10
11
12
13
  struct address
  {
    std::string street;
    std::string city;
    matador::object_ptr<person> citizen;

    template < typename Operator >
    void process(Operator &op)
    {
      // ...
      matador::access::belongs_to(op, "citizen", citizen, cascade_type::NONE);
    }
  };

In the persons declaration we add an matador::object_ptr<address> and the call to has_one() to the class. Here we use matador::cascade_type::ALL. That means if the person is removed the address is removed as well. That’s it. Now we have a one to one relationship beetween two classes.

1
2
3
4
5
6
7
8
9
10
11
12
  struct person
  {
    // ...
    matador::object_ptr<address> addr;

    template < typename Operator >
    void process(Operator &op)
    {
      // ...
      matador::access::has_one(op, "address", addr, cascade_type::ALL);
    }
  };

Note: With this kind of relationship we have a hard linked relationship. Which means if we remove the person from our store the address object is removed as well. The matador::cascade_type::ALL means the all operation of INSERT, UPDATE and DELETE will take affect on the member address as well

When using this construct the matador will take care of the following:

1
2
3
4
5
6
7
8
  // setup session/object_store
  auto george = s.insert<person>("george");
  auto home = s.insert<address>("homestreet", "homecity");

  george.modify()->addr = home;

  // person george will be set into address
  std::cout << "citizen: " << home->citizen->name << "\n";

OneToMany Relations

When it comes to one to many relationships it is also quiet easy. matador comes with a handy wrapper to the STL container class std::vector and std::list.

Note: The STL container class std::set and std::unordered_set will be supported soon.

Because these classes are designed in the same way as the STL classes we can use them in the same way plus the benefit of the relationship.

We change our handy person class that it has a lot of addresses. The address class can stay untouched because the belongs_to part doesn’t need to change.

1
2
3
4
5
6
7
8
9
10
11
12
  struct person
  {
    std::string name;
    matador::has_many<address> addresses;

    template < typename Operator >
    void process(Operator &op)
    {
      // ...
      matador::access::has_many(op, "address", addresses, "person_id", "address_id");
    }
  };

Now we can add several addresses to a person object and address’ citizen field is filled again automatically. But it works also in the opposite way: If a person is set into an address the address is automatically added to persons address list.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
  // ...
  // create a new person
  auto joe = s.insert<person>("joe");
  auto home = s.insert<address>("homestreet", "homecity");
  auto work = s.insert<address>("workstreet", "workc### Primary keys

Currently two types of  primary keys are supported: Integral type and
varchar e.g. string with size. The process method for integral types looks like this:


<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="code"><pre><span class="k">struct</span> <span class="nc">identifier_type_integral</span>
<span class="p">{</span>
  <span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">id</span><span class="p">{};</span>

  <span class="k">template</span> <span class="o">&lt;</span> <span class="k">typename</span> <span class="nc">Operator</span> <span class="p">&gt;</span>
  <span class="kt">void</span> <span class="n">process</span><span class="p">(</span><span class="n">Operator</span> <span class="o">&amp;</span><span class="n">op</span><span class="p">)</span>
  <span class="p">{</span>
    <span class="n">matador</span><span class="o">::</span><span class="n">access</span><span class="o">::</span><span class="n">primary_key</span><span class="p">(</span><span class="n">op</span><span class="p">,</span> <span class="s">"id"</span><span class="p">,</span> <span class="n">id</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">};</span>
</pre></td></tr></tbody></table></code></pre></figure>


Below an example of a string primary key. Note that the size parameter is mandatory.

<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="code"><pre><span class="k">struct</span> <span class="nc">identifier_type_string</span>
<span class="p">{</span>
  <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">id</span><span class="p">{};</span>

  <span class="k">template</span> <span class="o">&lt;</span> <span class="k">typename</span> <span class="nc">Operator</span> <span class="p">&gt;</span>
  <span class="kt">void</span> <span class="n">process</span><span class="p">(</span><span class="n">Operator</span> <span class="o">&amp;</span><span class="n">op</span><span class="p">)</span>
  <span class="p">{</span>
    <span class="n">matador</span><span class="o">::</span><span class="n">access</span><span class="o">::</span><span class="n">primary_key</span><span class="p">(</span><span class="n">op</span><span class="p">,</span> <span class="s">"id"</span><span class="p">,</span> <span class="n">id</span><span class="p">,</span> <span class="mi">255</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">};</span>
</pre></td></tr></tbody></table></code></pre></figure>

ity");

  joe.modify()->addresses.push_back(home);
  // homes citicen will be joe
  std::cout << "citizen: " << home->citizen->name << "\n";

  work.modify()->citizen = joe;
  // joes addresses have now increased to two
  std::cout << "joes addresses: " << joe->addresses.size() << "\n";

Now we can simply iterate over the list like we used to do it with all STL containers..

1
2
3
4
  // access all friends
  for (const auto &addr : joe->addresses) {
    std::cout << "address street: " << addr->street << "\n";
  }

ManyToMany Relations

Many to many relationships can also be used straight forward. Asume we have a class student taking a list of courses and a class course having a list of students.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  struct student
  {
    matador::has_many<course> courses;

    template < typename Operator >
    void process(Operator &op)
    {
      // ...
      matador::access::has_many(op, "student_course", courses, "student_id", "course_id");
    }
  };

  struct course
  {
    matador::has_many<student> students;

    template < typename Operator >
    void process(Operator &op)
    {
      // ...
      matador::access::has_many(op, "student_course", students, "student_id", "course_id");
    }
  };

Once a student adds a course to its course list the student is added to the list of students of the course. And the other way around if a student is added to a course the course is added to the list of students course list.

1
2
3
4
5
6
7
8
9
  auto jane = s.insert<student>("jane");
  auto art = s.insert<course>("art");
  auto algebra = s.insert<course>("algebra");

  jane.modify()->courses.push_back(art);
  std::cout << art->students.front()->name << "\n"; // prints out 'jane'

  art.modify()->students.push_back(jane);
  std::cout << jane->courses.size() << "\n"; // prints out '2'

Prototypes

Once all entities are designed you want to store them in the object store. Before this could be done all entities must be introduced into the object store.

Assume we have an abstract base class Vehicle and derived from this the classes Truck, Car and Bike. Now lets make this hierarchy known to the matador::object_store:

1
2
3
4
5
  matador::object_store ostore;
  ostore.attach<Vehicle>("vehicle", true);
  ostore.attach<Truck, Vehicle>("truck");
  ostore.attach<Car, Vehicle>("car");
  ostore.attach<Bike, Vehicle>("bike");

As you can see it is quite simple to add the hierarchy to the matador::object_store by calling method matador::object_store::attach. As there are several ways to call this method we decide to use the one with template arguments.

In the example above the vehicle class is an abstract base. Here we need only one template argument (the class itself: Vehicle). With the first method parameter you give your type a unique name. The second parameter is a flag telling the matador::object_store that this type is abstract. Settings this flag to true you can’t insert objects of this abstract type.

Inserting Objects

Now that we’ve setup up our hierarchy we can insert objects into the matador::object_store.

1
2
3
4
5
  typedef object_ptr<Vehicle> vehicle_ptr;

  vehicle_ptr truck = ostore.insert<Truck>("MAN");
  vehicle_ptr car   = ostore.insert<Car>("VW Beetle");
  vehicle_ptr bike  = ostore.insert<Bike>("Honda");

As you can see we use matador::object_ptr of type Vehicle. The vehicle class in our example is the abstract base class for all concrete vehicle types. So the concrete vehicle object is inserted correctly and assigned to the object_ptr.

That means once you have inserted an object of any concrete type you access it via an appropriate object_ptr afterwards. You should never work with the raw instance pointer. This could lead to inconsistencies.

Updating Objects

Now that we have some objects inserted we may want to modify them. The important thing here is as mentioned above that you don’t deal with raw pointer to your object when try to modify it. You always have a pointer object wrapped around the object (like shared_ptr). The matador::object_store returns an matador::object_ptr when an object is inserted. Once you received the matador::object_ptr you can change your object by using it like usual pointer.

1
2
3
4
5
  typedef matador::object_ptr<Truck> truck_ptr;
  truck_ptr truck = ostore.insert<Truck>("MAN");

  truck.modify()->weight = 2.5;
  truck.modify()->color = red;

Note: When using the raw pointer object store can’t guarantee that a change is properly rolled back when an exception occurs.

Deleting Objects

An already inserted object can be deleted from the object store using the remove method:

1
2
3
4
5
6
7
8
9
  typedef matador::object_ptr<Truck> truck_ptr;
  truck_ptr truck = ostore.insert<Truck>("MAN");

  if (ostore.is_removable(truck)) {
    // remove object
    ostore.remove(truck);
  } else {
    // object can't be removed
  }

The object store will check if the object is removable (i.e. there are no other objects referencing this object). If the object is not removable an object_exception is thrown. One can do this check manually by calling simply object_store::is_removable (as shown in the example above).

Views

In most cases we want to iterate over all objects of a certain type. How can we achieve this? We open a view for the type we want to inspect.

Again we use our little person class and insert some persons into our store.

1
2
3
4
5
  // add some friends
  ostore.insert<person>("joe")
  ostore.insert<person>("walter");
  ostore.insert<person>("helen");
  ostore.insert<person>("tim");

Than we create a view with the matador::object_view class. This class takes as the template parameter the desired class type. Then we can use the view like a STL list containing matador::object_ptr of our desired type.

1
2
3
4
5
6
7
8
9
  // shortcut to the person view
  using person_view_t = matador::object_view<person>;

  person_view_t pview(ostore);

  person_view_t::iterator i = pview.begin();
  for (const auto &p : pview) {
    std::cout << "person: " << p->name << std::endl;
  }

But this class can to somethig more for us. If we have a hierarchy of classes, we can create a view of the base type and easily iterate overall sub-types.

Look at the next example. We have a hierarchy with the person class as base class and inherited from that we have classes student and employee. And again we create a view of type person to access all objects of type person including sub-types student and employee.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  class student : public person { //... };
  class employee : public person { //... };

  matador::object_store ostore;
  ostore.attach<person>("person");
  ostore.attach<student, person>("student");
  ostore.attach<employee, person>("employee");

  using person_view_t = matador::object_view<person>;

  person_view_t pview(ostore);

  person_view_t::iterator i = pview.begin();
  for (const auto &p : pview) {
    std::cout << p->name() << std::endl;
  }

Expressions

When working with an matador::object_view you may want to find a certain object or you want to apply a function only for some of the objects in the view.

To achieve this we can use the matador::object_view::find_if method or we can use the global matador::for_each_if() function. But how can we define or filter criterion?

First add some elements (persons) to the store and create the view. As you can see we’ve extended the person class with an age attribute.

1
2
3
4
5
6
7
8
9
10
11
12
  // add some persons
  ostore.<person>("joe", 45);
  ostore.<person>("walter", 56);
  ostore.<person>("helen", 37);
  ostore.<person>("tim", 14);
  ostore.<person>("linda", 25);
  ostore.<person>("lea", 30);
  ostore.<person>("georg", 42);

  using person_view_t = matador::object_view<person>;

  person_view_t pview(ostore);

If we want to find the person of age 25 we can achieve this by using the matador::object_view::find_if method. But first we have to create a variable holding the method of our object which should be used for comparation. Here it is person::age.

1
2
3
4
5
6
  // create the variable
  // this returns a ```matador::variable<int>```
  auto x(matador::make_var(&person::age));

  // find the person of age 25
  auto i = pview.find_if(x == 25);

The call returns the first occurence of the object matching the expression or the end iterator.

If you want to apply a function to all objects matching a certain expression use matador::for_each_if() and use it in the following described way.

1
2
3
4
5
6
7
8
9
10
11
  // print all persons with age between 20 and 60 years
  void print(const person_ptr &p)
  {
    std::cout << p->name() << " is between 40 and 60.\n";
  }

  // declare a variable/literal for the age
  auto x(matador::make_var(&person::age));

  // use the for_each_if function
  for_each_if(pview.begin(), pview.end(), x > 20 && x < 60, print);

All persons whos age lays between 40 and 60 are printed.

Transactions

A handy feature ist the transaction mechanism provided by the matador::object_store. Once an object is inserted, updated or deleted the change is not reversible. If you use a transaction it is. Therefor a transaction must be started:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  matador::object_store store;
  store.attach<person>("person");

  matador::transaction tr(store);

  try {
    tr.begin();

    auto hans = store.insert<person>("Hans");

    /// do some stuff and commit
    tr.commit();
  } catch (std::exception &) {
    tr.rollback();
  }

Once a transaction is started all changed are tracked until a commit is called. When it comes to rollback a transaction rollback must be called (i.e an exception is thrown).

It is also possible to nest transaction:

1
2
3
4
5
6
7
8
9
10
11
  matador::transaction outer(store);

  outer.begin();
  // do something and start a second transaction

  matador::transaction inner(store);
  inner.begin();
  // change stuff and commit inner transaction
  inner.commit();
  // commit outer
  outer.commit();

Databases

To connect to a database you have to create and open a connection. Therefor you need a connection object. This is initialized with a connection string. The connection string commes in an uri way:

1
<database-type>://[<username>[:<password>]@][<server>[:<port>]]/<database-name>

Example:

1
2
3
4
5
  connection conn("mysql://test@localhost/mydb");

  conn.open();
  // ... use connection
  conn.close();

Supported Databases

There’re currently four supported databases and the in memory database. Next is the description of the database connection string for the supported databases.

PostgreSQL

1
2
3
4
5
  // PostgreSQL connection string with password
  session ses(ostore, "postgresql://user:passwd@host/db");

  // PostgreSQL connection string without password
  session ses(ostore, "postgresql://user@host/db");

MS SQL Server

1
2
3
4
5
  // MSSQL connection string with password
  session ses(ostore, "mssql://user:passwd@host/db");

  // MSSQL connection string without password
  session ses(ostore, "mssql://user@host/db");

MySQL / MariaDB

1
2
3
4
5
  // MySQL connection string with password
  session ses(ostore, "mysql://user:passwd@host/db");

  // MySQL connection string without password
  session ses(ostore, "mysql://user@host/db");

SQLite

1
2
  // MySQL connection string
  session ses(ostore, "sqlite://database.sqlite");

Handling Database errors

Each backend has its own error handling, error codes or error messages. Some use SQLSTATE other (like SQLite) don’t.

Once a backend delivers a database error matador collects all available information in a database_error object which is thrown.

1
2
3
4
5
6
7
8
  connection conn("mysql://test@localhost/mydb");

  try {
    conn.open();
  } catch (database_error &ex) {
    std::cout << "caught error '" << ex.what() << "' from backend '" << ex.source() << "' " \
              << "with sqlstate '" << ex.sql_state() << "' and error code " << ex.error_code() << "\n";
  }

Queries

On the low level side of the library resides the fluent query interface. It allows you to write a SQL query in safe way without concerning about the current SQL dialect. There are two type of queries a typed one and an anonymous one dealing with a row object.

Introdcution

Once you have established a connection to yout database you can execute a query.

1
2
3
4
5
6
7
8
    matador::connection conn("sqlite://test.sqlite");

    conn.open();
    // create a typed query
    matador::query<person> q("person");

    // create the table based on the given type
    q.create().execute(conn);

You can use an anonymous query as well. This will use an object matador::row to store the result. A can be build up by addiing several columns. This can be done programatically using add_columns(...) or in the case below determined by a query.

1
2
3
4
5
6
7
    matador::query<> q;
    auto res = count
        .select({columns::count_all()})
        .from("person")
        .execute(*conn);

    int count = res.begin()->at<int>(0);

Note: The query interface represents not the full command syntax. By now it provides basic functionality to create, drop, insert, update, select and delete a table.

Create

The create method is used to create a database table. The typed version looks like this:

1
2
3
4
    matador::query<person> q("person");

    // create the table based on the given type
    q.create().execute(conn);

When using the anonymous version you have to describe the fields of the table to create:

1
2
3
4
5
6
7
    matador::query<> q("person");
    // Todo: implement functionality
    q.create({
        make_pk_column<long>("id"),
        make_column<std::string>("name", 255),
        make_column<unsigned>("age")
    }).execute(conn);

Drop

The drop method is used to drop a table. The typed usage is as follows:

1
2
3
4
    matador::query<person> q("person");

    // create the table based on the given type
    q.drop().execute(conn);

The anonymous version is like this:

1
2
3
4
    matador::query<> q;

    // create the table based on the given type
    q.drop("person").execute(conn);

Insert

Inserting an object is done as simple. Just pass the object to the insert method and you’re done.

1
2
3
4
5
    matador::query<person> q("person");

    person jane("jane", 35);

    q.insert(jane).execute(conn);

When building an anonymous insert statement one has to column and value fields like that

1
2
3
4
5
6
    matador::query<> person_query("person");

    person_query
        .insert({"id", "name", "age"})
        .values({1, "jane", 35})
        .execute(conn);

Update

Updating an object works the same way as inserting an object. Asume there is an object one can modify it and pass it to the update method. Notice the where clause with expression to limit the update statement. These conditions are explained condition chapter bewlow

1
2
3
4
5
6
    person jane("jane", 35);
    matador::query<person> q("person");
    // insert ...
    jane.age = 47;

    q.update(jane).where("name"_col == "jane").execute(conn);

If you’re dealing with an anonymous row query the update query looks like the example below. As you can see, it is simple done with initializer list and pairs of columns and their new values.

1
2
3
4
5
6
7
    matador::query<> q("person");

    matador::column name("name");
    q.update({
        {"name", "otto"},
        {"age", 47}
    }).where(name == "jane");

Select

When select from a database you can add a where clause and write your condition like you’re writing an if statement in code. All you have todo is define variables of all columns you want to use in your condition.

Once the statement is executed you get a result object. You can iterate the result with STL iterators (iterator is a std::forward_iterator so you can only use it once).

1
2
3
4
5
6
7
8
9
10
11
12
    matador::query<person> q("person");

    auto name = "name"_col;
    auto age = "age"_col;

    auto res = q.select()
                .where(name != "hans" && (age > 35 && age < 45))
                .execute(conn);

    for (auto item : res) {
        std::cout << "name: " << item.name << "\n";
    }

The anonymous version works in the same way:

1
2
3
4
5
6
7
8
9
10
    matador::query<> q;

    auto rowres = q.select({"name", "age"})
                .from("person")
                .where("name"_col != "hans")
                .execute(conn);

    for (auto row : rowres) {
        std::cout << "name: " << row->at<std::string>("name") << "\n";
    }

Delete

The delete statement works similar to the other statements. If you want to delete an object the statement looks like this:

1
2
3
4
5
6
7
    person jane("jane", 35);
    matador::query<person> q("person");
    // insert ...
    person jane("jane", 35);
    q.insert(jane).execute(conn);

    q.delete().where("name"_col == "jane").execute(conn);

Conditions

With condition one can express a query where clause (for update, delete and select queries). There are five different types of conditions:

Compare Conditions

To express a simple compare condition one needs a column object of the column to compare. Then you can write the comparision:

1
2
3
4
5
    column name("name");
    name == "Jane"

    column age("age");
    age > 35
Logic Conditions

To concat to simple compare condition within a logical condition just write it:

1
2
3
4
    column name("name");
    column age("age");

    (name != "Theo" && name != "Jane") || age > 35
IN Condition

With the IN condition one can check if a column value is one of a list.

1
2
    column age("age");
    in(age, {23,45,72});
IN Query Condition

The IN condition works also with a query.

Note: You have to pass a dialect object as a third parameter to the function. You can retrieve the dialect from the connection object.

1
2
3
4
    column name("name");
    auto q = matador::select({name}).from("test");

    matador::in(name, q, &conn.dialect());
Like Condition

To express a like condition one has to call like with the column and the compare value.

1
2
3
    auto name = "name"_col;

    matador::like(name, "%ight%");
Range Condition

The range condition checks if a column value is between two given boundaries.

1
2
3
    column age("age");

    matador::between(age, 21, 30);

Take a look at the query API reference to get an overview of the provided syntax.

Persistence

The ORM layer makes internally use of the fluent query interface and the object store. The query interface is used to define the queries for all attached entities and the object store acts like an internal cache for the concrete entity objects.

Note: By now the ORM layer loads the database in whole. Which means internally all the data is loaded from the database into the object store. I plan to implement a lazy loading mechanism later.

Before you can use the ORM layer you have to setup the persistence layer. This means to configure all entities you want to use. For that you need an instance of class persistence and a database connection string. Once such an object is created you can attach entities in the same way it is done with the object_store.

1
2
3
4
    persistence p("sqlite://db.sqlite");
    p.attach<person>("person");
    p.attach<student, person>("student");
    p.attach<course>("course");

Now you can create the database schema simply by calling create:

1
    p.create();

Or you can drop it as well:

1
    p.drop();

After that is done you can deal with a session object and start building your app.

Sessions

Once the database schema is set up with the persistence object you need a session to use the ORM layer.

1
2
3
4
  persistence p("sqlite://db.sqlite");
  p.attach<person>("person");

  session s(p);

If you have already setup the persitence layer and inserted some data, just call load to get it into the layer.

1
2
  // load the data from database
  s.load();

Note: By now the database is loaded completely into the underlying object store. Lazy loading isn’t implemented yet. It will be relealized in a future version of matador.

Now you can start and insert, update or delete your data. This can be done in two ways. Using session::save() or session::flush() to write the changes directly to database or use a transactional scope to allow a rollback of a transaction.

Using the direct way it looks like the following code:

1
2
3
4
5
6
7
8
9
10
  // insert and save an object directly
  auto ptr = s.save(new person("james bond"));

  // do multiple modifications and flush them at once
  ptr.modify()->name = "james blunt"

  auto addr = s.insert(new address("downing street 10"));

  // flush changes
  s.flush();

When using the transactional way you have to create a instance of transaction with the current session and start the transaction by calling matador::transaction::begin(). After completing your modifications call matador::transaction::commit() to commit all your modifications to the database. If in error occurred while doing your modifications catch the exception. In the catch block you can call matador::transaction::rollback() to rollback all your modifications.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  // create a transaction for session

  matador::transaction tr = s.begin();
  try {
    // begin the transaction

    // insert some objects
    s.insert(new person("joe", 45))
    s.insert(new person("walter", 56));
    s.insert(new person("helen", 37));
    s.insert(new person("tim", 14));

    // commit the modifications
    tr.commit();
  } catch (exception &ex) {
    // on error rollback transactions
    tr.rollback();
  }

Time & Date

Matador comes with its own timeand date classes which are described below.

Time

matador comes with its own simple time class. It represents a time with milliseconds precisions. Once a time object exists it can be modified, copied or assigned. For full documentation see the api.

Creation

Time can be created from several constructors.

Constructor purpose
time() Creates a time of now
time(timt_t) Creates a time of time_t value
time(timeval) Creates a time of struct timeval
time(int, int, int, int, int, int, long) Creates time from year, month, day, hour, minutes, seconds and optional milliseconds. The given parameters are validated.

The obvious copy and assignment constructors exists as well as a static parsing function

1
matador::time t = matador::time::parse("03.04.2015 12:55:12.123", "%d.%m.%Y %H:%M:%S.%f");

The parse format tokens are the same as the ones from strptime plus the %f for the milliseconds.

Display

The time consists of an stream output operator which displays the time in ISO8601 format

1
2
3
matador::time t(2015, 1, 31, 11, 35, 7, 123);

std::cout << t;

Results in:

1
2015-01-31T11:35:07

There is also a to_string() function taking the time as first parameter and a format string as second parameter. It returns the formatted time as std::string.

1
2
3
matador::time t(2015, 1, 31, 11, 35, 7, 123);

std::cout << to_string(t, "%H:%M:%S.%f %d.%m.%Y");

Results in:

1
11:35:07.123 31.01.2015

Modify

To modify a time one can use the fluent interface allowing the user to concatenate all parts to be modified in sequence.

1
2
3
matador::time t(2015, 1, 31, 11, 35, 7);
// modification
t.year(2014).month(8).day(8);

Conversions

The time can be converted into a matador::date

1
2
3
matador::time t(2015, 1, 31, 11, 35, 7);

matador::date d = t.to_date();

There are also methods to retrieve tm and timeval struct:

1
2
3
4
matador::time t(2015, 1, 31, 11, 35, 7);

struct tm ttm = t.get_tm();
struct timeval tv = t.get_timeval();

Date

matador comes with its own simple date class. It represents a date consisting of year, month and day. The underlying calendar is a julian date calendar. Once a date object exists it can be modified, copied or assigned. For full documentation see the api.

Creation

Date can be created from several constructors.

Constructor purpose
date() Creates a date of now
date(int) Creates a date from julian date
date(const char *d, const char *format) Creates a date from date stamp with the given format
date(int, int, int) Creates a date from day, month and year

The obvious copy and assignment constructors exists as well.

Display

The date consists of an stream output operator which displays the date in ISO8601 format

1
2
3
matador::date d(31, 1, 2015);

std::cout << d;

Results in:

1
2015-01-31

There is also a to_string() function taking the date as first parameter and a format string as second parameter. It returns the formatted date as std::string.

1
2
3
matador::date d(31, 1, 2015);

std::cout << to_string(d, "%d.%m.%Y");

Results in:

1
31.01.2015

Modify

To modify a date one can use the fluent interface allowing the user to concatenate all parts to be modified in sequence.

1
2
3
matador::date d(31, 1, 2015);
// modification
d.year(2014).month(8).day(8);

The operators ++, --, += and -= are also available and increase or decrease the date by one day or the given amount of days for the latter two operators.

1
2
3
4
5
6
matador::date d(31, 1, 2015);
// modification

d += 4;

std:cout << d;

Leads to

1
2015-02-04

Conversions

The date can be retrieved as julian date value:

1
2
3
matador::date d(31, 1, 2015);

std::cout << "julian date: " << d.julian_date();

Dependency Injection

This part of matatdor provide a simple C++ dependency injection or service locator mechanism.

The mechanism is as simple as possible. Every service can be injected with the inject<T> class. Once the dependencies are set up each dependency can be injected like this:

1
2
di::inject<igreeter> greeter;
greeter->greet()

Setting up the Dependency Repository

Before injection can take place the dependencies must be configured. This means a dependency injection module must be filled with all mapped dependencies.

matador comes with a global service repository where the dependencies can be mapped.

1
2
3
di::install([](di::module &m) {
  m.bind<igreeter>()->to_singleton<simple_greeter>();
});

With this configuration it is possible to inject our greeter everywhere.

To ensure this behaviour a singleton repository providing all installed services is always accessible. Though the singleton pattern meant to be an anti-pattern I decided to use it here for practical reason.

But it is also possible to create a use such a module on your own. Then you have to pass it wherever you want to use dependency injection.

1
2
di::module m;
m.bind<igreeter>()->to_singleton<simple_greeter>();

Named Services

It is also possible to bind services with a name.

1
2
3
4
di::install([](di::module &m) {
  m.bind<igreeter>("students")->to_singleton<students_greeter>();
  m.bind<igreeter>("teachers")->to_singleton<teacher_greeter>();
});

Service Types

A service can be bound by now in four different ways:

Except for to_instance() every type is able to take parameters to create the service.

1
2
3
di::install([](di::module &m) {
  m.bind<igreeter>().to_singleton<custom_greeter>("hello everybody");
});

Injecting

1
2
3
4
5
6
7
8
9
10
// injection from global repository
inject<igreeter> i;

// named injection from global repository
inject<igreeter> i("students");

// named injection from local repository
di::module m;
// setup ...
inject<igreeter> i(m, "students");

Json

Matador provides a simple and lightweight json module including a parser, an object mapper for pod types and an object mapper for matador type entities.

Creating Json objects

To use Json object you have to include #include "matador/utils/json.hpp. Json supports the following datatypes:

Create json with standard types

Creating a Json object without initialization is initialized with null value.

1
2
3
4
json js;

// is true
js.is_null();

Its also possible to initialize a json object with a certain value.

1
2
3
4
5
json ji(7);
json jb(true);
json jd(3.5);
json js("hallo");
json jnill(nullptr);

Create json object

When it comes to objects or arrays it is as easy as handling standard types:

1
2
3
4
5
6
7
8
9
10
11
// create an empty object
json jo = json::object();

// is true
jo.is_object();

// assigning values
jo["name"] = "george";
jo["age"] = 35;
jo["weight"] = 78.5;
jo["male"] = true;

Create json array

Handling of json arrays is as easy as handling json objects:

1
2
3
4
5
6
7
8
9
// create an empty array
json ja = json::array();

// is true
jo.is_array();

ja.push_back("green");
ja.push_back(15);
ja.push_back(true);

Type checking

A json object can be checked on a spcific type

1
2
3
4
5
6
7
8
ji.is_number(); // returns true on arithmentic types (e.g. double, long, etc.)
ji.is_integer(); // returns true on integer types
jb.is_boolean(); // checks on boolean type
jd.is_real(); // checks on real type
js.is_string(); // checks on string type
jo.is_object(); // checks on object type
ja.is_array(); // checks on array type
jnill.is_null();

Accessing json values

The value of the json can be accessed via the as<T>() method. The requested type is passed with template parameter. If the json type doesn’t match the requested type and exception ist thrown.

1
2
auto i = js.as<int>();
auto s = js.as<std::string>();

In case the json is an object one can use the [] operator with the key string of the requested json value. If the key leads to a valid json this is returned otherwise a json null value is created and returned for the given key.

1
2
3
4
5
6
7
8
auto jo = json::object();
jo["name"] = "george";

// string "george" is returned
auto s = jo["name"];

// a json of type null is returned
auto n = jo["age"];

In case the json is an array one can also use the [] operator but with the index of the requested json value. If the index leads to a valid json this is returned otherwise am exception is thrown.

1
2
3
4
5
6
7
8
auto jo = json::array();
jo.push_back("george");

// string "george" is returned
auto s = jo[0];

// throws an exception
auto n = jo[1];

Retrieving json values

To retrieve a value from a json object the as<T>() can be used. It accesses the underlying concrete value und tries to convert it to the given type. There is a type checking with given template type defining that the type fits to the json type. Use the following table to find the corresponding convertion. If the type checking fails a logic_error is thrown.

Template type Json type
integral integer number
floating point real number
bool boolean
string,char* string

Mapping of objects

There are to classes providing json mapping functionality json_mapper and json_object_mapper. Where the first maps builtin types and std containers and the second one can handle matador types like has_one and has_many.

Json mapper

The json_mapper class comes with three kind of interfaces. One to convert to a json formatted string. Another to convert to a json object and the third one to convert to a object or a list of objects.

The following methods take a json, an object or a list of objects and as a second parameter a json format object and returns a formatted json string.

The next set of methods takes a json string, an object or a list of objects and convert it into a json object.

The last set of methods takes a json object or a json string and converts it into an object of the requested type of into a list of objects.

Json object mapper

The json_object_mapper class comes with the same three kind of interfaces as the json_mapper but with partially different arguments.

The to_string methods converts an array of objects, an object pointer or an object view in combination with a json format definition into a json formatted string.

To convert an object pointer or an object view to a json object use the to_json(...) methods.

The last set of methods converts a json object or a string into one object or a list of objects of a specific type.

Streams

Matador provides a stream module which allows the user to process a range in a streaming way applying element processor (like map, filter etc.) and terminators (collecting processed elements, evaluate expressions on resulting elements).

1
2
3
4
5
6
7
auto result = make_stream({1, 2, 3, 4, 9, 4, 2, 6})  // create a range of numbers
    .skip(3)                                         // skips 1, 2 and 3
    .take(2)                                         // takes 4 and 9
    .map([](const auto &val){ return val * 2; })     // multiplies each value with 2
    .filter([](const auto &val){ return val > 10; }) // filters a values greater 10
    .collect<std::vector>();                         // collects the resulting
                                                     // elements into a vector

Generators

Generator functions helps creating streams from containers and initializer lists or defined ranges with start and end and optionally a step size.

Furthermore an endless counter stream generator exists.

Make stream

With the make_stream(...) generator function it is possible to create several kinds of streams.

To create a stream from an existing container, just pass the container to the function make_stream(container).

1
2
3
std::vector<std::string> string_vec { "one", "two", "three", "four" };

auto result = make_stream(string_vec);

The make_stream(initializer_list) can also directly take an initializing list of elements to create a stream.

1
auto result = make_stream({ "one", "two", "three", "four" });

It is also possible to create ranges with make_stream(from, to). Where from and to are included values in the generated stream.

1
2
3
4
// creates a stream of ints from 1 to 8
auto result = make_stream(1, 8);

// resulting stream: 1, 2, 3, 4, 5, 6, 7, 8

In an extended version of the make_stream(from, to, stepsize) generator function a stepsize value can be added indicating the incremental value for the generated stream.

1
2
3
4
// creates a stream of ints from 1 to 8 with a step of two
auto result = make_stream(1, 8, 2);

// resulting stream: 1, 3, 5, 7

Make stream counter

The make_stream_counter(start) generator creates an endless steam starting from start.

1
2
3
4
5
auto result = make_stream_counter(1)
    .take(5)
    .collect<std::vector>();

// result: 1, 2, 3, 4, 5

In an extended version of the make_stream(start, stepsize) generator the stepsize parameter defines the increment value of the endless stream.

1
2
3
4
auto result = make_stream_counter(1, 2)
    .take(5)
    .collect<std::vector>();
// result: 1,3,5,7,9

Element processors

Take

The take(count) processor takes the first count elements of the incoming range and passes them to the next processor. All further elements are ignored and the stream is completed.

1
2
3
auto result = make_stream_counter(6)
    .take(3)
    .collect<std::vector>();

Take while

The take_while(predicate) processor takes all elements matching the given predicate of the incoming range and passes them to the next processor.

1
2
3
auto result = make_stream_counter(6)
    .take_while([](const int &i) { return i > 1 && i < 4; })
    .collect<std::vector>();

Skip

The skip(count) processor skips the first count elements of the incoming range and passes following elements to the next processor.

1
2
3
auto result = make_stream_counter(6)
    .skip(5)
    .collect<std::vector>();

Skip while

The skip_while(predicate) processor skips all elements matching the given predicate of the incoming range. All elements not matching the predicate are passed to the next processor.

1
2
3
auto result = make_stream_counter(6)
    .skip_while([](const int &i) { return i > 1 && i < 4; })
    .collect<std::vector>();

Every

The every(count) processor passes every count element of the incoming elements to the next processor.

1
2
3
auto result = make_stream(1, 12)
    .every(3)
    .collect<std::vector>();

Filter

The filter(predicate) processor applies the predicate to each incoming element. If the predicate returns true the element is passed to the next processor otherwise the element is skipped.

1
2
3
4
5
bool is_even(int val) { return val % 2 == 0; }

auto result = make_stream(1, 8)
    .filter(is_even)
    .collect<std::vector>();

Map

The map(predicate) processor applies the predicate to each incoming element. The predicate modifies the element or transforms it to new element of a different type and returns it. The returned element is than passed to the next processor.

1
2
3
auto result = make_stream(1, 8)
    .map([](const int &i) { return std::to_string(i); })
    .collect<std::vector>();

Flat map

The flatmap(predicate) flattens a nested container of the incoming elements (list, vectors, i.e.) into one new stream of elements containing all elements of the container.

1
2
3
4
5
6
7
8
auto result = make_stream<person>({
    {"otto",   34, { "red", "green" }},
    {"jane",   27, { "blue"}},
    {"george", 56, { "brown", "purple" }},
    {"bobby",  15, { "yellow", "gold", "silver" }}
  }).flatmap([](const person &p) {
    return p.colors;
  }).collect<std::vector>();

Peek

The peek(predicate) processor provides access to each incoming element in the stream and passes it to the next processor. It can be used to check or debug each stream element.

1
2
3
4
5
int result = 0;
make_stream(3, 9)
    .take(1)
    .peek([&result](const int &val) { result = val; })
    .count();

Concat

The concat(stream) concatenates the current stream with the given stream of the same type. Once the current stream finishes the elements of the next stream are processed seamlessly.

1
2
3
4
auto s = make_stream(6, 10);
auto result = make_stream(1, 5)
    .concat(s)
    .collect<std::vector>();

Pack every

The pack_every(packsize) processor packs every n elements of the stream into a container (vector) and create a new stream of it.

1
2
3
auto result = make_stream(1, 13)
    .pack_every(3)
    .collect<std::vector>();

The exmaple above leads to a collection/stream of the following elements:

1
std::vector<std::vector<int>> result = { {1,2,3},{4,5,6},{7,8,9},{10,11,12},{13} };

Terminators

First

The first() terminator returns an optional with the first element of the resulting element list. If no first element is in the list a null optional is returned.

1
2
auto first_value = make_stream(1, 8)
    .first();

Last

The last() terminator returns an optional with the last element of the resulting element list. If no last element is in the list a null optional is returned.

1
2
auto first_value = make_stream(1, 8)
    .last();

Min

The min() terminator determines the minimum value of all elements in the stream.

1
2
auto minval = make_stream(1, 8)
    .min();

Max

The max() terminator determines the maximum value of all elements in the stream.

1
2
auto minval = make_stream(1, 8)
    .max();

At

The at(index) terminator access an element at the requested index and return a optional<T> containing the element. If the index isn’t valid a null optional is returned.

1
2
auto value_at_index = make_stream(1, 8)
    .at(4);

Any

The any(predicate) terminator returns true if any of the incoming elements applies to the given predicate.

1
2
3
bool is_even(int val) { return val % 2 == 0; }

bool is_any_even = make_stream(1, 6).any(is_even);

All

The all(predicate) terminator returns true if all of the incoming elements applies to the given predicate.

1
2
3
bool is_less_seven(int val) { return val < 7; }

bool are_all_less_seven = make_stream(1, 6).all(is_less_seven);

None

The none(predicate) terminator returns true if none of the incoming elements applies to the given predicate.

1
2
3
bool is_greater_seven(int val) { return val > 7; }

bool are_all_greater_seven = make_stream(1, 6).none(is_greater_seven);

Count

The count() terminator returns the current number of elements in the stream.

1
2
auto size = make_stream(1, 8)
    .count();

A second version of count(predicate) takes a predicate and counts all elements for which the predicate returns true.

1
2
3
4
bool is_even(int val) { return val % 2 == 0; }

auto size = make_stream(1, 8)
    .count(is_even);

Reduce

The reduce(accumulator) terminator are reduced to one value applying the given accumulator function. The function takes the current accumulated value and the current element and returns the new accumulated value. An optional<T> is returned, because the stream could be empty and then a null optional is returned.

1
2
auto sum = make_stream(1, 8)
    .reduce(std::plus<>());

To always return a valid value it a second version of reduce(initial, accumulator) exists taking an initial value and the accumulator function. For the first element in the stream the initial value is used than it it works like the first version of this function. If the stream is empty the inital value is returned.

1
2
3
4
5
6
7
8
auto reduce_identity_result = make_stream(1, 8).reduce(std::string(), [](const std::string &result, int i) {
    std::stringstream istr;
    if (!result.empty()) {
      istr << ",";
    }
    istr << i;
    return result + istr.str();
  });

The third reduce_idfunc(initial_func, accumulator) terminator takes a function creating the initial value for the first stream element.

1
2
3
4
5
6
7
8
9
auto reduce_identity_func_result = make_stream(1, 8).reduce_idfunc([](const int &val) {
    std::stringstream istr;
    istr << val;
    return istr.str();
  }, [](const std::string &result, int i) {
    std::stringstream istr;
    istr << " <-> " << i;
    return result + istr.str();
  });

Collect

The collect<C>() terminator collects all resulting elements into a new container. The type of the container must be provided within the template type.

1
2
auto result = make_stream(3, 17)
    .collect<std::vector>();

For each

The for_each(predicate) terminator applies the given predicate function to each resulting element of the stream.

1
2
3
4
auto result = make_stream(3, 17)
    .for_each([](const auto &val){
        std::cout << "element: " << val << "\n";
    });

Join

The join() terminator concats all elements of a stream to a string and comes in three flavours. The first one takes no arguments and just concats all elements. The second joind(delimiter) terminator takes a delimiter and inserts that delimiter between every stream element when concatenating. The third and last join(delimiter, suffix, prefix) terminator takes additionally to the delimiter string a suffix and prefix string which will be prepended and appended to the resulting string.

1
2
3
4
5
6
7
8
9
10
11
auto result = make_stream({"one", "two", "three", "four"}).join();

// result: "onetwothreefour"

result = make_stream({"one", "two", "three", "four"}).join(", ");

// result: "one, two, three, four"

result = make_stream({"one", "two", "three", "four"}).join(", ", "[", "]");

// result: "[one, two, three, four]"

Logging

Matador comes with a simple logging system. The main concept uses internally instances of class log_domain. This class is the connection point between a list of log sinks defining where the log messages are written to and one or more instances of class logger. The logger let the user write a message with a certain log level for a defined source name.

All these components are managed with the log_manager singleton class.

Log sinks

A log sink is a defined destination for log messages. Sinks are added to a certain log_domain. They are added as std::shared_ptr<log_sink> so that they can be shared over several domains.

By now there are four sink types:

File sink

A file sink writes the log messages to a single file. The file is defined by a given path. If the complete path doesn’t exists, all necessary directories are created.

The helper function matador::create_file_sink(...) creates the file sink within a std::shared_ptr.

1
2
3
auto path = matador::os::build_path("my", "log", "log.txt");

auto logsink = matador::create_file_sink(path);

Stdout sink

The stdout sink writes the raw message to the standard output.

The helper function matador::create_stdout_sink() creates the file sink within a std::shared_ptr.

1
auto stdoutsink = matador::create_stdout_sink();

Stderr sink

The stdout sink writes the raw message to the standard error.

The helper function matador::create_stderr_sink() creates the file sink within a std::shared_ptr.

1
auto stdoutsink = matador::create_stderr_sink();

Rotating file sink

The rotating file sink writes the log message into a set of rotating files. The user can define the maximum number of log files and the maximum file size for one log file. The file is defined by a given path.

In the following example the rotating log files are located at ./my/log/. Where active log file name is log.txt. The max file size is 30 bytes and a maximum of 3 log files are allowed. Once the max file size is reached. The next file is created. This might be one of log-{1-3}.txt.

1
2
3
auto path = matador::os::build_path("my", "log", "log.txt");

auto logsink = matador::create_rotating_file_sink(path, 30, 3);

Log domain

The log_domain must have a unique name and bundles a bunch of log sinks. There is always a default log_domain where sinks can be added to.

Log sinks must be added to a specific domain. To add a sink to a domain call matador::add_log_sink(...). Called only with the sink to add, the sink is added to the default domain.

1
2
3
4
auto logsink = matador::create_file_sink("log.txt");

// the sink will be added to the default domain
matador::add_log_sink(logsink);

If called with the sink and a domain name, the sink is added to that log_domain. If the domain doesn’t exists, it is created.

1
2
3
4
auto logsink = matador::create_file_sink("log.txt");

// add sink to the domain "net"
matador::add_log_sink(logsink, "net");

Log Manager

The log_manager class acts like a singleton and stores all available log_domain instances. It is used to add log_domain instances and to create logger instances. The log_manager acts in the background. It is not reallay necessary to call it directly.

All interfaces like adding a log_domain or to creating a logger are wrapped through global functions in the matador namesapce.

Logger

Once all domains and sinks are configured, logger can be spread over the code to log messages of all log levels.

Each instance of logger consists of a name representing the source of the log message. The source name is also written to the log sink.

The class logger provides also an interface to write log messages with an apropriate log level. There are six valid levels:

Lets asume there is a class NetworkManager with a method send_data(...). When this method is called a debug log message should be written. Therefor one could place a logger as a member of the class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class NetworkManager
{
public:
    // init the logger
    NetworkManager()
        : log_(matador::create_logger("NetworkManager"))
    {}

    void send_data(char *data, size_t size)
    {
        // do stuff and the log message
        log_.debug("written %d bytes of data", size);
    }

private:
    matador::logger log_;
}

In line [6] the logger is created. The created logger uses the default domain with its configured sinks. The source name of the logger is “NetworkManager”.

In method send_data(...) the logger is called to write a debug message. The interface is the same as the ::printf(...) interface. There are placeholders of variables in the message followed by a list of values.

The resulting log message line looks like this:

2020-05-04 11:09:15.841 [DEBUG ] [NetworkManager]: written 678 bytes of data

The line consists of a timestamp with milliseconds, the log level, the source name and the message.

Note: By now you can’t change the order or the format of the message.

The following lists the log interface of the logger class:

Networking

Matador comes with a simple network stack realized through the reactor pattern. The groundwork is realized through the address, peer and socket classes. Based on these classes the reactor consists of the reactor, àcceptor, connector and the an implementation of a handler handling an open network connection.

Protocol

The reactor supports the TCP and the UDP protocoll. Based on these to protocolls there are two classes tcp and udp. These classes provides shortcuts to the socket and peer types.

Therefor two classes tcp and udp with type definitions for peer, socket, acceptor and (address) resolver exists in header ip.hpp.

Network Address

The address class represents either an IPv4 or an IPv6 address. An IpV4 or IPv6 address can easily created with the following lines:

1
2
auto google_v4 = address::v4::from_hostname("www.google.org");
auto google_v6 = address::v6::from_hostname("www.google.org");

In the same way an address can be created from an IP address:

1
2
auto localhost_v4 = address::v4::from_ip("127.0.0.1");
auto localhost_v6 = address::v6::from_ip("::1");

There are also shortcuts for broadcast, loopback and any addresses:

1
2
3
4
5
6
7
8
address broadcast_v4 = address::v4::broadcast();
address broadcast_v6 = address::v6::broadcast();

address localhost_v4 = address::v4::loopback();
address localhost_v6 = address::v6::loopback();

address any_v4 = address::v4::any();
address any_v6 = address::v6::any();

Peer

A peer constists of an address and a port. It represents a network connection endpoint of either the local or the remote side.

Below see an example to create a peer for localhost at port 8080:

1
2
address localhost = address::v4::loopback();
tcp::peer localhost8080(localhost, 8080);

Socket

A socket represents a network connection between two network endpoints (peers). The connection can either be connected or closed.

Handler

The handler class provides an interface for all connection processors handled by the reactor. The concrete implementation must overwrite interface

The following interfaces might be implemented depending on the purpose of the concrete handler implementation:

Acceptor

The acceptor class implements the handler interface and accepts a connection within the running reactor. It needs an endpoint to listen on and an accept handler which is called once a connection was established. It returns an implementation of the handler class which is then registered within the reactor.

Assuming there is a class EchoHandler handling an established connection the creation of an acceptor could look like the code below:

1
2
3
4
5
6
7
8
9
// create an endpoint (listens on port 7777)
auto ep = tcp::peer(address::v4::any(), 7777);

// creates the acceptor with endpoint and handler callback
auto ac = std::make_shared<acceptor>(ep, [](tcp::socket sock, tcp::peer p, acceptor *) {
    auto cl = std::make_shared<EchoHandler>();
    cl->init(std::move(sock), std::move(p));
    return cl;
  });

Connector

The connector class implements the handler interface and connects to a given connection endpoint and once the connection is established by the remote side a connect handler callback is called. It returns an implementation of the handler class which is then registered within the reactor.

1
2
3
4
5
6
7
8
// create the handler in advance
auto echo_conn = std::make_shared<EchoHandler>();

// creates the connector with handler callback
auto echo_connector = std::make_shared<connector>([echo_conn](const tcp::socket& sock, const tcp::peer &p, connector *) {
    echo_conn->init(sock, p);
    return echo_conn;
  });

Reactor

The reactor class implements a single threaded select() based reactor pattern. Interally it deals with a list of handlers for reading, writing and accepting io data or to schedule a call in a given interval.

Note: In a future release the reactor will be multi threaded to increase performance.

Once a reactor is instanciated handlers can be registered with register_handler(handler, event_type) where handler comes within a shared_ptr and the event type tells the reactor the use case of the handler (reading, writing, accepting, exceptionell).

Most common usage is here to register an accetpor handler processing incoming connections.

After the handler are registered the reactor can be started.

1
2
3
4
5
6
7
auto ac = std::make_shared<accpector>(...);

reactor r;

r.register_handler(ac, event_type::ACCEPT_MASK);

r.run();

The reactor provides also the possibility to schedule a timer calling a handler on timeout. Therefor the schedule_timer(handler, offset, interval) method can be used. The scheduling starts once the reactor is started.

1
2
3
4
5
6
7
auto h = std::make_shared<TimeoutHandler>(...);

reactor r;
// calls on_timeout() after 1 second and than every 3 seconds
r.schedule_timer(h, 1, 3);

r.run();

IO Service

The io_service encapsulates the reactor and provides an easy to use interface to register callbacks for established connections either active ones (connect()) or passive ones (accept()).

IO Stream

The io_stream defines an interface used by the io_service. There are two main interface methods read(buffer_view, read_handler) and write(buffer_view_list, write_handler).

The first interface is called when data should be read from a socket. Once the date was read the given read_handler is called.

The second nterface is called when data should be written to a socket. Once the data was written the given write handler is called.

Stream Handler

Finally the stream_handler class implements the handler and the io_stream interface and is used by the io_stream class.

It handles the reading and writing of data for every established connection.

Http

Based on the matador network stack a simple http server and client are also provided. These classes can handle the HTTP methods GET, POST, PUT and DELETE. The data is exchanged with a http::request and the result comes within a http::response.

Request

When dealing with HTTP communication there is always a request object representing a specific HTTP request. In matador there also a http::request.

Im most cases the request is either created by the http::client class or by the request parser called with the http::server. When accessing and evaluating the request in a regsitered callback the user can access all neccessary information from the request object.

The accessors of the request class are self explanatory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class request
{
  http::method_t method() const;
  std::string url() const;
  std::string fragment() const;
  http::version version() const;
  std::string host() const;
  const http::content& content() const;
  const t_string_param_map& headers() const;
  const t_string_param_map& path_params() const;
  const t_string_param_map& query_params() const;
  const t_string_param_map& form_data() const;
  const std::string& body() const;
};

Response

The http::response is the counterpart class to http::request. It provides the rqeuested answer and contains therefor all neccessary fields like headers, body and status code.

There are severval shortcuts to create a response object. A simple OK response can be created in the following way:

1
auto resp = http::response::ok("hello world", http::mime_types::TYPE_PLAIN_TEXT);

With this call all neccessary fields like body, header, date, length and type are set and the response can be send.

It is also possible to pass objects or json objects into this function but with out the mimetype.

1
2
template < class T >
static response ok(const T& obj) { //... }

This will also lead to an OK response with the given object converted into a json strig as body.

Http Server

With the http::server class it is easy to create a simple HTTP server in a few lines of code. Just instanciate a http::server, enable the builtin routing middleware and add a callback for a route. The start the server.

1
2
3
4
5
6
7
8
http::server server(8000);
server.add_routing_middleware();

server.on_get("/", [](const http::request &) {
    return http::response::ok("hello world", http::mime_types::TYPE_PLAIN_TEXT);
});

server.run();

Besides on_get(...) there are three other callback taking methods:

These four methods can be used to build up your REST server. The callback hides behind the RequestHandler and takes a http::request as a parameter. The return value of this function is a http::response object.

After build up the HTTP callbacks the server can be started with http::server::run().

Http Client

The http::client enables the user to write a http client and call remote HTTP functions like GET, POST, PUT and DELETE. The client is by now synchronous waiting for an answer of the remote side.

1
2
3
http::client client("www.remote.net:8000");

http::response resp =  client.get("/hello");

Besides get(...) there are three other methods used to send a request:

Middleware

The http::server uses the middleware pattern (which is a variation of the Chain of Responsibility pattern.

A middleware must implement the process(...) interface. The example below shows how an example logging middleware could be implemented.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class log_middleware : public http::middleware
{
public:
  log_middleware()
    : log_(matador::create_logger("LogMiddleware"))
  {}

  matador::http::response process(http::request &, const next_func_t &next) override
  {
    log_.info("before LogMiddleware");
    auto resp = next();
    log_.info("after LogMiddleware");
    return resp;
  }

private:
  matador::logger log_;
};

When entering the process(...) method the request itself is passed and a callback to the next middleware returning a http::response object. With this setup it is possible to make an action before and after the request was processed by all other middlewares. After the middle has done its before action it must call the passed function callback next() executing the next middleware and returning a response object.

In the after region it is possible to access, modify or ignore the response object returned by the call to the next() callback.

Once a middleware is implemented, an instance can be added to the HTTP server:

1
2
3
4
5
6
7
http::server svr(...);

srv.add_middleware(std::make_shared<log_middleware>());

// setup server routes
// ...
srv.run();

Template Engine

Matador comes also with a small template engine. It uses the Django Template language syntax but by now not all tags and filters are implemented at all. The advantage is that the langauge isn’t limited on XML/HTML content it can be used with any text based formats.

Just call render with the content string to be processed and a json object containing the data:

1
2
3
4
std::string content { ... };
json data { ... };

auto result = http::template_engine::render(content, data);

The returned result string contains the processed content based on the json data.

The engine handles variable and tags. Variables are indicated by two curly brackets {{ [var] }} and the containing variable is replaced by its value. Tags are like logic commands and are indicated by one curly bracket and a percent sign {\% [tag] \%}.

Tags

The following tags are available:

For Loop

The tag for implements the loop functionallity. Loop scan be nested. In the exmaple below names and items of all persons are listed.

1
2
3
4
5
6
7
8
9
10
11
12
<ul>
  {% for person in persons %}
  <li>
    <h2>{{ person.name }}</h2>
    <ol>
      {% for i in person.items %}
      <li>{{ i }}</li>
      {% endfor %}
    </ol>
  </li>
  {% endfor %}
</ul>

If Condition

The if condition tag works as unary operator (the variable evaluates to true or false) or as binary operator (the expression evaluates to true or false).

The common expression operators are available (==, !=, <, <=, > and >=).

1
2
3
4
5
6
7
8
9
{% if person.id == 2 %}
  <p>1 Details of {{ person.name }}</p>
{% elif person.id < 2 %}
  <p>2 Details of {{ person.name }}</p>
{% elif person.id >= 3 %}
  <p>3 Details of {{ person.name }}</p>
{% else %}
  <p>No details</p>
{% endif %}

Include File

Filter

And the following filters are available:

Escape filter

Capfirst filter

Upper filter

Lower filter