Work in progress.
This tutorial illustrates the use of the main structural functionality of the Balau library, via the construction of an HTTP server application. The following items are covered:
The tutorial application provides an HTTP web server that serves files from the local file system and also provides a web application handler to forward POST requests via email. This simple server implementation can be extended with additional/alternative web applications, according to your requirements.
The application's main function will perform the following tasks:
As application is intended to run as a service, the main function will call the blocking method httpServer->startSync(), and will shut down when a SIGINT or SIGTERM is received. The HTTP server's default signal handler is thus left enabled.
The tutorial's main function is presented with a minimum of validation code. A production ready application should provide additional validation.
int main(int argc, char * argv[]) {
using namespace Balau;
System::ThreadName::setName("Main");
Logger & log = Logger::getLogger("main");
log.info("Starting up server.");
const std::string defaultEnvPrefix = "environment";
const std::string defaultSpecPrefix = "specification";
const std::string envFilename = "env.hconf";
const std::string credsFilename = "creds.hconf";
const std::string specsFilename = "tutorial.thconf";
enum CommandLineKey { EnvPrefix, SpecPrefix, Env, Help };
const std::string pHelp = "Environment configurations location, default = environment";
const std::string sHelp = "Environment specifications location, default = specification";
auto cl = CommandLine<CommandLineKey>()
.withOption(EnvPrefix, "p", "env-prefix", true, pHelp)
.withOption(SpecPrefix, "s", "spec-prefix", true, sHelp)
.withOption(Env, "e", "env", true, "Environment folder name")
.withHelpOption(Help, "h", "help", "Display help");
try {
cl.parse(argc, argv, true);
} catch (...) {
log.error("Invalid arguments\n\n{}", cl.getHelpText(4, 80));
return 1;
}
if (cl.hasOption(Help)) {
log.info("{}", cl.getHelpText(4, 80));
return 0;
}
if (!cl.hasOption(Env)) {
log.error("Please specify the environment folder name (--env option).");
return 1;
}
const auto envRoot = Resource::File(cl.getOptionOrDefault(EnvPrefix, defaultEnvPrefix));
const auto specRoot = Resource::File(cl.getOptionOrDefault(SpecPrefix, defaultSpecPrefix));
const auto envFolderName = cl.getOption(Env);
const auto envPath = envRoot / envFolderName;
if (!envPath.exists() || !specRoot.exists() || resourcesRootPath.exists()) {
log.error("Environment configuration path not found.");
return 1;
}
const auto env = envPath / envFilename;
const auto creds = envPath / credsFilename;
const auto specs = specRoot / specsFilename;
if (!env.exists() || !specs.exists() || !creds.exists()) {
log.error("Environment configuration file not found.");
return 1;
}
// Register custom environment types.
EnvironmentConfiguration::registerValueType<Network::Endpoint>("endpoint");
EnvironmentConfiguration::registerUnsignedTypes();
try {
log.info("Creating injector.");
// The injection configuration creation is placed inside a function
// in order to allow it to be tested via a unit test.
auto configuration = Configuration::getConfiguration(env, creds, specs);
auto injector = Injector::create(configuration);
// Configure the logging system from the environment configuration.
log.info("Configuring logging system.");
const std::map<std::string, std::string> placeholderExpansions = {
{ "env.path", envPath.toAbsolutePath().toUriString() }
};
auto loggingConf = injector->getShared<EnvironmentProperties>("logging");
Logger::configure(loggingConf, placeholderExpansions);
log.info("Starting HTTP server.");
auto server = injector->getShared<Network::Http::HttpServer>();
// This will block until SIGINT/SIGTERM is received.
server->startSync();
return 0;
} catch (const std::exception & e) {
log.error("Error: {}", e.what());
return 1;
} catch (...) {
log.error("An unknown error occurred.");
return 1;
}
}
The complete injector configuration is contained within a single function. This allows a unit test to call the same configuration code, in order to validate the application's wiring.
struct Configuration {
static std::vector<std::shared_ptr<InjectorConfiguration>>
getConfiguration(const Resource::File & env,
const Resource::File & creds,
const Resource::File & specs) {
std::vector<std::shared_ptr<InjectorConfiguration>> conf;
conf.emplace_back(new AppConfig());
conf.emplace_back(new EnvironmentConfiguration({ env, creds }, { specs }));
return conf;
}
};
The ApplicationConfiguration defined for the tutorial application contains a system clock and the HTTP server, both bound via lazy singletons.
class AppConfig : public ApplicationConfiguration {
public: void configure() const override {
bind<System::Clock>().toSingleton<System::SystemClock>();
bind<Network::Http::HttpServer>().toSingleton();
}
};
logging {
. {
stream: ${env.path}/logs/app.log
level: debug
}
}
http.server {
info.log = file:///var/log/MyApp/access.log
error.log = file:///var/log/MyApp/error.log
server.id = MyServer
worker.count = 4
listen = 8080
@file:mime.types.hconf
thread.name.prefix = Http
http {
files {
location = /
root = file:///var/www
}
email.sender {
location = /1/contact
host = smtp.example.com
port = 465
user = info
subject = message
from = info@example.com
to = info@example.com
user-agent = MyAgent
success = /success.html
failure = /failure.html
# These must be synchronised with the form.
parameters {
Name = 1
Email = 2
Message = 3
}
}
}
}
TODO