ores.cpp.http-route

Table of Contents

This page is the literate source for the ores.cpp.http-route facet of the codegen template library. Each archetype below is documented, and holds its mustache source, in its own linked page — this page is the facet-level summary and the routing table to them.

1. Summary

Three templates that project one entity onto one HTTP route unit: a header declaring the unit class and its handlers, a source holding the route table and the handler bodies, and the test file that pins the table. The unit owns one route per verb the entity derives — a put becomes two, because a create and a replace are one verb stating two different claims — and registers that set on the router and the OpenAPI registry. Three more archetypes render an operation model, whose declared messages replace the derived verb set, and each becomes one route addressed by the subject it already states. The templates are opt-in and disabled by default.

2. The ores.cpp.http-route facet

The facet consumes a domain_entity model or a junction and writes the HTTP gateway's view of it. A junction is an entity whose key spans the two sides it links, so it derives the same verb set and reaches the same unit; the shape the templates read is the one the protocol was derived from, which is why a verb the model gains reaches the protocol and the gateway in the same commit rather than the route being written out by hand beside it.

The model already carries everything the templates need: the entity names and title, the component, the API sub-component, the primary key and the columns.

The facet writes into the entity's own adapter part under projects/ores.http/, not into the entity's domain component. A trading entity lands in projects/ores.http/trading/, at src/routes/trading/ and include/ores.http/routes/trading/. That part declares #+component_kind: adapter, so codegen writes only its file lists beside the hand-authored CMakeLists, as Component architecture describes.

One variable places that output. {component} resolves to the short component name without the product prefix, so a trading entity yields trading and not ores.trading. The template body reads the same value under {{component}}, and uses it for the ores::<component>::messaging namespace, the ores.<component>.<subcomponent> protocol include and the /api/v1/<component>/<entity_plural> path prefix. Output paths and template bodies therefore share one spelling of the component.

The per-entity split is the point of the shape. Each entity owns a file, so a component's routes can be added and removed one at a time. A hand-written aggregator then calls each unit's register_routes in turn, which is what the per-component route file does today for the assets domain. That aggregator needs a component-scoped archetype and a render-time list of the component's opted-in entities, the same pattern the generator already uses for _COMPONENT_FILES_TEMPLATES.

Transport: NATS, not the service in process. A generated route includes only ores.<component>.<subcomponent>/messaging/<entity>_protocol.hpp and speaks the canonical protocol. It builds the canonical request message, sends it over the ores.nats client on the subject the request states, and maps the reply to an HTTP response. The gateway holds no service, repository or database context, so it cannot decide anything the service decides.

Security: one declaration, one enforcement point. The service's generated handler already checks the entity's permission (assets::images:read, write, delete). The gateway must not re-implement authorisation, and does two things instead:

  • it states the route's position explicitly from the operation: .auth_required() when the operation states requires_session, and .auth_optional() when it does not, so every route declares one of the two and a route that declares neither is refused rather than read as public; and
  • it forwards the caller's own bearer token on the NATS request through nats_client::with_delegation, so the service validates that caller and its own permission check governs. A caller the service refuses is refused, whatever the gateway believes.

The token is read from the HTTP request for every route, including the ones that require no session (where it is absent and the delegated client behaves as the service client it was built from).

Opt-in, disabled by default — enabling the facet is a per-entity decision, expressed as an :ores.cpp.http-route.enabled: true entry in the entity model's properties drawer, exactly as ores.cpp.shell-command does it. In ores.assets the three models the gateway first serves state it directly, so the facet's blast radius stays the domain it was landed with. A domain entity or junction that opts in exposes its whole derived CRUD surface, because that surface is the component's canonical verb set; an operation model that opts in exposes only the messages that state :http_route: true, because a declared protocol holds operations meant for NATS callers alone.

The route table. One derived operation becomes one route. The mapping is:

Verb Method Path Request source
list GET /api/v1/<c>/<plural> query params
get GET /api/v1/<c>/<plural>/<key> path params
get_many POST /api/v1/<c>/<plural>/get-many JSON body
put (create) POST /api/v1/<c>/<plural> JSON body
put (replace) PUT /api/v1/<c>/<plural> JSON body
put_many POST /api/v1/<c>/<plural>/put-many JSON body
delete DELETE /api/v1/<c>/<plural>/<key> path + query
delete_many POST /api/v1/<c>/<plural>/delete-many JSON body
list_versions GET /api/v1/<c>/<plural>/<key>/versions path params
get_version GET /api/v1/<c>/<plural>/<key>/versions/{version} path params
list_by_<rel> GET /api/v1/<c>/<plural>/by-<rel>/{<rel>} path params

A write carries the canonical request as its body, so the row, the intent and the precondition travel exactly as the protocol spells them. A create and a replace share a path and differ by method, because that is the whole of what separates them: one claims the row is absent, the other that it may exist. A key-addressed route states the key in the path because the request's only member is that key; a put states it in the body, because the wire write record already carries the whole row and a second copy could contradict it. A delete has no body to carry an intent, so its reason, commentary and optional version arrive as query parameters.

3. Archetypes

Archetype Description
ores.cpp.http-route.operation_header HTTP route unit header for an operation model: the unit class and one handler per declared message.
ores.cpp.http-route.operation_implementation HTTP route unit implementation for an operation model: the route table and one handler body per declared message.
ores.cpp.http-route.operation_tests HTTP route unit tests for an operation model: the registered method and pattern of every declared message, and the auth flag each states.
ores.cpp.http-route.route_header HTTP route unit header: the unit class, its registration entry point and one handler per derived verb.
ores.cpp.http-route.route_implementation HTTP route unit implementation: the route table, the parameter readers and the handler bodies for one entity.
ores.cpp.http-route.route_tests HTTP route unit tests: the registered method and pattern of every derived route, and the auth flag each operation states.

4. The operation archetypes

An operation model states its whole protocol rather than deriving a verb set, so it reaches the facet through three archetypes that render what it declares and exposes: one route per message that carries a :subject:, a :response: and :http_route: true. A message that states no exposure answers NATS callers alone and renders no route, because an operation model describes one protocol for several surfaces and not every operation is an HTTP endpoint. A message with no response has nothing for a route to return and a payload struct has no subject to address, so neither becomes a route.

The exposure is a per-message property in the message's own :PROPERTIES: drawer, beside :subject:, :response: and :auth:. Its default is the absence of the property, so a declared operation is not exposed unless it says so, and a new operation cannot widen the gateway by accident:

#+begin_src org

4.1. lock_account_request

#+end_src

The route states the address the protocol already agreed on. The subject iam.v1.ops.lock_account reaches /api/v1/iam/accounts/lock: the path is the subject transliterated, so the URL and the subject cannot disagree and a message the model renames moves its route in the same commit. A declared operation states no verb, so every route is a POST whose body is the canonical request.

Security is the same one declaration the entity archetypes make: the message's own :auth: property decides .auth_required() or .auth_optional(), and the caller's token travels through nats_client::with_delegation, so the service's permission check governs. Login, signup and bootstrap declare :auth: none in their models and are therefore public by the model's own statement.

5. See also

  • ores.cpp — the parent technical space's generated facet/archetype catalogue and shared Mustache conventions.
  • ores.cpp.shell-command — the facet this one mirrors, which addresses the same derived verb set from the REPL.
  • Component architecture — the composite and adapter kinds.

Emacs 29.3 (Org mode 9.6.15)