libpromeki 1.0.0-alpha
PROfessional MEdia toolKIt
 
Loading...
Searching...
No Matches
httpapi.h
Go to the documentation of this file.
1
8#pragma once
9
10
11#include <promeki/config.h>
12#if PROMEKI_ENABLE_HTTP
13#include <functional>
14#include <promeki/function.h>
15#include <promeki/namespace.h>
16#include <promeki/objectbase.h>
17#include <promeki/error.h>
18#include <promeki/result.h>
19#include <promeki/string.h>
20#include <promeki/stringlist.h>
21#include <promeki/list.h>
22#include <promeki/variant.h>
23#include <promeki/variantspec.h>
26#include <promeki/json.h>
27#include <promeki/httpmethod.h>
28#include <promeki/httprequest.h>
30#include <promeki/httphandler.h>
31#include <promeki/httpserver.h>
32
33PROMEKI_NAMESPACE_BEGIN
34
35class HttpServer;
36
108class HttpApi : public ObjectBase {
109 PROMEKI_OBJECT(HttpApi, ObjectBase)
110 public:
122 static const String DefaultPrefix;
123
125 static const String DefaultTitle;
126
128 static const String DefaultVersion;
129
130 // ============================================================
131 // Endpoint descriptor types
132 // ============================================================
133
141 enum class ParamIn {
142 Path,
143 Query,
144 Header,
145 Body,
146 };
147
156 struct Param {
157 using List = ::promeki::List<Param>;
158
159 String name;
160 ParamIn in = ParamIn::Query;
161 bool required = false;
162 VariantSpec spec;
163 };
164
175 struct ErrorResponse {
176 using List = ::promeki::List<ErrorResponse>;
177
178 int status = 0;
179 String description;
180 VariantSpec body;
181 };
182
194 struct Endpoint {
195 using List = ::promeki::List<Endpoint>;
196
212 String path;
213 HttpMethod method;
214 String title;
215 String summary;
216 StringList tags;
217 Param::List params;
218 VariantSpec response;
219 String responseContentType = "application/json";
220 ErrorResponse::List errors;
221 bool deprecated = false;
222
223 // FIXME(auth): when an authentication/authorization
224 // story lands, add a `security` field here (list of
225 // required scope/scheme names) and a matching
226 // HttpApi::setSecurityScheme() / addSecurityScheme()
227 // accessor for the OpenAPI components.securitySchemes
228 // block. The catalog and OpenAPI generators must
229 // surface the per-endpoint security[] array, and the
230 // route-installer must enforce it (returning 401/403
231 // before invoking the handler) — neither is done
232 // today. Tracked so this isn't silently forgotten.
233
235 Endpoint &addParam(Param p) {
236 params.pushToBack(std::move(p));
237 return *this;
238 }
239
241 Endpoint &addTag(const String &t) {
242 tags.pushToBack(t);
243 return *this;
244 }
245 };
246
247 // ============================================================
248 // RPC convenience
249 // ============================================================
250
270 using RpcCall = Function<Result<Variant>(const VariantMap &args)>;
271
272 // ============================================================
273 // Construction / lifetime
274 // ============================================================
275
290 explicit HttpApi(HttpServer &server, const String &prefix = DefaultPrefix,
291 ObjectBase *parent = nullptr);
292
294 ~HttpApi() override;
295
297 HttpServer &server() { return _server; }
298
300 const HttpServer &server() const { return _server; }
301
303 const String &prefix() const { return _prefix; }
304
314 String resolve(const String &relative) const;
315
316 // ============================================================
317 // OpenAPI metadata
318 // ============================================================
319
321 void setTitle(const String &title) { _title = title; }
322
324 const String &title() const { return _title; }
325
327 void setVersion(const String &version) { _version = version; }
328
330 const String &version() const { return _version; }
331
333 void setDescription(const String &desc) { _description = desc; }
334
336 const String &description() const { return _description; }
337
347 void addServer(const String &url, const String &description = String());
348
358 void setDefaultErrors(ErrorResponse::List errors);
359
360 // ============================================================
361 // Endpoint registration
362 // ============================================================
363
378 Error route(Endpoint ep, HttpHandlerFunc handler);
379
395 Error rpc(Endpoint ep, RpcCall call);
396
417 template <CompiledString N>
418 Error exposeDatabase(const String &mountPath, const String &title, VariantDatabase<N> &db,
419 bool readOnly = false);
420
436 template <typename T> Error exposeLookup(const String &mountPath, const String &title, T &target);
437
438 // ============================================================
439 // Bundled module installers
440 // ============================================================
441
465 Error installPromekiAPI();
466
467 // ============================================================
468 // Catalog / mount
469 // ============================================================
470
472 Endpoint::List endpoints() const;
473
477 int endpointCount() const;
478
492 Error mount();
493
495 bool isMounted() const { return _mounted; }
496
497 // ============================================================
498 // Catalog / OpenAPI rendering (also used by the routes)
499 // ============================================================
500
517 JsonObject toCatalog() const;
518
532 JsonObject toOpenApi() const;
533
596 static JsonObject variantSpecToJsonSchema(const VariantSpec &spec, JsonObject *componentsOut = nullptr);
597
598 private:
599 struct ServerEntry {
600 using List = ::promeki::List<ServerEntry>;
601 String url;
602 String description;
603 };
604
605 // Internal: install a single endpoint into both the
606 // catalog and the underlying HTTP server. Returns
607 // Error::Exists if (path, method) already exists.
608 Error registerEndpoint(Endpoint ep, HttpHandler::Ptr handler);
609
610 // Internal: render error responses, falling back to
611 // _defaultErrors when the endpoint declares none.
612 const ErrorResponse::List &errorsFor(const Endpoint &ep) const;
613
614 // Internal: the three handler factories for mount().
615 HttpHandlerFunc makeCatalogHandler() const;
616 HttpHandlerFunc makeOpenApiHandler() const;
617 HttpHandler::Ptr makeExplorerHandler() const;
618
619 HttpServer &_server;
620 String _prefix;
621 bool _mounted = false;
622 String _title;
623 String _version;
624 String _description;
625 ServerEntry::List _servers;
626 ErrorResponse::List _defaultErrors;
627 Endpoint::List _endpoints;
628
629 // Internal helpers used by exposeDatabase / exposeLookup.
630 Error addEndpointDescriptor(Endpoint ep);
631 static VariantSpec keyParamSpec(const StringList &knownKeys);
632};
633
634// ============================================================
635// Reflection adapter template definitions
636// ============================================================
637
638template <CompiledString N>
639Error HttpApi::exposeDatabase(const String &mountPath, const String &title, VariantDatabase<N> &db, bool readOnly) {
640 using DB = VariantDatabase<N>;
641 using ID = typename DB::ID;
642
643 // mountPath is relative to this api's prefix. Resolve it
644 // before handing it to HttpServer (which doesn't know about
645 // the api's prefix) and to the catalog (which stores
646 // absolute paths).
647 const String absMount = resolve(mountPath);
648
649 // Hand the actual route registration off to HttpServer; the
650 // catalog publication is the only thing layered on top.
651 _server.exposeDatabase(absMount, db, readOnly);
652
653 // Collect declared keys for the {key} param description so the
654 // explorer's free-form input has a hint of valid values.
655 StringList knownKeys;
656 const auto specs = DB::registeredSpecs();
657 for (auto it = specs.cbegin(); it != specs.cend(); ++it) {
658 knownKeys.pushToBack(ID::fromId(it->first).name());
659 }
660
661 // GET <absMount> — full snapshot.
662 Endpoint epAll;
663 epAll.path = absMount;
664 epAll.method = HttpMethod::Get;
665 epAll.title = title + ": snapshot";
666 epAll.summary = String("Returns every key/value pair in ") + title + " as a single JSON object.";
667 epAll.tags = {title};
668 epAll.response = VariantSpec().setDescription("JSON object whose keys are the database IDs.");
669 if (Error err = addEndpointDescriptor(epAll); err.isError()) return err;
670
671 // GET <absMount>/_schema — registered specs.
672 Endpoint epSchema;
673 epSchema.path = absMount + "/_schema";
674 epSchema.method = HttpMethod::Get;
675 epSchema.title = title + ": schema";
676 epSchema.summary = "Returns the registered VariantSpec for every "
677 "declared key (type, default, range, description).";
678 epSchema.tags = {title};
679 epSchema.response = VariantSpec().setDescription("JSON object keyed by ID name; each value carries the spec.");
680 if (Error err = addEndpointDescriptor(epSchema); err.isError()) return err;
681
682 // GET <absMount>/{key} — single value.
683 Endpoint epGet;
684 epGet.path = absMount + "/{key}";
685 epGet.method = HttpMethod::Get;
686 epGet.title = title + ": get key";
687 epGet.summary = "Returns a single value plus its spec.";
688 epGet.tags = {title};
689 epGet.params = {Param{
690 .name = "key",
691 .in = ParamIn::Path,
692 .required = true,
693 .spec = keyParamSpec(knownKeys),
694 }};
695 epGet.response = VariantSpec().setDescription("JSON object with the value under its name and an "
696 "optional `_spec` companion.");
697 if (Error err = addEndpointDescriptor(epGet); err.isError()) return err;
698
699 if (readOnly) return Error::Ok;
700
701 // PUT <absMount>/{key} — set.
702 Endpoint epPut;
703 epPut.path = absMount + "/{key}";
704 epPut.method = HttpMethod::Put;
705 epPut.title = title + ": set key";
706 epPut.summary = "Updates a single value, validating it against the "
707 "registered spec.";
708 epPut.tags = {title};
709 epPut.params = {
710 Param{
711 .name = "key",
712 .in = ParamIn::Path,
713 .required = true,
714 .spec = keyParamSpec(knownKeys),
715 },
716 Param{
717 .name = "value",
718 .in = ParamIn::Body,
719 .required = true,
720 .spec = VariantSpec().setDescription("New value for the key (shape depends on spec)."),
721 },
722 };
723 epPut.response = VariantSpec().setDescription("JSON object with the stored value under its name.");
724 if (Error err = addEndpointDescriptor(epPut); err.isError()) return err;
725
726 // DELETE <absMount>/{key} — clear.
727 Endpoint epDel;
728 epDel.path = absMount + "/{key}";
729 epDel.method = HttpMethod::Delete;
730 epDel.title = title + ": delete key";
731 epDel.summary = "Clears the entry for a single key.";
732 epDel.tags = {title};
733 epDel.params = {Param{
734 .name = "key",
735 .in = ParamIn::Path,
736 .required = true,
737 .spec = keyParamSpec(knownKeys),
738 }};
739 return addEndpointDescriptor(epDel);
740}
741
742template <typename T> Error HttpApi::exposeLookup(const String &mountPath, const String &title, T &target) {
743 const String absMount = resolve(mountPath);
744 _server.exposeLookup(absMount, target);
745
746 Endpoint ep;
747 ep.path = absMount + "/{path}";
748 ep.method = HttpMethod::Get;
749 ep.title = title;
750 ep.summary = "Resolves a path-style key against the live object "
751 "tree (slashes mapped to dots; bare integer segments "
752 "to [N] index suffixes).";
753 ep.tags = {title};
754 ep.params = {Param{
755 .name = "path",
756 .in = ParamIn::Path,
757 .required = true,
758 .spec = VariantSpec().setType(DataTypeString).setDescription("Greedy lookup path."),
759 }};
760 ep.response = VariantSpec().setDescription("JSON object {\"value\": <variant>}.");
761 return addEndpointDescriptor(ep);
762}
763
764PROMEKI_NAMESPACE_END
765
766#endif // PROMEKI_ENABLE_HTTP