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 prefixesidentity.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 isuser, 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
- Parent facet: ores.cpp.shell-command.
- Sibling archetype: ores.cpp.shell-command.command_header.
- Template variable reference — the variables this template reads.