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.
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);
}
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";
}
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();
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();
There are installation packages for Linux and Windows available.
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 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);
}
};
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
NOT NULL
UNIQUE
PRIMARY KEY
FOREIGN KEY
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});
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()
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()
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()
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.
The following types are supported and can be used within the matador::access::attribute()
functions:
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_);
}
};
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
}
};
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_);
}
};
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
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:
citizen
field the persons
address
field is automatically updated with this address.address
field the address’
citizen
field is automatically updated with this person.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";
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"><</span> <span class="k">typename</span> <span class="nc">Operator</span> <span class="p">></span>
<span class="kt">void</span> <span class="n">process</span><span class="p">(</span><span class="n">Operator</span> <span class="o">&</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"><</span> <span class="k">typename</span> <span class="nc">Operator</span> <span class="p">></span>
<span class="kt">void</span> <span class="n">process</span><span class="p">(</span><span class="n">Operator</span> <span class="o">&</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";
}
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'
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.
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.
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.
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).
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;
}
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.
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();
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>
database-type:
One of postgresql
, mysql
, mssql
or sqlite
username:
The username of the database connectionpassword:
The password of the database connectionserver:
The server address or hostname of the databaseport:
The server port of the databasedatabase-name:
The name of the database instanceExample:
1
2
3
4
5
connection conn("mysql://test@localhost/mydb");
conn.open();
// ... use connection
conn.close();
There’re currently four supported databases and the in memory database. Next is the description of the database connection string for the supported databases.
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");
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");
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");
1
2
// MySQL connection string
session ses(ostore, "sqlite://database.sqlite");
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";
}
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.
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.
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);
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);
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);
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");
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";
}
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);
With condition one can express a query where clause (for update
, delete
and
select
queries). There are five different types of 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
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
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});
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());
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%");
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.
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.
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();
}
Matador comes with its own time
and date
classes which are
described below.
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.
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.
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
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);
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();
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.
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.
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
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
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();
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()
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>();
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>();
});
A service can be bound by now in four different ways:
1
2
3
// register the transient strategy (always a new instance)
template < class T, typename ...Args >
di::proxy<i>::to(Args &&...args);
1
2
3
// register the singleton strategy (always the same instance)
template < class T, typename ...Args >
di::proxy<i>::to_singleton(Args &&...args);
1
2
3
// register the given instance
template < class T >
di::proxy<i>::to_instance(T &&obj);
1
2
3
// register the singleton per thread strategy (always the same instance)
template < class T, typename ...Args >
di::proxy<i>::to_singleton_per_thread(Args &&...args);
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");
});
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");
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.
To use Json object you have to include #include "matador/utils/json.hpp
. Json supports the following datatypes:
bool
)double
or long long
)std::string
)std::map<std::string, json>
)std::vector<T>
)std::nullptr_t
)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);
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;
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);
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();
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];
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 |
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
.
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.
to_string(json, format)
to_string<T>(object, format)
to_string<T>(vector<object>, format)
The next set of methods takes a json string, an object or a list of objects and convert it into a json object.
to_json(string)
to_json<T>(object)
to_json<T>(vector<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.
to_object<T>(json)
to_object<T>(string)
to_objects<T>(json)
to_objects<T>(string)
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_string<T>(array<objects>, format)
to_string<T>(object_ptr, format)
to_string<T>(object_view, format)
To convert an object pointer or an object view to a json object use the to_json(...)
methods.
to_json<T>(object_ptr)
to_json<T>(object_view)
The last set of methods converts a json object or a string into one object or a list of objects of a specific type.
to_object<T>(json)
to_object<T>(string)
to_objects<T>(json)
to_objects<T>(string)
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
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.
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
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
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>();
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>();
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>();
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>();
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>();
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>();
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>();
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>();
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();
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>();
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} };
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();
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();
The min()
terminator determines the minimum value of all elements in the stream.
1
2
auto minval = make_stream(1, 8)
.min();
The max()
terminator determines the maximum value of all elements in the stream.
1
2
auto minval = make_stream(1, 8)
.max();
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);
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);
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);
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);
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);
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();
});
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>();
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";
});
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]"
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.
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
: Log message is written to one filestdout_sink
: Log message is written to stdoutstderr_sink
: Log message is written to stderrrotating_file_sink
: Log message is written to several rotating filesA 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);
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();
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();
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);
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");
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.
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:
log_level::LVL_FATAL
)log_level::LVL_ERROR
)log_level::LVL_WARN
)log_level::LVL_INFO
)log_level::LVL_DEBUG
)log_level::LVL_TRACE
)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:
logger::fatal(message, ...)
logger::error(message, ...)
logger::warn(message, ...)
logger::info(message, ...)
logger::debug(message, ...)
logger::trace(message, ...)
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.
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
.
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();
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);
A socket represents a network connection between two network endpoints (peers). The connection can either be connected or closed.
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:
on_input()
: Called every time the connection has data to readon_output()
: Called every time the connection has data to writeon_except()
: Called every time the connection has exceptionell data availableon_timeout()
: Called every time a scheduled timeout for this handlers occurredon_close()
: Called when the connection is about to closeopen()
: Called when an handler is openedclose()
: Called when an handler is closedis_ready_write()
: Called by the reactor to check if there is data ready to writeis_ready_read()
: Called by the reactor to check if there is data ready to readname()
: Returns a name for this handlerThe 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;
});
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;
});
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();
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()
).
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.
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.
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
.
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;
};
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.
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:
on_get(const std::string &route, RequestHandler request_handler)
on_post(const std::string &route, RequestHandler request_handler)
on_put(const std::string &route, RequestHandler request_handler)
on_remove(const std::string &route, RequestHandler request_handler)
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()
.
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:
get(const std::string &route)
post(const std::string &route, const std::string &body)
put(const std::string &route, const std::string &body)
remove(const std::string &route)
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();
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] \%}.
The following tags are available:
for
A for loopif
Conditionsinclude
Include and parse other filesThe 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>
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 %}
And the following filters are available:
escape
Escapes a string variablecapfirst
Makes the first character of a string uppercaseupper
Makes the string uppercaselower
Makes the string lowercase