ores.cpp.shell-command.command_implementation

Table of Contents

The bodies behind the declarations of the sibling ores.cpp.shell-command.command_header archetype. Four handlers, one per CRUD verb, plus the submenu registration.

The add verb takes free-form tokens rather than one typed parameter per column. The cli library expands a typed handler into one template instantiation per argument, and the Windows clang toolchain abandons the mangler on the deeper chains. A token vector holds the argument list at a fixed shape.

See the Template variable reference for the complete list of available variables and their semantics.

1. Mustache incantations

Three constructs carry weight here.

  • {{user_supplied_count}} is the positional arity, computed by the generator from each column's :supplied_by: annotation. It counts only the columns the verb reads from the command line.
  • {{member_access}} is the column's path on the domain struct. The generator prefixes identity. when the column belongs to the identity group, so the template never branches on nesting.
  • One {{#is_*}} section per supply source selects how the member is filled. The default is user, which consumes the next positional.

A malformed token must name the field it belongs to, so every conversion passes the column name to from_token, and the block that builds the entity sits inside one try. The handler reports the failure through fail() and returns, as the hand-written units it replaces do.

The column loop stays free of type branching. Each column's cpp_type is already its target type, including the std::optional wrapper, so one conversion helper named for that type serves every column.

The tenant comes from the session, not from the command line, and it is a member of the identity field group rather than a declared column. The template names it directly, as it already names the audit members.

2. Template

The full template source. Edit here and re-tangle with compass build --direct tangle_codegen_templates to regenerate library/templates/cpp_shell_command_impl.cpp.mustache.

{{! GENERATED FILE — tangled from projects/ores.codegen/library/templates/ores.cpp.shell-command.command_implementation.org. Edit the org source. }}
{{{cpp_license}}}
{{#domain_entity}}
#include "ores.shell/app/commands/{{component}}/{{entity_singular}}_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.
{{#has_base64_columns}}
#include "ores.utility/convert/base64_converter.hpp"
{{/has_base64_columns}}
#include "ores.platform/time/datetime.hpp"
#include <boost/asio/ip/address.hpp>
#include <boost/uuid/random_generator.hpp>
#include <chrono>
#include <cli/cli.h>
#include <cstddef>
#include <functional>
#include <optional>
#include <ostream>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <vector>

namespace ores::shell::app::commands {

using namespace logging;
using ores::nats::service::nats_client;
namespace messaging = ores::{{component}}::messaging;
{{#shell.has_helpers}}

namespace {

/**
 * @brief Fill one request member from one token.
 *
 * A member's own type decides how its token reads, so the caller states the
 * token and the name it answers to and nothing else. The four cases are the
 * four shapes a token has: a word, a flag, a comma-separated list and
 * everything from_token already converts.
 */
template <typename T>
void read_token(T& target, const std::string& raw, const std::string& name) {
    if constexpr (std::is_same_v<T, std::string>) {
        target = raw;
    } else if constexpr (std::is_same_v<T, bool>) {
        if (raw.empty() || raw == "false") {
            target = false;
        } else if (raw == "true") {
            target = true;
        } else {
            throw std::invalid_argument(name + " must be 'true' or 'false'");
        }
    } else if constexpr (std::is_same_v<T, std::chrono::system_clock::time_point>) {
        target = ores::platform::time::datetime::from_iso8601_utc(raw);
    } else if constexpr (std::is_same_v<T, boost::asio::ip::address>) {
        target = boost::asio::ip::make_address(raw);
    } else if constexpr (std::is_same_v<T, std::vector<std::string>>) {
        target.clear();
        std::string current;
        for (const char c : raw) {
            if (c == ',') {
                target.push_back(current);
                current.clear();
            } else {
                current.push_back(c);
            }
        }
        target.push_back(current);
{{#has_base64_columns}}
    } else if constexpr (std::is_same_v<T, std::vector<std::uint8_t>>) {
        target = ores::utility::convert::base64_converter::convert(raw);
{{/has_base64_columns}}
    } else {
        target = ores::shell::app::from_token<T>(raw, name);
    }
}
{{#shell.has_order}}

/// Apply the page and the order a caller stated, leaving the defaults alone.
template <typename Request>
void apply_page(Request& req, const parsed_args& parsed) {
    if (const auto& raw = parsed.flag("offset"); !raw.empty()) {
        req.offset = ores::shell::app::from_token<std::uint32_t>(raw, "offset");
    }
    if (const auto& raw = parsed.flag("limit"); !raw.empty()) {
        req.limit = ores::shell::app::from_token<std::uint32_t>(raw, "limit");
    }
    if (const auto& raw = parsed.flag("order"); !raw.empty()) {
        req.order.field = raw;
    }
    req.order.descending = parsed.flag_set("desc");
}
{{/shell.has_order}}

} // namespace
{{/shell.has_helpers}}

void {{entity_singular}}_commands::register_commands(cli::Menu& root_menu, nats_client& session) {
    auto menu = std::make_unique<cli::Menu>("{{entity_plural}}");
{{#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}}_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 = messaging::{{request}};
    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 = "{{.}}", .requires_value = true, .default_value = ""},
{{/flags}}
{{#switches}}
        {.name = "{{.}}", .requires_value = false, .default_value = "false"},
{{/switches}}
    };
    const auto parsed = parse_args(args, specs);
    if (!parsed) {
        fail(out) << parsed.error() << std::endl;
        return;
    }

    request_type req;
    [[maybe_unused]] std::size_t next = 0;
    try {
{{#is_paged}}

    apply_page(req, *parsed);
    if (!parsed->positionals.empty()) {
        fail(out) << "Expected no arguments, got " << parsed->positionals.size() << "."
                  << std::endl;
        return;
    }
{{/is_paged}}
{{#is_list_by}}

    if (parsed->positionals.size() != 1) {
        fail(out) << "Expected 1 argument, got " << parsed->positionals.size() << "."
                  << std::endl;
        return;
    }
{{#is_string}}
    req.{{relation.name}} = parsed->positionals[next++];
{{/is_string}}
{{^is_string}}
    req.{{relation.name}} = ores::shell::app::from_token<{{relation.cpp_type}}>(
        parsed->positionals[next++], "{{relation.name}}");
{{/is_string}}
    if (const auto& raw = parsed->flag("scope"); !raw.empty()) {
        req.scope = raw == "subtree" ? ores::utility::domain::scope::subtree
                                    : ores::utility::domain::scope::direct;
    }
    apply_page(req, *parsed);
{{/is_list_by}}
{{#is_versions}}

    if (parsed->positionals.size() != {{key_arity}}) {
        fail(out) << "Expected {{key_arity}} arguments, got " << parsed->positionals.size()
                  << "." << std::endl;
        return;
    }
{{#keys}}
{{#is_user}}
    read_token(req.key.{{name}}, parsed->positionals[next++], "{{name}}");
{{/is_user}}
{{/keys}}
    apply_page(req, *parsed);
{{/is_versions}}
{{#is_key_read}}

    if (parsed->positionals.size() != {{key_arity}}) {
        fail(out) << "Expected {{key_arity}} arguments, got " << parsed->positionals.size()
                  << "." << std::endl;
        return;
    }
{{#keys}}
{{#is_user}}
    read_token(req.key.{{name}}, parsed->positionals[next++], "{{name}}");
{{/is_user}}
{{/keys}}
{{/is_key_read}}
{{#is_version_read}}

    if (parsed->positionals.size() != {{key_arity}}) {
        fail(out) << "Expected {{key_arity}} arguments, got " << parsed->positionals.size()
                  << "." << std::endl;
        return;
    }
{{#keys}}
{{#is_user}}
    read_token(req.key.{{entity_singular}}.{{name}}, parsed->positionals[next++], "{{name}}");
{{/is_user}}
{{/keys}}
    req.key.version = ores::shell::app::from_token<std::uint32_t>(
        parsed->flag("version"), "version");
{{/is_version_read}}
{{#is_put}}

    if (parsed->positionals.size() != {{put_arity}} + 2) {
        fail(out) << "Expected " << ({{put_arity}} + 2) << " arguments, got "
                  << parsed->positionals.size() << "." << std::endl;
        return;
    }
{{#writes}}
{{#is_user}}
    read_token(req.change.write.{{name}}, parsed->positionals[next++], "{{name}}");
{{/is_user}}
{{#is_minted}}
    req.change.write.{{name}} = boost::uuids::random_generator()();
{{/is_minted}}
{{#is_session_party}}
    req.change.write.{{name}} = ores::shell::app::from_token<boost::uuids::uuid>(
        session.auth().default_party_id, "{{name}}");
{{/is_session_party}}
{{/writes}}
    req.intent.reason_code = parsed->positionals[next++];
    req.intent.commentary = parsed->positionals[next++];
    req.change.precondition.kind =
        ores::utility::domain::precondition_kind::{{precondition}};
{{#allows_version}}
    if (const auto& raw = parsed->flag("version"); !raw.empty()) {
        req.change.precondition.kind =
            ores::utility::domain::precondition_kind::must_match_version;
        req.change.precondition.version =
            ores::shell::app::from_token<std::uint32_t>(raw, "version");
    }
{{/allows_version}}
{{/is_put}}
{{#is_delete}}

    if (parsed->positionals.size() != {{key_arity}} + 2) {
        fail(out) << "Expected " << ({{key_arity}} + 2) << " arguments, got "
                  << parsed->positionals.size() << "." << std::endl;
        return;
    }
{{#keys}}
{{#is_user}}
    read_token(req.removal.key.{{name}}, parsed->positionals[next++], "{{name}}");
{{/is_user}}
{{/keys}}
    req.intent.reason_code = parsed->positionals[next++];
    req.intent.commentary = parsed->positionals[next++];
{{#allows_version}}
    if (const auto& raw = parsed->flag("version"); !raw.empty()) {
        req.removal.precondition.kind =
            ores::utility::domain::precondition_kind::must_match_version;
        req.removal.precondition.version =
            ores::shell::app::from_token<std::uint32_t>(raw, "version");
    }
{{/allows_version}}
{{/is_delete}}
{{#is_get_many}}

    if (parsed->positionals.empty() || parsed->positionals.size() % {{key_arity}} != 0) {
        fail(out) << "Expected a multiple of {{key_arity}} arguments, got "
                  << parsed->positionals.size() << "." << std::endl;
        return;
    }
    for (std::size_t i = 0; i < parsed->positionals.size(); i += {{key_arity}}) {
        messaging::{{entity_singular}}_key key;
{{#keys}}
        read_token(key.{{name}}, parsed->positionals[i + {{index}}], "{{name}}");
{{/keys}}
        req.keys.push_back(std::move(key));
    }
{{/is_get_many}}
{{#is_delete_many}}

    if (parsed->positionals.size() < {{key_arity}} + 2 ||
        (parsed->positionals.size() - 2) % {{key_arity}} != 0) {
        fail(out) << "Expected a whole number of key groups and an intent, got "
                  << parsed->positionals.size() << "." << std::endl;
        return;
    }
    const std::size_t key_groups = (parsed->positionals.size() - 2) / {{key_arity}};
    for (std::size_t i = 0; i < key_groups; ++i) {
        messaging::{{entity_singular}}_key key;
{{#keys}}
        read_token(key.{{name}}, parsed->positionals[i * {{key_arity}} + {{index}}],
                   "{{name}}");
{{/keys}}
        req.removals.push_back(messaging::{{entity_singular}}_removal{.key = std::move(key)});
    }
    req.intent.reason_code = parsed->positionals[parsed->positionals.size() - 2];
    req.intent.commentary = parsed->positionals[parsed->positionals.size() - 1];
{{/is_delete_many}}
{{#is_put_many}}

    const auto count_raw = parsed->flag("count");
    if (count_raw.empty()) {
        fail(out) << "--count is required." << std::endl;
        return;
    }
    const auto change_count = ores::shell::app::from_token<std::uint32_t>(count_raw, "count");
    if (parsed->positionals.size() != change_count * {{write_arity}} + 2) {
        fail(out) << "Expected " << (change_count * {{write_arity}} + 2)
                  << " arguments, got " << parsed->positionals.size() << "." << std::endl;
        return;
    }
    for (std::uint32_t i = 0; i < change_count; ++i) {
        messaging::{{entity_singular}}_change change;
{{#writes}}
        read_token(change.write.{{name}}, parsed->positionals[next++], "{{name}}");
{{/writes}}
        change.precondition.kind =
            ores::utility::domain::precondition_kind::must_not_exist;
        req.changes.push_back(std::move(change));
    }
    req.intent.reason_code = parsed->positionals[next++];
    req.intent.commentary = parsed->positionals[next++];
{{/is_put_many}}
    } catch (const std::exception& e) {
        fail(out) << e.what() << std::endl;
        return;
    }

    auto result = do_auth_request<messaging::{{response_type}}>(
        out, session, std::string(req.nats_subject), req);
    if (!result)
        return;

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

}
{{/domain_entity}}

3. See also

Emacs 29.3 (Org mode 9.6.15)