Skip to content

Commit 9daf303

Browse files
etrclaude
andcommitted
feat(TASK-025): GREEN seven on_* lambda registration entry points
Adds the public on_get / on_post / on_put / on_delete / on_patch / on_options / on_head member functions on webserver, each accepting a std::function<http_response(const http_request&)>. All seven forward to a private webserver::on_method_ helper that: - Builds (or merges into) a hidden detail::lambda_resource shim at the given path. The shim is a final subclass of http_resource holding one callable slot per http_method enumerator. It starts with every method disallowed (disallow_all()), so 405 dispatch falls through the existing finalize_answer mask path with no edit to the dispatch glue; on_* enables the matching bit per slot via set_allowing. - Throws std::invalid_argument on conflict (slot already populated for the requested method, class-resource already registered at the path) and on bad input (empty handler, single_resource path violation). - Inserts into the same three storage maps register_impl_ uses, so exact and parameterized lambda routes both land in the right tier (str fast-path map for exact non-parameterized; regex map for parameterized). Adds detail::route_entry (architecture spec §4.7) holding method_set + std::variant<lambda_handler, shared_ptr<http_resource>> + bool is_prefix; pinned at compile time via static_assert in the test TU. TASK-027 will plumb route_entry into a real 3-tier route table; until then the lambda_resource shim is the dispatch carrier. Both new headers live in src/httpserver/detail/ and are gated on HTTPSERVER_COMPILATION; added to noinst_HEADERS so they're tarred but never installed under $prefix/include. 35 tests pass (was 34); webserver_on_methods's eight runtime cases plus its compile-time signature contract covers PRD §3.4 hello-world, per-method 405 + Allow header, multi-method composition, all seven overloads dispatch their method, duplicate (method, path) throws, merge-then-conflict still throws, parameterized path binds {id}, and class-resource registration on a lambda-owned path throws. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4726527 commit 9daf303

6 files changed

Lines changed: 405 additions & 1 deletion

File tree

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: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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 <cstddef>
49+
#include <cstdint>
50+
#include <memory>
51+
#include <utility>
52+
53+
#include "httpserver/http_method.hpp"
54+
#include "httpserver/http_resource.hpp"
55+
#include "httpserver/detail/route_entry.hpp"
56+
57+
namespace httpserver {
58+
class http_request;
59+
class http_response;
60+
} // namespace httpserver
61+
62+
namespace httpserver {
63+
namespace detail {
64+
65+
// Tiny adapter that wraps a single lambda_handler as an http_resource
66+
// virtual override. We keep one slot per method enum and dispatch in
67+
// each render_* override.
68+
class lambda_resource final : public ::httpserver::http_resource {
69+
public:
70+
lambda_resource() {
71+
// Lambda routes are opt-in per method (the default
72+
// http_resource constructor enables every method via
73+
// set_all()). on_* sets the matching bit when populating a slot.
74+
disallow_all();
75+
}
76+
77+
// Install (or replace) the slot for `method`. Caller must have
78+
// already verified that no slot is currently set for `method`
79+
// (webserver::on_method_ enforces this and throws on conflict).
80+
void set_slot(http_method method, lambda_handler h) {
81+
slots_[static_cast<std::size_t>(method)] = std::move(h);
82+
set_allowing(method, true);
83+
}
84+
85+
bool has_slot(http_method method) const noexcept {
86+
return is_allowed(method);
87+
}
88+
89+
std::shared_ptr<::httpserver::http_response>
90+
render_get(const ::httpserver::http_request& r) override {
91+
return invoke_(http_method::get, r);
92+
}
93+
std::shared_ptr<::httpserver::http_response>
94+
render_post(const ::httpserver::http_request& r) override {
95+
return invoke_(http_method::post, r);
96+
}
97+
std::shared_ptr<::httpserver::http_response>
98+
render_put(const ::httpserver::http_request& r) override {
99+
return invoke_(http_method::put, r);
100+
}
101+
std::shared_ptr<::httpserver::http_response>
102+
render_delete(const ::httpserver::http_request& r) override {
103+
return invoke_(http_method::del, r);
104+
}
105+
std::shared_ptr<::httpserver::http_response>
106+
render_patch(const ::httpserver::http_request& r) override {
107+
return invoke_(http_method::patch, r);
108+
}
109+
std::shared_ptr<::httpserver::http_response>
110+
render_options(const ::httpserver::http_request& r) override {
111+
return invoke_(http_method::options, r);
112+
}
113+
std::shared_ptr<::httpserver::http_response>
114+
render_head(const ::httpserver::http_request& r) override {
115+
return invoke_(http_method::head, r);
116+
}
117+
118+
private:
119+
std::shared_ptr<::httpserver::http_response>
120+
invoke_(http_method m, const ::httpserver::http_request& r) {
121+
auto& slot = slots_[static_cast<std::size_t>(m)];
122+
if (!slot) {
123+
// Reachable only if dispatch arrives for a method whose
124+
// bit was set but whose slot was somehow cleared. The
125+
// existing finalize_answer 405 path normally fires before
126+
// we get here.
127+
return ::httpserver::detail::empty_render(r);
128+
}
129+
return std::make_shared<::httpserver::http_response>(slot(r));
130+
}
131+
132+
std::array<lambda_handler,
133+
static_cast<std::size_t>(http_method::count_)> slots_{};
134+
};
135+
136+
} // namespace detail
137+
} // namespace httpserver
138+
139+
#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)