Skip to content

Commit

Permalink
Update endpoint installation API (#1310)
Browse files Browse the repository at this point in the history
  • Loading branch information
eddyashton authored Jun 22, 2020
1 parent 78e5d49 commit 7f13107
Show file tree
Hide file tree
Showing 39 changed files with 1,296 additions and 943 deletions.
18 changes: 9 additions & 9 deletions doc/developers/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Developer API
A CCF application is composed of the following:

- The :ref:`Application Entry Point <developers/api:Application Entry Point>` which registers the application in CCF.
- A collection of :cpp:class:`ccf::HandlerRegistry::Handler` as endpoints to user HTTP requests and grouped in a single :cpp:class:`ccf::HandlerRegistry`. A :cpp:class:`ccf::HandlerRegistry::Handler` reads and writes to the key-value store via the :ref:`Key-Value Store API <developers/kv/api:Key-Value Store API>`.
- A collection of :cpp:class:`ccf::EndpointRegistry::Endpoint`s to user HTTP requests and grouped in a single :cpp:class:`ccf::EndpointRegistry`. A :cpp:class:`ccf::EndpointRegistry::Endpoint` reads and writes to the key-value store via the :ref:`Key-Value Store API <developers/kv/api:Key-Value Store API>`.

Application Entry Point
-----------------------
Expand All @@ -17,19 +17,19 @@ Application Entry Point
:project: CCF


Application RPC Handlers
------------------------
Application Endpoint Registration
---------------------------------

Handler Registry
~~~~~~~~~~~~~~~~
Endpoint Registry
~~~~~~~~~~~~~~~~~

.. doxygenclass:: ccf::HandlerRegistry
.. doxygenclass:: ccf::EndpointRegistry
:project: CCF
:members: install, set_default

Handler
~~~~~~~
Endpoint
~~~~~~~~

.. doxygenstruct:: ccf::HandlerRegistry::Handler
.. doxygenstruct:: ccf::EndpointRegistry::Endpoint
:project: CCF
:members:
2 changes: 1 addition & 1 deletion doc/developers/kv/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Key-Value Store API

This page presents the API that a CCF application must use to read and mutate the replicated key-value store.

A CCF application should define one or multiple public or private :cpp:class:`kv::Map`. Then, each :cpp:class:`ccf::HandlerRegistry::Handler` should use the :cpp:class:`kv::Tx` transaction object to read and write to specific :cpp:class:`kv::Map`.
A CCF application should define one or multiple public or private :cpp:class:`kv::Map`. Then, each :cpp:class:`ccf::EndpointRegistry::Endpoint` should use the :cpp:class:`kv::Tx` transaction object to read and write to specific :cpp:class:`kv::Map`.

Store
-----
Expand Down
22 changes: 20 additions & 2 deletions doc/developers/kv/kv_how_to.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ A ``Map`` can either be created as private (default) or public. Transactions on
Accessing the ``Transaction``
-----------------------------

A :cpp:class:`kv::Tx` corresponds to the atomic operations that can be executed on the Key-Value ``Store``. A transaction can affect one or multiple ``Map`` and are automatically committed by CCF once a RPC handler returns.
A :cpp:class:`kv::Tx` corresponds to the atomic operations that can be executed on the Key-Value ``Store``. A transaction can affect one or multiple ``Map`` and are automatically committed by CCF once the endpoint's handler returns successfully.

A single ``Transaction`` (``tx``) is passed to all the end-points of an application and should be used to interact with the Key-Value ``Store``.

Expand Down Expand Up @@ -74,6 +74,24 @@ Once a ``View`` on a specific ``Map`` has been obtained, it is possible to:
view_map1->get("key1");
assert(v1.has_value() == false);
Read-only views
---------------

For operations which only read from a map, it is possible to retrieve a :cpp:class:`kv::Map::ReadOnlyTxView` which only supports the `get` operation:

.. code-block:: cpp
// Read-only view on map_priv
auto view_map1 = tx.get_read_only_view(map_priv);
// Reading from that view
auto v1 = view_map1->get("key1");
assert(v1.value() == "value1");
// Writes are blocked at compile time
view_map1->put("key1", "value2"); // Does not compile
view_map1->remove("key1"); // Does not compile
Removing a key
--------------

Expand Down Expand Up @@ -126,7 +144,7 @@ Custom key and value types

User-defined types can also be used for the types of the key and value mapping of each :cpp:class:`kv::Map`. When defining each custom type, the following conditions must be met:

- For both the custom key and value types, the ``MSGPACK_DEFINE();`` macro should be used to declare each members of the custom type for serialisation.
- For both the custom key and value types, the ``MSGPACK_DEFINE();`` macro should be used to declare each member of the custom type for serialisation.
- For the custom key type, the ``==`` operator should be defined.

.. code-block:: cpp
Expand Down
46 changes: 26 additions & 20 deletions doc/developers/logging_cpp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,64 +41,70 @@ The Logging application simply has:
RPC Handler
-----------

The handler returned by :cpp:func:`ccfapp::get_rpc_handler()` should subclass :cpp:class:`ccf::UserRpcFrontend`, providing an implementation of :cpp:class:`ccf::HandlerRegistry`:
The type returned by :cpp:func:`ccfapp::get_rpc_handler()` should subclass :cpp:class:`ccf::UserRpcFrontend`, passing the base constructor a reference to an implementation of :cpp:class:`ccf::EndpointRegistry`:

.. literalinclude:: ../../src/apps/logging/logging.cpp
:language: cpp
:start-after: SNIPPET: inherit_frontend
:lines: 1
:dedent: 2

The logging app defines :cpp:class:`ccfapp::LoggerHandlers`, which creates and installs handler functions or lambdas for each transaction type. These take a transaction object and the request's ``params``, interact with the KV tables, and return a result:
The logging app defines :cpp:class:`ccfapp::LoggerHandlers`, which creates and installs handler functions or lambdas for several different HTTP endpoints. Each of these functions takes as input the details of the current request (such as the URI which was called, the query string, the request body), interacts with the KV tables using the given :cpp:class:`kv::Tx` object, and returns a result:

.. literalinclude:: ../../src/apps/logging/logging.cpp
:language: cpp
:start-after: SNIPPET_START: get
:end-before: SNIPPET_END: get
:start-after: SNIPPET_START: record
:end-before: SNIPPET_END: record
:dedent: 6

This handler uses the simple signatures provided by the ``json_adapter`` wrapper function, which handles parsing of a JSON params object from the HTTP request body.
This example uses the ``json_adapter`` wrapper function, which handles parsing of a JSON params object from the HTTP request body.

Each function is installed as the handler for a specific RPC ``method``, the name of the HTTP resource at which your handler will be invoked:
Each function is installed as the handler for a specific HTTP resource, defined by a verb and URI:

.. literalinclude:: ../../src/apps/logging/logging.cpp
:language: cpp
:start-after: SNIPPET_START: install_get
:end-before: SNIPPET_END: install_get
:start-after: SNIPPET_START: install_record
:end-before: SNIPPET_END: install_record
:dedent: 6

The return value from ``install`` is a ``Handler&`` object which can be used to alter how the handler is executed. For example, the handler for ``LOG_get`` shown above sets a `schema` for the handler, which will be used in calls to the ``/getSchema`` endpoint. It also marks the handler as `GET-only`, so the framework will return a ``405 Method Not Allowed`` for any requests which are not HTTP ``GET``.
This example installs at ``"LOG_record", HTTP_POST``, so will be invoked for requests beginning ``POST /users/LOG_record``.

The return value from ``make_endpoint`` is an ``Endpoint&`` object which can be used to alter how the handler is executed. For example, the handler for ``LOG_record`` shown above sets a `schema` for the handler, declaring the types of its request and response bodies. These will be used in calls to the ``/api/schema`` endpoint to generate JSON documents describing the API. Since this is the only handler installed for ``"LOG_record"`` only HTTP ``POST``s will be accepted for this URI - the framework will return a ``405 Method Not Allowed`` for requests with any other verb.

To process the raw body directly, a handler should use the general lambda signature which takes a single ``RequestArgs&`` parameter. Examples of this are also included in the logging sample app. For instance the ``log_record_text`` handler takes a raw string as the request body:
To process the raw body directly, a handler should use the general lambda signature which takes a single ``EndpointContext&`` parameter. Examples of this are also included in the logging sample app. For instance the ``log_record_text`` handler takes a raw string as the request body:

.. literalinclude:: ../../src/apps/logging/logging.cpp
:language: cpp
:start-after: SNIPPET_START: log_record_text
:end-before: SNIPPET_END: log_record_text
:dedent: 6

Rather than parsing the request body as JSON and extracting the message from it, in this case `the entire body` is the message to be logged, and the ID to associate it with is passed as a request header.
Rather than parsing the request body as JSON and extracting the message from it, in this case `the entire body` is the message to be logged, and the ID to associate it with is passed as a request header. This requires some additional code in the handler, but provides complete control of the request and response formats.

This general form of handler (taking a single ``RequestArgs&`` parameter) also allows a handler to see additional caller context. An example of this is the ``log_record_prefix_cert`` handler:
This general signature also allows a handler to see additional caller context. An example of this is the ``log_record_prefix_cert`` handler:

.. literalinclude:: ../../src/apps/logging/logging.cpp
:language: cpp
:start-after: SNIPPET_START: log_record_prefix_cert
:end-before: SNIPPET_END: log_record_prefix_cert
:dedent: 6

This uses mbedtls to parse the caller's TLS certificate, and prefixes the logged message with the Subject field extracted from this certificate.
This uses mbedtls to parse the caller's TLS certificate, and prefixes the logged message with the ``Subject`` field extracted from this certificate.

A handler can either be installed as:
If a handler makes no writes to the KV, it may be installed as read-only:

.. literalinclude:: ../../src/apps/logging/logging.cpp
:language: cpp
:start-after: SNIPPET_START: install_get
:end-before: SNIPPET_END: install_get
:dedent: 6

- ``Write``: this handler can only be executed on the primary of the consensus network.
- ``Read``: this handler can be executed on any node of the network.
- ``MayWrite``: the execution of this handler on a specific node depends on the value of the ``x-ccf-readonly`` header in the HTTP request.
This offers some additional type safety (accidental `put`s or `remove`s will be caught at compile-time) and also enables performance scaling since read-only operations can be executed on any receiving node, whereas writes must always be executed on the primary node.

API Schema
~~~~~~~~~~

These handlers also demonstrate two different ways of defining schema for RPCs, and validating incoming requests against them. The record/get methods operating on public tables have manually defined schema and use [#valijson]_ for validation, returning an error if the input is not compliant with the schema:
These handlers also demonstrate two different ways of defining the type schema for each endpoint, and validating incoming requests against them. The record/get methods operating on public tables have manually defined schema and use [#valijson]_ for validation, returning an error if the input is not compliant with the schema:

.. literalinclude:: ../../src/apps/logging/logging.cpp
:language: cpp
Expand All @@ -122,9 +128,9 @@ The methods operating on private tables use an alternative approach, with a macr
:end-before: SNIPPET_END: macro_validation_record
:dedent: 6

This produces validation error messages with a lower performance overhead, and ensures the schema and parsing logic stay in sync, but is only suitable for simple schema with required and optional fields of supported types.
This produces validation error messages with a lower performance overhead, and ensures the schema and parsing logic stay in sync, but is only suitable for simple schema - an object with some required and some optional fields, each of a supported type.

Both approaches register their RPC's params and result schema, allowing them to be retrieved at runtime with calls to the getSchema RPC.
Both approaches register their endpoint's request and response schema, allowing them to be retrieved at runtime with calls to the ``/api/schema`` endpoint.

.. rubric:: Footnotes

Expand Down
63 changes: 30 additions & 33 deletions samples/apps/smallbank/app/smallbank.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ namespace ccfapp
{}
};

class SmallBankHandlers : public UserHandlerRegistry
class SmallBankHandlers : public UserEndpointRegistry
{
private:
SmallBankTables tables;

bool headers_unmatched(RequestArgs& args)
bool headers_unmatched(EndpointContext& args)
{
const auto actual =
args.rpc_ctx->get_request_header(http::headers::CONTENT_TYPE)
Expand All @@ -57,7 +57,7 @@ namespace ccfapp
return false;
}

void set_unmatched_header_status(RequestArgs& args)
void set_unmatched_header_status(EndpointContext& args)
{
args.rpc_ctx->set_response_status(HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE);
args.rpc_ctx->set_response_header(
Expand All @@ -69,38 +69,39 @@ namespace ccfapp
.value_or("")));
}

void set_error_status(RequestArgs& args, int status, std::string&& message)
void set_error_status(
EndpointContext& args, int status, std::string&& message)
{
args.rpc_ctx->set_response_status(status);
args.rpc_ctx->set_response_header(
http::headers::CONTENT_TYPE, http::headervalues::contenttype::TEXT);
args.rpc_ctx->set_response_body(std::move(message));
}

void set_ok_status(RequestArgs& args)
void set_ok_status(EndpointContext& args)
{
args.rpc_ctx->set_response_status(HTTP_STATUS_OK);
args.rpc_ctx->set_response_header(
http::headers::CONTENT_TYPE,
http::headervalues::contenttype::OCTET_STREAM);
}

void set_no_content_status(RequestArgs& args)
void set_no_content_status(EndpointContext& args)
{
args.rpc_ctx->set_response_status(HTTP_STATUS_NO_CONTENT);
}

public:
SmallBankHandlers(kv::Store& store) :
UserHandlerRegistry(store),
UserEndpointRegistry(store),
tables(store)
{}

void init_handlers(kv::Store& store) override
{
UserHandlerRegistry::init_handlers(store);
UserEndpointRegistry::init_handlers(store);

auto create = [this](RequestArgs& args) {
auto create = [this](auto& args) {
// Create an account with a balance from thin air.
BankDeserializer bd(args.rpc_ctx->get_request_body().data());
auto name = bd.name();
Expand Down Expand Up @@ -146,7 +147,7 @@ namespace ccfapp
set_no_content_status(args);
};

auto create_batch = [this](RequestArgs& args) {
auto create_batch = [this](auto& args) {
// Create N accounts with identical balances from thin air.
// Create an account with a balance from thin air.
AccountsDeserializer ad(args.rpc_ctx->get_request_body().data());
Expand Down Expand Up @@ -203,7 +204,7 @@ namespace ccfapp
set_no_content_status(args);
};

auto balance = [this](RequestArgs& args) {
auto balance = [this](auto& args) {
// Check the combined balance of an account
BankDeserializer bd(args.rpc_ctx->get_request_body().data());
auto name = bd.name();
Expand Down Expand Up @@ -246,7 +247,7 @@ namespace ccfapp
std::vector<uint8_t>(buff.p, buff.p + buff.n));
};

auto transact_savings = [this](RequestArgs& args) {
auto transact_savings = [this](auto& args) {
// Add or remove money to the savings account
TransactionDeserializer td(args.rpc_ctx->get_request_body().data());
auto name = td.name();
Expand Down Expand Up @@ -291,7 +292,7 @@ namespace ccfapp
set_no_content_status(args);
};

auto deposit_checking = [this](RequestArgs& args) {
auto deposit_checking = [this](auto& args) {
// Desposit money into the checking account out of thin air
TransactionDeserializer td(args.rpc_ctx->get_request_body().data());
auto name = td.name();
Expand Down Expand Up @@ -333,7 +334,7 @@ namespace ccfapp
set_no_content_status(args);
};

auto amalgamate = [this](RequestArgs& args) {
auto amalgamate = [this](auto& args) {
// Move the contents of one users account to another users account
AmalgamateDeserializer ad(args.rpc_ctx->get_request_body().data());
auto name_1 = ad.name_src();
Expand Down Expand Up @@ -405,7 +406,7 @@ namespace ccfapp
set_no_content_status(args);
};

auto writeCheck = [this](RequestArgs& args) {
auto writeCheck = [this](auto& args) {
// Write a check, if not enough funds then also charge an extra 1 money
TransactionDeserializer td(args.rpc_ctx->get_request_body().data());
auto name = td.name();
Expand Down Expand Up @@ -450,24 +451,20 @@ namespace ccfapp
set_no_content_status(args);
};

install(Procs::SMALL_BANKING_CREATE, create, HandlerRegistry::Write);
install(
Procs::SMALL_BANKING_CREATE_BATCH,
create_batch,
HandlerRegistry::Write);
install(Procs::SMALL_BANKING_BALANCE, balance, HandlerRegistry::Read);
install(
Procs::SMALL_BANKING_TRANSACT_SAVINGS,
transact_savings,
HandlerRegistry::Write);
install(
Procs::SMALL_BANKING_DEPOSIT_CHECKING,
deposit_checking,
HandlerRegistry::Write);
install(
Procs::SMALL_BANKING_AMALGAMATE, amalgamate, HandlerRegistry::Write);
install(
Procs::SMALL_BANKING_WRITE_CHECK, writeCheck, HandlerRegistry::Write);
make_endpoint(Procs::SMALL_BANKING_CREATE, HTTP_POST, create).install();
make_endpoint(Procs::SMALL_BANKING_CREATE_BATCH, HTTP_POST, create_batch)
.install();
make_endpoint(Procs::SMALL_BANKING_BALANCE, HTTP_POST, balance).install();
make_endpoint(
Procs::SMALL_BANKING_TRANSACT_SAVINGS, HTTP_POST, transact_savings)
.install();
make_endpoint(
Procs::SMALL_BANKING_DEPOSIT_CHECKING, HTTP_POST, deposit_checking)
.install();
make_endpoint(Procs::SMALL_BANKING_AMALGAMATE, HTTP_POST, amalgamate)
.install();
make_endpoint(Procs::SMALL_BANKING_WRITE_CHECK, HTTP_POST, writeCheck)
.install();
}
};

Expand Down
10 changes: 5 additions & 5 deletions src/apps/js_generic/js_generic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,21 +139,21 @@ namespace ccfapp
return r;
}

class JSHandlers : public UserHandlerRegistry
class JSHandlers : public UserEndpointRegistry
{
private:
NetworkTables& network;
Table& table;

public:
JSHandlers(NetworkTables& network) :
UserHandlerRegistry(network),
UserEndpointRegistry(network),
network(network),
table(network.tables->create<Table>("data"))
{
auto& tables = *network.tables;

auto default_handler = [this](RequestArgs& args) {
auto default_handler = [this](EndpointContext& args) {
const auto method = args.rpc_ctx->get_method();
const auto local_method = method.substr(method.find_first_not_of('/'));
if (local_method == UserScriptIds::ENV_HANDLER)
Expand Down Expand Up @@ -269,14 +269,14 @@ namespace ccfapp
return;
};

set_default(default_handler, Write);
set_default(default_handler);
}

// Since we do our own dispatch within the default handler, report the
// supported methods here
void list_methods(kv::Tx& tx, ListMethods::Out& out) override
{
UserHandlerRegistry::list_methods(tx, out);
UserEndpointRegistry::list_methods(tx, out);

auto scripts = tx.get_view(this->network.app_scripts);
scripts->foreach([&out](const auto& key, const auto&) {
Expand Down
Loading

0 comments on commit 7f13107

Please sign in to comment.