REST сервис на C++: POCO+Angular TODO

в 11:18, , рубрики: AngularJS, c++, Poco, rest, Веб-разработка, Программирование, метки: ,

POCO — кроссплатформенная open-source библиотека на С++ под Boost Software License: ru.wikipedia.org/wiki/POCO.
POCO имеет в своем составе средства для создания веб-сервисов c RESTful API.
В данной статье рассмотрено создание такого сервиса на примере TODO.
REST сервис на C++: POCO+Angular TODO - 1

Простейшее приложение TODO — это список задач с возможностью добавить новую или удалить выполненную задачи.

Задача реализована в CTodo. Каждой задаче в списке присваиваются уникальный идентификатор (id) и пользовательское описание (text).
Список задач реализован в CTodoList. Управление списком задач производится методами CRUD Для хранения списка задач используется std::map.
Http сервер бэкенда реализован в TodoServerApp. Данный класс содержит методы CRUD, а также список задач и мьютекс для синхронизации доступа к нему. Метод main вызывается из базового класса POCO ServerApplication.

TodoServerApp.h
#pragma once

#include <Poco/Mutex.h>
#include <Poco/Net/HTTPServerRequest.h>
#include <Poco/Net/HTTPRequestHandler.h>
#include <Poco/Net/HTTPServerResponse.h>
#include <Poco/Net/HTTPServer.h>
#include <Poco/Net/HTTPRequestHandlerFactory.h>
#include <Poco/Net/HTMLForm.h>
#include <Poco/Path.h>
#include <Poco/ScopedLock.h>
#include <Poco/StringTokenizer.h>
#include <Poco/URI.h>
#include <Poco/Util/ServerApplication.h>

using namespace Poco;
using namespace Poco::Net;
using namespace Poco::Util;
using namespace std;

/**
    Todo
*/
class CTodo {
    size_t id;
    string text;
public:
    CTodo(string text): text(text){
    }
    /* getters & setters */
    size_t getId(){ return id; }
    void setId(size_t id){ this->id = id; }
    string getText(){ return text; }
};

/**
    Список Todo
*/
class CTodoList {
    size_t id;
    map<size_t, CTodo> todos;

public:
    CTodoList():id(0){}
    /* CRUD */
    void create(CTodo& todo){ todo.setId(++id); todos.insert(pair<size_t,CTodo>(id, todo)); };
    map<size_t, CTodo>& readList(){ return todos; }
    void del(size_t id){ todos.erase(id); };
};

/**
    Сервер
*/
class TodoServerApp : public ServerApplication
{
public:
    /* CRUD */
    static void createTodo(CTodo& todo);
    static CTodoList& readTodoList();
    //static void updateTodo(size_t id, CTodo& todo);
    static void deleteTodo(size_t id);

protected:
    int main(const vector<string> &);
    static Mutex todoLock;
    static CTodoList todoList;
};

При вызове метода main в классе TodoServerApp создается Http сервер с заданными параметрами (порт 8000 и т.д.). Также серверу передается фабрика обработки запросов TodoRequestHandlerFactory.
После инициализации сервера запускается бесконечный цикл.
В данном случае фабрика TodoRequestHandlerFactory имеет всего два обработчика: CFileHandler для отдачи статики и CTodoHandler собственно для самого REST API.
Обработчик REST API CTodoHandler определяет тип запроса (GET/POST/PUT/DELETE). Для методов PUT/DELETE вычисляется идентификатор по URI. В соответствие с типом запроса производятся необходимые действия с данными списка.
Поскольку данное приложение очень простое, метод PUT не используется. Выдача информации для GET по id конкретного задания также не реализовано.
Далее производится формирование ответа в выходной поток сервера. С этой целью для задачи CTodo и списка задач CTodoList перегружен оператор <<.

TodoServerApp.cpp

#include <iostream>
#include <string>

#include "TodoServerApp.h"

Mutex TodoServerApp::todoLock;
CTodoList TodoServerApp::todoList;

ostream& operator<<(ostream& os, CTodo& todo)
{
    os << "{ "_id": "<< todo.getId() <<  ", "text": "" << todo.getText() << "" }";
    return os;
}

ostream& operator<<(ostream& os, CTodoList& todoList)
{
    map<size_t, CTodo> todos = todoList.readList();

    os << "[";
    if(!todos.empty())
    {
        if(todos.size() == 1)
            os << todos.begin()->second;
        else
            for ( map<size_t, CTodo>::iterator it = todos.begin();;)
            {
                os << it->second ;
                if(++it != todos.end())
                    os << ',';
                else
                    break;
            }

    }
    os << "]n";

    return os;
}

class CTodoHandler : public HTTPRequestHandler
{
public:
    void handleRequest(HTTPServerRequest &req, HTTPServerResponse &resp)
    {
        URI uri(req.getURI());
        string method = req.getMethod();

        cerr << "URI: " << uri.toString() << endl;
        cerr << "Method: " << req.getMethod() << endl;

        StringTokenizer tokenizer(uri.getPath(), "/", StringTokenizer::TOK_TRIM);
        HTMLForm form(req,req.stream());

        if(!method.compare("POST"))
        {
            cerr << "Create:" << form.get("text") << endl;
            CTodo todo(form.get("text"));
            TodoServerApp::createTodo(todo);
        }
        else if(!method.compare("PUT"))
        {
            cerr << "Update id:" << *(--tokenizer.end()) << endl;
            cerr << "Update text:" << form.get("text") << endl;
            //size_t id=stoull(*(--tokenizer.end()));
            //TodoServerApp::updateTodo(id, form.get("text"));
        }
        else if(!method.compare("DELETE"))
        {
            cerr << "Delete id:" << *(--tokenizer.end()) << endl;
            size_t id=stoull(*(--tokenizer.end()));
            TodoServerApp::deleteTodo(id);
        }

        resp.setStatus(HTTPResponse::HTTP_OK);
        resp.setContentType("application/json");
        ostream& out = resp.send();

        cerr << TodoServerApp::readTodoList() << endl;
        out << TodoServerApp::readTodoList() << endl;

        out.flush();
    }
};

#include <iostream>     // std::cout
#include <fstream>      // std::ifstream
#include <map>          // std::ifstream

class CFileHandler : public HTTPRequestHandler
{
    typedef std::map<const std::string, const std::string> TStrStrMap;
    TStrStrMap CONTENT_TYPE = {
#include "MimeTypes.h"
    };

    string getPath(string& path){

        if(path == "/"){
            path="/index.html";
        }

        path.insert(0, "./www");

        return path;
    }

    string getContentType(string& path){

        string contentType("text/plain");
        Poco::Path p(path);

        TStrStrMap::const_iterator i=CONTENT_TYPE.find(p.getExtension());

        if (i != CONTENT_TYPE.end())
        { /* Found, i->first is f, i->second is ++-- */
           contentType = i->second;
        }

        if(contentType.find("text/") != std::string::npos)
        {
            contentType+="; charset=utf-8";
        }

        cerr << path << " : " << contentType << endl;

        return contentType;
    }

public:

    void handleRequest(HTTPServerRequest &req, HTTPServerResponse &resp)
    {
        cerr << "Get static page: ";
        //system("echo -n '1. Current Directory is '; pwd");

        URI uri(req.getURI());
        string path(uri.getPath());

        ifstream ifs (getPath(path).c_str(), ifstream::in);

        if(ifs)
        {
            resp.setStatus(HTTPResponse::HTTP_OK);
            resp.setContentType(getContentType(path));
            ostream& out = resp.send();

            char c = ifs.get();

            while (ifs.good()) {
                out << c;
                c = ifs.get();
            }

            out.flush();
        }
        else
        {
            resp.setStatus(HTTPResponse::HTTP_NOT_FOUND);
            ostream& out = resp.send();

            out << "File not found" << endl;

            out.flush();
        }

        ifs.close();
    }
};

class TodoRequestHandlerFactory : public HTTPRequestHandlerFactory
{
public:
    virtual HTTPRequestHandler* createRequestHandler(const HTTPServerRequest & request)
    {
        if (!request.getURI().find("/api/"))
            return new CTodoHandler;
        else
            return new CFileHandler;
    }
};

void TodoServerApp::createTodo(CTodo& todo)
{
    ScopedLock<Mutex> lock(todoLock);
    todoList.create(todo);
}

CTodoList& TodoServerApp::readTodoList()
{
    ScopedLock<Mutex> lock(todoLock);
    return todoList;
}

void TodoServerApp::deleteTodo(size_t id)
{
    ScopedLock<Mutex> lock(todoLock);
    todoList.del(id);
}

int TodoServerApp::main(const vector<string> &)
{
    HTTPServerParams* pParams = new HTTPServerParams;

    pParams->setMaxQueued(100);
    pParams->setMaxThreads(16);

    HTTPServer s(new TodoRequestHandlerFactory, ServerSocket(8000), pParams);

    s.start();
    cerr << "Server started" << endl;

    waitForTerminationRequest();  // wait for CTRL-C or kill

    cerr << "Shutting down..." << endl;
    s.stop();

    return Application::EXIT_OK;
}

Независимо от типа запроса сервис всегда возвращает текущий список задач в формате JSON.

Response.json

[
    {
        "_id": 1,
        "text": "First" 
    },
    {
        "_id": 2,
        "text": "Second"
    }
]

Фронтенд загружается из ./www/ в дирректории приложения.
Для фронтенда используется AngularJs. Код фронтенда практически полностью взят из статьи scotch.io/tutorials/creating-a-single-page-todo-app-with-node-and-angular
Вид в index.html состоит из заголовка со счетчиком заданий, списка заданий и формы добавления нового задания.
Контроллер js/app.js вызывает GET для списка при инициализации. Далее — POST при добавлении нового задания из формы, либо DELETE при клике на чекбокс задания.

index.html
<!-- index.html -->
<!doctype html>

<!-- ASSIGN OUR ANGULAR MODULE -->
<html ng-app="pocoTodo">
<head>
    <!-- META -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1"><!-- Optimize mobile viewport -->

    <title>POCO/Angular Todo App</title>

    <!-- SCROLLS -->
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"><!-- load bootstrap -->
    <style>
        html                    { overflow-y:scroll; }
        body                    { padding-top:50px; }
        #todo-list              { margin-bottom:30px; }
    </style>

    <!-- SPELLS -->
    <script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script><!-- load jquery -->
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"></script><!-- load angular -->
    <script src="js/app.js"></script>

</head>
<!-- SET THE CONTROLLER AND GET ALL TODOS -->
<body ng-controller="mainController">
    <div class="container">

        <!-- HEADER AND TODO COUNT -->
        <div class="jumbotron text-center">
            <h2>POCO/Angular Todo App <span class="label label-info">{{ todos.length }}</span></h2>
        </div>

        <!-- TODO LIST -->
        <div id="todo-list" class="row">
            <div class="col-sm-4 col-sm-offset-4">

                <!-- LOOP OVER THE TODOS IN $scope.todos -->
                <div class="checkbox" ng-repeat="todo in todos">
                    <label>
                        <input type="checkbox" ng-click="deleteTodo(todo._id)"> {{ todo.text }}
                    </label>
                </div>

            </div>
        </div>

        <!-- FORM TO CREATE TODOS -->
        <div id="todo-form" class="row">
            <div class="col-sm-8 col-sm-offset-2 text-center">
                <form>
                    <div class="form-group">

                        <!-- BIND THIS VALUE TO formData.text IN ANGULAR -->
                        <input type="text" class="form-control input-lg text-center" placeholder="I want to buy a puppy that will love me forever" ng-model="formData.text">
                    </div>

                    <!-- createToDo() WILL CREATE NEW TODOS -->
                    <button type="submit" class="btn btn-primary btn-lg" ng-click="createTodo()">Add</button>
                </form>
            </div>
        </div>

    </div>

</body>
</html>

js/app.js

// js/app.js
var pocoTodo = angular.module('pocoTodo', []);

function mainController($scope, $http) {
    $scope.formData = {};
    $http.defaults.headers.post["Content-Type"] = "application/x-www-form-urlencoded";

    // when landing on the page, get all todos and show them
    $http.get('/api/todos')
        .success(function(data) {
            $scope.todos = data;
            console.log(data);
        })
        .error(function(data) {
            console.log('Error: ' + data);
        });

    // when submitting the add form, send the text to the node API
    $scope.createTodo = function() {
        $http.post('/api/todos', $.param($scope.formData)) // $scope.formData)
            .success(function(data) {
                $scope.formData = {}; // clear the form so our user is ready to enter another
                $scope.todos = data;
                console.log(data);
            })
            .error(function(data) {
                console.log('Error: ' + data);
            });
    };

    // delete a todo after checking it
    $scope.deleteTodo = function(id) {
        $http.delete('/api/todos/' + id)
            .success(function(data) {
                $scope.todos = data;
                console.log(data);
            })
            .error(function(data) {
                console.log('Error: ' + data);
            });
    };

}

Репозиторий проекта: github.com/spot62/PocoAngularTodo

Автор: spot62

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js