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.

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.{{component}}.{{subcomponent}}/messaging/{{entity_singular}}_protocol.hpp"
#include "ores.utility/rfl/reflectors.hpp" // IWYU pragma: keep.
#include <cli/cli.h>
#include <cstddef>
#include <functional>
#include <optional>
#include <ostream>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <stdexcept>
#include <string>
#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.
 */
std::vector<std::string> split_list_token(const std::string& value) {
    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}}

} // namespace
{{/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}}

    root_menu.Insert(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}} = 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}} = 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);
    } else {
        result = do_request<{{namespace}}::{{response_type}}>(
            out, session, std::string(req.nats_subject), req);
    }
    if (!result)
        return;

    out << rfl::json::write(*result) << std::endl;
}
{{/shell_commands}}

}
{{/operation}}

2. See also

Emacs 29.3 (Org mode 9.6.15)