lintel 0.2.0
Loading...
Searching...
No Matches
Conventions

Conventions worth knowing before reading the headers or contributing.

Header hygiene

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.
  • Boost is limited to the shared-memory boundary described in Architecture ("Quarantined boost").
  • 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.
  • Forward declarations are preferred over includes for types that only appear in a signature.

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.

Namespaces

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.

Errors and degradation

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:

  • A 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.
  • Thread-unsafe C functions (std::localtime, std::ctime) are not used; the _r variants write into caller-owned storage.

Tests

The suite is Catch2 plus trompeloeil, built as the single base_tests binary. Two constraints that are easy to trip over:

  • New test files must be added to the 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.