Skip to content

Commit 39c3c0d

Browse files
etrclaude
andcommitted
Merge TASK-025: Lambda handler entry points on_*
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 parents 744187f + c17dce7 commit 39c3c0d

10 files changed

Lines changed: 1117 additions & 10 deletions

File tree

specs/tasks/M4-handlers/TASK-025.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
Add the lambda-first handler model that lets a stateless endpoint be registered without subclassing.
99

1010
**Action Items:**
11-
- [ ] Add `webserver::on_get(const std::string& path, std::function<http_response(const http_request&)> handler);`.
12-
- [ ] Same for `on_post`, `on_put`, `on_delete`, `on_patch`, `on_options`, `on_head`.
13-
- [ ] Internally, each `on_*` builds a `route_entry` whose `method_set` carries exactly that one method, then registers it in the appropriate route-table tier (hash for exact, radix for parameterized).
14-
- [ ] Multiple `on_*` calls on the same path compose: each call adds the corresponding method bit; conflicting handlers on the same (method, path) pair throw `std::invalid_argument`.
15-
- [ ] Make sure the variant in `route_entry` can hold both `std::function<http_response(const http_request&)>` (lambda) and `std::shared_ptr<http_resource>` (class) — see §4.7.
16-
- [ ] Add a parallel `on_get` (etc.) that takes `(method_set methods, ...)` if useful, or defer that to TASK-026's generic `route()`.
11+
- [x] Add `webserver::on_get(const std::string& path, std::function<http_response(const http_request&)> handler);`.
12+
- [x] Same for `on_post`, `on_put`, `on_delete`, `on_patch`, `on_options`, `on_head`.
13+
- [x] Internally, each `on_*` builds a `route_entry` whose `method_set` carries exactly that one method, then registers it in the appropriate route-table tier (hash for exact, radix for parameterized). (TASK-025 ships the §4.7-shape `detail::route_entry` type plus the on_* entry points; storage is the existing v1 three-map shape via a hidden `detail::lambda_resource` shim. TASK-027 will plumb `route_entry` into the real 3-tier table.)
14+
- [x] Multiple `on_*` calls on the same path compose: each call adds the corresponding method bit; conflicting handlers on the same (method, path) pair throw `std::invalid_argument`.
15+
- [x] Make sure the variant in `route_entry` can hold both `std::function<http_response(const http_request&)>` (lambda) and `std::shared_ptr<http_resource>` (class) — see §4.7. (Pinned by `static_assert` in `test/unit/webserver_on_methods_test.cpp`.)
16+
- [ ] Add a parallel `on_get` (etc.) that takes `(method_set methods, ...)` if useful, or defer that to TASK-026's generic `route()`. (Deferred to TASK-026 per plan §4.3.)
1717

1818
**Dependencies:**
1919
- Blocked by: TASK-005, TASK-009, TASK-014
@@ -28,4 +28,4 @@ Add the lambda-first handler model that lets a stateless endpoint be registered
2828
**Related Requirements:** PRD-HDL-REQ-001, PRD-HDL-REQ-002
2929
**Related Decisions:** DR-004, §4.7
3030

31-
**Status:** Not Started
31+
**Status:** Done

specs/tasks/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ Nominally: **13 sequential tasks**, each S–XL. Most other tasks parallelize of
107107
| TASK-022 | Snake_case `render_*` overrides on `http_resource` | M4 | Done | TASK-021 |
108108
| TASK-023 | Smart-pointer `register_resource` overloads | M4 | Done | TASK-014 |
109109
| TASK-024 | `register_path` and `register_prefix` (replace `bool family`) | M4 | Done | TASK-023 |
110-
| TASK-025 | Lambda handler entry points `on_*` | M4 | Not Started | TASK-005, TASK-009, TASK-014 |
110+
| TASK-025 | Lambda handler entry points `on_*` | M4 | Done | TASK-005, TASK-009, TASK-014 |
111111
| TASK-026 | Generic `webserver::route(method, path, handler)` | M4 | Not Started | TASK-005, TASK-025 |
112112
| TASK-027 | 3-tier route table with LRU cache | M5 | Not Started | TASK-005, TASK-014, TASK-021, TASK-024, TASK-025, TASK-026 |
113113
| TASK-028 | Routing-semantics regression gate | M5 | Not Started | TASK-027 |

specs/unworked_review_issues/2026-05-10_191459_task-025.md

Lines changed: 169 additions & 0 deletions
Large diffs are not rendered by default.

src/Makefile.am

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ libhttpserver_la_SOURCES = string_utilities.cpp webserver.cpp http_utils.cpp fil
2323
# noinst_HEADERS: shipped in the tarball but NEVER installed under $prefix/include.
2424
# Detail headers (httpserver/detail/*.hpp) live here so they cannot leak to
2525
# downstream consumers — the public surface comes in through <httpserver.hpp>.
26-
noinst_HEADERS = httpserver/string_utilities.hpp httpserver/detail/modded_request.hpp httpserver/detail/http_endpoint.hpp httpserver/detail/body.hpp httpserver/detail/webserver_impl.hpp httpserver/detail/http_request_impl.hpp gettext.h
26+
noinst_HEADERS = httpserver/string_utilities.hpp httpserver/detail/modded_request.hpp httpserver/detail/http_endpoint.hpp httpserver/detail/body.hpp httpserver/detail/webserver_impl.hpp httpserver/detail/http_request_impl.hpp httpserver/detail/route_entry.hpp httpserver/detail/lambda_resource.hpp gettext.h
2727
nobase_include_HEADERS = httpserver.hpp httpserver/body_kind.hpp httpserver/constants.hpp httpserver/create_webserver.hpp httpserver/create_test_request.hpp httpserver/webserver.hpp httpserver/http_utils.hpp httpserver/file_info.hpp httpserver/http_request.hpp httpserver/http_response.hpp httpserver/http_resource.hpp httpserver/feature_unavailable.hpp httpserver/iovec_entry.hpp httpserver/http_arg_value.hpp httpserver/http_method.hpp
2828

2929
if HAVE_WEBSOCKET
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
This file is part of libhttpserver
3+
Copyright (C) 2011-2026 Sebastiano Merlino
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
18+
USA
19+
*/
20+
21+
// TASK-025: dispatch shim used by webserver::on_method_ to slot lambda
22+
// handlers into the existing v1-shaped route table.
23+
//
24+
// The shim is a sub-class of http_resource that holds one slot per
25+
// http_method enumerator. Its render_* virtuals look up the slot for
26+
// the dispatched method and invoke it. The shim starts with EVERY
27+
// method disallowed (`disallow_all()`); each on_* call enables exactly
28+
// the matching bit via `set_allowing(method, true)`. The existing
29+
// finalize_answer dispatch glue therefore returns 405 for unregistered
30+
// methods automatically — no edit to webserver.cpp's dispatch path is
31+
// needed.
32+
//
33+
// `final` is intentional: the conflict check in webserver::on_method_
34+
// uses dynamic_pointer_cast<lambda_resource>(...) to distinguish
35+
// lambda-owned routes from class-owned routes. A subclass would hide
36+
// in that test and break the invariant.
37+
//
38+
// Internal header — only reachable when compiling libhttpserver. NOT
39+
// included from the public umbrella <httpserver.hpp>.
40+
#if !defined(HTTPSERVER_COMPILATION)
41+
#error "lambda_resource.hpp is internal; only reachable when compiling libhttpserver."
42+
#endif
43+
44+
#ifndef SRC_HTTPSERVER_DETAIL_LAMBDA_RESOURCE_HPP_
45+
#define SRC_HTTPSERVER_DETAIL_LAMBDA_RESOURCE_HPP_
46+
47+
#include <array>
48+
#include <cassert>
49+
#include <cstddef>
50+
#include <cstdint>
51+
#include <memory>
52+
#include <utility>
53+
54+
#include "httpserver/http_method.hpp"
55+
#include "httpserver/http_resource.hpp"
56+
#include "httpserver/detail/route_entry.hpp"
57+
58+
namespace httpserver {
59+
class http_request;
60+
class http_response;
61+
} // namespace httpserver
62+
63+
namespace httpserver {
64+
namespace detail {
65+
66+
// Tiny adapter that wraps a single lambda_handler as an http_resource
67+
// virtual override. We keep one slot per method enum and dispatch in
68+
// each render_* override.
69+
class lambda_resource final : public ::httpserver::http_resource {
70+
public:
71+
lambda_resource() {
72+
// Lambda routes are opt-in per method (the default
73+
// http_resource constructor enables every method via
74+
// set_all()). on_* sets the matching bit when populating a slot.
75+
disallow_all();
76+
}
77+
78+
// Install (or replace) the slot for `method`. Caller must have
79+
// already verified that no slot is currently set for `method`
80+
// (webserver::on_method_ enforces this and throws on conflict).
81+
void set_slot(http_method method, lambda_handler h) {
82+
slots_[static_cast<std::size_t>(method)] = std::move(h);
83+
set_allowing(method, true);
84+
}
85+
86+
bool has_slot(http_method method) const noexcept {
87+
return is_allowed(method);
88+
}
89+
90+
// These seven overrides are mechanical delegations to invoke_().
91+
// Each differs only by the http_method enum constant forwarded.
92+
// They are required by the http_resource base-class interface and
93+
// cannot be collapsed further without changing that interface.
94+
std::shared_ptr<::httpserver::http_response>
95+
render_get(const ::httpserver::http_request& r) override {
96+
return invoke_(http_method::get, r);
97+
}
98+
std::shared_ptr<::httpserver::http_response>
99+
render_post(const ::httpserver::http_request& r) override {
100+
return invoke_(http_method::post, r);
101+
}
102+
std::shared_ptr<::httpserver::http_response>
103+
render_put(const ::httpserver::http_request& r) override {
104+
return invoke_(http_method::put, r);
105+
}
106+
std::shared_ptr<::httpserver::http_response>
107+
render_delete(const ::httpserver::http_request& r) override {
108+
return invoke_(http_method::del, r);
109+
}
110+
std::shared_ptr<::httpserver::http_response>
111+
render_patch(const ::httpserver::http_request& r) override {
112+
return invoke_(http_method::patch, r);
113+
}
114+
std::shared_ptr<::httpserver::http_response>
115+
render_options(const ::httpserver::http_request& r) override {
116+
return invoke_(http_method::options, r);
117+
}
118+
std::shared_ptr<::httpserver::http_response>
119+
render_head(const ::httpserver::http_request& r) override {
120+
return invoke_(http_method::head, r);
121+
}
122+
123+
private:
124+
std::shared_ptr<::httpserver::http_response>
125+
invoke_(http_method m, const ::httpserver::http_request& r) {
126+
auto& slot = slots_[static_cast<std::size_t>(m)];
127+
// Invariant: set_slot stores the handler AND calls
128+
// set_allowing(method, true); the finalize_answer 405 path fires
129+
// before reaching invoke_ unless has_slot is true. A populated
130+
// slot is therefore guaranteed here.
131+
assert(slot);
132+
// DR-004 contract: lambda_handler returns http_response by value.
133+
// The make_shared here is the known cost of that ergonomic choice;
134+
// slot(r) is already a prvalue so this uses move-construction with
135+
// no extra copy. TASK-036 will address the dispatch cutover.
136+
return std::make_shared<::httpserver::http_response>(slot(r));
137+
}
138+
139+
std::array<lambda_handler,
140+
static_cast<std::size_t>(http_method::count_)> slots_{};
141+
};
142+
143+
} // namespace detail
144+
} // namespace httpserver
145+
146+
#endif // SRC_HTTPSERVER_DETAIL_LAMBDA_RESOURCE_HPP_
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
This file is part of libhttpserver
3+
Copyright (C) 2011-2026 Sebastiano Merlino
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
18+
USA
19+
*/
20+
21+
// TASK-025: route table value type as pinned by architecture spec §4.7.
22+
//
23+
// `route_entry` carries the method-set the entry covers, the actual
24+
// handler (either a stateless lambda or a shared_ptr to a class-derived
25+
// http_resource), and a `is_prefix` flag that lets the route table
26+
// distinguish exact-match registrations (register_path / on_*) from
27+
// prefix-match registrations (register_prefix). TASK-027 will plug this
28+
// type into the real 3-tier route table; TASK-025 only ships the type
29+
// and the on_* entry points that build it.
30+
//
31+
// The header is internal — only reachable when compiling libhttpserver
32+
// itself (HTTPSERVER_COMPILATION is supplied via src/Makefile.am
33+
// AM_CPPFLAGS, and via test/Makefile.am for the test TUs that need to
34+
// pin the variant shape with static_assert). It must NOT be included
35+
// from the public umbrella <httpserver.hpp>.
36+
#if !defined(HTTPSERVER_COMPILATION)
37+
#error "route_entry.hpp is internal; only reachable when compiling libhttpserver."
38+
#endif
39+
40+
#ifndef SRC_HTTPSERVER_DETAIL_ROUTE_ENTRY_HPP_
41+
#define SRC_HTTPSERVER_DETAIL_ROUTE_ENTRY_HPP_
42+
43+
#include <functional>
44+
#include <memory>
45+
#include <variant>
46+
47+
#include "httpserver/http_method.hpp"
48+
49+
namespace httpserver {
50+
class http_request;
51+
class http_response;
52+
class http_resource;
53+
} // namespace httpserver
54+
55+
namespace httpserver {
56+
namespace detail {
57+
58+
// The lambda arm of the route_entry payload variant. Returns
59+
// http_response by value (DR-004) and takes the request by const
60+
// reference. std::function is the chosen storage so users can pass any
61+
// callable (lambda, function pointer, std::bind result, member-function
62+
// adaptor) without leaking the concrete callable type into the route
63+
// table.
64+
using lambda_handler = std::function<::httpserver::http_response(const ::httpserver::http_request&)>; // NOLINT(whitespace/line_length)
65+
66+
// route_entry: §4.7-shape value type stored per route in the route
67+
// table. The `methods` mask holds every HTTP method this entry serves;
68+
// the variant payload holds either a lambda (for on_* registrations)
69+
// or a shared_ptr<http_resource> (for register_path / register_prefix
70+
// registrations). `is_prefix` distinguishes prefix matching from exact
71+
// matching at lookup time.
72+
struct route_entry {
73+
method_set methods{};
74+
std::variant<lambda_handler,
75+
std::shared_ptr<::httpserver::http_resource>> handler;
76+
bool is_prefix = false;
77+
};
78+
79+
} // namespace detail
80+
} // namespace httpserver
81+
82+
#endif // SRC_HTTPSERVER_DETAIL_ROUTE_ENTRY_HPP_

src/httpserver/webserver.hpp

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@
2929
#include <stdint.h>
3030
#include <stdlib.h>
3131

32+
#include <functional>
3233
#include <memory>
3334
#include <string>
3435
#include <type_traits>
3536
#include <utility>
3637
#include <vector>
3738

3839
#include "httpserver/constants.hpp"
40+
#include "httpserver/http_method.hpp"
3941
#include "httpserver/http_utils.hpp"
4042
#include "httpserver/create_webserver.hpp"
4143

@@ -186,6 +188,45 @@ class webserver {
186188
void register_resource(const std::string& path,
187189
std::shared_ptr<http_resource> res);
188190

191+
/**
192+
* Register a lambda handler for HTTP GET on @p path.
193+
*
194+
* The seven on_* entry points (on_get, on_post, on_put, on_delete,
195+
* on_patch, on_options, on_head) let stateless endpoints be
196+
* registered without subclassing http_resource. Each accepts a
197+
* std::function<http_response(const http_request&)>.
198+
*
199+
* Multiple on_* calls on the SAME path COMPOSE: each call adds the
200+
* matching method bit to a single route entry. A second on_get on
201+
* the same path -- or on_get after another on_* already covers GET
202+
* on this path -- throws std::invalid_argument. Mixing class-based
203+
* registration (register_path / register_prefix) and lambda
204+
* registration on the same path also throws.
205+
*
206+
* @param path URL path; may be parameterized as /foo/{id}.
207+
* @param handler invoked per request; returns http_response by value.
208+
**/
209+
void on_get(const std::string& path,
210+
std::function<http_response(const http_request&)> handler);
211+
/// @copydoc on_get(const std::string&, std::function<http_response(const http_request&)>)
212+
void on_post(const std::string& path,
213+
std::function<http_response(const http_request&)> handler);
214+
/// @copydoc on_get(const std::string&, std::function<http_response(const http_request&)>)
215+
void on_put(const std::string& path,
216+
std::function<http_response(const http_request&)> handler);
217+
/// @copydoc on_get(const std::string&, std::function<http_response(const http_request&)>)
218+
void on_delete(const std::string& path,
219+
std::function<http_response(const http_request&)> handler);
220+
/// @copydoc on_get(const std::string&, std::function<http_response(const http_request&)>)
221+
void on_patch(const std::string& path,
222+
std::function<http_response(const http_request&)> handler);
223+
/// @copydoc on_get(const std::string&, std::function<http_response(const http_request&)>)
224+
void on_options(const std::string& path,
225+
std::function<http_response(const http_request&)> handler);
226+
/// @copydoc on_get(const std::string&, std::function<http_response(const http_request&)>)
227+
void on_head(const std::string& path,
228+
std::function<http_response(const http_request&)> handler);
229+
189230
/**
190231
* Unregister an exact-match (register_path) registration.
191232
* No-op if no exact registration exists at @p path.
@@ -392,6 +433,19 @@ class webserver {
392433
// registration of the requested kind.
393434
void unregister_impl_(const std::string& path, bool family);
394435

436+
// TASK-025: shared lambda-registration helper. Builds-or-merges a
437+
// hidden detail::lambda_resource shim at @p path, sets the @p method
438+
// bit on it, and stores @p handler into that method's slot. All
439+
// seven public on_* overloads forward to this single entry point so
440+
// the merge-and-conflict logic lives in one place. Throws
441+
// std::invalid_argument if @p handler is empty, if the path
442+
// conflicts with single_resource mode, if a class-based resource
443+
// is already registered at the path, or if a lambda is already
444+
// registered for (method, path).
445+
void on_method_(http_method method,
446+
const std::string& path,
447+
std::function<http_response(const http_request&)> handler);
448+
395449
// PIMPL: backend-coupled state (MHD daemon, pthread mutexes, route
396450
// table, ban set, route cache, websocket registry, GnuTLS SNI cache,
397451
// and the dispatch helpers / MHD trampolines that operate on those)

0 commit comments

Comments
 (0)