From dfcac91d37aa5770868a6452681893b3bbf3f888 Mon Sep 17 00:00:00 2001 From: Gaute Lindkvist Date: Sat, 2 Mar 2024 13:03:56 +0000 Subject: [PATCH] First move towards OpenAPI 3.1.0 compliance --- Core/cafJsonSerializer.cpp | 51 +- Core/cafObjectHandle.h | 4 +- Core/cafSerializer.cpp | 2 + Core/cafSerializer.h | 1 + RestInterface/CMakeLists.txt | 12 +- RestInterface/cafRestAppService.cpp | 137 ++--- RestInterface/cafRestAppService.h | 20 +- RestInterface/cafRestClient.cpp | 139 ++--- RestInterface/cafRestDocumentService.cpp | 286 +++++++++++ RestInterface/cafRestDocumentService.h | 63 +++ RestInterface/cafRestObjectService.cpp | 481 +++++++++++------- RestInterface/cafRestObjectService.h | 55 +- RestInterface/cafRestOpenApiService.cpp | 110 ++++ RestInterface/cafRestOpenApiService.h | 65 +++ RestInterface/cafRestSchemaService.cpp | 66 +-- RestInterface/cafRestSchemaService.h | 14 +- RestInterface/cafRestServerApplication.cpp | 12 +- RestInterface/cafRestServiceInterface.cpp | 52 ++ RestInterface/cafRestServiceInterface.h | 75 ++- RestInterface/cafRestSession.cpp | 137 +++-- RestInterface/cafRestSession.h | 2 +- RestInterface/cafRestSessionService.cpp | 360 +++++++------ RestInterface/cafRestSessionService.h | 36 +- .../cafRpcClientPassByRefObjectFactory.cpp | 48 +- RpcBase/cafRpcClientPassByRefObjectFactory.h | 7 +- 25 files changed, 1581 insertions(+), 654 deletions(-) create mode 100644 RestInterface/cafRestDocumentService.cpp create mode 100644 RestInterface/cafRestDocumentService.h create mode 100644 RestInterface/cafRestOpenApiService.cpp create mode 100644 RestInterface/cafRestOpenApiService.h diff --git a/Core/cafJsonSerializer.cpp b/Core/cafJsonSerializer.cpp index 4826bc17..0f2d7e86 100644 --- a/Core/cafJsonSerializer.cpp +++ b/Core/cafJsonSerializer.cpp @@ -114,18 +114,38 @@ void JsonSerializer::writeObjectToJson( const ObjectHandle* object, nlohmann::js if ( this->serializationType() == Serializer::SerializationType::SCHEMA ) { - jsonObject["type"] = "object"; + std::set parentalFields; + std::set parentalMethods; + + auto parentClassKeyword = object->parentClassKeyword(); + + auto parentClassInstance = DefaultObjectFactory::instance()->create( parentClassKeyword ); + if ( parentClassInstance ) + { + for ( auto field : parentClassInstance->fields() ) + { + parentalFields.insert( field->keyword() ); + } + for ( auto method : parentClassInstance->methods() ) + { + parentalMethods.insert( method->keyword() ); + } + } + + auto jsonClass = nlohmann::json::object(); + + jsonClass["type"] = "object"; auto jsonProperties = nlohmann::json::object(); jsonProperties["keyword"] = { { "type", "string" } }; jsonProperties["uuid"] = { { "type", "string" } }; jsonObject["$schema"] = "https://json-schema.org/draft/2020-12/schema"; - jsonObject["$id"] = "/schemas/" + object->classKeyword(); - jsonObject["title"] = object->classKeyword(); + jsonObject["$id"] = "/components/object_schemas/" + object->classKeyword(); + // jsonObject["title"] = object->classKeyword(); if ( !object->classDocumentation().empty() ) { - jsonObject["description"] = object->classDocumentation(); + jsonClass["description"] = object->classDocumentation(); } for ( auto field : object->fields() ) @@ -133,6 +153,7 @@ void JsonSerializer::writeObjectToJson( const ObjectHandle* object, nlohmann::js if ( this->fieldSelector() && !this->fieldSelector()( field ) ) continue; auto keyword = field->keyword(); + if ( parentalFields.contains( keyword ) ) continue; const FieldJsonCapability* ioCapability = field->capability(); if ( ioCapability && keyword != "uuid" && ( field->isReadable() || field->isWritable() ) ) @@ -146,7 +167,10 @@ void JsonSerializer::writeObjectToJson( const ObjectHandle* object, nlohmann::js auto methods = nlohmann::json::object(); for ( auto method : object->methods() ) { - methods[method->keyword()] = method->jsonSchema(); + auto keyword = method->keyword(); + if ( parentalMethods.contains( keyword ) ) continue; + + methods[keyword] = method->jsonSchema(); } if ( !methods.empty() ) { @@ -156,8 +180,21 @@ void JsonSerializer::writeObjectToJson( const ObjectHandle* object, nlohmann::js jsonProperties["methods"] = methodsObject; } - jsonObject["properties"] = jsonProperties; - jsonObject["required"] = { "keyword" }; + jsonClass["properties"] = jsonProperties; + jsonClass["required"] = { "keyword", "uuid" }; + + if ( parentClassInstance ) + { + auto jsonAllOf = nlohmann::json::array(); + jsonAllOf.push_back( { { "$ref", "#/components/object_schemas/" + parentClassKeyword } } ); + + jsonAllOf.push_back( jsonClass ); + jsonObject["allOf"] = jsonAllOf; + } + else + { + jsonObject = jsonClass; + } } else { diff --git a/Core/cafObjectHandle.h b/Core/cafObjectHandle.h index 78b87b22..41af9b81 100644 --- a/Core/cafObjectHandle.h +++ b/Core/cafObjectHandle.h @@ -241,7 +241,7 @@ struct PortableDataType static nlohmann::json jsonType() { auto object = nlohmann::json::object(); - object["$ref"] = std::string( "#/schemas/" ) + DataType::classKeywordStatic(); + object["$ref"] = std::string( "#/components/object_schemas/" ) + DataType::classKeywordStatic(); return object; } }; @@ -257,7 +257,7 @@ struct PortableDataType static nlohmann::json jsonType() { auto object = nlohmann::json::object(); - object["$ref"] = std::string( "#/schemas/" ) + DataType::element_type::classKeywordStatic(); + object["$ref"] = std::string( "#/components/object_schemas/" ) + DataType::element_type::classKeywordStatic(); return object; } }; diff --git a/Core/cafSerializer.cpp b/Core/cafSerializer.cpp index 1adc6cb9..747aa83d 100644 --- a/Core/cafSerializer.cpp +++ b/Core/cafSerializer.cpp @@ -30,6 +30,8 @@ std::string Serializer::serializationTypeLabel( SerializationType type ) return "DATA_SKELETON"; case SerializationType::SCHEMA: return "SCHEMA"; + case SerializationType::PATH: + return "PATH"; } CAFFA_ASSERT( false ); return ""; diff --git a/Core/cafSerializer.h b/Core/cafSerializer.h index 0bbbada3..0658476c 100644 --- a/Core/cafSerializer.h +++ b/Core/cafSerializer.h @@ -40,6 +40,7 @@ class Serializer DATA_FULL, DATA_SKELETON, SCHEMA, + PATH }; using FieldSelector = std::function; diff --git a/RestInterface/CMakeLists.txt b/RestInterface/CMakeLists.txt index 024a4caa..3c1a0df2 100644 --- a/RestInterface/CMakeLists.txt +++ b/RestInterface/CMakeLists.txt @@ -11,10 +11,12 @@ set(PUBLIC_HEADERS cafRestClientApplication.h cafRestServiceInterface.h cafRestAppService.h + cafRestDocumentService.h cafRestObjectService.h cafRestSessionService.h - cafRestSchemaService.h - cafRestRequest.h) + cafRestRequest.h + cafRestOpenApiService.h + cafRestSessionService.h) set(PROJECT_FILES ${PUBLIC_HEADERS} @@ -25,10 +27,12 @@ set(PROJECT_FILES cafRestClientApplication.cpp cafRestServiceInterface.cpp cafRestAppService.cpp + cafRestDocumentService.cpp cafRestObjectService.cpp cafRestSessionService.cpp - cafRestSchemaService.cpp - cafRestRequest.cpp) + cafRestRequest.cpp + cafRestOpenApiService.cpp + cafRestSessionService.cpp) if(CAFFA_BUILD_SHARED) message(STATUS "Building ${PROJECT_NAME} shared") diff --git a/RestInterface/cafRestAppService.cpp b/RestInterface/cafRestAppService.cpp index e74dc3b4..196a9693 100644 --- a/RestInterface/cafRestAppService.cpp +++ b/RestInterface/cafRestAppService.cpp @@ -26,101 +26,102 @@ using namespace caffa::rpc; -RestAppService::ServiceResponse RestAppService::perform( http::verb verb, - const std::list& path, - const nlohmann::json& arguments, - const nlohmann::json& metaData ) +RestAppService::ServiceResponse RestAppService::perform( http::verb verb, + std::list path, + const nlohmann::json& queryParams, + const nlohmann::json& body ) { - auto allCallbacks = callbacks(); - - if ( path.empty() ) + if ( verb == http::verb::get ) { - auto jsonArray = nlohmann::json::array(); - for ( auto [name, callback] : allCallbacks ) - { - jsonArray.push_back( name ); - } - return std::make_tuple( http::status::ok, jsonArray.dump(), nullptr ); + return info(); } - auto name = path.front(); - - auto it = allCallbacks.find( name ); - if ( it != allCallbacks.end() ) + else if ( verb == http::verb::delete_ ) { - return it->second( verb, arguments, metaData ); + return quit(); } - return std::make_tuple( http::status::not_found, "No such method", nullptr ); + return std::make_tuple( http::status::bad_request, "Only GET or DELETE makes any sense with app requests", nullptr ); } -//-------------------------------------------------------------------------------------------------- -/// The app service uses session uuids to decide if it accepts the request or not -//-------------------------------------------------------------------------------------------------- -bool RestAppService::requiresAuthentication( const std::list& path ) const +bool RestAppService::requiresAuthentication( http::verb verb, const std::list& path ) const { return false; } -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -RestAppService::ServiceResponse - RestAppService::info( http::verb verb, const nlohmann::json& arguments, const nlohmann::json& metaData ) +bool RestAppService::requiresSession( http::verb verb, const std::list& path ) const { - CAFFA_TRACE( "Received appInfo request" ); + return verb == http::verb::delete_; +} - if ( RestServiceInterface::refuseDueToTimeLimiter() ) - { - return std::make_tuple( http::status::too_many_requests, "Too many unauthenticated requests", nullptr ); - } +std::map RestAppService::servicePathEntries() const +{ + auto infoRequest = nlohmann::json::object(); - if ( verb != http::verb::get ) - { - return std::make_tuple( http::status::bad_request, "Only GET makes any sense with app/info", nullptr ); - } - auto app = RestServerApplication::instance(); - auto appInfo = app->appInfo(); + auto getContent = nlohmann::json::object(); + getContent["application/json"] = { { "schema", { { "$ref", "#/components/app_schemas/AppInfo" } } } }; + auto getResponses = nlohmann::json::object(); - nlohmann::json json = appInfo; - return std::make_tuple( http::status::ok, json.dump(), nullptr ); + auto errorContent = nlohmann::json::object(); + errorContent["text/plain"] = { { "schema", { { "$ref", "#/components/error_schemas/PlainError" } } } }; + + getResponses[std::to_string( static_cast( http::status::ok ) )] = { { "description", + "Application Information" }, + { "content", getContent } }; + getResponses[std::to_string( static_cast( http::status::too_many_requests ) )] = + { { "description", "Too many Requests Error Message" }, { "content", errorContent } }; + + infoRequest["get"] = { { "summary", "Get Application Information" }, + { "operationId", "info" }, + { "responses", getResponses }, + { "tags", { "app" } } }; + + auto quitRequest = nlohmann::json::object(); + auto quitResponses = nlohmann::json::object(); + + quitResponses[std::to_string( static_cast( http::status::accepted ) )] = { + { "description", "Success" }, + }; + + quitResponses[std::to_string( static_cast( http::status::forbidden ) )] = { { "description", + "Quit Error Message" }, + { "content", errorContent } }; + + quitRequest["delete"] = { { "summary", "Quit Application" }, + { "operationId", "quit" }, + { "responses", quitResponses }, + { "tags", { "app" } } }; + + return { { "/app/info", infoRequest }, { "/app/quit", quitRequest } }; } +std::map RestAppService::serviceComponentEntries() const +{ + auto appInfo = AppInfo::jsonSchema(); + + return { { "app_schemas", { { "AppInfo", appInfo } } } }; +}; + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -RestAppService::ServiceResponse - RestAppService::quit( http::verb verb, const nlohmann::json& arguments, const nlohmann::json& metaData ) +RestAppService::ServiceResponse RestAppService::info() { - std::string session_uuid = ""; - if ( arguments.contains( "session_uuid" ) ) - { - session_uuid = arguments["session_uuid"].get(); - } - else if ( metaData.contains( "session_uuid" ) ) - { - session_uuid = metaData["session_uuid"].get(); - } + CAFFA_TRACE( "Received info request" ); - auto session = RestServerApplication::instance()->getExistingSession( session_uuid ); - if ( !session && RestServerApplication::instance()->requiresValidSession() ) - { - return std::make_tuple( http::status::forbidden, "Session '" + session_uuid + "' is not valid", nullptr ); - } - else if ( RestServerApplication::instance()->requiresValidSession() && session->isExpired() ) - { - return std::make_tuple( http::status::forbidden, "Session '" + session_uuid + "' is expired", nullptr ); - } + auto app = RestServerApplication::instance(); + auto appInfo = app->appInfo(); - return std::make_tuple( http::status::ok, "", []() { RestServerApplication::instance()->quit(); } ); + nlohmann::json json = appInfo; + return std::make_tuple( http::status::ok, json.dump(), nullptr ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -std::map RestAppService::callbacks() const +RestAppService::ServiceResponse RestAppService::quit() { - std::map callbacks = { - { "info", &RestAppService::info }, - { "quit", &RestAppService::quit }, - }; - return callbacks; + CAFFA_DEBUG( "Received quit request" ); + + return std::make_tuple( http::status::accepted, + "Told to quit. It will happen soon", + []() { RestServerApplication::instance()->quit(); } ); } diff --git a/RestInterface/cafRestAppService.h b/RestInterface/cafRestAppService.h index 0f5ea12a..7eb44a34 100644 --- a/RestInterface/cafRestAppService.h +++ b/RestInterface/cafRestAppService.h @@ -28,19 +28,21 @@ namespace caffa::rpc class RestAppService : public RestServiceInterface { public: - ServiceResponse perform( http::verb verb, - const std::list& path, - const nlohmann::json& arguments, - const nlohmann::json& metaData ) override; + ServiceResponse perform( http::verb verb, + std::list path, + const nlohmann::json& queryParams, + const nlohmann::json& body ) override; - bool requiresAuthentication( const std::list& path ) const override; + bool requiresAuthentication( http::verb verb, const std::list& path ) const override; + bool requiresSession( http::verb verb, const std::list& path ) const override; + + std::map servicePathEntries() const override; + std::map serviceComponentEntries() const override; private: using ServiceCallback = std::function; - std::map callbacks() const; - - static ServiceResponse info( http::verb verb, const nlohmann::json& arguments, const nlohmann::json& metaData ); - static ServiceResponse quit( http::verb verb, const nlohmann::json& arguments, const nlohmann::json& metaData ); + static ServiceResponse info(); + static ServiceResponse quit(); }; } // namespace caffa::rpc diff --git a/RestInterface/cafRestClient.cpp b/RestInterface/cafRestClient.cpp index fe404419..d07ea8d0 100644 --- a/RestInterface/cafRestClient.cpp +++ b/RestInterface/cafRestClient.cpp @@ -163,7 +163,7 @@ class Request : public std::enable_shared_from_this if ( ec ) { - CAFFA_ERROR( "Failed to read from stream" ); + CAFFA_ERROR( "Failed to read from stream from thread ID " << std::this_thread::get_id() << "-> " << ec.message()); m_result.set_value( std::make_pair( http::status::network_connect_timeout_error, "Failed to read from stream" ) ); return; @@ -244,7 +244,12 @@ RestClient::RestClient( const std::string& hostname, int port /*= 50000 */ ) //-------------------------------------------------------------------------------------------------- RestClient::~RestClient() { - destroySession(); + try { + destroySession(); + } + catch(const std::exception& e) { + CAFFA_CRITICAL("Failed to destroy session " << e.what()); + } } //-------------------------------------------------------------------------------------------------- @@ -275,7 +280,7 @@ std::shared_ptr RestClient::document( const std::string& do auto [status, body] = performGetRequest( hostname(), port(), - std::string( "/" ) + documentId + "?skeleton=true&session_uuid=" + m_sessionUuid ); + std::string( "/documents/" ) + documentId + "?skeleton=true&session_uuid=" + m_sessionUuid ); if ( status != http::status::ok ) { @@ -297,7 +302,7 @@ std::vector> RestClient::documents() const std::scoped_lock lock( m_sessionMutex ); auto [status, body] = - performGetRequest( hostname(), port(), std::string( "/?skeleton=true&session_uuid=" ) + m_sessionUuid ); + performGetRequest( hostname(), port(), std::string( "/documents/?skeleton=true&session_uuid=" ) + m_sessionUuid ); if ( status != http::status::ok ) { @@ -332,10 +337,10 @@ std::string RestClient::execute( caffa::not_null sel std::scoped_lock lock( m_sessionMutex ); auto [status, body] = - performRequest( http::verb::put, + performRequest( http::verb::post, hostname(), port(), - "/object/uuid/" + selfObject->uuid() + "/" + methodName + "?session_uuid=" + m_sessionUuid, + "/objects/" + selfObject->uuid() + "/methods/" + methodName + "?session_uuid=" + m_sessionUuid, jsonArguments ); if ( status != http::status::ok ) { @@ -352,27 +357,30 @@ bool RestClient::stopServer() { { std::scoped_lock lock( m_sessionMutex ); + auto sessionUuid = m_sessionUuid; + + m_sessionUuid = ""; + + if ( m_keepAliveThread ) + { + m_keepAliveThread->join(); + m_keepAliveThread.reset(); + } auto [status, body] = performRequest( http::verb::delete_, hostname(), port(), - std::string( "/app/quit?session_uuid=" ) + m_sessionUuid, + std::string( "/app/quit?session_uuid=" ) + sessionUuid, "" ); - if ( status != http::status::ok ) + if ( status != http::status::accepted) { throw std::runtime_error( "Failed to stop server: " + body ); } CAFFA_TRACE( "Stopped server, which also destroys session" ); - m_sessionUuid = ""; } - if ( m_keepAliveThread ) - { - m_keepAliveThread->join(); - m_keepAliveThread.reset(); - } return true; } @@ -383,13 +391,10 @@ void RestClient::sendKeepAlive() { std::scoped_lock lock( m_sessionMutex ); - auto jsonObject = nlohmann::json::object(); - jsonObject["session_uuid"] = m_sessionUuid; - auto [status, body] = - performRequest( http::verb::patch, hostname(), port(), std::string( "/session/keepalive" ), jsonObject.dump() ); + performRequest( http::verb::patch, hostname(), port(), std::string( "/sessions/" ) + m_sessionUuid + "?session_uuid=" + m_sessionUuid, ""); - if ( status != http::status::ok ) + if ( status != http::status::accepted ) { throw std::runtime_error( "Failed to keep server alive: " + body ); } @@ -424,6 +429,7 @@ void RestClient::startKeepAliveThread() } } } ); + CAFFA_INFO("Thread ID: " << m_keepAliveThread->get_id()); } //-------------------------------------------------------------------------------------------------- @@ -438,7 +444,7 @@ bool RestClient::isReady( caffa::Session::Type type ) const auto unsignedType = static_cast( type ); auto [status, body] = - performGetRequest( hostname(), port(), std::string( "/session/ready?type=" + std::to_string( unsignedType ) ) ); + performGetRequest( hostname(), port(), std::string( "/sessions/?type=" + std::to_string( unsignedType ) ) ); if ( status != http::status::ok ) { @@ -469,7 +475,7 @@ void RestClient::createSession( caffa::Session::Type type, const std::string& us arguments["type"] = static_cast( type ); auto [status, body] = - performRequest( http::verb::put, hostname(), port(), "/session/create", arguments.dump(), username, password ); + performRequest( http::verb::post, hostname(), port(), "/sessions", arguments.dump(), username, password ); if ( status != http::status::ok ) { @@ -478,12 +484,12 @@ void RestClient::createSession( caffa::Session::Type type, const std::string& us CAFFA_TRACE( "Got result: " << body ); auto jsonObject = nlohmann::json::parse( body ); - if ( !jsonObject.contains( "session_uuid" ) ) + if ( !jsonObject.contains( "uuid" ) ) { throw std::runtime_error( "Failed to create session" ); } - m_sessionUuid = jsonObject["session_uuid"].get(); + m_sessionUuid = jsonObject["uuid"].get(); CAFFA_DEBUG( "Created session with UUID: " << m_sessionUuid ); } @@ -494,11 +500,14 @@ caffa::Session::Type RestClient::checkSession() const { std::scoped_lock lock( m_sessionMutex ); - auto jsonObject = nlohmann::json::object(); - jsonObject["session_uuid"] = m_sessionUuid; + auto jsonObject = nlohmann::json::object(); + jsonObject["uuid"] = m_sessionUuid; - auto [status, body] = - performRequest( http::verb::get, hostname(), port(), std::string( "/session/check" ), jsonObject.dump() ); + auto [status, body] = performRequest( http::verb::get, + hostname(), + port(), + std::string( "/sessions/" + m_sessionUuid + "?session_uuid=" ) + m_sessionUuid, + jsonObject.dump() ); if ( status != http::status::ok ) { @@ -519,12 +528,15 @@ void RestClient::changeSession( caffa::Session::Type newType ) { std::scoped_lock lock( m_sessionMutex ); - auto jsonObject = nlohmann::json::object(); - jsonObject["session_uuid"] = m_sessionUuid; - jsonObject["type"] = static_cast( newType ); + auto jsonObject = nlohmann::json::object(); + jsonObject["uuid"] = m_sessionUuid; + jsonObject["type"] = static_cast( newType ); - auto [status, body] = - performRequest( http::verb::put, hostname(), port(), std::string( "/session/change" ), jsonObject.dump() ); + auto [status, body] = performRequest( http::verb::put, + hostname(), + port(), + std::string( "/sessions/" + m_sessionUuid + "?session_uuid=" ) + m_sessionUuid, + jsonObject.dump() ); if ( status != http::status::ok ) { @@ -544,14 +556,15 @@ void RestClient::destroySession() CAFFA_DEBUG( "Destroying session " << m_sessionUuid ); - auto jsonObject = nlohmann::json::object(); - jsonObject["session_uuid"] = m_sessionUuid; - - auto [status, body] = - performRequest( http::verb::delete_, hostname(), port(), std::string( "/session/destroy" ), jsonObject.dump() ); + auto [status, body] = performRequest( http::verb::delete_, + hostname(), + port(), + std::string( "/sessions/" + m_sessionUuid + "?session_uuid=" ) + m_sessionUuid, + "" ); - if ( status != http::status::ok && status != http::status::not_found ) + if ( status != http::status::accepted && status != http::status::not_found ) { + CAFFA_ERROR("Failed to destroy session: " << body); throw std::runtime_error( "Failed to destroy session: " + body ); } @@ -582,10 +595,10 @@ void RestClient::setJson( const caffa::ObjectHandle* objectHandle, const std::st auto [status, body] = performRequest( http::verb::put, hostname(), port(), - std::string( "/uuid/" ) + objectHandle->uuid() + "/" + fieldName + + std::string( "/objects/" ) + objectHandle->uuid() + "/fields/" + fieldName + "?session_uuid=" + m_sessionUuid, value.dump() ); - if ( status != http::status::ok ) + if ( status != http::status::accepted ) { throw std::runtime_error( "Failed to set field value" ); } @@ -598,7 +611,7 @@ nlohmann::json RestClient::getJson( const caffa::ObjectHandle* objectHandle, con { auto [status, body] = performGetRequest( hostname(), port(), - std::string( "/uuid/" ) + objectHandle->uuid() + "/" + fieldName + + std::string( "/objects/" ) + objectHandle->uuid() + "/fields/" + fieldName + "?skeleton=true&session_uuid=" + m_sessionUuid ); if ( status != http::status::ok ) { @@ -616,7 +629,7 @@ std::shared_ptr RestClient::getShallowCopyOfChildObject( co { auto [status, body] = performGetRequest( hostname(), port(), - std::string( "/uuid/" ) + objectHandle->uuid() + "/" + fieldName + + std::string( "/objects/" ) + objectHandle->uuid() + "/fields/" + fieldName + "?skeleton=true&session_uuid=" + m_sessionUuid ); if ( status != http::status::ok ) { @@ -637,7 +650,7 @@ std::shared_ptr RestClient::getDeepCopyOfChildObject( const { auto [status, body] = performGetRequest( hostname(), port(), - std::string( "/uuid/" ) + objectHandle->uuid() + "/" + fieldName + + std::string( "/objects/" ) + objectHandle->uuid() + "/fields/" + fieldName + "?session_uuid=" + m_sessionUuid ); if ( status != http::status::ok ) { @@ -666,12 +679,12 @@ void RestClient::deepCopyChildObjectFrom( const caffa::ObjectHandle* objectHandl auto [status, body] = performRequest( http::verb::put, hostname(), port(), - std::string( "/uuid/" ) + objectHandle->uuid() + "/" + fieldName + + std::string( "/objects/" ) + objectHandle->uuid() + "/fields/" + fieldName + "?session_uuid=" + m_sessionUuid, childString ); - if ( status != http::status::ok ) + if ( status != http::status::accepted ) { - throw std::runtime_error( body ); + throw std::runtime_error( "Failed to deep copy object with status " + std::to_string(static_cast(status)) + " and body: " + body ); } } @@ -685,7 +698,7 @@ std::vector> RestClient::getChildObjects( c auto [status, body] = performGetRequest( hostname(), port(), - std::string( "/uuid/" ) + objectHandle->uuid() + "/" + fieldName + + std::string( "/objects/" ) + objectHandle->uuid() + "/fields/" + fieldName + "?skeleton=true&session_uuid=" + m_sessionUuid ); if ( status != http::status::ok ) { @@ -718,12 +731,12 @@ void RestClient::setChildObject( const caffa::ObjectHandle* objectHandle, auto [status, body] = performRequest( http::verb::put, hostname(), port(), - std::string( "/uuid/" ) + objectHandle->uuid() + "/" + fieldName + + std::string( "/objects/" ) + objectHandle->uuid() + "/fields/" + fieldName + "?session_uuid=" + m_sessionUuid, caffa::JsonSerializer().writeObjectToString( childObject ) ); - if ( status != http::status::ok ) + if ( status != http::status::accepted ) { - throw std::runtime_error( body ); + throw std::runtime_error( "Failed to set child object with status " + std::to_string(static_cast(status)) + " and body: " + body ); } } @@ -735,15 +748,14 @@ void RestClient::removeChildObject( const caffa::ObjectHandle* objectHandle, con auto [status, body] = performRequest( http::verb::delete_, hostname(), port(), - std::string( "/uuid/" ) + objectHandle->uuid() + "/" + fieldName + "[" + + std::string( "/objects/" ) + objectHandle->uuid() + "/fields/" + fieldName + "?index=" + std::to_string( index ) + - "]" - "?session_uuid=" + + "&session_uuid=" + m_sessionUuid, "" ); - if ( status != http::status::ok ) + if ( status != http::status::accepted ) { - throw std::runtime_error( body ); + throw std::runtime_error( "Failed to remove child object with status " + std::to_string(static_cast(status)) + " and body: " + body ); } } @@ -755,12 +767,12 @@ void RestClient::clearChildObjects( const caffa::ObjectHandle* objectHandle, con auto [status, body] = performRequest( http::verb::delete_, hostname(), port(), - std::string( "/uuid/" ) + objectHandle->uuid() + "/" + fieldName + + std::string( "/objects/" ) + objectHandle->uuid() + "/fields/" + fieldName + "?session_uuid=" + m_sessionUuid, "" ); - if ( status != http::status::ok ) + if ( status != http::status::accepted ) { - throw std::runtime_error( body ); + throw std::runtime_error( "Failed to clear all child objects with status " + std::to_string(static_cast(status)) + " and body: " + body ); } } @@ -773,17 +785,16 @@ void RestClient::insertChildObject( const caffa::ObjectHandle* objectHandle, const caffa::ObjectHandle* childObject ) { auto childString = caffa::JsonSerializer().writeObjectToString( childObject ); - auto [status, body] = performRequest( http::verb::put, + auto [status, body] = performRequest( http::verb::post, hostname(), port(), - std::string( "/uuid/" ) + objectHandle->uuid() + "/" + fieldName + "[" + + std::string( "/objects/" ) + objectHandle->uuid() + "/fields/" + fieldName + "?index=" + std::to_string( index ) + - "]" - "?session_uuid=" + + "&session_uuid=" + m_sessionUuid, childString ); - if ( status != http::status::ok ) + if ( status != http::status::accepted ) { - throw std::runtime_error( body ); + throw std::runtime_error( "Failed to insert child object at index " + std::to_string(index) + " with status " + std::to_string(static_cast(status)) + " and body: " + body ); } } diff --git a/RestInterface/cafRestDocumentService.cpp b/RestInterface/cafRestDocumentService.cpp new file mode 100644 index 00000000..643e5ccc --- /dev/null +++ b/RestInterface/cafRestDocumentService.cpp @@ -0,0 +1,286 @@ + +// ################################################################################################## +// +// Caffa +// Copyright (C) 2023- Kontur AS +// +// GNU Lesser General Public License Usage +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU Lesser General Public License at <> +// for more details. +// +#include "cafRestDocumentService.h" + +#include "cafRpcClientPassByRefObjectFactory.h" +#include "cafRpcServer.h" +#include "cafSession.h" + +#include "cafChildArrayField.h" +#include "cafChildField.h" +#include "cafDocument.h" +#include "cafField.h" +#include "cafFieldDocumentationCapability.h" +#include "cafFieldProxyAccessor.h" +#include "cafFieldScriptingCapability.h" +#include "cafJsonSerializer.h" +#include "cafMethod.h" +#include "cafObject.h" +#include "cafObjectCollector.h" +#include "cafObjectPerformer.h" +#include "cafPortableDataType.h" +#include "cafRestServerApplication.h" +#include "cafRpcObjectConversion.h" + +#include +#include +#include + +using namespace caffa; +using namespace caffa::rpc; + +RestDocumentService::ServiceResponse RestDocumentService::perform( http::verb verb, + std::list path, + const nlohmann::json& queryParams, + const nlohmann::json& body ) +{ + caffa::SessionMaintainer session; + if ( queryParams.contains( "session_uuid" ) ) + { + auto session_uuid = queryParams["session_uuid"].get(); + session = RestServerApplication::instance()->getExistingSession( session_uuid ); + } + + bool skeleton = queryParams.contains( "skeleton" ) && queryParams["skeleton"].get(); + + if ( path.empty() ) + { + return documents( session.get(), skeleton ); + } + + auto documentId = path.front(); + caffa::ObjectHandle* object = document( documentId, session.get() ); + if ( !object ) + { + return std::make_tuple( http::status::not_found, "Document " + documentId + " not found", nullptr ); + } + CAFFA_ASSERT( object ); + + if ( skeleton ) + { + return std::make_tuple( http::status::ok, createJsonSkeletonFromProjectObject( object ).dump(), nullptr ); + } + else + { + return std::make_tuple( http::status::ok, createJsonFromProjectObject( object ).dump(), nullptr ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// The object service uses session uuids to decide if it accepts the request or not +//-------------------------------------------------------------------------------------------------- +bool RestDocumentService::requiresAuthentication( http::verb verb, const std::list& path ) const +{ + return false; +} + +bool RestDocumentService::requiresSession( http::verb verb, const std::list& path ) const +{ + return true; +} + +class PathCreator : public Inspector +{ +public: + PathCreator() + : m_pathStack( { "/documents" } ) + { + m_serializer.setSerializationType( Serializer::SerializationType::PATH ); + } + + const std::map& pathSchemas() const { return m_pathSchemas; } + + void visitObject( const ObjectHandle* object ) override + { + if ( auto doc = dynamic_cast( object ); doc ) + { + m_pathStack.push_back( doc->id() ); + auto schema = nlohmann::json::object(); + m_serializer.writeObjectToJson( object, schema ); + auto path = StringTools::join( m_pathStack.begin(), m_pathStack.end(), "/" ); + m_pathSchemas[path] = schema; + } + } + + void visitField( const FieldHandle* field ) override + { + m_pathStack.push_back( field->keyword() ); + auto schema = nlohmann::json::object(); + + if ( auto scriptability = field->capability(); scriptability ) + { + auto jsonCapability = field->capability(); + CAFFA_ASSERT( jsonCapability ); + if ( scriptability->isReadable() ) + { + auto operationId = field->keyword(); + operationId[0] = std::toupper( operationId[0] ); + operationId = field->ownerObject()->classKeyword() + ".get" + operationId; + + auto getOperation = + nlohmann::json{ { "summary", "Get " + field->keyword() }, { "operationId", operationId } }; + + auto fieldContent = nlohmann::json::object(); + fieldContent["application/json"] = { { "schema", jsonCapability->jsonType() } }; + + std::string description; + if ( auto doc = field->template capability(); doc ) + { + description = doc->documentation(); + } + + auto fieldResponse = nlohmann::json{ { "description", description }, { "content", fieldContent } }; + + getOperation["responses"] = fieldResponse; + schema["get"] = getOperation; + } + if ( scriptability->isWritable() ) + { + auto operationId = field->keyword(); + operationId[0] = std::toupper( operationId[0] ); + operationId = field->ownerObject()->classKeyword() + ".set" + operationId; + + auto setOperation = + nlohmann::json{ { "summary", "Set " + field->keyword() }, { "operationId", operationId } }; + + auto fieldContent = nlohmann::json::object(); + fieldContent["application/json"] = { { "schema", jsonCapability->jsonType() } }; + + std::string description; + if ( auto doc = field->template capability(); doc ) + { + description = doc->documentation(); + } + + auto acceptedOrFailureResponses = + nlohmann::json{ { RestServiceInterface::HTTP_ACCEPTED, + { { "description", "Success" } }, + { "default", RestServiceInterface::plainErrorResponse() } } }; + + setOperation["responses"] = acceptedOrFailureResponses; + setOperation["requestBody"] = nlohmann::json{ { "content", fieldContent } }; + schema["set"] = setOperation; + } + } + + auto path = StringTools::join( m_pathStack.begin(), m_pathStack.end(), "/" ); + m_pathSchemas[path] = schema; + } + + void leaveObject( const ObjectHandle* object ) override + { + if ( auto doc = dynamic_cast( object ); doc ) + { + m_pathStack.pop_back(); + } + } + void leaveField( const FieldHandle* field ) override { m_pathStack.pop_back(); } + +private: + std::list m_pathStack; + JsonSerializer m_serializer; + + std::map m_pathSchemas; +}; + +std::map RestDocumentService::servicePathEntries() const +{ + CAFFA_DEBUG( "Get service path entries" ); + // Create a trial tree with an object for each entry + auto documents = rpc::RestServerApplication::instance()->defaultDocuments(); + JsonSerializer serializer; + serializer.setSerializationType( Serializer::SerializationType::PATH ); + + std::map pathEntries; + + for ( auto document : documents ) + { + auto operationId = document->id(); + operationId[0] = std::toupper( operationId[0] ); + operationId = "get" + operationId; + + auto getOperation = nlohmann::json{ { "summary", "Get " + document->id() }, + { "operationId", operationId }, + { "tags", { "documents" } } }; + + auto objectContent = nlohmann::json::object(); + objectContent["application/json"] = { + { "schema", { { "$ref", "#/components/object_schemas/" + document->classKeyword() } } } }; + auto objectResponse = + nlohmann::json{ { "description", document->classDocumentation() }, { "content", objectContent } }; + + auto getResponses = + nlohmann::json{ { HTTP_OK, objectResponse }, { "default", RestServiceInterface::plainErrorResponse() } }; + getOperation["responses"] = getResponses; + + auto schema = nlohmann::json::object(); + schema["get"] = getOperation; + + pathEntries["/documents/" + document->id()] = schema; + } + + return pathEntries; +} + +std::map RestDocumentService::serviceComponentEntries() const +{ + return {}; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +Document* RestDocumentService::document( const std::string& documentId, const Session* session ) +{ + CAFFA_TRACE( "Got document request for " << documentId ); + + auto document = RestServerApplication::instance()->document( documentId, session ); + if ( document ) + { + CAFFA_TRACE( "Found document with UUID: " << document->uuid() ); + return document.get(); + } + return nullptr; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RestDocumentService::ServiceResponse RestDocumentService::documents( const Session* session, bool skeleton ) +{ + CAFFA_DEBUG( "Got list document request for" ); + + auto documents = RestServerApplication::instance()->documents( session ); + CAFFA_DEBUG( "Found " << documents.size() << " document" ); + + auto jsonResult = nlohmann::json::array(); + for ( auto document : documents ) + { + if ( skeleton ) + { + jsonResult.push_back( createJsonSkeletonFromProjectObject( document.get() ) ); + } + else + { + jsonResult.push_back( createJsonFromProjectObject( document.get() ) ); + } + } + return std::make_tuple( http::status::ok, jsonResult.dump(), nullptr ); +} diff --git a/RestInterface/cafRestDocumentService.h b/RestInterface/cafRestDocumentService.h new file mode 100644 index 00000000..67e98dfd --- /dev/null +++ b/RestInterface/cafRestDocumentService.h @@ -0,0 +1,63 @@ +// ################################################################################################## +// +// Caffa +// Copyright (C) 2023- Kontur AS +// +// GNU Lesser General Public License Usage +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU Lesser General Public License at <> +// for more details. +// +#pragma once + +#include "cafRestServiceInterface.h" + +#include + +#include +#include + +namespace caffa +{ +class Document; +class FieldHandle; +class MethodHandle; +class ObjectAttribute; +class ObjectHandle; +class Session; +} // namespace caffa + +namespace caffa::rpc +{ +/** + * @brief REST-service answering request for explicit paths in the project tree + * + */ +class RestDocumentService : public RestServiceInterface +{ +public: + ServiceResponse perform( http::verb verb, + std::list path, + const nlohmann::json& queryParams, + const nlohmann::json& body ) override; + + bool requiresAuthentication( http::verb verb, const std::list& path ) const override; + bool requiresSession( http::verb verb, const std::list& path ) const override; + + std::map servicePathEntries() const override; + std::map serviceComponentEntries() const override; + +private: + static caffa::Document* document( const std::string& documentId, const caffa::Session* session ); + static ServiceResponse documents( const caffa::Session* session, bool skeleton ); +}; + +} // namespace caffa::rpc diff --git a/RestInterface/cafRestObjectService.cpp b/RestInterface/cafRestObjectService.cpp index 57e88256..6bca3daa 100644 --- a/RestInterface/cafRestObjectService.cpp +++ b/RestInterface/cafRestObjectService.cpp @@ -32,6 +32,7 @@ #include "cafObject.h" #include "cafObjectCollector.h" #include "cafRestServerApplication.h" +#include "cafRpcClientPassByRefObjectFactory.h" #include "cafRpcObjectConversion.h" #include @@ -40,237 +41,299 @@ using namespace caffa::rpc; -RestObjectService::ServiceResponse RestObjectService::perform( http::verb verb, - const std::list& path, - const nlohmann::json& arguments, - const nlohmann::json& metaData ) +RestObjectService::ServiceResponse RestObjectService::perform( http::verb verb, + std::list path, + const nlohmann::json& queryParams, + const nlohmann::json& body ) { - std::string session_uuid = ""; - if ( arguments.contains( "session_uuid" ) ) + caffa::SessionMaintainer session; + + CAFFA_ASSERT( !path.empty() ); + + if ( queryParams.contains( "session_uuid" ) ) { - session_uuid = arguments["session_uuid"].get(); + auto session_uuid = queryParams["session_uuid"].get(); + session = RestServerApplication::instance()->getExistingSession( session_uuid ); } - else if ( metaData.contains( "session_uuid" ) ) + + if ( path.empty() ) { - session_uuid = metaData["session_uuid"].get(); + return std::make_tuple( http::status::bad_request, "Object uuid not specified", nullptr ); } - if ( session_uuid.empty() && RestServerApplication::instance()->requiresValidSession() ) + auto uuid = path.front(); + path.pop_front(); + + CAFFA_TRACE( "Trying to look for uuid '" << uuid << "'" ); + + auto object = findObject( uuid, session.get() ); + if ( !object ) { - CAFFA_ERROR( "No session uuid provided" ); - return std::make_tuple( http::status::forbidden, "No session provided", nullptr ); + return std::make_tuple( http::status::not_found, "Object " + uuid + " not found", nullptr ); } - auto session = RestServerApplication::instance()->getExistingSession( session_uuid ); - if ( !session && RestServerApplication::instance()->requiresValidSession() ) + CAFFA_ASSERT( object ); + if ( path.empty() ) { - return std::make_tuple( http::status::forbidden, "Session " + session_uuid + " is not valid!", nullptr ); + bool skeleton = queryParams.contains( "skeleton" ) && body["skeleton"].get(); + if ( skeleton ) + { + return std::make_tuple( http::status::ok, createJsonSkeletonFromProjectObject( object ).dump(), nullptr ); + } + else + { + return std::make_tuple( http::status::ok, createJsonFromProjectObject( object ).dump(), nullptr ); + } } - else if ( RestServerApplication::instance()->requiresValidSession() && session->isExpired() ) + + auto fieldOrMethod = path.front(); + path.pop_front(); + + if ( path.empty() ) { - return std::make_tuple( http::status::forbidden, "Session '" + session_uuid + "' is expired", nullptr ); + return std::make_tuple( http::status::bad_request, "No field or method keyword specified", nullptr ); } - bool skeleton = metaData.contains( "skeleton" ) && metaData["skeleton"].get(); - bool replace = metaData.contains( "replace" ) && metaData["replace"].get(); + auto keyword = path.front(); + path.pop_front(); - if ( path.empty() ) + if ( fieldOrMethod == "fields" ) { - return documents( session.get(), skeleton ); + return performFieldOperation( *session, verb, object, keyword, queryParams, body ); + } + else if ( fieldOrMethod == "methods" ) + { + return performMethodOperation( *session, verb, object, keyword, queryParams, body ); } else { - caffa::ObjectHandle* object; - - auto documentId = path.front(); - auto reducedPath = path; - reducedPath.pop_front(); - - CAFFA_TRACE( "Trying to look for document id '" << documentId << "'" ); - if ( documentId == "uuid" && !reducedPath.empty() ) - { - auto uuid = reducedPath.front(); - CAFFA_TRACE( "Using uuid: " << uuid ); - object = findObject( reducedPath.front(), session.get() ); - reducedPath.pop_front(); - if ( !object ) - { - return std::make_tuple( http::status::not_found, "Object " + uuid + " not found", nullptr ); - } - } - else - { - object = document( documentId, session.get() ); - if ( !object ) - { - return std::make_tuple( http::status::not_found, "Document not found '" + documentId + "'", nullptr ); - } - } - - CAFFA_ASSERT( object ); - if ( reducedPath.empty() ) - { - if ( skeleton ) - { - return std::make_tuple( http::status::ok, createJsonSkeletonFromProjectObject( object ).dump(), nullptr ); - } - else - { - return std::make_tuple( http::status::ok, createJsonFromProjectObject( object ).dump(), nullptr ); - } - } - return perform( *session, verb, object, reducedPath, arguments, skeleton, replace ); + return std::make_tuple( http::status::bad_request, + "No such target " + fieldOrMethod + " available for Objects", + nullptr ); } } //-------------------------------------------------------------------------------------------------- /// The object service uses session uuids to decide if it accepts the request or not //-------------------------------------------------------------------------------------------------- -bool RestObjectService::requiresAuthentication( const std::list& path ) const +bool RestObjectService::requiresAuthentication( http::verb verb, const std::list& path ) const { return false; } -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -caffa::Document* RestObjectService::document( const std::string& documentId, const caffa::Session* session ) +bool RestObjectService::requiresSession( http::verb verb, const std::list& path ) const { - CAFFA_TRACE( "Got document request for " << documentId ); - - auto document = RestServerApplication::instance()->document( documentId, session ); - if ( document ) - { - CAFFA_TRACE( "Found document with UUID: " << document->uuid() ); - return document.get(); - } - return nullptr; + return true; } -caffa::ObjectHandle* RestObjectService::findObject( const std::string& uuid, const caffa::Session* session ) +std::map RestObjectService::servicePathEntries() const { - return findCafObjectFromUuid( session, uuid ); -} + auto emptyResponseContent = nlohmann::json{ { "description", "Success" } }; -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -RestObjectService::ServiceResponse RestObjectService::documents( const caffa::Session* session, bool skeleton ) -{ - CAFFA_DEBUG( "Got list document request for" ); + auto acceptedOrFailureResponses = nlohmann::json{ { HTTP_ACCEPTED, emptyResponseContent }, + { "default", RestServiceInterface::plainErrorResponse() } }; - auto documents = RestServerApplication::instance()->documents( session ); - CAFFA_DEBUG( "Found " << documents.size() << " document" ); + auto uuidParameter = nlohmann::json{ { "name", "uuid" }, + { "in", "path" }, + { "required", true }, + { "description", "The object UUID of the object to get" }, + { "schema", { { "type", "string" } } } }; - auto jsonResult = nlohmann::json::array(); - for ( auto document : documents ) + auto objectContent = nlohmann::json::object(); + auto classArray = nlohmann::json::array(); + for ( auto classKeyword : DefaultObjectFactory::instance()->classes() ) { - if ( skeleton ) + auto schemaRef = nlohmann::json{ { "$ref", "#/components/object_schemas/" + classKeyword } }; + classArray.push_back( schemaRef ); + } + auto classSchema = nlohmann::json{ { "oneOf", classArray }, { "discriminator", "keyword" } }; + objectContent["application/json"] = { { "schema", classSchema } }; + auto objectResponse = nlohmann::json{ { "description", "Specific object" }, { "content", objectContent } }; + + auto getResponses = + nlohmann::json{ { HTTP_OK, objectResponse }, { "default", RestServiceInterface::plainErrorResponse() } }; + + auto object = nlohmann::json::object(); + object["get"] = + createOperation( "getObject", "Get a particular object", uuidParameter, getResponses, nullptr, { "objects" } ); + object["delete"] = createOperation( "deleteObject", + "Destroy a particular object", + uuidParameter, + acceptedOrFailureResponses, + nullptr, + { "objects" } ); + + auto field = nlohmann::json::object(); + { + auto fieldKeywordParameter = nlohmann::json{ { "name", "fieldKeyword" }, + { "in", "path" }, + { "required", true }, + { "description", "The field keyword" }, + { "schema", { { "type", "string" } } } }; + + auto indexParameter = nlohmann::json{ { "name", "index" }, + { "in", "query" }, + { "required", false }, + { "default", -1 }, + { "description", "The index of the child object field." }, + { "schema", { { "type", "integer" } } } }; + auto skeletonParameter = + nlohmann::json{ { "name", "skeleton" }, + { "in", "query" }, + { "required", false }, + { "default", false }, + { "description", "Whether to only retrieve the structure and not field values" }, + { "schema", { { "type", "boolean" } } } }; + + auto oneOf = nlohmann::json::array(); + for ( auto dataType : caffa::rpc::ClientPassByRefObjectFactory::instance()->supportedDataTypes() ) { - jsonResult.push_back( createJsonSkeletonFromProjectObject( document.get() ) ); + oneOf.push_back( nlohmann::json::parse( dataType ) ); } - else + for ( auto classEntry : classArray ) { - jsonResult.push_back( createJsonFromProjectObject( document.get() ) ); + oneOf.push_back( classEntry ); + auto array = nlohmann::json{ { "type", "array" }, { "items", classEntry } }; + oneOf.push_back( array ); } + + auto fieldValue = nlohmann::json{ { "application/json", { { "schema", { { "oneOf", oneOf } } } } } }; + auto fieldContent = nlohmann::json{ { "description", "JSON content representing a valid Caffa data type" }, + { "content", fieldValue } }; + + auto fieldParameters = nlohmann::json::array( { uuidParameter, fieldKeywordParameter, indexParameter } ); + auto fieldResponses = + nlohmann::json{ { HTTP_OK, fieldContent }, { "default", RestServiceInterface::plainErrorResponse() } }; + + field["put"] = createOperation( "replaceFieldValue", + "Replace a particular field value", + fieldParameters, + acceptedOrFailureResponses, + fieldContent, + { "fields" } ); + field["post"] = createOperation( "insertFieldValue", + "Insert a particular field value", + fieldParameters, + acceptedOrFailureResponses, + fieldContent, + { "fields" } ); + field["delete"] = createOperation( "deleteFieldValue", + "Delete a value from a field. For array fields.", + fieldParameters, + acceptedOrFailureResponses, + nullptr, + { "fields" } ); + + fieldParameters.push_back( skeletonParameter ); + field["get"] = createOperation( "getFieldValue", + "Get a particular field value", + fieldParameters, + fieldResponses, + nullptr, + { "fields" } ); + } + + auto method = nlohmann::json::object(); + { + auto methodKeywordParameter = nlohmann::json{ { "name", "methodKeyword" }, + { "in", "path" }, + { "required", true }, + { "description", "The method keyword" }, + { "schema", { { "type", "string" } } } }; + + auto methodBody = nlohmann::json{ { "application/json", { { "schema", nlohmann::json::object() } } } }; + + auto jsonContentObject = nlohmann::json{ { "description", "JSON content representing a valid Caffa data type" }, + { "content", methodBody } }; + + auto methodParameters = nlohmann::json::array( { uuidParameter, methodKeywordParameter } ); + auto methodResponses = + nlohmann::json{ { HTTP_OK, jsonContentObject }, { "default", RestServiceInterface::plainErrorResponse() } }; + + method["post"] = createOperation( "executeMethod", + "Execute an Object Method", + methodParameters, + methodResponses, + jsonContentObject, + { "methods" } ); } - return std::make_tuple( http::status::ok, jsonResult.dump(), nullptr ); + + return { { "/objects/{uuid}", object }, + { "/objects/{uuid}/fields/{fieldKeyword}", field }, + { "/objects/{uuid}/methods/{methodKeyword}", method } }; } -RestObjectService::ServiceResponse RestObjectService::perform( std::shared_ptr session, - http::verb verb, - caffa::ObjectHandle* object, - const std::list& path, - const nlohmann::json& arguments, - bool skeleton, - bool replace ) +std::map RestObjectService::serviceComponentEntries() const { - auto [fieldOrMethod, index] = findFieldOrMethod( object, path ); + auto factory = DefaultObjectFactory::instance(); + + auto schemas = nlohmann::json::object(); - if ( !fieldOrMethod ) + for ( auto className : factory->classes() ) { - return std::make_tuple( http::status::not_found, "Failed to find field or method", nullptr ); + auto object = factory->create( className ); + schemas[className] = createJsonSchemaFromProjectObject( object.get() ); } + return { { "object_schemas", schemas } }; +} - auto field = dynamic_cast( fieldOrMethod ); - if ( field ) +caffa::ObjectHandle* RestObjectService::findObject( const std::string& uuid, const caffa::Session* session ) +{ + return findCafObjectFromUuid( session, uuid ); +} + +RestObjectService::ServiceResponse RestObjectService::performFieldOperation( std::shared_ptr session, + http::verb verb, + caffa::ObjectHandle* object, + const std::string& keyword, + const nlohmann::json& queryParams, + const nlohmann::json& body ) +{ + if ( auto field = object->findField( keyword ); field ) { + CAFFA_DEBUG( "Found field: " << field->keyword() ); + + int index = queryParams.contains( "index" ) ? queryParams["index"].get() : -1; + bool skeleton = queryParams.contains( "skeleton" ) && queryParams["skeleton"].get(); + if ( verb == http::verb::get ) { return getFieldValue( field, index, skeleton ); } else if ( verb == http::verb::put ) { - return putFieldValue( field, index, arguments, replace ); + return replaceFieldValue( field, index, body ); } + else if ( verb == http::verb::post ) + { + return insertFieldValue( field, index, body ); + } + else if ( verb == http::verb::delete_ ) { - return deleteChildObject( field, index, arguments ); + return deleteFieldValue( field, index ); } return std::make_tuple( http::status::bad_request, "Verb not implemented", nullptr ); } - - auto method = dynamic_cast( fieldOrMethod ); - CAFFA_ASSERT( method ); - - CAFFA_TRACE( "Found method: " << method->keyword() ); - - auto result = method->execute( session, arguments.dump() ); - return std::make_tuple( http::status::ok, result, nullptr ); + return std::make_tuple( http::status::not_found, "No field named " + keyword + " found", nullptr ); } -std::pair RestObjectService::findFieldOrMethod( caffa::ObjectHandle* object, - const std::list& path ) +RestObjectService::ServiceResponse RestObjectService::performMethodOperation( std::shared_ptr session, + http::verb verb, + caffa::ObjectHandle* object, + const std::string& keyword, + const nlohmann::json& queryParams, + const nlohmann::json& body ) { - auto pathComponent = path.front(); - - std::regex arrayRgx( "(.+)\\[(\\d+)\\]" ); - std::smatch matches; - - std::string fieldOrMethodName = pathComponent; - int64_t index = -1; - if ( std::regex_match( pathComponent, matches, arrayRgx ) ) - { - if ( matches.size() == 3 ) - { - fieldOrMethodName = matches[1]; - auto optindex = caffa::StringTools::toInt64( matches[2] ); - if ( optindex ) index = *optindex; - } - } - - CAFFA_TRACE( "Looking for field '" << fieldOrMethodName << "', index: " << index ); - if ( auto currentLevelField = object->findField( fieldOrMethodName ); currentLevelField ) + if ( auto method = object->findMethod( keyword ); method ) { - auto reducedPath = path; - reducedPath.pop_front(); - if ( !reducedPath.empty() ) - { - auto childField = dynamic_cast( currentLevelField ); - if ( childField ) - { - auto childObjects = childField->childObjects(); - if ( index == -1 ) - { - index = 0; - } + CAFFA_TRACE( "Found method: " << method->keyword() ); - CAFFA_TRACE( "Looking for index " << index << " in an array of size " << childObjects.size() ); - if ( index >= static_cast( childObjects.size() ) ) - { - return std::make_pair( nullptr, -1 ); - } - return findFieldOrMethod( childObjects[index].get(), reducedPath ); - } - return std::make_pair( nullptr, -1 ); - } - return std::make_pair( currentLevelField, index ); + auto result = method->execute( session, body.dump() ); + return std::make_tuple( http::status::ok, result, nullptr ); } - else if ( auto currentLevelMethod = object->findMethod( fieldOrMethodName ); currentLevelMethod ) - { - return std::make_pair( currentLevelMethod, -1 ); - } - return std::make_pair( nullptr, -1 ); + + return std::make_tuple( http::status::not_found, "No method named " + keyword + " found", nullptr ); } RestObjectService::ServiceResponse @@ -328,7 +391,7 @@ RestObjectService::ServiceResponse } RestObjectService::ServiceResponse - RestObjectService::putFieldValue( caffa::FieldHandle* field, int64_t index, const nlohmann::json& arguments, bool replace ) + RestObjectService::replaceFieldValue( caffa::FieldHandle* field, int64_t index, const nlohmann::json& body ) { auto scriptability = field->capability(); if ( !scriptability || !scriptability->isWritable() ) @@ -355,38 +418,28 @@ RestObjectService::ServiceResponse "Index does not make sense for a simple Child Field", nullptr ); } - ioCapability->readFromJson( arguments, serializer ); - return std::make_tuple( http::status::ok, "", nullptr ); + ioCapability->readFromJson( body, serializer ); + return std::make_tuple( http::status::accepted, "", nullptr ); } auto childArrayField = dynamic_cast( field ); if ( childArrayField ) { - CAFFA_DEBUG( "Inserting into child array field with index " << index ); - if ( index >= 0 ) + CAFFA_DEBUG( "Replacing child array object at index " << index ); + + auto childObjects = childArrayField->childObjects(); + if ( index >= 0 && static_cast( index ) < childObjects.size() ) { auto childObjects = childArrayField->childObjects(); - if ( index >= static_cast( childObjects.size() ) ) - { - auto object = serializer.createObjectFromString( arguments.dump() ); - childArrayField->push_back_obj( object ); - return std::make_tuple( http::status::ok, "", nullptr ); - } - else if ( !replace ) - { - auto object = serializer.createObjectFromString( arguments.dump() ); - childArrayField->insertAt( index, object ); - return std::make_tuple( http::status::ok, "", nullptr ); - } - else - { - serializer.readObjectFromString( childObjects[index].get(), arguments.dump() ); - return std::make_tuple( http::status::ok, "", nullptr ); - } + serializer.readObjectFromString( childObjects[index].get(), body.dump() ); + return std::make_tuple( http::status::accepted, "", nullptr ); } + return std::make_tuple( http::status::bad_request, + "Index out of bounds for array field replace item request", + nullptr ); } - ioCapability->readFromJson( arguments, serializer ); - return std::make_tuple( http::status::ok, "", nullptr ); + ioCapability->readFromJson( body, serializer ); + return std::make_tuple( http::status::accepted, "", nullptr ); } catch ( const std::exception& e ) { @@ -396,7 +449,55 @@ RestObjectService::ServiceResponse } RestObjectService::ServiceResponse - RestObjectService::deleteChildObject( caffa::FieldHandle* field, int64_t index, const nlohmann::json& arguments ) + RestObjectService::insertFieldValue( caffa::FieldHandle* field, int64_t index, const nlohmann::json& body ) +{ + auto scriptability = field->capability(); + if ( !scriptability || !scriptability->isWritable() ) + return std::make_tuple( http::status::forbidden, "Field " + field->keyword() + " is not remote writable", nullptr ); + + auto ioCapability = field->capability(); + if ( !ioCapability ) + { + return std::make_tuple( http::status::forbidden, + "Field " + field->keyword() + " found, but it has no JSON capability", + nullptr ); + } + + JsonSerializer serializer; + + try + { + auto childArrayField = dynamic_cast( field ); + if ( childArrayField ) + { + CAFFA_INFO( "Inserting into child array field with index " << index ); + auto existingSize = childArrayField->size(); + if ( index >= 0 && static_cast( index ) < existingSize ) + { + auto object = serializer.createObjectFromString( body.dump() ); + childArrayField->insertAt( index, object ); + return std::make_tuple( http::status::accepted, "", nullptr ); + } + else + { + auto object = serializer.createObjectFromString( body.dump() ); + childArrayField->push_back_obj( object ); + return std::make_tuple( http::status::accepted, "", nullptr ); + } + } + else + { + return std::make_tuple( http::status::bad_request, "Insert only makes sense for a Child Array Fields", nullptr ); + } + } + catch ( const std::exception& e ) + { + CAFFA_ERROR( "Failed to insert field value for '" << field->keyword() << "' with error: '" << e.what() << "'" ); + return std::make_tuple( http::status::internal_server_error, e.what(), nullptr ); + } +} + +RestObjectService::ServiceResponse RestObjectService::deleteFieldValue( caffa::FieldHandle* field, int64_t index ) { auto scriptability = field->capability(); if ( !scriptability || !scriptability->isWritable() ) @@ -428,7 +529,7 @@ RestObjectService::ServiceResponse else { childField->clear(); - return std::make_tuple( http::status::ok, "", nullptr ); + return std::make_tuple( http::status::accepted, "", nullptr ); } } @@ -443,7 +544,7 @@ RestObjectService::ServiceResponse { childArrayField->clear(); } - return std::make_tuple( http::status::ok, "", nullptr ); + return std::make_tuple( http::status::accepted, "", nullptr ); } return std::make_tuple( http::status::bad_request, "Can not delete from a non-child field", nullptr ); } diff --git a/RestInterface/cafRestObjectService.h b/RestInterface/cafRestObjectService.h index 4ccaa44c..083e6e5a 100644 --- a/RestInterface/cafRestObjectService.h +++ b/RestInterface/cafRestObjectService.h @@ -37,43 +37,44 @@ class Session; namespace caffa::rpc { -//================================================================================================== -// -// REST-service answering request searching for Objects in property tree -// -//================================================================================================== +/** + * @brief REST-service answering request searching for arbitrary Objects in project tree + * + */ class RestObjectService : public RestServiceInterface { public: - ServiceResponse perform( http::verb verb, - const std::list& path, - const nlohmann::json& arguments, - const nlohmann::json& metaData ) override; + ServiceResponse perform( http::verb verb, + std::list path, + const nlohmann::json& queryParams, + const nlohmann::json& body ) override; - bool requiresAuthentication( const std::list& path ) const override; + bool requiresAuthentication( http::verb verb, const std::list& path ) const override; + bool requiresSession( http::verb verb, const std::list& path ) const override; + + std::map servicePathEntries() const override; + std::map serviceComponentEntries() const override; private: - static caffa::Document* document( const std::string& documentId, const caffa::Session* session ); static caffa::ObjectHandle* findObject( const std::string& uuid, const caffa::Session* session ); - static ServiceResponse documents( const caffa::Session* session, bool skeleton ); - - static ServiceResponse perform( std::shared_ptr session, - http::verb verb, - caffa::ObjectHandle* object, - const std::list& path, - const nlohmann::json& arguments, - bool skeleton, - bool replace ); - static std::pair findFieldOrMethod( caffa::ObjectHandle* object, - const std::list& path ); + static ServiceResponse performFieldOperation( std::shared_ptr session, + http::verb verb, + caffa::ObjectHandle* object, + const std::string& keyword, + const nlohmann::json& queryParams, + const nlohmann::json& body ); + static ServiceResponse performMethodOperation( std::shared_ptr session, + http::verb verb, + caffa::ObjectHandle* object, + const std::string& keyword, + const nlohmann::json& queryParams, + const nlohmann::json& body ); static ServiceResponse getFieldValue( const caffa::FieldHandle* fieldHandle, int64_t index, bool skeleton ); - static ServiceResponse putFieldValue( caffa::FieldHandle* fieldHandle, - int64_t index, - const nlohmann::json& arguments, - bool replace = false ); - static ServiceResponse deleteChildObject( caffa::FieldHandle* field, int64_t index, const nlohmann::json& arguments ); + static ServiceResponse replaceFieldValue( caffa::FieldHandle* fieldHandle, int64_t index, const nlohmann::json& body ); + static ServiceResponse insertFieldValue( caffa::FieldHandle* fieldHandle, int64_t index, const nlohmann::json& body ); + static ServiceResponse deleteFieldValue( caffa::FieldHandle* field, int64_t index ); }; } // namespace caffa::rpc diff --git a/RestInterface/cafRestOpenApiService.cpp b/RestInterface/cafRestOpenApiService.cpp new file mode 100644 index 00000000..d32f19cf --- /dev/null +++ b/RestInterface/cafRestOpenApiService.cpp @@ -0,0 +1,110 @@ +// ################################################################################################## +// +// Caffa +// Copyright (C) 2024- Kontur AS +// +// GNU Lesser General Public License Usage +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU Lesser General Public License at <> +// for more details. +// +#include "cafRestOpenApiService.h" + +#include "cafSession.h" + +#include "cafDefaultObjectFactory.h" +#include "cafDocument.h" +#include "cafFieldJsonCapability.h" +#include "cafFieldScriptingCapability.h" +#include "cafJsonSerializer.h" +#include "cafRestServerApplication.h" +#include "cafRpcObjectConversion.h" + +#include +#include +#include + +using namespace caffa::rpc; + +RestServiceInterface::ServiceResponse RestOpenApiService::perform( http::verb verb, + std::list path, + const nlohmann::json& queryParams, + const nlohmann::json& body ) +{ + CAFFA_DEBUG( "Perfoming OpenAPI request" ); + if ( verb != http::verb::get ) + { + return std::make_tuple( http::status::bad_request, "Only GET requests are allowed for api queries", nullptr ); + } + + return std::make_tuple( http::status::ok, getOpenApiV31Schema().dump(), nullptr ); +} + +bool RestOpenApiService::requiresAuthentication( http::verb verb, const std::list& path ) const +{ + return false; +} + +bool RestOpenApiService::requiresSession( http::verb verb, const std::list& path ) const +{ + return false; +} + +nlohmann::json RestOpenApiService::getOpenApiV31Schema() const +{ + auto root = nlohmann::json::object(); + root["openapi"] = "3.1.0"; + + auto info = nlohmann::json::object(); + info["version"] = RestServerApplication::instance()->appInfo().version_string(); + info["title"] = RestServerApplication::instance()->appInfo().name; + info["description"] = RestServerApplication::instance()->appInfo().description; + + auto contact = nlohmann::json::object(); + contact["email"] = RestServerApplication::instance()->appInfo().contactEmail; + info["contact"] = contact; + root["info"] = info; + + auto components = nlohmann::json::object(); + auto paths = nlohmann::json::object(); + + for ( auto [key, jsonSchema] : RestServiceInterface::basicServiceSchemas() ) + { + components[key] = jsonSchema; + } + + for ( auto key : RestServiceFactory::instance()->allKeys() ) + { + auto service = std::shared_ptr( RestServiceFactory::instance()->create( key ) ); + for ( auto [key, jsonObject] : service->serviceComponentEntries() ) + { + components[key] = jsonObject; + } + for ( auto [key, jsonObject] : service->servicePathEntries() ) + { + paths[key] = jsonObject; + } + } + + root["components"] = components; + root["paths"] = paths; + + return root; +} + +std::map RestOpenApiService::servicePathEntries() const +{ + return {}; +} +std::map RestOpenApiService::serviceComponentEntries() const +{ + return {}; +} \ No newline at end of file diff --git a/RestInterface/cafRestOpenApiService.h b/RestInterface/cafRestOpenApiService.h new file mode 100644 index 00000000..eb581c7b --- /dev/null +++ b/RestInterface/cafRestOpenApiService.h @@ -0,0 +1,65 @@ +// ################################################################################################## +// +// Caffa +// Copyright (C) 2023- Kontur AS +// +// GNU Lesser General Public License Usage +// This library is free software; you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU Lesser General Public License at <> +// for more details. +// +#pragma once + +#include "cafRestServiceInterface.h" + +#include + +#include +#include +#include +#include +#include + +namespace caffa +{ +class Document; +class FieldHandle; +class MethodHandle; +class ObjectAttribute; +class ObjectHandle; +class Session; +} // namespace caffa + +namespace caffa::rpc +{ +/** + * @brief Rest-service producing an OpenAPI v3.1 schema + * + */ +class RestOpenApiService : public RestServiceInterface +{ +public: + ServiceResponse perform( http::verb verb, + std::list path, + const nlohmann::json& queryParams, + const nlohmann::json& body ) override; + + bool requiresAuthentication( http::verb verb, const std::list& path ) const override; + bool requiresSession( http::verb verb, const std::list& path ) const override; + + std::map servicePathEntries() const override; + std::map serviceComponentEntries() const override; + +private: + nlohmann::json getOpenApiV31Schema() const; +}; + +} // namespace caffa::rpc diff --git a/RestInterface/cafRestSchemaService.cpp b/RestInterface/cafRestSchemaService.cpp index 73c150bc..d568df0d 100644 --- a/RestInterface/cafRestSchemaService.cpp +++ b/RestInterface/cafRestSchemaService.cpp @@ -21,6 +21,7 @@ #include "cafSession.h" #include "cafDefaultObjectFactory.h" +#include "cafDocument.h" #include "cafFieldJsonCapability.h" #include "cafFieldScriptingCapability.h" #include "cafJsonSerializer.h" @@ -35,38 +36,14 @@ using namespace caffa::rpc; RestSchemaService::ServiceResponse RestSchemaService::perform( http::verb verb, const std::list& path, - const nlohmann::json& arguments, - const nlohmann::json& metaData ) + const nlohmann::json& queryParams, + const nlohmann::json& body ) { if ( verb != http::verb::get ) { return std::make_tuple( http::status::bad_request, "Only GET requests are allowed for schema queries", nullptr ); } - std::string session_uuid = ""; - if ( arguments.contains( "session_uuid" ) ) - { - session_uuid = arguments["session_uuid"].get(); - } - else if ( metaData.contains( "session_uuid" ) ) - { - session_uuid = metaData["session_uuid"].get(); - } - - if ( session_uuid.empty() ) - { - CAFFA_WARNING( "No session uuid provided" ); - } - auto session = RestServerApplication::instance()->getExistingSession( session_uuid ); - - if ( RestServerApplication::instance()->requiresValidSession() && ( !session || session->isExpired() ) ) - { - if ( RestServiceInterface::refuseDueToTimeLimiter() ) - { - return std::make_tuple( http::status::too_many_requests, "Too many unauthenticated equests", nullptr ); - } - } - if ( path.empty() ) { return getAllSchemas(); @@ -89,33 +66,39 @@ RestSchemaService::ServiceResponse RestSchemaService::perform( http::verb return getFieldSchema( object.get(), reducedPath.front() ); } -bool RestSchemaService::requiresAuthentication( const std::list& path ) const +bool RestSchemaService::requiresAuthentication( http::verb verb, const std::list& path ) const { return false; } -RestSchemaService::ServiceResponse RestSchemaService::getAllSchemas() +bool RestSchemaService::requiresSession( http::verb verb, const std::list& path ) const { - auto factory = DefaultObjectFactory::instance(); + return false; +} + +std::map RestSchemaService::servicePathEntries() const +{ + return {}; +} + +std::map RestSchemaService::serviceComponentEntries() const +{ + return {}; +} - auto root = nlohmann::json::object(); - root["$id"] = "/schemas"; - root["type"] = "object"; +nlohmann::json RestSchemaService::getJsonForAllSchemas() +{ + auto factory = DefaultObjectFactory::instance(); - auto oneOf = nlohmann::json::array(); auto schemas = nlohmann::json::object(); for ( auto className : factory->classes() ) { - auto object = factory->create( className ); - - oneOf.push_back( { { "$ref", "#/schemas/" + className } } ); + auto object = factory->create( className ); schemas[className] = createJsonSchemaFromProjectObject( object.get() ); } - root["oneOf"] = oneOf; - root["schemas"] = schemas; - return std::make_tuple( http::status::ok, root.dump(), nullptr ); + return schemas; } RestSchemaService::ServiceResponse RestSchemaService::getFieldSchema( const caffa::ObjectHandle* object, @@ -143,3 +126,8 @@ RestSchemaService::ServiceResponse RestSchemaService::getFieldSchema( const caff ioCapability->writeToJson( json, serializer ); return std::make_tuple( http::status::ok, json.dump(), nullptr ); } + +RestSchemaService::ServiceResponse RestSchemaService::getAllSchemas() +{ + return std::make_tuple( http::status::ok, getJsonForAllSchemas().dump(), nullptr ); +} diff --git a/RestInterface/cafRestSchemaService.h b/RestInterface/cafRestSchemaService.h index c977bf5f..99ca0989 100644 --- a/RestInterface/cafRestSchemaService.h +++ b/RestInterface/cafRestSchemaService.h @@ -50,13 +50,19 @@ class RestSchemaService : public RestServiceInterface public: ServiceResponse perform( http::verb verb, const std::list& path, - const nlohmann::json& arguments, - const nlohmann::json& metaData ) override; + const nlohmann::json& queryParams, + const nlohmann::json& body ) override; - bool requiresAuthentication( const std::list& path ) const override; + bool requiresAuthentication( http::verb verb, const std::list& path ) const override; + bool requiresSession( http::verb verb, const std::list& path ) const override; - static ServiceResponse getFieldSchema( const caffa::ObjectHandle* object, const std::string& fieldName ); + static nlohmann::json plainErrorResponse(); + + std::map servicePathEntries() const override; + std::map serviceComponentEntries() const override; + static nlohmann::json getJsonForAllSchemas(); + static ServiceResponse getFieldSchema( const caffa::ObjectHandle* object, const std::string& fieldName ); static ServiceResponse getAllSchemas(); }; diff --git a/RestInterface/cafRestServerApplication.cpp b/RestInterface/cafRestServerApplication.cpp index 424b7552..f0432a8e 100644 --- a/RestInterface/cafRestServerApplication.cpp +++ b/RestInterface/cafRestServerApplication.cpp @@ -20,8 +20,9 @@ #include "cafRestAppService.h" #include "cafRestAuthenticator.h" +#include "cafRestDocumentService.h" #include "cafRestObjectService.h" -#include "cafRestSchemaService.h" +#include "cafRestOpenApiService.h" #include "cafRestServer.h" #include "cafRestServiceInterface.h" #include "cafRestSessionService.h" @@ -43,9 +44,10 @@ RestServerApplication::RestServerApplication( unsigned short , m_ioContext( threads ) { caffa::rpc::RestServiceFactory::instance()->registerCreator( "app" ); - caffa::rpc::RestServiceFactory::instance()->registerCreator( "object" ); - caffa::rpc::RestServiceFactory::instance()->registerCreator( "schemas" ); - caffa::rpc::RestServiceFactory::instance()->registerCreator( "session" ); + caffa::rpc::RestServiceFactory::instance()->registerCreator( "objects" ); + caffa::rpc::RestServiceFactory::instance()->registerCreator( "sessions" ); + caffa::rpc::RestServiceFactory::instance()->registerCreator( "openapi" ); + caffa::rpc::RestServiceFactory::instance()->registerCreator( "documents" ); auto cert = authenticator->sslCertificate(); auto key = authenticator->sslKey(); @@ -121,4 +123,4 @@ void RestServerApplication::quit() bool RestServerApplication::running() const { return !m_ioContext.stopped(); -} \ No newline at end of file +} diff --git a/RestInterface/cafRestServiceInterface.cpp b/RestInterface/cafRestServiceInterface.cpp index 4c1bf8cf..1c5b1e09 100644 --- a/RestInterface/cafRestServiceInterface.cpp +++ b/RestInterface/cafRestServiceInterface.cpp @@ -9,6 +9,13 @@ constexpr size_t RATE_LIMITER_MAX_REQUESTS = 20; std::mutex RestServiceInterface::s_requestMutex; std::list RestServiceInterface::s_requestTimes; +const std::string RestServiceInterface::HTTP_OK = std::to_string( static_cast( http::status::ok ) ); +const std::string RestServiceInterface::HTTP_ACCEPTED = std::to_string( static_cast( http::status::accepted ) ); +const std::string RestServiceInterface::HTTP_FORBIDDEN = std::to_string( static_cast( http::status::forbidden ) ); +const std::string RestServiceInterface::HTTP_TOO_MANY_REQUESTS = + std::to_string( static_cast( http::status::too_many_requests ) ); +const std::string RestServiceInterface::HTTP_NOT_FOUND = std::to_string( static_cast( http::status::not_found ) ); + bool RestServiceInterface::refuseDueToTimeLimiter() { std::scoped_lock lock( s_requestMutex ); @@ -33,4 +40,49 @@ bool RestServiceInterface::refuseDueToTimeLimiter() s_requestTimes.push_back( now ); return false; +} + +nlohmann::json RestServiceInterface::plainErrorResponse() +{ + auto errorContent = nlohmann::json::object(); + errorContent["text/plain"] = { { "schema", { { "$ref", "#/components/error_schemas/PlainError" } } } }; + auto errorResponse = nlohmann::json{ { "description", "Error message" }, { "content", errorContent } }; + return errorResponse; +} + +std::map RestServiceInterface::basicServiceSchemas() +{ + auto plainError = nlohmann::json{ { "type", "string" }, { "example", "An example error" } }; + + return { { "error_schemas", { { "PlainError", plainError } } } }; +} + +nlohmann::json RestServiceInterface::createOperation( const std::string& operationId, + const std::string& summary, + const nlohmann::json& parameters, + const nlohmann::json& responses, + const nlohmann::json& requestBody, + const nlohmann::json& tags ) +{ + auto schema = nlohmann::json{ { "operationId", operationId }, { "summary", summary }, { "responses", responses } }; + if ( !parameters.is_null() ) + { + if ( parameters.is_array() ) + { + schema["parameters"] = parameters; + } + else + { + schema["parameters"] = nlohmann::json::array( { parameters } ); + } + } + if ( !requestBody.is_null() ) + { + schema["requestBody"] = requestBody; + } + if ( !tags.is_null() ) + { + schema["tags"] = tags; + } + return schema; } \ No newline at end of file diff --git a/RestInterface/cafRestServiceInterface.h b/RestInterface/cafRestServiceInterface.h index feef8fc6..d93ddac4 100644 --- a/RestInterface/cafRestServiceInterface.h +++ b/RestInterface/cafRestServiceInterface.h @@ -43,21 +43,82 @@ class AbstractRestCallback; class RestServiceInterface { public: + static const std::string HTTP_OK; + static const std::string HTTP_ACCEPTED; + static const std::string HTTP_FORBIDDEN; + static const std::string HTTP_TOO_MANY_REQUESTS; + static const std::string HTTP_NOT_FOUND; + // Callback to be called after sending response. Can be nullptr using CleanupCallback = std::function; using ServiceResponse = std::tuple; virtual ~RestServiceInterface() = default; - virtual ServiceResponse perform( http::verb verb, - const std::list& path, - const nlohmann::json& arguments, - const nlohmann::json& metaData ) = 0; + virtual ServiceResponse perform( http::verb verb, + std::list path, + const nlohmann::json& queryParams, + const nlohmann::json& body ) = 0; + + /** + * @brief Check whether the service requires authentication + * Any service implementing the interface makes its own decision on this. + * Unauthenticated services are still subject to rate limiting + * + * @param verb The verb used to access the service + * @param path The path used to access the service + * @return true if the service requires authentication for this verb and path + * @return false if the service does not require authentication for this verb and path + */ + virtual bool requiresAuthentication( http::verb verb, const std::list& path ) const = 0; + + /** + * @brief Check whether the service requires a valid session + * Any service implementing the interface makes its own decision on this. + * Unauthenticated services are still subject to rate limiting + * + * @param verb The verb used to access the service + * @param path The path used to access the service + * @return true if the service requires a valid session for this verb and path + * @return false if the service does not requires a valid session for this verb and path + */ + virtual bool requiresSession( http::verb verb, const std::list& path ) const = 0; + + /** + * @brief Create a plain text OpenAPI error response + * + * @return nlohmann::json a response object + */ + static nlohmann::json plainErrorResponse(); + + /** + * @brief Get the basic OpenAPI service schemas required for all services + * + * @return std::map + */ + static std::map basicServiceSchemas(); + + /** + * @brief Entries into the OpenAPI paths objects + * + * @return std::map + */ + virtual std::map servicePathEntries() const = 0; - virtual bool requiresAuthentication( const std::list& path ) const = 0; + /** + * @brief Entries into the OpenAPI components objects + * + * @return std::map + */ + virtual std::map serviceComponentEntries() const = 0; -protected: - static bool refuseDueToTimeLimiter(); + static bool refuseDueToTimeLimiter(); + static nlohmann::json createOperation( const std::string& operationId, + const std::string& summary, + const nlohmann::json& parameters, + const nlohmann::json& responses, + const nlohmann::json& requestBody = nullptr, + const nlohmann::json& tags = nullptr ); private: static std::list s_requestTimes; diff --git a/RestInterface/cafRestSession.cpp b/RestInterface/cafRestSession.cpp index b7f524ff..93996a30 100644 --- a/RestInterface/cafRestSession.cpp +++ b/RestInterface/cafRestSession.cpp @@ -27,6 +27,9 @@ // ################################################################################################## #include "cafRestSession.h" +#include "cafRestServerApplication.h" +#include "cafSession.h" + namespace caffa::rpc { @@ -83,12 +86,13 @@ RestServiceInterface::CleanupCallback { http::response res{ status, req.version() }; res.set( http::field::server, BOOST_BEAST_VERSION_STRING ); - if ( status == http::status::ok ) + if ( status == http::status::ok || status == http::status::accepted ) { res.set( http::field::content_type, "application/json" ); } else { + CAFFA_DEBUG( "Sending failure response: " << response ); res.set( http::field::content_type, "text/plain" ); } @@ -96,17 +100,32 @@ RestServiceInterface::CleanupCallback { res.set( http::field::www_authenticate, "Basic realm=\"Restricted Area\"" ); } + res.set( http::field::access_control_allow_origin, "http://localhost:8080" ); + res.insert( boost::beast::http::field::access_control_allow_methods, "GET, POST, OPTIONS, PUT, PATCH, DELETE" ); + res.insert( boost::beast::http::field::access_control_allow_headers, "X-Requested-With,content-type" ); res.keep_alive( req.keep_alive() ); res.body() = std::string( response ); res.prepare_payload(); return res; }; + auto method = req.method(); + CAFFA_DEBUG( "VERB: " << method ); + + auto accessControlMethod = req.find( http::field::access_control_request_method ); + if ( method == http::verb::options && accessControlMethod != req.end() ) + { + CAFFA_DEBUG( "Access control method: " << accessControlMethod->value().data() ); + method = http::string_to_verb( + std::string( accessControlMethod->value().data(), accessControlMethod->value().size() ) ); + } + // Make sure we can handle the method - if ( req.method() != http::verb::post && req.method() != http::verb::delete_ && req.method() != http::verb::patch && - req.method() != http::verb::put && req.method() != http::verb::get && req.method() != http::verb::head ) + if ( method != http::verb::post && method != http::verb::delete_ && method != http::verb::patch && + method != http::verb::put && method != http::verb::get && method != http::verb::head ) { - send( createResponse( http::status::bad_request, "Unknown HTTP-method" ) ); + send( createResponse( http::status::bad_request, + "Unknown HTTP-method " + std::string( http::to_string( method ) ) ) ); return nullptr; } @@ -119,46 +138,62 @@ RestServiceInterface::CleanupCallback std::string target( req.target() ); - CAFFA_TRACE( req.method() << " request for " << target << ", body length: " << req.body().length() ); + CAFFA_DEBUG( "Target: " << target << ", body length: " << req.body().length() ); std::regex paramRegex( "[\?&]" ); auto targetComponents = caffa::StringTools::split>( target, paramRegex ); if ( targetComponents.empty() ) { + CAFFA_WARNING( "Sending malformed request" ); send( createResponse( http::status::bad_request, "Malformed request" ) ); return nullptr; } auto path = targetComponents.front(); - std::vector params; + std::vector queryParams; for ( size_t i = 1; i < targetComponents.size(); ++i ) { - params.push_back( targetComponents[i] ); + queryParams.push_back( targetComponents[i] ); } std::shared_ptr service; auto pathComponents = caffa::StringTools::split>( path, "/", true ); - if ( pathComponents.size() >= 1u ) + CAFFA_DEBUG( "Path component size: " << pathComponents.size() ); + if ( !pathComponents.empty() ) { - auto docOrServiceComponent = pathComponents.front(); - service = findRestService( docOrServiceComponent, services ); + auto serviceComponent = pathComponents.front(); + service = findRestService( serviceComponent, services ); if ( service ) { pathComponents.pop_front(); } } + else + { + service = findRestService( "openapi", services ); + } if ( !service ) { - service = findRestService( "object", services ); + CAFFA_ERROR( "Could not find service " << path ); + send( createResponse( http::status::not_found, "Service not found from path " + path ) ); } CAFFA_ASSERT( service ); - if ( service->requiresAuthentication( pathComponents ) ) + bool requiresAuthentication = service->requiresAuthentication( method, pathComponents ); + bool requiresValidSession = ServerApplication::instance()->requiresValidSession() && service->requiresSession( method, pathComponents ); + + if ( !(requiresAuthentication || requiresValidSession) && RestServiceInterface::refuseDueToTimeLimiter() ) + { + send( createResponse( http::status::too_many_requests, "Too many unauthenticated requests" ) ); + return nullptr; + } + + if ( requiresAuthentication ) { auto authorisation = req[http::field::authorization]; auto trimmed = caffa::StringTools::replace( std::string( authorisation ), "Basic ", "" ); @@ -176,59 +211,81 @@ RestServiceInterface::CleanupCallback } } - nlohmann::json jsonArguments = nlohmann::json::object(); - if ( !req.body().empty() ) - { - try - { - jsonArguments = nlohmann::json::parse( req.body() ); - } - catch ( const nlohmann::detail::parse_error& ) - { - CAFFA_ERROR( "Could not parse arguments \'" << req.body() << "\'" ); - send( createResponse( http::status::bad_request, - std::string( "Could not parse arguments \'" ) + req.body() + "\'" ) ); - return nullptr; - } - } - nlohmann::json jsonMetaData = nlohmann::json::object(); - for ( auto param : params ) + nlohmann::json queryParamsJson = nlohmann::json::object(); + for ( auto param : queryParams ) { auto keyValue = caffa::StringTools::split>( param, "=", true ); if ( keyValue.size() == 2 ) { if ( auto intValue = caffa::StringTools::toInt64( keyValue[1] ); intValue ) { - jsonMetaData[keyValue[0]] = *intValue; + queryParamsJson[keyValue[0]] = *intValue; } else if ( auto doubleValue = caffa::StringTools::toDouble( keyValue[1] ); doubleValue ) { - jsonMetaData[keyValue[0]] = *doubleValue; + queryParamsJson[keyValue[0]] = *doubleValue; } else if ( caffa::StringTools::tolower( keyValue[1] ) == "true" ) { - jsonMetaData[keyValue[0]] = true; + queryParamsJson[keyValue[0]] = true; } else if ( caffa::StringTools::tolower( keyValue[1] ) == "false" ) { - jsonMetaData[keyValue[0]] = false; + queryParamsJson[keyValue[0]] = false; } else { - jsonMetaData[keyValue[0]] = keyValue[1]; + queryParamsJson[keyValue[0]] = keyValue[1]; } } } - CAFFA_TRACE( "Arguments: " << jsonArguments.dump() << ", Meta data: " << jsonMetaData.dump() ); + if ( requiresValidSession ) + { + caffa::SessionMaintainer session; + std::string session_uuid = "NONE"; + if ( queryParamsJson.contains( "session_uuid" ) ) + { + session_uuid = queryParamsJson["session_uuid"].get(); + session = RestServerApplication::instance()->getExistingSession( session_uuid ); + } + + if ( !session ) + { + send( createResponse( http::status::forbidden, "Session '" + session_uuid + "' is not valid" ) ); + return nullptr; + } + else if ( session->isExpired() ) + { + send( createResponse( http::status::forbidden, "Session '" + session_uuid + "' is not valid" ) ); + return nullptr; + } + } + + nlohmann::json bodyJson = nlohmann::json::object(); + if ( !req.body().empty() ) + { + try + { + bodyJson = nlohmann::json::parse( req.body() ); + } + catch ( const nlohmann::detail::parse_error& ) + { + CAFFA_ERROR( "Could not parse arguments \'" << req.body() << "\'" ); + send( createResponse( http::status::bad_request, + std::string( "Could not parse arguments \'" ) + req.body() + "\'" ) ); + return nullptr; + } + } + + CAFFA_DEBUG( "Path: " << path << ", Query Arguments: " << queryParamsJson.dump() << ", Body: " << bodyJson.dump() ); try { - auto [status, message, cleanupCallback] = - service->perform( req.method(), pathComponents, jsonArguments, jsonMetaData ); - if ( status == http::status::ok ) + auto [status, message, cleanupCallback] = service->perform( method, pathComponents, queryParamsJson, bodyJson ); + if ( status == http::status::ok || status == http::status::accepted ) { - CAFFA_TRACE( "Responding with " << status << ": " << message ); + CAFFA_DEBUG( "Responding with " << status << ": " << message ); send( createResponse( status, message ) ); } else @@ -510,4 +567,4 @@ void DetectSession::onDetect( beast::error_code ec, bool result ) } } -} // namespace caffa::rpc \ No newline at end of file +} // namespace caffa::rpc diff --git a/RestInterface/cafRestSession.h b/RestInterface/cafRestSession.h index feefe1fd..ff9484cc 100644 --- a/RestInterface/cafRestSession.h +++ b/RestInterface/cafRestSession.h @@ -173,4 +173,4 @@ class DetectSession : public std::enable_shared_from_this std::shared_ptr m_authenticator; }; -} // namespace caffa::rpc \ No newline at end of file +} // namespace caffa::rpc diff --git a/RestInterface/cafRestSessionService.cpp b/RestInterface/cafRestSessionService.cpp index 0d57f004..dd9e97e8 100644 --- a/RestInterface/cafRestSessionService.cpp +++ b/RestInterface/cafRestSessionService.cpp @@ -26,72 +26,151 @@ using namespace caffa::rpc; -RestSessionService::ServiceResponse RestSessionService::perform( http::verb verb, - const std::list& path, - const nlohmann::json& arguments, - const nlohmann::json& metaData ) +RestSessionService::ServiceResponse RestSessionService::perform( http::verb verb, + std::list path, + const nlohmann::json& queryParams, + const nlohmann::json& body ) { - auto allCallbacks = callbacks(); - if ( path.empty() ) { - auto jsonArray = nlohmann::json::array(); - for ( auto [name, callback] : allCallbacks ) - { - jsonArray.push_back( name ); - } - return std::make_tuple( http::status::ok, jsonArray.dump(), nullptr ); + return performOnAll( verb, queryParams, body ); } - auto name = path.front(); + CAFFA_DEBUG( "Performing session request: " << path.front() ); - auto it = allCallbacks.find( name ); - if ( it != allCallbacks.end() ) - { - return it->second( verb, arguments, metaData ); - } - return std::make_tuple( http::status::not_found, "No such method", nullptr ); + auto uuid = path.front(); + return performOnOne( verb, body, uuid ); } -bool RestSessionService::requiresAuthentication( const std::list& path ) const +bool RestSessionService::requiresAuthentication( http::verb verb, const std::list& path ) const { - return !path.empty() && path.front() == "create"; + // Create requests have no session and requires authentication + bool isCreateRequest = path.empty() && verb == http::verb::post; + return isCreateRequest; } -std::map RestSessionService::callbacks() const +bool RestSessionService::requiresSession( http::verb verb, const std::list& path ) const { - return { { "ready", &RestSessionService::ready }, - { "check", &RestSessionService::check }, - { "change", &RestSessionService::change }, - { "create", &RestSessionService::create }, - { "keepalive", &RestSessionService::keepalive }, - { "destroy", &RestSessionService::destroy } }; + // Create requests use authentication instead while the ready requests are completely unauthenticated + bool isCreateRequest = path.empty() && verb == http::verb::post; + bool isReadyRequest = path.empty() && verb == http::verb::get; + // Allow destruction of a session without a valid session uuid just so we can return not_found instead of forbidden + bool isDestroyRequest = !path.empty() && verb == http::verb::delete_; + return !( isCreateRequest || isReadyRequest || isDestroyRequest); } -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -RestSessionService::ServiceResponse - RestSessionService::ready( http::verb verb, const nlohmann::json& arguments, const nlohmann::json& metaData ) +nlohmann::json RestSessionService::createOperation( const std::string& operationId, + const std::string& summary, + const nlohmann::json& parameters, + const nlohmann::json& responses, + const nlohmann::json& requestBody ) +{ + auto tags = nlohmann::json::array( { "sessions" } ); + return RestServiceInterface::createOperation( operationId, summary, parameters, responses, requestBody, tags ); +} + +std::map RestSessionService::servicePathEntries() const +{ + auto sessionObject = nlohmann::json::object(); + sessionObject["application/json"] = { { "schema", { { "$ref", "#/components/session_schemas/Session" } } } }; + auto sessionContent = nlohmann::json{ { "description", "An application session" }, { "content", sessionObject } }; + + auto createGetPutResponses = + nlohmann::json{ { HTTP_OK, sessionContent }, { "default", RestServiceInterface::plainErrorResponse() } }; + + auto emptyResponseContent = nlohmann::json{ { "description", "Success" } }; + + auto acceptedOrFailureResponses = nlohmann::json{ { HTTP_ACCEPTED, emptyResponseContent }, + { "default", RestServiceInterface::plainErrorResponse() } }; + + auto sessions = nlohmann::json::object(); + + auto typeParameter = nlohmann::json{ { "name", "type" }, + { "in", "path" }, + { "required", false }, + { "default", static_cast( caffa::Session::Type::REGULAR ) }, + { "description", "The type of session to query for" }, + { "schema", { { "type", "integer" }, { "format", "int32" } } } }; + + auto readyValue = nlohmann::json{ + { "application/json", { { "schema", { { "$ref", "#/components/session_schemas/ReadyState" } } } } } }; + auto readyContent = nlohmann::json{ { "description", "Ready State" }, { "content", readyValue } }; + + auto readyResponses = + nlohmann::json{ { HTTP_OK, readyContent }, { "default", RestServiceInterface::plainErrorResponse() } }; + + sessions["get"] = + createOperation( "readyForSession", "Check if app is ready for session", typeParameter, readyResponses ); + + sessions["post"] = createOperation( "createSession", "Create a new session", typeParameter, createGetPutResponses ); + + auto uuidParameter = nlohmann::json{ { "name", "uuid" }, + { "in", "query" }, + { "required", true }, + { "description", "The session UUID of the session to get" }, + { "schema", { { "type", "string" } } } }; + + auto session = nlohmann::json::object(); + session["get"] = createOperation( "getSession", "Get a particular session", uuidParameter, createGetPutResponses ); + + session["delete"] = + createOperation( "destroySession", "Destroy a particular session", uuidParameter, acceptedOrFailureResponses ); + + session["patch"] = + createOperation( "keepSessionAlive", "Keep a particular session alive", uuidParameter, acceptedOrFailureResponses ); + + session["put"] = + createOperation( "changeSession", "Change a session", uuidParameter, createGetPutResponses, sessionContent ); + + return { { "/sessions", sessions }, { "/sessions/{uuid}", session } }; +} + +std::map RestSessionService::serviceComponentEntries() const { - CAFFA_TRACE( "Received ready for session request with arguments " << arguments ); + auto session = nlohmann::json{ { "type", "object" }, + { "properties", + { { "uuid", { { "type", "string" } } }, + { "type", { { "type", "integer" }, { "format", "int32" } } }, + { "valid", { { "type", "boolean" } } } } } }; + + auto ready = nlohmann::json{ { "type", "object" }, + { "properties", + { + { "ready", { { "type", "boolean" } } }, + { "other_sessions", { { "type", "boolean" } } }, + } } }; + + return { { "session_schemas", { { "Session", session }, { "ReadyState", ready } } } }; +} - if ( RestServiceInterface::refuseDueToTimeLimiter() ) +RestSessionService::ServiceResponse RestSessionService::performOnAll( http::verb verb, const nlohmann::json& queryParams, const nlohmann::json& body ) +{ + switch ( verb ) { - return std::make_tuple( http::status::too_many_requests, "Too many unauthenticated requests", nullptr ); + case http::verb::get: + return ready( queryParams ); + case http::verb::post: + return create( body ); + default: + CAFFA_WARNING( "Invalid sessions request " << http::to_string(verb) ); } + return std::make_tuple( http::status::bad_request, "Invalid sessions requests", nullptr ); +} +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RestSessionService::ServiceResponse RestSessionService::ready( const nlohmann::json& body ) +{ + CAFFA_DEBUG( "Received ready for session request with metadata " << body ); try { caffa::Session::Type type = caffa::Session::Type::REGULAR; - if ( arguments.contains( "type" ) ) + if ( body.contains( "type" ) ) { - type = caffa::Session::typeFromUint( arguments["type"].get() ); - } - else if ( metaData.contains( "type" ) ) - { - type = caffa::Session::typeFromUint( metaData["type"].get() ); + type = caffa::Session::typeFromUint( body["type"].get() ); } auto jsonResponse = nlohmann::json::object(); + CAFFA_DEBUG("Checking if we're ready for a session of type " << static_cast(type)); bool ready = RestServerApplication::instance()->readyForSession( type ); @@ -108,186 +187,145 @@ RestSessionService::ServiceResponse //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -RestSessionService::ServiceResponse - RestSessionService::check( http::verb verb, const nlohmann::json& arguments, const nlohmann::json& metaData ) +RestSessionService::ServiceResponse RestSessionService::create( const nlohmann::json& body ) { - CAFFA_TRACE( "Got session check request with arguments " << arguments ); + CAFFA_DEBUG( "Received create session request" ); - std::string session_uuid = ""; - if ( arguments.contains( "session_uuid" ) ) + try { - session_uuid = arguments["session_uuid"].get(); + caffa::Session::Type type = caffa::Session::Type::REGULAR; + if ( body.contains( "type" ) ) + { + type = caffa::Session::typeFromUint( body["type"].get() ); + } + auto session = RestServerApplication::instance()->createSession( type ); + + CAFFA_TRACE( "Created session: " << session->uuid() ); + + auto jsonResponse = nlohmann::json::object(); + jsonResponse["uuid"] = session->uuid(); + jsonResponse["type"] = static_cast( session->type() ); + jsonResponse["valid"] = !session->isExpired(); + return std::make_tuple( http::status::ok, jsonResponse.dump(), nullptr ); } - else if ( metaData.contains( "session_uuid" ) ) + catch ( const std::exception& e ) { - session_uuid = metaData["session_uuid"].get(); + CAFFA_ERROR( "Failed to create session with error: " << e.what() ); + return std::make_tuple( http::status::forbidden, e.what(), nullptr ); } - auto session = RestServerApplication::instance()->getExistingSession( session_uuid ); - if ( !session ) +} + +RestSessionService::ServiceResponse + RestSessionService::performOnOne( http::verb verb, const nlohmann::json& body, const std::string& uuid ) +{ + switch ( verb ) { - return std::make_tuple( http::status::forbidden, "Session '" + session_uuid + "' is not valid", nullptr ); + case http::verb::get: + return get( uuid ); + case http::verb::put: + return change( uuid, body ); + case http::verb::delete_: + return destroy( uuid ); + case http::verb::patch: + return keepalive( uuid ); + default: + CAFFA_WARNING( "Invalid individual session request" ); } - else if ( session->isExpired() ) + return std::make_tuple( http::status::bad_request, "Invalid indidual session request", nullptr ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RestSessionService::ServiceResponse RestSessionService::get( const std::string& uuid ) +{ + CAFFA_TRACE( "Got session get request for uuid " << uuid ); + + caffa::SessionMaintainer session = RestServerApplication::instance()->getExistingSession( uuid ); + if ( !session ) { - return std::make_tuple( http::status::forbidden, "Session '" + session_uuid + "' is expired", nullptr ); + return std::make_tuple( http::status::not_found, "Session '" + uuid + "' is not valid", nullptr ); } - auto jsonResponse = nlohmann::json::object(); - jsonResponse["session_uuid"] = session->uuid(); - jsonResponse["type"] = static_cast( session->type() ); - jsonResponse["timeout"] = session->timeout().count(); + auto jsonResponse = nlohmann::json::object(); + jsonResponse["uuid"] = session->uuid(); + jsonResponse["type"] = static_cast( session->type() ); + jsonResponse["valid"] = !session->isExpired(); + return std::make_tuple( http::status::ok, jsonResponse.dump(), nullptr ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -RestSessionService::ServiceResponse - RestSessionService::change( http::verb verb, const nlohmann::json& arguments, const nlohmann::json& metaData ) +RestSessionService::ServiceResponse RestSessionService::change( const std::string& uuid, const nlohmann::json& body ) { - CAFFA_TRACE( "Got session check request with arguments " << arguments ); + CAFFA_TRACE( "Got session change request for " << uuid ); + + caffa::SessionMaintainer session = RestServerApplication::instance()->getExistingSession( uuid ); - std::string session_uuid = ""; - if ( arguments.contains( "session_uuid" ) ) - { - session_uuid = arguments["session_uuid"].get(); - } - else if ( metaData.contains( "session_uuid" ) ) - { - session_uuid = metaData["session_uuid"].get(); - } - auto session = RestServerApplication::instance()->getExistingSession( session_uuid ); if ( !session ) { - return std::make_tuple( http::status::forbidden, "Session '" + session_uuid + "' is not valid", nullptr ); + return std::make_tuple( http::status::not_found, "Session '" + uuid + "' is not valid", nullptr ); } else if ( session->isExpired() ) { - return std::make_tuple( http::status::forbidden, "Session '" + session_uuid + "' is expired", nullptr ); + return std::make_tuple( http::status::gone, "Session '" + uuid + "' is expired", nullptr ); } - if ( !arguments.contains( "type" ) && !metaData.contains( "type" ) ) + if ( !body.contains( "type" ) ) { return std::make_tuple( http::status::bad_request, "No new type provided", nullptr ); } - caffa::Session::Type type = caffa::Session::Type::REGULAR; - if ( arguments.contains( "type" ) ) - { - type = caffa::Session::typeFromUint( arguments["type"].get() ); - } - else if ( metaData.contains( "type" ) ) - { - type = caffa::Session::typeFromUint( metaData["type"].get() ); - } + caffa::Session::Type type = caffa::Session::typeFromUint( body["type"].get() ); RestServerApplication::instance()->changeSession( session.get(), type ); - auto jsonResponse = nlohmann::json::object(); - jsonResponse["session_uuid"] = session->uuid(); - jsonResponse["type"] = static_cast( session->type() ); - jsonResponse["timeout"] = session->timeout().count(); - return std::make_tuple( http::status::ok, jsonResponse.dump(), nullptr ); -} + auto jsonResponse = nlohmann::json::object(); + jsonResponse["uuid"] = session->uuid(); + jsonResponse["type"] = static_cast( session->type() ); + jsonResponse["valid"] = !session->isExpired(); -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -RestSessionService::ServiceResponse - RestSessionService::create( http::verb verb, const nlohmann::json& arguments, const nlohmann::json& metaData ) -{ - CAFFA_DEBUG( "Received create session request" ); - - try - { - caffa::Session::Type type = caffa::Session::Type::REGULAR; - if ( arguments.contains( "type" ) ) - { - type = caffa::Session::typeFromUint( arguments["type"].get() ); - } - else if ( metaData.contains( "type" ) ) - { - type = caffa::Session::typeFromUint( metaData["type"].get() ); - } - auto session = RestServerApplication::instance()->createSession( type ); - - CAFFA_TRACE( "Created session: " << session->uuid() ); - - auto jsonResponse = nlohmann::json::object(); - jsonResponse["session_uuid"] = session->uuid(); - jsonResponse["type"] = static_cast( session->type() ); - jsonResponse["timeout"] = session->timeout().count(); - return std::make_tuple( http::status::ok, jsonResponse.dump(), nullptr ); - } - catch ( const std::exception& e ) - { - CAFFA_ERROR( "Failed to create session with error: " << e.what() ); - return std::make_tuple( http::status::forbidden, e.what(), nullptr ); - } + return std::make_tuple( http::status::ok, jsonResponse.dump(), nullptr ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -RestSessionService::ServiceResponse - RestSessionService::keepalive( http::verb verb, const nlohmann::json& arguments, const nlohmann::json& metaData ) +RestSessionService::ServiceResponse RestSessionService::keepalive( const std::string& uuid ) { - CAFFA_TRACE( "Got session keep-alive request with arguments " << arguments ); - - std::string session_uuid = ""; - if ( arguments.contains( "session_uuid" ) ) - { - session_uuid = arguments["session_uuid"].get(); - } - else if ( metaData.contains( "session_uuid" ) ) - { - session_uuid = metaData["session_uuid"].get(); - } + CAFFA_TRACE( "Got session keep-alive request for " << uuid ); - auto session = RestServerApplication::instance()->getExistingSession( session_uuid ); + auto session = RestServerApplication::instance()->getExistingSession( uuid ); if ( !session ) { - return std::make_tuple( http::status::forbidden, "Session '" + session_uuid + "' is not valid", nullptr ); + return std::make_tuple( http::status::not_found, "Session '" + uuid + "' is not valid", nullptr ); } else if ( session->isExpired() ) { - return std::make_tuple( http::status::forbidden, "Session '" + session_uuid + "' is expired", nullptr ); + return std::make_tuple( http::status::gone, "Session '" + uuid + "' is expired", nullptr ); } session->updateKeepAlive(); - - auto jsonResponse = nlohmann::json::object(); - jsonResponse["session_uuid"] = session->uuid(); - jsonResponse["type"] = static_cast( session->type() ); - jsonResponse["timeout"] = session->timeout().count(); - return std::make_tuple( http::status::ok, jsonResponse.dump(), nullptr ); + return std::make_tuple( http::status::accepted, "", nullptr ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -RestSessionService::ServiceResponse - RestSessionService::destroy( http::verb verb, const nlohmann::json& arguments, const nlohmann::json& metaData ) +RestSessionService::ServiceResponse RestSessionService::destroy( const std::string& uuid ) { - CAFFA_DEBUG( "Got destroy session request with arguments " << arguments ); + CAFFA_DEBUG( "Got destroy session request for " << uuid ); - std::string session_uuid = ""; - if ( arguments.contains( "session_uuid" ) ) - { - session_uuid = arguments["session_uuid"].get(); - } - else if ( metaData.contains( "session_uuid" ) ) - { - session_uuid = metaData["session_uuid"].get(); - } try { - RestServerApplication::instance()->destroySession( session_uuid ); - return std::make_tuple( http::status::ok, "Session successfully destroyed", nullptr ); + RestServerApplication::instance()->destroySession( uuid ); + return std::make_tuple( http::status::accepted, "Session successfully destroyed", nullptr ); } catch ( const std::exception& e ) { - CAFFA_WARNING( "Session '" << session_uuid + CAFFA_WARNING( "Session '" << uuid << "' did not exist. It may already have been destroyed due to lack of keepalive" ); return std::make_tuple( http::status::not_found, "Failed to destroy session. It may already have been destroyed.", diff --git a/RestInterface/cafRestSessionService.h b/RestInterface/cafRestSessionService.h index 72117bb5..73e4573a 100644 --- a/RestInterface/cafRestSessionService.h +++ b/RestInterface/cafRestSessionService.h @@ -28,23 +28,35 @@ namespace caffa::rpc class RestSessionService : public RestServiceInterface { public: - ServiceResponse perform( http::verb verb, - const std::list& path, - const nlohmann::json& arguments, - const nlohmann::json& metaData ) override; + ServiceResponse perform( http::verb verb, + std::list path, + const nlohmann::json& queryParams, + const nlohmann::json& body ) override; - bool requiresAuthentication( const std::list& path ) const override; + bool requiresAuthentication( http::verb verb, const std::list& path ) const override; + bool requiresSession( http::verb verb, const std::list& path ) const override; + + std::map servicePathEntries() const override; + std::map serviceComponentEntries() const override; private: using ServiceCallback = std::function; - std::map callbacks() const; + static nlohmann::json createOperation( const std::string& operationId, + const std::string& summary, + const nlohmann::json& parameters, + const nlohmann::json& responses, + const nlohmann::json& requestBody = nullptr ); + + static ServiceResponse performOnAll( http::verb verb, const nlohmann::json& queryParams, const nlohmann::json& body ); + static ServiceResponse ready( const nlohmann::json& body ); + static ServiceResponse create( const nlohmann::json& body ); + + static ServiceResponse performOnOne( http::verb verb, const nlohmann::json& body, const std::string& uuid ); + static ServiceResponse get( const std::string& uuid ); - static ServiceResponse ready( http::verb verb, const nlohmann::json& arguments, const nlohmann::json& metaData ); - static ServiceResponse check( http::verb verb, const nlohmann::json& arguments, const nlohmann::json& metaData ); - static ServiceResponse change( http::verb verb, const nlohmann::json& arguments, const nlohmann::json& metaData ); - static ServiceResponse create( http::verb verb, const nlohmann::json& arguments, const nlohmann::json& metaData ); - static ServiceResponse destroy( http::verb verb, const nlohmann::json& arguments, const nlohmann::json& metaData ); - static ServiceResponse keepalive( http::verb verb, const nlohmann::json& arguments, const nlohmann::json& metaData ); + static ServiceResponse change( const std::string& uuid, const nlohmann::json& body ); + static ServiceResponse destroy( const std::string& uuid ); + static ServiceResponse keepalive( const std::string& uuid ); }; } // namespace caffa::rpc diff --git a/RpcBase/cafRpcClientPassByRefObjectFactory.cpp b/RpcBase/cafRpcClientPassByRefObjectFactory.cpp index f26bf046..f4d4255c 100644 --- a/RpcBase/cafRpcClientPassByRefObjectFactory.cpp +++ b/RpcBase/cafRpcClientPassByRefObjectFactory.cpp @@ -25,6 +25,7 @@ #include "cafDefaultObjectFactory.h" #include "cafField.h" #include "cafFieldScriptingCapability.h" +#include "cafPortableDataType.h" #include "cafRpcChildArrayFieldAccessor.h" #include "cafRpcChildFieldAccessor.h" #include "cafRpcClient.h" @@ -94,6 +95,18 @@ void ClientPassByRefObjectFactory::setClient( Client* client ) m_client = client; } +std::list ClientPassByRefObjectFactory::supportedDataTypes() const +{ + std::list dataTypes; + + for ( const auto& [type, creator] : m_accessorCreatorMap ) + { + dataTypes.push_back( type ); + } + + return dataTypes; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -112,23 +125,34 @@ void ClientPassByRefObjectFactory::applyAccessorToField( caffa::ObjectHandle* fi } else if ( auto dataField = dynamic_cast( fieldHandle ); dataField ) { - CAFFA_TRACE( "Looking for an accessor creator for data type: " << fieldHandle->dataType() ); + auto jsonCapability = fieldHandle->capability(); AccessorCreatorBase* accessorCreator = nullptr; - for ( auto& [dataType, storedAccessorCreator] : m_accessorCreatorMap ) - { - CAFFA_TRACE( "Found one for " << dataType << " is that right?" ); - if ( dataType == fieldHandle->dataType() ) + + if (jsonCapability) { + auto jsonType = jsonCapability->jsonType(); + + CAFFA_TRACE( "Looking for an accessor creator for data type: " << jsonType ); + for ( auto& [dataType, storedAccessorCreator] : m_accessorCreatorMap ) { - CAFFA_TRACE( "Yes!" ); - accessorCreator = storedAccessorCreator.get(); - break; + CAFFA_TRACE( "Found one for " << dataType << " is that right?" ); + if ( dataType == jsonType.dump()) + { + CAFFA_TRACE( "Yes!" ); + accessorCreator = storedAccessorCreator.get(); + break; + } } + if ( !accessorCreator ) + { + throw std::runtime_error( std::string( "Data type " ) + fieldHandle->dataType() + " not implemented in client" ); + } + dataField->setUntypedAccessor( accessorCreator->create( m_client, fieldHandle ) ); } - if ( !accessorCreator ) - { - throw std::runtime_error( std::string( "Data type " ) + fieldHandle->dataType() + " not implemented in client" ); + else { + CAFFA_ASSERT(false && "All fields that are scriptable has to be serializable"); + throw std::runtime_error("Field " + fieldHandle->keyword() + " is not serializable"); } - dataField->setUntypedAccessor( accessorCreator->create( m_client, fieldHandle ) ); + } else { diff --git a/RpcBase/cafRpcClientPassByRefObjectFactory.h b/RpcBase/cafRpcClientPassByRefObjectFactory.h index eb929857..f0b4691a 100644 --- a/RpcBase/cafRpcClientPassByRefObjectFactory.h +++ b/RpcBase/cafRpcClientPassByRefObjectFactory.h @@ -71,11 +71,14 @@ class ClientPassByRefObjectFactory : public ObjectFactory template void registerBasicAccessorCreators() { - registerAccessorCreator( PortableDataType::name(), std::make_unique>() ); - registerAccessorCreator( PortableDataType>::name(), + registerAccessorCreator( PortableDataType::jsonType().dump(), + std::make_unique>() ); + registerAccessorCreator( PortableDataType>::jsonType().dump(), std::make_unique>>() ); } + std::list supportedDataTypes() const; + private: std::shared_ptr doCreate( const std::string_view& classKeyword ) override;