Published at February 23, 2022 · Last Modified at March 5, 2022 · 5 min read · Tags: rest api
Here is a simple project to implement a general REST API service module in c++ using reasbed library. The library uses a modern c++ to implement REST API calls. And it is possible to use the REST service to access databases, access, and control a complex application to help the QA & verification process by accessing internal SW modules. Moreover, the module is simple and easily removed in production modes.
Download & install restbed
The project uses CMake. Therefore, I added the task to download and compile the library to the CMake tasks. The Cmake downloads and builds the library before any other dependency is executed. It is also possible to use the distribution to install it (apt-get, yam install, etc.'), but compiling and building the library as part of the code allows the programmer to debug it and step into the library code.
ExternalProject_Add(restbed_external
GIT_REPOSITORY https://github.com/corvusoft/restbed.git
CMAKE_ARGS -DBUILD_SSL=NO -DBUILD_SHARED=YES -DCMAKE_INSTALL_PREFIX=${CMAKE_SOURCE_DIR}/${3rd_part}/restbed/${CMAKE_BUILD_TYPE} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
INSTALL_DIR ${CMAKE_SOURCE_DIR}/${3rd_part}/restbed/${CMAKE_BUILD_TYPE}
DEPENDS restbed
)
The REST API Server
The REST server is based on the IRestServer class:
class IRestServer {
public:
void get_handler_callback( std::shared_ptr< restbed::Session > session ,
std::function<Json::Value(std::shared_ptr< restbed::Session > session)> callback,
const std::vector<std::string> & ids = {} );
void post_handler_callcback( std::shared_ptr< restbed::Session > session ,
std::function<Json::Value(std::shared_ptr< restbed::Session > session, Json::Value root)> callback,
const std::vector<std::string> & ids = {} );
virtual Json::Value secondary_get_handler_callback( std::shared_ptr< restbed::Session > session ,
std::function<Json::Value(std::shared_ptr< restbed::Session > session)> callback,
const std::vector<std::string> & ids = {} ) = 0;
virtual Json::Value secondary_post_handler_callback( std::shared_ptr< restbed::Session > session ,
std::function<Json::Value(std::shared_ptr< restbed::Session > session,
Json::Value root)> callback,
const std::vector<std::string> & ids = {} ) = 0;
...
};
The functions get_handler_callback and post_handler_callcback treat GET and POST messages respectively. It extracts the JSON or creates JSON data for the GET and the POST requests. The functions secondary_get_handler_callback and secondary_post_handler_callback executes the user callback. The purpose of the secondary functions is to allow another layer of try/catch exceptions and make the callback function clear without a try/catch block. Here is an example of a user callback for getting an SW version. The function makes sure that all necessary parameters arrive with the API call.
void BrokerResetServer::gethandler_getVersion( const std::shared_ptr< restbed::Session > session )
{
auto func = [this](std::shared_ptr< restbed::Session > session) ->Json::Value {
Json::Value root;
root["error"] = 0;
root["error-msg"]="OK";
root["version"]="1.0.0";
return root;
};
// validate "id" parameter
get_handler_callback(session , func, {"id","symbol"});
}
Database access
This example is a REST SERVICE the access to a database. I have used PostgreSQL. First, it is also needed to install its development libraries. Then, here is an interface class to access databases.
class idb {
public:
virtual Json::Value query(const std::string & sql, const std::vector<std::string> & f = {} ) = 0 ;
virtual bool execute(std::string sql) = 0;
virtual void close() = 0;
};
I have defined the class manage to manage the SQL operations. This class is injected with idb object and wraps its operations.
class manage {
public:
manage() = default;
~manage() {
if (m_db != nullptr)
m_db->close();
}
void set_db(std::unique_ptr<idb> db ) {
m_db = std::move(db);
}
Json::Value query(std::string q, const std::vector<std::string> & f = {} ) {
return m_db->query(q, f);
}
void close () {
m_db->close();
}
bool execute(std::string sql) {
return m_db->execute(sql);
}
private:
std::unique_ptr<idb> m_db;
};
and here is a class to implement the interface for PostgreSQL:
class psql:public idb {
public:
psql(std::string connection_str):
m_postgressConnection (connection_str) {};
void close() override{
m_postgressConnection.close();
}
Json::Value query(const std::string & sql, const std::vector<std::string> & f={}) override
{
Json::Value root;
/* Reads symbols table */
try {
/* Create a non-transactional object. */
pqxx::nontransaction N( m_postgressConnection);
/* Execute SQL query */
pqxx::result R(N.exec(sql));
/* List down all the records */
int idx = 0;
for (pqxx::result::const_iterator c = R.begin(); c != R.end(); ++c) {
for (int j = 0; j < c.size(); j++) {
std::string name;
if (j < f.size() )
name = f[j];
else
name = R.column_name(j);
if (!c[j].is_null())
root[idx][name] = c[j].as<std::string>();
else
root[idx][name] = "empty";
}
idx++;
}
} catch (const std::exception &e) {
root["error"] = -1;
root["msg"] = e.what();
std::cerr << e.what() << std::endl;
}
return root;
}
bool execute(std::string sql) override
{
pqxx::work W(m_postgressConnection);
try {
W.exec( sql );
W.commit();
} catch (const std::exception &e) {
W.abort();
std::cerr << e.what() << std::endl;
return false;
}
return true;
}
private:
pqxx::connection m_postgressConnection;
};
The function query can be used as the follwoing :
query ("select name, age from names", {"name-tag", "age-tag"})
For example, If the are two names in the table, The function will retrieve the following JSON object:
[{"name-tag":"Alice", "age":47}, {"name-tag":"Bob", "age":35}
if the function is called without the second argument, the resulted JSON will be like:
[{"name":"Alice", "age":47}, {"name":"Bob", "age":35}
And finally, here is the class that encapsulates the operation of the database. In general, this class is a kind of system controller, where it allows to read/write from/to internal application states. In this example, it is a controller to access databases. Another controller example can be a class that sends parameters to the application to control its states. For example, such a controller can be used for QA and will remove in production mode.
class data_server: public manage {
public:
data_server() = default;
data_server(const db::data_server&) {
}
Json::Value names(int age) {
std::stringstream sql;
if (age == -1)
sql<< "select * from names";
else
sql<< "select * from names where age="<<age<<";";
return query(sql.str(),{"id","name-tag","age-tag"});
}
}
Installation and Running
It can download the project from here. There is a docker-compose file to build and run the project. The script also installs a docker of PostgreSQL. To build the project run the following script:
git clone git@github.com:yairgd/rest_server.git
cd docker
docker-compose up appserver
The docker-compose has two services, and it is possible to install them one by one:
cd docker
docker-compose up appserver
docker-compose up appdb
After installation, when the docker of PostgreSQL is running, it can log in to the database console using the following command.
export PGPASSWORD=docker
psql docker -d docker -U docker -h 10.5.0.27
The build process should create the table names. if not, then in the console, type the following to make the name table:
CREATE TABLE names (
id SERIAL ,
name VARCHAR (32) ,
age integer
);
insert into names (name,age) values ('alice',47);
insert into names (name,age) values ('bob',35);
Use curl in a bash shell:
curl http://10.5.0.23:1984/names/?age=35
[{"age-tag":"35","id":"2","name-tag":"bob"}]
curl http://10.5.0.23:1984/names
[{"age-tag":"47","id":"1","name-tag":"alice"},{"age-tag":"35","id":"2","name-tag":"bob"}]