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