Contents
Tutorial

Introduction

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.

Main function

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;
				}
			}
		

Configuration

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;
				}
			};
		

Application configuration

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();
				}
			};
		

Environment configuration

Logging

			logging {
				. {
					stream: ${env.path}/logs/app.log
					level: debug
				}
			}
		

Web applications

			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
						}
					}
				}
			}
		

Testing

TODO