|
lintel 0.2.0
|
Conventions worth knowing before reading the headers or contributing.
Public headers under src/include/lintel/ are the supported surface. The build is deliberately expensive to get wrong, so the rules are enforced:
spdlog does not appear in public headers. LoggerService.h forward declares fmt types instead, which removed the spdlog dependency from the include graph entirely.cpp-httplib is confined to the HTTP feature; controllers depend on an internal interface, and the implementation sits behind a pimpl so the server type does not leak into public headers.The gcc-debug-nonunity CI entry exists specifically to protect these rules: a unity build silently supplies includes that a single translation unit would not have, so building non-unity in Debug is what actually proves header completeness.
All public classes and free functions live in the lintel namespace, with the persistence layer nested one level deeper in lintel::db, so a consumer with its own Property or Result class is not forced to namespace-qualify deliberately. The CMake export namespace is lintel::lintel and the include guard of a public header is LINTEL_..., so the library name appears in all three places. This was a deliberate breaking change; the CHANGELOG migration table lists the former global names next to their lintel:: counterparts.
The library is exception-based; there is no std::expected/Result error channel. The decision of who propagates and who degrades is made per layer and is what keeps the layering (core ← base ← property ← http ← cli) honest:
| Layer | Contract |
|---|---|
core persistence | throws db::SQLException, whose message carries the driver's own error text; never returns a partial result silently. |
core configuration | throws ConfigurationException carrying the tinyxml2 error text and the offending line number. |
core models / core shm | no I/O, therefore no error channel. |
| feature repositories | degrade deliberately: a persistence failure is logged (LOG_ERROR) and surfaced as an empty list or a no-op, because the repository is an override layer over the configured defaults - an empty result means "keep the default", not "data lost". Domain-validation failures, in contrast, do throw (std::runtime_error, HttpBadRequestException). |
| feature services | propagate domain exceptions; where a change is already effective in memory they log and continue instead of turning a local success into an HTTP 500. |
features/http | maps internal exceptions to status codes (HttpBadRequestException → 400, unauthorized → 401) and catches at the handler boundary so a stray exception cannot become a dropped connection. |
The rule of thumb: a layer that can substitute a sensible default degrades and logs; a layer that cannot must propagate. DatabasePropertyRepository is the reference implementation of the "degrade" case and spells out its reasoning at the top of the file.
Two further conventions:
switch over an enum is required to have a default branch that either handles the case or throws. A fall-through out of a non-void function is undefined behaviour, and CodeQL flagged exactly that once in this codebase.std::localtime, std::ctime) are not used; the _r variants write into caller-owned storage.The suite is Catch2 plus trompeloeil, built as the single base_tests binary. Two constraints that are easy to trip over:
add_executable(base_tests ...) list in tests/CMakeLists.txt. There is no globbing, so a new file is invisible until it is listed.ctest --label-regex unit runs each test case as its own process (via catch_discover_tests), which hides cross-test environment leakage that a single-process run of ./bin/base_tests does expose. Both are worth running.