7 changed files with 391 additions and 9 deletions
@ -0,0 +1,101 @@ |
|||||||
|
#include "infrastructure/http/HttpServer.h" |
||||||
|
#include <iostream> |
||||||
|
|
||||||
|
namespace nxl::autostore::infrastructure { |
||||||
|
|
||||||
|
HttpServer::HttpServer(int port, const std::string& host) |
||||||
|
: host(host), port(port), running(false) |
||||||
|
{} |
||||||
|
|
||||||
|
HttpServer::~HttpServer() |
||||||
|
{ |
||||||
|
if (running) { |
||||||
|
stop(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
bool HttpServer::start() |
||||||
|
{ |
||||||
|
if (running) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
// Set up error handler
|
||||||
|
server.set_error_handler([](const auto& req, auto& res) { |
||||||
|
auto fmt = "<p>Error Status: <span style='color:red;'>%d</span></p>"; |
||||||
|
char buf[BUFSIZ]; |
||||||
|
snprintf(buf, sizeof(buf), fmt, res.status); |
||||||
|
res.set_content(buf, "text/html"); |
||||||
|
}); |
||||||
|
|
||||||
|
// Set up exception handler
|
||||||
|
server.set_exception_handler( |
||||||
|
[](const auto& req, auto& res, std::exception_ptr ep) { |
||||||
|
auto fmt = "<h1>Error 500</h1><p>%s</p>"; |
||||||
|
char buf[BUFSIZ]; |
||||||
|
try { |
||||||
|
std::rethrow_exception(ep); |
||||||
|
} catch (std::exception& e) { |
||||||
|
snprintf(buf, sizeof(buf), fmt, e.what()); |
||||||
|
} catch (...) { |
||||||
|
snprintf(buf, sizeof(buf), fmt, "Unknown Exception"); |
||||||
|
} |
||||||
|
res.set_content(buf, "text/html"); |
||||||
|
res.status = 500; |
||||||
|
}); |
||||||
|
|
||||||
|
std::cout << "Starting HTTP server on " << host << ":" << port << std::endl; |
||||||
|
|
||||||
|
// Start server in a separate thread
|
||||||
|
serverThread = std::thread([this]() { |
||||||
|
std::cout << "Server thread started, listening on " << host << ":" << port |
||||||
|
<< std::endl; |
||||||
|
bool listenResult = server.listen(host.c_str(), port); |
||||||
|
std::cout << "Server stopped, listen result: " << listenResult << std::endl; |
||||||
|
}); |
||||||
|
|
||||||
|
// Give the server a moment to start
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(500)); |
||||||
|
|
||||||
|
// Check if server is listening by trying to bind to the port
|
||||||
|
if (!server.is_running()) { |
||||||
|
std::cerr << "Failed to start HTTP server - server is not running" |
||||||
|
<< std::endl; |
||||||
|
if (serverThread.joinable()) { |
||||||
|
serverThread.join(); |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
running = true; |
||||||
|
std::cout << "HTTP server is running" << std::endl; |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
void HttpServer::stop() |
||||||
|
{ |
||||||
|
if (!running) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
std::cout << "Stopping HTTP server..." << std::endl; |
||||||
|
server.stop(); |
||||||
|
|
||||||
|
// Wait for the server to stop
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(500)); |
||||||
|
|
||||||
|
running = false; |
||||||
|
std::cout << "HTTP server stopped" << std::endl; |
||||||
|
} |
||||||
|
|
||||||
|
bool HttpServer::isRunning() const |
||||||
|
{ |
||||||
|
return running; |
||||||
|
} |
||||||
|
|
||||||
|
httplib::Server& HttpServer::getServer() |
||||||
|
{ |
||||||
|
return server; |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace nxl::autostore::infrastructure
|
||||||
@ -0,0 +1,30 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <httplib.h> |
||||||
|
#include <memory> |
||||||
|
#include <string> |
||||||
|
#include <thread> |
||||||
|
|
||||||
|
namespace nxl::autostore::infrastructure { |
||||||
|
|
||||||
|
class HttpServer |
||||||
|
{ |
||||||
|
public: |
||||||
|
explicit HttpServer(int port = 8080, const std::string& host = "0.0.0.0"); |
||||||
|
~HttpServer(); |
||||||
|
|
||||||
|
bool start(); |
||||||
|
void stop(); |
||||||
|
bool isRunning() const; |
||||||
|
|
||||||
|
httplib::Server& getServer(); |
||||||
|
|
||||||
|
private: |
||||||
|
std::string host; |
||||||
|
int port; |
||||||
|
bool running; |
||||||
|
httplib::Server server; |
||||||
|
std::thread serverThread; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace nxl::autostore::infrastructure
|
||||||
@ -0,0 +1,121 @@ |
|||||||
|
#include "webapi/controllers/StoreController.h" |
||||||
|
#include <chrono> |
||||||
|
#include <ctime> |
||||||
|
#include <iomanip> |
||||||
|
#include <sstream> |
||||||
|
|
||||||
|
namespace nxl::autostore::webapi { |
||||||
|
|
||||||
|
StoreController::StoreController(application::IItemRepository& itemRepository, |
||||||
|
application::IClock& clock, |
||||||
|
application::IOrderService& orderService) |
||||||
|
: addItemUseCase(itemRepository, clock, orderService) |
||||||
|
{} |
||||||
|
|
||||||
|
void StoreController::registerRoutes(httplib::Server& server) |
||||||
|
{ |
||||||
|
server.Post("/api/items", |
||||||
|
[this](const httplib::Request& req, httplib::Response& res) { |
||||||
|
this->addItem(req, res); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
void StoreController::addItem(const httplib::Request& req, |
||||||
|
httplib::Response& res) |
||||||
|
{ |
||||||
|
try { |
||||||
|
if (req.body.empty()) { |
||||||
|
res.status = 400; |
||||||
|
res.set_content(createErrorResponse("Request body is empty"), |
||||||
|
"application/json"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
auto item = parseItemFromJson(req.body); |
||||||
|
|
||||||
|
// Execute the use case with a simple presenter
|
||||||
|
bool success = false; |
||||||
|
addItemUseCase.execute(std::move(item), |
||||||
|
[&success](int result) { success = (result == 1); }); |
||||||
|
|
||||||
|
if (success) { |
||||||
|
res.status = 201; |
||||||
|
res.set_content(createSuccessResponse(item), "application/json"); |
||||||
|
} else { |
||||||
|
res.status = 500; |
||||||
|
res.set_content(createErrorResponse("Failed to add item"), |
||||||
|
"application/json"); |
||||||
|
} |
||||||
|
} catch (const std::exception& e) { |
||||||
|
res.status = 400; |
||||||
|
res.set_content(createErrorResponse(e.what()), "application/json"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
domain::Item StoreController::parseItemFromJson(const std::string& jsonBody) |
||||||
|
{ |
||||||
|
auto json = nlohmann::json::parse(jsonBody); |
||||||
|
|
||||||
|
domain::Item item; |
||||||
|
item.id = json.value("id", ""); |
||||||
|
item.name = json.value("name", ""); |
||||||
|
item.orderUrl = json.value("orderUrl", ""); |
||||||
|
item.userId = json.value("userId", "default-user"); |
||||||
|
|
||||||
|
// Parse expiration date
|
||||||
|
if (json.contains("expirationDate")) { |
||||||
|
std::string dateStr = json["expirationDate"]; |
||||||
|
std::tm tm = {}; |
||||||
|
std::istringstream ss(dateStr); |
||||||
|
ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S"); |
||||||
|
if (ss.fail()) { |
||||||
|
throw std::runtime_error("Invalid expiration date format. Use ISO 8601 " |
||||||
|
"format: YYYY-MM-DDTHH:MM:SS"); |
||||||
|
} |
||||||
|
item.expirationDate = |
||||||
|
std::chrono::system_clock::from_time_t(std::mktime(&tm)); |
||||||
|
} else { |
||||||
|
// Default to 24 hours from now
|
||||||
|
item.expirationDate = |
||||||
|
std::chrono::system_clock::now() + std::chrono::hours(24); |
||||||
|
} |
||||||
|
|
||||||
|
if (item.id.empty()) { |
||||||
|
// Generate a simple ID if not provided
|
||||||
|
item.id = "item-" |
||||||
|
+ std::to_string( |
||||||
|
std::chrono::system_clock::now().time_since_epoch().count()); |
||||||
|
} |
||||||
|
|
||||||
|
if (item.name.empty()) { |
||||||
|
throw std::runtime_error("Item name is required"); |
||||||
|
} |
||||||
|
|
||||||
|
return item; |
||||||
|
} |
||||||
|
|
||||||
|
std::string StoreController::createSuccessResponse(const domain::Item& item) |
||||||
|
{ |
||||||
|
nlohmann::json response; |
||||||
|
response["success"] = true; |
||||||
|
response["message"] = "Item added successfully"; |
||||||
|
response["item"]["id"] = item.id; |
||||||
|
response["item"]["name"] = item.name; |
||||||
|
response["item"]["expirationDate"] = |
||||||
|
std::chrono::system_clock::to_time_t(item.expirationDate); |
||||||
|
response["item"]["orderUrl"] = item.orderUrl; |
||||||
|
response["item"]["userId"] = item.userId; |
||||||
|
|
||||||
|
return response.dump(); |
||||||
|
} |
||||||
|
|
||||||
|
std::string StoreController::createErrorResponse(const std::string& message) |
||||||
|
{ |
||||||
|
nlohmann::json response; |
||||||
|
response["success"] = false; |
||||||
|
response["message"] = message; |
||||||
|
|
||||||
|
return response.dump(); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace nxl::autostore::webapi
|
||||||
@ -0,0 +1,32 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "application/commands/AddItem.h" |
||||||
|
#include "application/interfaces/IItemRepository.h" |
||||||
|
#include "application/interfaces/IClock.h" |
||||||
|
#include "application/interfaces/IOrderService.h" |
||||||
|
#include "domain/entities/Item.h" |
||||||
|
#include <httplib.h> |
||||||
|
#include <nlohmann/json.hpp> |
||||||
|
|
||||||
|
namespace nxl::autostore::webapi { |
||||||
|
|
||||||
|
class StoreController |
||||||
|
{ |
||||||
|
public: |
||||||
|
StoreController(application::IItemRepository& itemRepository, |
||||||
|
application::IClock& clock, |
||||||
|
application::IOrderService& orderService); |
||||||
|
|
||||||
|
void registerRoutes(httplib::Server& server); |
||||||
|
|
||||||
|
private: |
||||||
|
void addItem(const httplib::Request& req, httplib::Response& res); |
||||||
|
|
||||||
|
domain::Item parseItemFromJson(const std::string& jsonBody); |
||||||
|
std::string createSuccessResponse(const domain::Item& item); |
||||||
|
std::string createErrorResponse(const std::string& message); |
||||||
|
|
||||||
|
application::AddItem addItemUseCase; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace nxl::autostore::webapi
|
||||||
Loading…
Reference in new issue