11#include <promeki/config.h>
12#if PROMEKI_ENABLE_HTTP
33PROMEKI_NAMESPACE_BEGIN
108class HttpApi :
public ObjectBase {
109 PROMEKI_OBJECT(HttpApi, ObjectBase)
122 static const String DefaultPrefix;
125 static const String DefaultTitle;
128 static const String DefaultVersion;
157 using List = ::promeki::List<Param>;
160 ParamIn in = ParamIn::Query;
161 bool required =
false;
175 struct ErrorResponse {
176 using List = ::promeki::List<ErrorResponse>;
195 using List = ::promeki::List<Endpoint>;
218 VariantSpec response;
219 String responseContentType =
"application/json";
220 ErrorResponse::List errors;
221 bool deprecated =
false;
235 Endpoint &addParam(Param p) {
236 params.pushToBack(std::move(p));
241 Endpoint &addTag(
const String &t) {
270 using RpcCall = Function<Result<Variant>(
const VariantMap &args)>;
290 explicit HttpApi(HttpServer &server,
const String &prefix = DefaultPrefix,
291 ObjectBase *parent =
nullptr);
297 HttpServer &server() {
return _server; }
300 const HttpServer &server()
const {
return _server; }
303 const String &prefix()
const {
return _prefix; }
314 String resolve(
const String &relative)
const;
321 void setTitle(
const String &title) { _title = title; }
324 const String &title()
const {
return _title; }
327 void setVersion(
const String &version) { _version = version; }
330 const String &version()
const {
return _version; }
333 void setDescription(
const String &desc) { _description = desc; }
336 const String &description()
const {
return _description; }
347 void addServer(
const String &url,
const String &description = String());
358 void setDefaultErrors(ErrorResponse::List errors);
378 Error route(Endpoint ep, HttpHandlerFunc handler);
395 Error rpc(Endpoint ep, RpcCall call);
417 template <CompiledString N>
418 Error exposeDatabase(
const String &mountPath,
const String &title, VariantDatabase<N> &db,
419 bool readOnly =
false);
436 template <
typename T> Error exposeLookup(
const String &mountPath,
const String &title, T &target);
465 Error installPromekiAPI();
472 Endpoint::List endpoints()
const;
477 int endpointCount()
const;
495 bool isMounted()
const {
return _mounted; }
517 JsonObject toCatalog()
const;
532 JsonObject toOpenApi()
const;
596 static JsonObject variantSpecToJsonSchema(
const VariantSpec &spec, JsonObject *componentsOut =
nullptr);
600 using List = ::promeki::List<ServerEntry>;
608 Error registerEndpoint(Endpoint ep, HttpHandler::Ptr handler);
612 const ErrorResponse::List &errorsFor(
const Endpoint &ep)
const;
615 HttpHandlerFunc makeCatalogHandler()
const;
616 HttpHandlerFunc makeOpenApiHandler()
const;
617 HttpHandler::Ptr makeExplorerHandler()
const;
621 bool _mounted =
false;
625 ServerEntry::List _servers;
626 ErrorResponse::List _defaultErrors;
627 Endpoint::List _endpoints;
630 Error addEndpointDescriptor(Endpoint ep);
631 static VariantSpec keyParamSpec(
const StringList &knownKeys);
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;
647 const String absMount = resolve(mountPath);
651 _server.exposeDatabase(absMount, db, readOnly);
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());
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;
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;
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{
693 .spec = keyParamSpec(knownKeys),
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;
699 if (readOnly)
return Error::Ok;
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 "
708 epPut.tags = {title};
714 .spec = keyParamSpec(knownKeys),
720 .spec = VariantSpec().setDescription(
"New value for the key (shape depends on spec)."),
723 epPut.response = VariantSpec().setDescription(
"JSON object with the stored value under its name.");
724 if (Error err = addEndpointDescriptor(epPut); err.isError())
return err;
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{
737 .spec = keyParamSpec(knownKeys),
739 return addEndpointDescriptor(epDel);
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);
747 ep.path = absMount +
"/{path}";
748 ep.method = HttpMethod::Get;
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).";
758 .spec = VariantSpec().setType(DataTypeString).setDescription(
"Greedy lookup path."),
760 ep.response = VariantSpec().setDescription(
"JSON object {\"value\": <variant>}.");
761 return addEndpointDescriptor(ep);