Files
scylladb/utils/enum_option.hh
Nadav Har'El cc69177dcc config: fix printing of experimental feature list
Recently we noticed a regression where with certain versions of the fmt
library,

   SELECT value FROM system.config WHERE name = 'experimental_features'

returns string numbers, like "5", instead of feature names like "raft".

It turns out that the fmt library keep changing their overload resolution
order when there are several ways to print something. For enum_option<T> we
happen to have to conflicting ways to print it:
  1. We have an explicit operator<<.
  2. We have an *implicit* convertor to the type held by T.

We were hoping that the operator<< always wins. But in fmt 8.1, there is
special logic that if the type is convertable to an int, this is used
before operator<<()! For experimental_features_t, the type held in it was
an old-style enum, so it is indeed convertible to int.

The solution I used in this patch is to replace the old-style enum
in experimental_features_t by the newer and more recommended "enum class",
which does not have an implicit conversion to int.

I could have fixed it in other ways, but it wouldn't have been much
prettier. For example, dropping the implicit convertor would require
us to change a bunch of switch() statements over enum_option (and
not just experimental_features_t, but other types of enum_option).

Going forward, all uses of enum_option should use "enum class", not
"enum". tri_mode_restriction_t was already using an enum class, and
now so does experimental_features_t. I changed the examples in the
comments to also use "enum class" instead of enum.

This patch also adds to the existing experimental_features test a
check that the feature names are words that are not numbers.

Fixes #11003.

Signed-off-by: Nadav Har'El <nyh@scylladb.com>

Closes #11004
2022-07-11 09:17:30 +02:00

118 lines
3.8 KiB
C++

/*
* Copyright (C) 2015-present ScyllaDB
*/
/*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
// TODO: upstream this to Boost.
#pragma once
#include <boost/program_options/errors.hpp>
#include <iostream>
#include <sstream>
#include <type_traits>
template<typename T>
concept HasMapInterface = requires(T t) {
typename std::remove_reference<T>::type::mapped_type;
typename std::remove_reference<T>::type::key_type;
typename std::remove_reference<T>::type::value_type;
t.find(typename std::remove_reference<T>::type::key_type());
t.begin();
t.end();
t.cbegin();
t.cend();
};
/// A Boost program option holding an enum value.
///
/// The options parser will parse enum values with the help of the Mapper class, which provides a mapping
/// between some parsable form (eg, string) and the enum value. For example, it may map the word "January" to
/// the enum value JANUARY.
///
/// Mapper must have a static method `map()` that returns a map from a streamable key type (eg, string) to the
/// enum in question. In fact, enum_option knows which enum it represents only by referencing
/// Mapper::map().mapped_type.
///
/// \note one enum_option holds only one enum value. When multiple choices are allowed, use
/// vector<enum_option>.
///
/// Example:
///
/// struct Type {
/// enum class ty { a1, a2, b1 };
/// static unordered_map<string, ty> map();
/// };
/// unordered_map<string, Type::ty> Type::map() {
/// return {{"a1", Type::ty::a1}, {"a2", Type::ty::a2}, {"b1", Type::ty::b1}};
/// }
/// int main(int ac, char* av[]) {
/// namespace po = boost::program_options;
/// po::options_description desc("Allowed options");
/// desc.add_options()
/// ("val", po::value<enum_option<Type>>(), "Single Type")
/// ("vec", po::value<vector<enum_option<Type>>>()->multitoken(), "Type vector");
/// }
template<typename Mapper>
requires HasMapInterface<decltype(Mapper::map())>
class enum_option {
using map_t = typename std::remove_reference<decltype(Mapper::map())>::type;
typename map_t::mapped_type _value;
map_t _map;
public:
// For smooth conversion from enum values:
enum_option(const typename map_t::mapped_type& v) : _value(v), _map(Mapper::map()) {}
// So values can be default-constructed before streaming into them:
enum_option() : _map(Mapper::map()) {}
bool operator==(const enum_option<Mapper>& that) const {
return _value == that._value;
}
// For comparison with enum values using if or switch:
bool operator==(typename map_t::mapped_type value) const {
return _value == value;
}
operator typename map_t::mapped_type() const {
return _value;
}
// For program_options parser:
friend std::istream& operator>>(std::istream& s, enum_option<Mapper>& opt) {
typename map_t::key_type key;
s >> key;
const auto found = opt._map.find(key);
if (found == opt._map.end()) {
std::string text;
if (s.rdstate() & s.failbit) {
// key wasn't read successfully.
s >> text;
} else {
// Turn key into text.
std::ostringstream temp;
temp << key;
text = temp.str();
}
throw boost::program_options::invalid_option_value(text);
}
opt._value = found->second;
return s;
}
// For various printers and formatters:
friend std::ostream& operator<<(std::ostream& s, const enum_option<Mapper>& opt) {
auto found = find_if(opt._map.cbegin(), opt._map.cend(),
[&opt](const typename map_t::value_type& e) { return e.second == opt._value; });
if (found == opt._map.cend()) {
return s << "?unknown";
} else {
return s << found->first;
}
}
};