modules/mod_shell_reports.cc (970 lines of code) (raw):
/*
* Copyright (c) 2018, 2024, Oracle and/or its affiliates.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0,
* as published by the Free Software Foundation.
*
* This program is designed to work with certain software (including
* but not limited to OpenSSL) that is licensed under separate terms,
* as designated in a particular file or component or in included license
* documentation. The authors of MySQL hereby grant you an additional
* permission to link the program and your derivative works with the
* separately licensed software that they have either included with
* the program or referenced in the documentation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
* the GNU General Public License, version 2.0, for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "modules/mod_shell_reports.h"
#include <algorithm>
#include <limits>
#include <locale>
#include <set>
#include "modules/reports/query.h"
#include "modules/reports/thread.h"
#include "modules/reports/threads.h"
#include "modules/reports/utils.h"
#include "mysqlshdk/include/shellcore/utils_help.h"
#include "mysqlshdk/libs/textui/textui.h"
#include "mysqlshdk/libs/utils/options.h"
#include "mysqlshdk/libs/utils/threads.h"
#include "mysqlshdk/libs/utils/utils_general.h"
#include "mysqlshdk/libs/utils/utils_string.h"
namespace mysqlsh {
namespace {
const std::set<std::string> kReportOptionTypes = {"string", "integer", "float",
"bool"};
REGISTER_HELP_OBJECT(reports, shell);
REGISTER_HELP(REPORTS_BRIEF,
"Gives access to built-in and user-defined reports.");
REGISTER_HELP(REPORTS_DETAIL,
"The 'reports' object provides access to built-in reports.");
REGISTER_HELP(REPORTS_DETAIL1,
"All user-defined reports registered using the "
"shell.<<<registerReport>>>() method are also available here.");
REGISTER_HELP(REPORTS_DETAIL2,
"The reports are provided as methods of this object, with names "
"corresponding to the names of the available reports.");
REGISTER_HELP(REPORTS_DETAIL3,
"All methods have the same signature: <b>Dict report(Session "
"session, List argv, Dict options)</b>, where:");
REGISTER_HELP(
REPORTS_DETAIL4,
"@li session - Session object used by the report to obtain the data.");
REGISTER_HELP(REPORTS_DETAIL5,
"@li argv (optional) - Array of strings representing additional "
"arguments.");
REGISTER_HELP(REPORTS_DETAIL6,
"@li options (optional) - Dictionary with values for various "
"report-specific options.");
REGISTER_HELP(REPORTS_DETAIL7,
"Each report returns a dictionary with the following keys:");
REGISTER_HELP(REPORTS_DETAIL8,
"@li report (required) - List of JSON objects containing the "
"report. The number and types of items in this list depend on "
"type of the report.");
REGISTER_HELP(REPORTS_DETAIL9,
"For more information on a report use: "
"<b>shell.reports.help('report_name')</b>.");
static constexpr auto k_report_key = "report";
static constexpr auto k_vertical_key = "vertical";
static constexpr auto k_wildcard_character = "*";
static constexpr auto k_report_type_list = "list";
static constexpr auto k_report_type_report = "report";
static constexpr auto k_report_type_print = "print";
static constexpr auto k_report_type_custom = "custom";
Report::Type to_report_type(const std::string &type) {
if (k_report_type_list == type) {
return Report::Type::LIST;
} else if (k_report_type_report == type) {
return Report::Type::REPORT;
} else if (k_report_type_print == type) {
return Report::Type::PRINT;
} else {
// user cannot register reports of custom type
throw shcore::Exception::argument_error(
"Report type must be one of: " +
shcore::str_join(
std::vector<std::string>{k_report_type_list, k_report_type_report,
k_report_type_print},
", ") +
".");
}
}
std::string to_string(Report::Type type) {
switch (type) {
case Report::Type::LIST:
return k_report_type_list;
case Report::Type::REPORT:
return k_report_type_report;
case Report::Type::PRINT:
return k_report_type_print;
case Report::Type::CUSTOM:
return k_report_type_custom;
}
throw std::logic_error("Unknown value of Report::Type");
}
std::string to_string(const Report::Argc &argc) {
std::string result;
if (argc.first == 0 && argc.second == Report::k_asterisk) {
result += "any number of";
} else {
result += std::to_string(argc.first);
if (argc.first != argc.second) {
result += "-";
if (argc.second == Report::k_asterisk) {
result += k_wildcard_character;
} else {
result += std::to_string(argc.second);
}
}
}
result += " argument";
if (argc.first != argc.second || argc.first != 1) {
result += "s";
}
return result;
}
void validate_option_type(shcore::Value_type type) {
switch (type) {
case shcore::Value_type::String:
case shcore::Value_type::Bool:
case shcore::Value_type::Integer:
case shcore::Value_type::Float:
// valid type
return;
default:
throw shcore::Exception::argument_error(
"Option type must be one of: 'string', 'bool', 'integer', 'float'.");
}
}
uint32_t to_uint32(const std::string &v) {
try {
return shcore::lexical_cast<uint32_t>(v);
} catch (const std::invalid_argument &) {
throw shcore::Exception::argument_error("Cannot convert '" + v +
"' to an unsigned integer.");
}
}
Report::Argc get_report_argc(const std::string &argc_s) {
if (argc_s.empty()) {
return {0, 0};
}
const auto argc = shcore::str_split(argc_s, "-");
Report::Argc result;
switch (argc.size()) {
case 1:
if (argc[0] == k_wildcard_character) {
result = std::make_pair(0, Report::k_asterisk);
} else {
const auto limit = to_uint32(argc[0]);
result = std::make_pair(limit, limit);
}
break;
case 2: {
const auto lower = to_uint32(argc[0]);
const auto upper = argc[1] == k_wildcard_character ? Report::k_asterisk
: to_uint32(argc[1]);
result = std::make_pair(lower, upper);
} break;
default:
throw shcore::Exception::argument_error(
"The value associated with the key named 'argc' has wrong format.");
}
return result;
}
Report::Examples get_report_examples(const shcore::Array_t &ex) {
Report::Examples examples;
if (ex) {
for (const auto &e : *ex) {
if (shcore::Value_type::Map != e.type) {
throw shcore::Exception::argument_error(
"The value associated with the key named 'examples' should be a "
"list of dictionaries.");
}
Report::Example example;
shcore::Option_unpacker{e.as_map()}
.required("description", &example.description)
.optional("args", &example.args)
.optional("options", &example.options)
.end();
examples.emplace_back(std::move(example));
}
}
return examples;
}
Report::Option *find_option(const Report::Options &options,
const std::string &name) {
const auto pos =
std::find_if(options.begin(), options.end(),
[&name](const Report::Options::value_type &o) {
return o->parameter->name == name || o->short_name == name;
});
return options.end() == pos ? nullptr : pos->get();
}
std::vector<shcore::Help_registry::Example> get_function_examples(
const Report &report) {
std::vector<shcore::Help_registry::Example> examples;
for (const auto &example : report.examples()) {
shcore::Help_registry::Example e;
e.code = report.name() + "(session";
if (report.argc().second > 0 || report.has_options()) {
e.code.append(", [");
for (const auto &a : example.args) {
e.code.append(shcore::quote_string(a, '"')).append(", ");
}
if (!example.args.empty()) {
e.code.pop_back();
e.code.pop_back();
}
e.code.append("]");
}
if (report.has_options()) {
e.code.append(", {");
for (const auto &o : example.options) {
const auto spec = find_option(report.options(), o.first);
assert(spec);
e.code.append(shcore::quote_string(spec->parameter->name, '"'))
.append(": ");
switch (spec->parameter->type()) {
case shcore::Value_type::String:
e.code.append(shcore::quote_string(o.second, '"'));
break;
case shcore::Value_type::Bool:
case shcore::Value_type::Integer:
case shcore::Value_type::Float:
e.code.append(o.second);
break;
default:
throw std::logic_error("Unknown option type.");
}
e.code.append(", ");
}
if (!example.options.empty()) {
e.code.pop_back();
e.code.pop_back();
}
e.code.append("}");
}
e.code.append(")");
e.description = example.description;
examples.emplace_back(std::move(e));
}
return examples;
}
class Native_report_function : public shcore::Cpp_function {
public:
Native_report_function(const std::string &name,
const Report::Native_report &report)
: shcore::Cpp_function(
&m_metadata, [report](const shcore::Argument_list &args) {
const auto &session = args.object_at<ShellBaseSession>(0);
if (!session) {
throw shcore::Exception::argument_error(
"Argument #1 is expected to be one of 'ClassicSession, "
"Session'.");
}
if (!session->is_open()) {
throw shcore::Exception::argument_error(
"Executing the report requires an open session.");
}
// it's not an array if it's null
const auto argv =
args.size() < 2 || args[1].type != shcore::Value_type::Array
? shcore::make_array()
: args.array_at(1);
// it's not a map if it's null
const auto options =
args.size() < 3 || args[2].type != shcore::Value_type::Map
? shcore::make_dict()
: args.map_at(2);
return shcore::Value(report(session, argv, options));
}) {
for (std::size_t i = 0; i < shcore::array_size(m_metadata.name); ++i) {
m_metadata.name[i] = name;
}
m_metadata.param_types = {{"session", shcore::Value_type::Object},
{"?argv", shcore::Value_type::Array},
{"?options", shcore::Value_type::Map}};
m_metadata.signature = gen_signature(m_metadata.param_types);
m_metadata.return_type = shcore::Value_type::Map;
}
~Native_report_function() override = default;
private:
shcore::Cpp_function::Metadata m_metadata;
};
std::string list_formatter(const shcore::Array_t &a,
const shcore::Dictionary_t &options) {
bool is_vertical = false;
shcore::Option_unpacker{options}.required(k_vertical_key, &is_vertical).end();
if (a->size() < 1) {
throw shcore::Exception::runtime_error(
"List report should contain at least one row.");
}
for (const auto &row : *a) {
if (row.type != shcore::Value_type::Array) {
throw shcore::Exception::runtime_error(
"List report should return a list of lists.");
}
}
bool is_gui_mode = mysqlsh::current_shell_options()->get().gui_mode;
const auto output = is_vertical
? reports::vertical_formatter(a)
: (is_gui_mode ? reports::gui_table_formatter(a)
: reports::table_formatter(a));
// output is empty if report returned just names of columns
return (!is_gui_mode && (output.empty() || output == "\n"))
? "Report returned no data."
: output;
}
std::string report_formatter(const shcore::Array_t &a,
const shcore::Dictionary_t &) {
if (a->size() != 1) {
throw shcore::Exception::runtime_error(
"Report of type 'report' should contain exactly one element.");
}
return a->at(0).yaml();
}
std::string print_formatter(const shcore::Array_t &,
const shcore::Dictionary_t &) {
// print formatter suppresses all output
return "";
}
Parameter_definition::Options upcast(const Report::Options &in) {
return Parameter_definition::Options(in.begin(), in.end());
}
Report::Options downcast(const Parameter_definition::Options &in) {
Report::Options out;
for (const auto &p : in) {
out.emplace_back(std::dynamic_pointer_cast<Report::Option>(p));
}
return out;
}
struct Argv_validator : public shcore::Parameter_validator {
explicit Argv_validator(const Report::Argc &argc) : m_argc(argc) {}
void validate(const shcore::Parameter ¶m, const shcore::Value &data,
shcore::Parameter_context *context) const override {
Parameter_validator::validate(param, data, context);
const auto argv = data.as_array();
const auto argc = argv ? argv->size() : 0;
if (argc < m_argc.first || argc > m_argc.second) {
throw shcore::Exception::argument_error(shcore::str_format(
"%s 'argv' is expecting %s.", context->str().c_str(),
to_string(m_argc).c_str()));
}
}
private:
Report::Argc m_argc;
};
std::string normalize_report_name(const std::string &name) {
return shcore::str_lower(shcore::str_replace(name, "-", "_"));
}
} // namespace
class Shell_reports::Report_options : public shcore::Options {
public:
explicit Report_options(std::unique_ptr<Report> r)
: m_report_name(std::move(r->m_name)),
m_options(std::move(r->m_options)),
m_argc(std::move(r->m_argc)),
m_formatter(std::move(r->m_formatter)) {
{
// each report has an option to display help
Report::Option help{"help", shcore::Value_type::Bool};
help.brief = "Display this help and exit.";
help.short_name = "h";
add_startup_options()(&m_show_help, false, help.command_line_names(), "");
m_command_line_options.emplace_back(std::move(help));
}
// list reports can be displayed vertically
if (Report::Type::LIST == r->type()) {
Report::Option vertical{"vertical", shcore::Value_type::Bool};
vertical.brief = "Display records vertically.";
vertical.short_name = "E";
add_startup_options()(&m_vertical, false, vertical.command_line_names(),
"");
m_command_line_options.emplace_back(std::move(vertical));
}
// add options expected by the report
for (const auto &o : m_options) {
const auto &p = o->parameter;
// register the option
add_startup_options()(
o->command_line_names(), "",
[&o, &p, this](const std::string &, const char *new_value) {
// convert to the expected type
shcore::Value value;
switch (p->type()) {
case shcore::Value_type::String: {
value = shcore::Value(new_value ? new_value : "");
shcore::Parameter_context context{
m_report_name + ":",
{{"option '--" + o->command_line_name() + "'", {}}}};
p->validator<shcore::String_validator>()->validate(*p, value,
&context);
break;
}
case shcore::Value_type::Bool:
value = shcore::Value(true);
break;
case shcore::Value_type::Integer:
try {
value =
shcore::Value(shcore::lexical_cast<int64_t>(new_value));
} catch (const std::invalid_argument &) {
throw prepare_exception("cannot convert '" +
std::string(new_value) +
"' to a signed integer");
}
break;
case shcore::Value_type::Float:
try {
value =
shcore::Value(shcore::lexical_cast<double>(new_value));
} catch (const std::invalid_argument &) {
throw prepare_exception("cannot convert '" +
std::string(new_value) +
"' to a floating-point number");
}
break;
default:
throw std::logic_error("Unexpected type: " +
shcore::type_name(p->type()));
}
// store the value
m_parsed_options->emplace(p->name, value);
// if option is required, erase it from the list of missing ones
if (o->is_required()) {
m_missing_options.erase(
std::remove(m_missing_options.begin(),
m_missing_options.end(), p->name),
m_missing_options.end());
}
},
o->command_line_name());
}
initialize_help(r->brief(), r->details(), r->examples());
}
const std::string &name() const { return m_report_name; }
bool show_help() const { return m_show_help; }
bool vertical() const { return m_vertical; }
void parse_args(const std::vector<std::string> &args) {
// reset previous values and prepare for parsing
reset();
// prepare arguments
std::vector<const char *> raw_args;
raw_args.emplace_back(m_report_name.c_str());
for (const auto &a : args) {
raw_args.emplace_back(a.c_str());
}
// NULL must be the last argument in the vector
raw_args.emplace_back(nullptr);
// validate and parse provided arguments
// NULL should not be included in the size of arguments
handle_cmdline_options(raw_args.size() - 1, &raw_args[0], false,
[this](Iterator *it) {
// handle extra arguments
if (it->valid() && '-' != it->option()[0]) {
// first option which does not begin with '-'
// marks the beginning of arguments
const auto cmdline = it->iterator();
while (cmdline->valid()) {
m_arguments.emplace_back(cmdline->get());
}
return true;
} else {
return false;
}
});
// if help was not requested, perform additional validations
if (!show_help()) {
const auto usage = "For usage information please run: \\show " +
m_report_name + " --help";
// check if all required options are here
if (!m_missing_options.empty()) {
std::transform(m_missing_options.begin(), m_missing_options.end(),
m_missing_options.begin(),
[](const std::string &o) { return "--" + o; });
throw prepare_exception("missing required option(s): " +
shcore::str_join(m_missing_options, ", ") +
". " + usage);
}
// check if there's the right amount of additional arguments
const auto argc = m_arguments.size();
if (argc < m_argc.first || argc > m_argc.second) {
throw prepare_exception("report is expecting " + to_string(m_argc) +
", " + std::to_string(argc) + " provided. " +
usage);
}
// optionally add additional arguments
for (const auto &a : m_arguments) {
m_parsed_arguments->emplace_back(a);
}
}
}
shcore::Array_t get_parsed_arguments() const {
if (show_help()) {
throw std::logic_error(
"User requested help, arguments are not available.");
}
return m_parsed_arguments;
}
shcore::Dictionary_t get_parsed_options() const {
if (show_help()) {
throw std::logic_error("User requested help, options are not available.");
}
return m_parsed_options;
}
std::string help() const {
return shcore::Help_manager{}.get_help(*m_help_topic);
}
bool requires_argv() const { return m_argc.second > 0 || requires_options(); }
bool requires_options() const { return !m_options.empty(); }
const Report::Formatter &formatter() const { return m_formatter; }
private:
void reset() {
// reset to default values
m_show_help = false;
m_vertical = false;
m_missing_options.clear();
m_arguments.clear();
m_parsed_options = shcore::make_dict();
m_parsed_arguments = shcore::make_array();
// mark all required options as missing
for (const auto &o : m_options) {
if (o->is_required()) {
m_missing_options.emplace_back(o->parameter->name);
}
}
}
void initialize_help(const std::string &brief,
const std::vector<std::string> &details,
const Report::Examples &examples) {
if (nullptr == m_help_topic) {
const auto help = shcore::Help_registry::get();
const auto prefix = "CMD_SHOW_" + shcore::str_upper(m_report_name);
const auto topic =
help->add_help_topic(m_report_name, shcore::Topic_type::COMMAND,
prefix, "CMD_SHOW",
shcore::IShell_core::all_scripting_modes(),
shcore::Keyword_location::LOCAL_CTX)
.back();
if (!brief.empty())
help->add_help(prefix, "BRIEF", brief,
shcore::Keyword_location::LOCAL_CTX);
const auto has_arguments = m_argc.second > 0;
{
std::vector<std::string> syntax;
std::string required;
for (const auto &o : m_options) {
if (o->is_required()) {
required.append(" ").append(
shcore::str_join(o->command_line_names(), "|"));
}
}
for (const auto &command : {"show", "watch"}) {
std::string line = "\\" + std::string(command) + " <b>" +
m_report_name + "</b>" + required + " [OPTIONS]";
if (has_arguments) {
line += " [ARGS]";
}
syntax.emplace_back(std::move(line));
}
help->add_help(prefix, "SYNTAX", syntax,
shcore::Keyword_location::LOCAL_CTX);
}
{
std::vector<std::string> contents;
for (const auto &d : details) {
contents.emplace_back(d);
}
contents.emplace_back("Options:");
const auto add_option = [&contents](const Report::Option &o) {
std::string line;
line.append("@entrynl{");
line.append(shcore::str_join(o.command_line_names(), ", "));
line.append("} ").append(o.command_line_brief());
contents.emplace_back(std::move(line));
};
for (const auto &o : m_command_line_options) {
add_option(o);
}
for (const auto &o : m_options) {
add_option(*o);
}
for (const auto &o : m_options) {
for (const auto &d : o->details) {
contents.emplace_back(d);
}
}
if (has_arguments) {
contents.emplace_back("Arguments:");
contents.emplace_back("This report accepts " + to_string(m_argc) +
".");
}
help->add_help(prefix, "DETAIL", contents,
shcore::Keyword_location::LOCAL_CTX);
}
{
// examples
std::vector<shcore::Help_registry::Example> ex;
for (const auto &example : examples) {
shcore::Help_registry::Example e;
e.code = "\\show " + m_report_name;
for (const auto &o : example.options) {
const auto spec = find_option(m_options, o.first);
assert(spec);
const auto name =
(o.first.length() > 1 ? "-" : "") + ("-" + o.first);
switch (spec->parameter->type()) {
case shcore::Value_type::String:
e.code.append(" ").append(name);
if (!o.second.empty()) {
e.code.append(" ");
if (std::string::npos != o.second.find(" ")) {
e.code.append(shcore::quote_string(o.second, '"'));
} else {
e.code.append(o.second);
}
}
break;
case shcore::Value_type::Bool:
if ("true" == o.second) {
e.code.append(" ").append(name);
}
break;
case shcore::Value_type::Integer:
case shcore::Value_type::Float:
e.code.append(" ").append(name).append(" ").append(o.second);
break;
default:
throw std::logic_error("Unknown option type.");
}
}
for (const auto &a : example.args) {
e.code.append(" ");
if (std::string::npos != a.find(" ")) {
e.code.append(shcore::quote_string(a, '"'));
} else {
e.code.append(a);
}
}
e.description = example.description;
ex.emplace_back(std::move(e));
}
help->add_help(prefix, ex, shcore::Keyword_location::LOCAL_CTX);
}
m_help_topic = topic;
}
}
shcore::Exception prepare_exception(const std::string &e) const {
return shcore::Exception::argument_error(m_report_name + ": " + e);
}
const std::string m_report_name;
const Report::Options m_options;
const Report::Argc m_argc;
const Report::Formatter m_formatter;
shcore::Help_topic *m_help_topic = nullptr;
bool m_show_help;
bool m_vertical;
std::vector<std::string> m_missing_options;
std::vector<std::string> m_arguments;
shcore::Dictionary_t m_parsed_options;
shcore::Array_t m_parsed_arguments;
std::vector<Report::Option> m_command_line_options;
};
Report::Option::Option(const std::string &n, shcore::Value_type type,
bool required)
: Parameter_definition(n, type,
required ? shcore::Param_flag::Mandatory
: shcore::Param_flag::Optional) {}
std::vector<std::string> Report::Option::command_line_names() const {
std::vector<std::string> names;
const auto &p = this->parameter;
std::string long_name = "--" + command_line_name();
// non-Boolean reports require a value
if (shcore::Value_type::Bool != p->type()) {
if (empty) long_name += "[";
long_name += "=" + shcore::str_lower(shcore::type_name(p->type()));
if (empty) long_name += "]";
}
names.emplace_back(std::move(long_name));
// options can optionally have short (one letter) form
if (!this->short_name.empty()) {
names.emplace_back("-" + this->short_name);
}
return names;
}
std::string Report::Option::command_line_name() const {
return shcore::from_camel_case_to_dashes(parameter->name);
}
std::string Report::Option::command_line_brief() const {
std::string cmdline_brief;
if (this->is_required()) {
cmdline_brief += "(required) ";
}
cmdline_brief += this->brief;
const auto validator = this->parameter->validator<shcore::String_validator>();
if (validator) {
const auto &allowed = validator->allowed();
if (!allowed.empty()) {
cmdline_brief +=
" Allowed values: " + shcore::str_join(allowed, ", ") + ".";
}
}
return cmdline_brief;
}
const uint32_t Report::k_asterisk = std::numeric_limits<uint32_t>::max();
Report::Report(const std::string &name, Type type,
const shcore::Function_base_ref &function)
: m_name(name), m_type(type), m_function(function) {
switch (type) {
case Type::LIST:
m_formatter = list_formatter;
break;
case Type::REPORT:
m_formatter = report_formatter;
break;
case Type::PRINT:
m_formatter = print_formatter;
break;
default:
// formatter needs to be set explicitly
break;
}
}
Report::Report(const std::string &name, Type type,
const Native_report &function)
: Report(name, type,
std::make_shared<Native_report_function>(name, function)) {}
const std::string &Report::name() const { return m_name; }
Report::Type Report::type() const { return m_type; }
const shcore::Function_base_ref &Report::function() const { return m_function; }
const std::string &Report::brief() const { return m_brief; }
void Report::set_brief(const std::string &brief) { m_brief = brief; }
const std::vector<std::string> &Report::details() const { return m_details; }
void Report::set_details(const std::vector<std::string> &details) {
m_details = details;
}
const Report::Options &Report::options() const { return m_options; }
void Report::set_options(const Options &options) {
// Validates options are not in the reserved options for the \watch
// command. Other options such as help and vertical(E) will be properly
// reported as clashes with command line options
std::set<std::string> reserved = {"interval", "nocls", "i"};
for (const auto &o : options) {
validate_option(*o);
if (reserved.find(o->parameter->name) != reserved.end()) {
throw shcore::Exception::argument_error(
"Option '" + o->parameter->name +
"' is reserved for use with the \\watch command.");
}
if (!o->short_name.empty() &&
reserved.find(o->short_name) != reserved.end()) {
throw shcore::Exception::argument_error(
"Short name '" + o->short_name +
"' is reserved for use with the \\watch command.");
}
}
m_options = options;
}
const Report::Argc &Report::argc() const { return m_argc; }
void Report::set_argc(const Argc &argc) {
if (argc.first > argc.second) {
throw shcore::Exception::argument_error(
"The lower limit of 'argc' cannot be greater than upper limit.");
}
m_argc = argc;
}
const Report::Formatter &Report::formatter() const { return m_formatter; }
void Report::set_formatter(const Report::Formatter &formatter) {
m_formatter = formatter;
}
const Report::Examples &Report::examples() const { return m_examples; }
void Report::set_examples(Examples &&examples) {
for (const auto &e : examples) {
validate_example(e);
}
m_examples = std::move(examples);
}
bool Report::requires_options() const {
return m_options.end() !=
std::find_if(m_options.begin(), m_options.end(),
[](const std::shared_ptr<Report::Option> &o) {
return o->parameter->flag ==
shcore::Param_flag::Mandatory;
});
}
bool Report::has_options() const { return !m_options.empty(); }
void Report::validate_option(const Option &option) {
if (!option.short_name.empty()) {
if (option.short_name.length() > 1) {
throw shcore::Exception::argument_error(
"Short name of an option must be exactly one character long.");
}
if (!std::isalnum(option.short_name[0], std::locale{})) {
throw shcore::Exception::argument_error(
"Short name of an option must be an alphanumeric character.");
}
}
validate_option_type(option.parameter->type());
if (shcore::Value_type::Bool == option.parameter->type() &&
option.is_required()) {
throw shcore::Exception::argument_error(
"Option of type 'bool' cannot be required.");
}
if (option.empty && shcore::Value_type::String != option.parameter->type()) {
throw shcore::Exception::argument_error(
"Only option of type 'string' can accept empty values.");
}
}
void Report::validate_example(const Example &example) const {
if (example.args.size() < m_argc.first ||
example.args.size() > m_argc.second) {
throw shcore::Exception::argument_error(
"Report expects " + to_string(m_argc) +
", example has: " + std::to_string(example.args.size()));
}
for (const auto &o : example.options) {
const auto spec = find_option(m_options, o.first);
if (nullptr == spec) {
throw shcore::Exception::argument_error(
"Option named '" + o.first + "' used in example does not exist.");
}
const auto type = spec->parameter->type();
if (type == shcore::Value_type::Bool && "true" != o.second &&
"false" != o.second) {
throw shcore::Exception::argument_error(
"Option named '" + o.first +
"' used in example is a Boolean, should have value 'true' or "
"'false'.");
}
if (type == shcore::Value_type::String && "" == o.second && !spec->empty) {
throw shcore::Exception::argument_error(
"Option named '" + o.first +
"' used in example cannot be an empty string.");
}
}
}
Shell_reports::Shell_reports(const std::string &name,
const std::string &qualified_name)
: Extensible_object(name, qualified_name, true) {
// Do not register the reports for the second time if we're inside of a
// thread.
if (mysqlshdk::utils::in_main_thread()) {
reports::register_query_report(this);
reports::register_threads_report(this);
reports::register_thread_report(this);
}
}
// needs to be defined here due to Shell_reports::Report_options being
// incomplete type
Shell_reports::~Shell_reports() {
auto topic = shcore::Help_registry::get()->get_topic(
get_qualified_name(), true, shcore::Topic_mask::any(), true);
if (topic && topic->is_enabled()) {
// disable help for all the children, including methods except the help
// member This is only needed for the test suite where the Reports object is
// recreated
for (const auto &child : topic->m_childs) {
// Mode does not really matter as "help" is the same in all
if (child->get_name(shcore::IShell_core::Mode::JavaScript) != "help") {
child->set_enabled(false);
}
}
}
}
std::shared_ptr<Parameter_definition> Shell_reports::start_parsing_parameter(
const shcore::Dictionary_t &definition,
shcore::Option_unpacker *unpacker) const {
auto option = std::make_shared<Report::Option>("");
unpacker->optional("shortcut", &option->short_name);
unpacker->optional("empty", &option->empty);
// type is optional here, but base class requires it, insert default value if
// not present
if (definition->end() == definition->find("type")) {
definition->emplace("type", "string");
}
return option;
}
void Shell_reports::register_report(const std::string &name,
const std::string &type,
const shcore::Function_base_ref &report,
const shcore::Dictionary_t &description) {
if (!report) {
throw shcore::Exception::type_error(
"Argument #3 is expected to be a function");
}
auto new_report =
std::make_unique<Report>(name, to_report_type(type), report);
if (description) {
std::string brief;
std::vector<std::string> details;
shcore::Array_t options;
std::string argc;
shcore::Array_t examples;
shcore::Option_unpacker{description}
.optional("brief", &brief)
.optional("details", &details)
.optional("options", &options)
.optional("argc", &argc)
.optional("examples", &examples)
.end();
new_report->set_brief(brief);
new_report->set_details(details);
shcore::Parameter_context context{"", {{"option", {}}}};
new_report->set_options(downcast(
parse_parameters(options, &context, kReportOptionTypes, false)));
new_report->set_argc(get_report_argc(argc));
new_report->set_examples(get_report_examples(examples));
}
register_report(std::move(new_report));
}
void Shell_reports::register_report(std::unique_ptr<Report> report) {
auto normalized_name = normalize_report_name(report->name());
{
const auto it = m_reports.find(normalized_name);
if (m_reports.end() != it) {
const auto error = report->name() == it->second->name()
? "Duplicate report: " + report->name()
: "Name '" + report->name() +
"' conflicts with an existing report: " +
it->second->name();
throw shcore::Exception::argument_error(error);
}
}
std::vector<std::string> details = {
"This is a '" + to_string(report->type()) + "' type report."};
details.insert(details.end(), report->details().begin(),
report->details().end());
std::vector<std::shared_ptr<Parameter_definition>> parameters;
{
// first parameter - session (required)
auto session = std::make_shared<Parameter_definition>(
"session", shcore::Value_type::Object, shcore::Param_flag::Mandatory);
session->brief = "A Session object to be used to execute the report.";
session->parameter->validator<shcore::Object_validator>()->set_allowed(
{"ClassicSession", "Session"});
parameters.emplace_back(std::move(session));
}
// second parameter - argv - if report expects arguments or has any options
if (report->argc().second > 0 || report->has_options()) {
// argv is mandatory if report expects at least one argument or if options
// are required
auto argv = std::make_shared<Parameter_definition>(
"argv", shcore::Value_type::Array,
report->argc().first > 0 || report->requires_options()
? shcore::Param_flag::Mandatory
: shcore::Param_flag::Optional);
argv->brief =
"Extra arguments. Report expects " + to_string(report->argc()) + ".";
argv->parameter->set_validator(
std::make_unique<Argv_validator>(report->argc()));
parameters.emplace_back(std::move(argv));
}
// third parameter - options - only if report has any options
if (report->has_options()) {
// this parameter is mandatory if any of the options is required
auto options = std::make_shared<Parameter_definition>(
"options", shcore::Value_type::Map,
report->requires_options() ? shcore::Param_flag::Mandatory
: shcore::Param_flag::Optional);
options->brief = "Options expected by the report.";
options->set_options(upcast(report->m_options));
parameters.emplace_back(std::move(options));
}
// Make sure the report_option creation does not error before
// attempting registering both the report and the function, for
// that, backups the data needed for the function registration
const auto brief = report->brief();
const auto function = report->function();
const auto examples = get_function_examples(*report);
auto roptions = std::make_unique<Report_options>(std::move(report));
// Reports must have the same name in both Python and JavaScript
auto fd = std::make_shared<Function_definition>(
roptions->name() + "|" + roptions->name(), parameters, brief, details,
examples);
// method should have the same name in both JS and Python
register_function(fd, function);
m_reports.emplace(std::move(normalized_name), std::move(roptions));
}
std::vector<std::string> Shell_reports::list_reports() const {
std::vector<std::string> reports;
std::transform(m_reports.begin(), m_reports.end(),
std::back_inserter(reports),
[](const decltype(m_reports)::value_type &pair) {
return pair.second->name();
});
return reports;
}
std::string Shell_reports::call_report(
const std::string &name, const std::shared_ptr<ShellBaseSession> &session,
const std::vector<std::string> &args) {
// report must exist
const auto report_iterator = m_reports.find(normalize_report_name(name));
if (m_reports.end() == report_iterator) {
throw shcore::Exception::argument_error("Unknown report: " + name);
}
// arguments must be valid
const auto report_options = report_iterator->second.get();
report_options->parse_args(args);
if (report_options->show_help()) {
// get help
return report_options->help();
} else {
// session must be open
if (!session || !session->is_open()) {
throw shcore::Exception::argument_error(
"Executing the report requires an existing, open session.");
}
// prepare arguments
shcore::Argument_list arguments;
arguments.push_back(shcore::Value(session));
if (report_options->requires_argv()) {
arguments.push_back(
shcore::Value(report_options->get_parsed_arguments()));
}
if (report_options->requires_options()) {
// convert args into dictionary
arguments.push_back(shcore::Value(report_options->get_parsed_options()));
}
// call the report
const auto result = call(report_options->name(), arguments);
if (shcore::Value_type::Map != result.type) {
throw shcore::Exception::runtime_error(
"Report should return a dictionary.");
}
shcore::Array_t report;
shcore::Option_unpacker{result.as_map()}
.required(k_report_key, &report)
.end();
if (!report) {
throw shcore::Exception::runtime_error(
"Option 'report' is expected to be of type Array, but is Null");
}
const auto display_options = shcore::make_dict();
display_options->emplace(k_vertical_key, report_options->vertical());
return report_options->formatter()(report, display_options);
}
}
} // namespace mysqlsh