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

Emacs 29.3 (Org mode 9.6.15)