ores.cpp.shell-command.operation_implementation
Table of Contents
The body of one operation model's shell command unit.
Each handler fills the declared request from a token vector and sends it on the
request's own nats_subject, so the command and the protocol cannot disagree
about where a message goes. The reply is written as JSON: a declared response
has whatever shape its model gave it, and printing it whole is the only
projection that stays true to every one of them. A response the model gives a
richer reading belongs in a hand-written unit, and the entity archetypes are
where a projection the shell owns is written.
A field the shell cannot fill from a token is a model defect, not a rendering
one: _reject_silent_shell_gap refuses the model rather than dropping the
argument, because a command that silently asks for fewer things than the
protocol declares is worse than one that does not build.
An operation whose work outlives the transport's default request timeout states
its own budget with :request_timeout_seconds: in the message's drawer, and
the command passes it to do_auth_request. The value is the caller's ceiling,
so it has to exceed the handler's own. Declaring it on a message that takes no
session is refused, because only the authenticated call carries a timeout to
raise. A unit with no such command includes no <chrono> and passes nothing,
so the transport default stays the default.
1. Template
The full template source. Edit here and re-tangle with
compass build --direct tangle_codegen_templates to regenerate
library/templates/cpp_shell_operation_implementation.cpp.mustache.
{{! GENERATED FILE — tangled from projects/ores.codegen/library/templates/ores.cpp.shell-command.operation_implementation.org. Edit the org source. }}
{{{cpp_license}}}
{{#operation}}
#include "ores.shell/app/commands/{{component}}/{{entity_singular}}_operations_commands.hpp"
#include "ores.shell/app/command_args.hpp"
#include "ores.shell/app/command_feedback.hpp"
#include "ores.shell/app/command_token.hpp"
#include "ores.shell/app/request_helpers.hpp"
#include "ores.shell/app/shell_root_menu.hpp"
#include "ores.{{component}}.{{subcomponent}}/messaging/{{entity_singular}}_protocol.hpp"
{{^has_generated_wire_codec}}
#include "ores.utility/codec/json_codec.hpp"
{{/has_generated_wire_codec}}
{{#has_generated_wire_codec}}
#include "ores.utility/codec/wire_traits_fwd.hpp"
{{/has_generated_wire_codec}}
#include <cli/cli.h>
{{#shell_has_request_timeout}}
#include <chrono>
{{/shell_has_request_timeout}}
#include <cstddef>
#include <functional>
#include <optional>
#include <ostream>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <vector>
namespace ores::shell::app::commands {
using namespace logging;
using ores::nats::service::nats_client;
{{#shell_has_helpers}}
namespace {
{{/shell_has_helpers}}
{{#shell_has_list}}
/**
* @brief Split a comma-separated token into the elements of a list field.
*
* The token __none__ states the empty list, because the command line cannot
* carry an empty argument: the tokenizer drops one, and a command whose list
* field should be empty has no other way to say so.
*/
std::vector<std::string> {{entity_singular}}{{^entity_singular}}{{name_singular}}{{/entity_singular}}_split_list_token(const std::string& value) {
if (value == "__none__")
return {};
std::vector<std::string> parts;
std::string current;
for (const char c : value) {
if (c == ',') {
parts.push_back(current);
current.clear();
} else {
current.push_back(c);
}
}
parts.push_back(current);
return parts;
}
{{/shell_has_list}}
{{#shell_has_bool}}
/**
* @brief Read a boolean token, which the shell spells out as a word.
*/
bool parse_flag(const std::string& value, bool& out) {
if (value.empty() || value == "false") {
out = false;
return true;
}
if (value == "true") {
out = true;
return true;
}
return false;
}
{{/shell_has_bool}}
{{#shell_has_helpers}}
}
{{/shell_has_helpers}}
void {{entity_singular}}_operations_commands::register_commands(cli::Menu& root_menu,
nats_client& session) {
auto menu = std::make_unique<cli::Menu>("{{entity_singular}}");
{{#shell_commands}}
menu->Insert(
"{{command}}",
[&session](std::ostream& out, std::vector<std::string> args) {
process_{{identifier}}(std::ref(out), std::ref(session), std::move(args));
},
"{{usage}}");
{{/shell_commands}}
ores::shell::app::insert_menu(root_menu, std::move(menu));
}
{{#shell_commands}}
void {{entity_singular}}_operations_commands::process_{{identifier}}(
std::ostream& out, nats_client& session, const std::vector<std::string>& args) {
BOOST_LOG_SEV(lg(), debug) << "Initiating {{command}} request.";
using request_type = {{namespace}}::{{request}};
// Whether the command presents a token is the protocol's own statement, so
// a message that establishes the session is never asked for one.
if constexpr (request_type::requires_session) {
if (!session.is_logged_in()) {
fail(out) << "You must be logged in to run {{command}}." << std::endl;
return;
}
}
const std::vector<flag_spec> specs{
{{#flags}}
{.name = "{{name}}", .requires_value = true, .default_value = ""},
{{/flags}}
};
const auto parsed = parse_args(args, specs);
if (!parsed) {
fail(out) << parsed.error() << std::endl;
return;
}
constexpr std::size_t positional_count = {{positional_count}};
if (parsed->positionals.size() != positional_count) {
fail(out) << "Expected " << positional_count << " arguments, got "
<< parsed->positionals.size() << "." << std::endl;
return;
}
request_type req;
{{#has_positionals}}
std::size_t next = 0;
{{/has_positionals}}
try {
{{#positionals}}
{{#is_list}}
req.{{name}} = {{entity_singular}}{{^entity_singular}}{{name_singular}}{{/entity_singular}}_split_list_token(parsed->positionals[next++]);
{{/is_list}}
{{#is_bool}}
if (!parse_flag(parsed->positionals[next++], req.{{name}})) {
fail(out) << "{{name}} must be 'true' or 'false'." << std::endl;
return;
}
{{/is_bool}}
{{#is_string}}
req.{{name}} = parsed->positionals[next++];
{{/is_string}}
{{#needs_from_token}}
req.{{name}} = ores::shell::app::from_token<{{cpp_type}}>(
parsed->positionals[next++], "{{name}}");
{{/needs_from_token}}
{{/positionals}}
{{#flags}}
if (const auto& raw_{{name}} = parsed->flag("{{name}}"); !raw_{{name}}.empty()) {
{{#is_list}}
req.{{name}} = {{entity_singular}}{{^entity_singular}}{{name_singular}}{{/entity_singular}}_split_list_token(raw_{{name}});
{{/is_list}}
{{#is_bool}}
if (!parse_flag(raw_{{name}}, req.{{name}})) {
fail(out) << "{{name}} must be 'true' or 'false'." << std::endl;
return;
}
{{/is_bool}}
{{#is_string}}
req.{{name}} = raw_{{name}};
{{/is_string}}
{{#needs_from_token}}
req.{{name}} = ores::shell::app::from_token<{{cpp_type}}>(
raw_{{name}}, "{{name}}");
{{/needs_from_token}}
}
{{/flags}}
} catch (const std::exception& e) {
fail(out) << e.what() << std::endl;
return;
}
std::optional<{{namespace}}::{{response_type}}> result;
if constexpr (request_type::requires_session) {
result = do_auth_request<{{namespace}}::{{response_type}}>(
out, session, std::string(req.nats_subject), req{{#request_timeout_seconds}},
std::chrono::seconds{ {{request_timeout_seconds}} }{{/request_timeout_seconds}});
} else {
result = do_request<{{namespace}}::{{response_type}}>(
out, session, std::string(req.nats_subject), req);
}
if (!result)
return;
{{#adopts_token_field}}
// The answer carries a token scoped to the party the request named. The session keeps
// it, so the commands that follow run in that party and not in the one the login
// opened in.
if (result->success && !result->token.empty()) {
auto info = session.auth();
info.jwt = result->token;
info.default_party_id = req.{{adopts_token_field}};
session.set_auth(std::move(info));
}
{{/adopts_token_field}}
{{#has_generated_wire_codec}}
out << ores::utility::codec::wire_traits<std::remove_cvref_t<decltype(*result)>>::write_json(
*result)
<< std::endl;
{{/has_generated_wire_codec}}
{{^has_generated_wire_codec}}
out << ores::utility::codec::json::write(*result) << std::endl;
{{/has_generated_wire_codec}}
}
{{/shell_commands}}
}
{{/operation}}
2. See also
- Parent facet: ores.cpp.shell-command.
- Sibling archetype: ores.cpp.shell-command.command_header.
- Sibling archetype: ores.cpp.shell-command.command_tests.
- Template variable reference — the variables this template reads.