diff --git a/docs/supported_formats/csv.md b/docs/supported_formats/csv.md index 0065ea67..eeb2fb6e 100644 --- a/docs/supported_formats/csv.md +++ b/docs/supported_formats/csv.md @@ -40,24 +40,35 @@ const rfl::Result> result = rfl::csv::read()` accessor that takes either a pointer-to-member or +the field name as a template argument and returns a new copy of `Settings` +with the chosen field replaced: ```cpp const auto settings = rfl::csv::Settings{} - .with_delimiter(';') - .with_quoting(true) - .with_quote_char('"') - .with_null_string("n/a") - .with_double_quote(true) - .with_escaping(false) - .with_escape_char('\\') - .with_newlines_in_values(false) - .with_ignore_empty_lines(true) - .with_batch_size(1024); + .with<&rfl::csv::Settings::delimiter>(';') + .with<&rfl::csv::Settings::quoting>(true) + .with<&rfl::csv::Settings::quote_char>('"') + .with<&rfl::csv::Settings::null_string>("n/a") + .with<&rfl::csv::Settings::double_quote>(true) + .with<&rfl::csv::Settings::escaping>(false) + .with<&rfl::csv::Settings::escape_char>('\\') + .with<&rfl::csv::Settings::newlines_in_values>(false) + .with<&rfl::csv::Settings::ignore_empty_lines>(true) + .with<&rfl::csv::Settings::batch_size>(1024); const std::string csv_text = rfl::csv::write(people, settings); ``` +The same call is also available with a string literal naming the field: + +```cpp +const auto settings = rfl::csv::Settings{} + .with<"delimiter">(';') + .with<"quoting">(true); +``` + Key options: - `batch_size` - Maximum number of rows processed per batch (performance tuning) - `delimiter` - Field delimiter character @@ -84,7 +95,7 @@ rfl::csv::save("/path/to/file.csv", people); With custom settings: ```cpp -const auto settings = rfl::csv::Settings{}.with_delimiter(';'); +const auto settings = rfl::csv::Settings{}.with<"delimiter">(';'); rfl::csv::save("/path/to/file.csv", people, settings); ``` @@ -102,7 +113,7 @@ rfl::csv::write(people, my_ostream); With custom settings: ```cpp -const auto settings = rfl::csv::Settings{}.with_delimiter(';'); +const auto settings = rfl::csv::Settings{}.with<"delimiter">(';'); rfl::csv::write(people, my_ostream, settings); ``` diff --git a/docs/supported_formats/parquet.md b/docs/supported_formats/parquet.md index 0cba3e1b..b4723061 100644 --- a/docs/supported_formats/parquet.md +++ b/docs/supported_formats/parquet.md @@ -39,16 +39,27 @@ const rfl::Result> result = rfl::parquet::read()` accessor that takes either a pointer-to-member or the +field name as a template argument and returns a new copy of `Settings` with +the chosen field replaced: ```cpp const auto settings = rfl::parquet::Settings{} - .with_compression(rfl::parquet::Compression::GZIP) - .with_chunksize(1000); + .with<&rfl::parquet::Settings::compression>(rfl::parquet::Compression::GZIP) + .with<&rfl::parquet::Settings::chunksize>(1000); const std::vector bytes = rfl::parquet::write(people, settings); ``` +The same call is also available with a string literal naming the field: + +```cpp +const auto settings = rfl::parquet::Settings{} + .with<"compression">(rfl::parquet::Compression::GZIP); +``` + Available compression options include: - `UNCOMPRESSED` - No compression, fastest read/write but largest file size @@ -65,16 +76,16 @@ Available compression options include: ```cpp // Examples of different compression settings const auto snappy_settings = rfl::parquet::Settings{} - .with_compression(rfl::parquet::Compression::SNAPPY); + .with<&rfl::parquet::Settings::compression>(rfl::parquet::Compression::SNAPPY); const auto gzip_settings = rfl::parquet::Settings{} - .with_compression(rfl::parquet::Compression::GZIP); + .with<&rfl::parquet::Settings::compression>(rfl::parquet::Compression::GZIP); const auto zstd_settings = rfl::parquet::Settings{} - .with_compression(rfl::parquet::Compression::ZSTD); + .with<&rfl::parquet::Settings::compression>(rfl::parquet::Compression::ZSTD); const auto uncompressed_settings = rfl::parquet::Settings{} - .with_compression(rfl::parquet::Compression::UNCOMPRESSED); + .with<&rfl::parquet::Settings::compression>(rfl::parquet::Compression::UNCOMPRESSED); ``` ## Loading and saving @@ -92,7 +103,7 @@ With custom settings: ```cpp const auto settings = rfl::parquet::Settings{} - .with_compression(rfl::parquet::Compression::GZIP); + .with<&rfl::parquet::Settings::compression>(rfl::parquet::Compression::GZIP); rfl::parquet::save("/path/to/file.parquet", people, settings); ``` @@ -111,7 +122,7 @@ With custom settings: ```cpp const auto settings = rfl::parquet::Settings{} - .with_compression(rfl::parquet::Compression::GZIP); + .with<&rfl::parquet::Settings::compression>(rfl::parquet::Compression::GZIP); rfl::parquet::write(people, my_ostream, settings); ``` diff --git a/include/rfl.hpp b/include/rfl.hpp index a8c6dcff..f0d0be8a 100644 --- a/include/rfl.hpp +++ b/include/rfl.hpp @@ -80,6 +80,7 @@ #include "rfl/patterns.hpp" #include "rfl/remove_fields.hpp" #include "rfl/replace.hpp" +#include "rfl/Settings.hpp" #include "rfl/to_generic.hpp" #include "rfl/to_named_tuple.hpp" #include "rfl/to_view.hpp" diff --git a/include/rfl/Settings.hpp b/include/rfl/Settings.hpp new file mode 100644 index 00000000..a1cf2c08 --- /dev/null +++ b/include/rfl/Settings.hpp @@ -0,0 +1,103 @@ +#ifndef RFL_SETTINGS_HPP_ +#define RFL_SETTINGS_HPP_ + +#include +#include +#include + +#include "Field.hpp" +#include "internal/StringLiteral.hpp" +#include "internal/field_index_by_name.hpp" +#include "internal/field_index_from_ptm.hpp" +#include "internal/get_field_names.hpp" +#include "internal/get_ith_field_from_fake_object.hpp" +#include "replace.hpp" + +namespace rfl::internal { + +template +struct member_ptr_traits : std::false_type {}; + +template +struct member_ptr_traits + : std::bool_constant && + std::is_const_v> {}; + +template +concept const_member_of = member_ptr_traits::value; + +/// Returns a copy of `_obj` with the field designated by `FieldPtr` replaced +/// by `_value`. Used as the body of the with() method generated by +/// RFL_SETTINGS_OPS. Field name is recovered through the fake-object path +/// that the rest of reflect-cpp uses, so MSVC parses it correctly. +template + requires const_member_of +T settings_with_replaced( + const T& _obj, + std::remove_const_t< + std::remove_reference_t().*FieldPtr)>> + _value) { + constexpr std::size_t I = field_index_v; + static_assert( + I != static_cast(-1), + "FieldPtr does not refer to a non-static data member of T. " + "Make sure you pass &T::field, where field is declared inside T."); + constexpr auto name = + get_field_name_str_lit()>(); + return rfl::replace(_obj, rfl::make_field(std::move(_value))); +} + +/// Returns a copy of `_obj` with the field with the given Name replaced by +/// `_value`. Used as the body of the with<"name">() overload generated by +/// RFL_SETTINGS_OPS. +template +T settings_with_replaced_by_name( + const T& _obj, + field_value_type_at_t> _value) { + constexpr std::size_t I = field_index_by_name_v; + static_assert(I != static_cast(-1), + "No field with the given name exists in T."); +#ifdef _MSC_VER + static_assert(ith_field_is_const(I)>(), + "Fields in Settings structs must be const."); +#endif + return rfl::replace(_obj, rfl::make_field(std::move(_value))); +} + +} // namespace rfl::internal + +/// Defines the standard with<&T::field>(value) and with<"field">(value) +/// accessors inside a settings struct. The struct must be a flat aggregate +/// (no base classes). Fields should be declared const so the only way to +/// mutate them is via with(), which returns a new copy with the chosen field +/// replaced. Place the macro at the end of the struct body, after all data +/// members. +/// +/// Usage: +/// struct MySettings { +/// const int some_option = 42; +/// RFL_SETTINGS_OPS(MySettings) +/// }; +/// +/// auto a = MySettings{}.with<&MySettings::some_option>(100); +/// auto b = MySettings{}.with<"some_option">(100); +#define RFL_SETTINGS_OPS(Derived) \ + template \ + requires ::rfl::internal::const_member_of \ + Derived with(std::remove_const_t().*FieldPtr)>> \ + _value) const { \ + return ::rfl::internal::settings_with_replaced( \ + *this, std::move(_value)); \ + } \ + template <::rfl::internal::StringLiteral Name> \ + Derived with(::rfl::internal::field_value_type_at_t< \ + Derived, \ + ::rfl::internal::field_index_by_name_v> \ + _value) const { \ + return ::rfl::internal::settings_with_replaced_by_name( \ + *this, std::move(_value)); \ + } + +#endif diff --git a/include/rfl/csv/Settings.hpp b/include/rfl/csv/Settings.hpp index fa015213..bf9573a4 100644 --- a/include/rfl/csv/Settings.hpp +++ b/include/rfl/csv/Settings.hpp @@ -4,8 +4,8 @@ #include #include -#include "../Field.hpp" -#include "../replace.hpp" +#include "../Settings.hpp" +#include "../internal/deprecated_with.hpp" namespace rfl::csv { @@ -13,78 +13,90 @@ struct Settings { /// Maximum number of rows processed at a time. /// Data is processed in batches of N rows. This number /// can impact performance. - int32_t batch_size = 1024; + const int32_t batch_size = 1024; /// Field delimiter. - char delimiter = ','; + const char delimiter = ','; /// Whether quoting is used. - bool quoting = true; + const bool quoting = true; /// Quoting character (if quoting is true). Only relevant for reading. - char quote_char = '"'; + const char quote_char = '"'; /// The string to be used for null values. Quotes are not allowed in this /// string. - std::string null_string = "n/a"; + const std::string null_string = "n/a"; /// Whether a quote inside a value is double-quoted. Only relevant for /// reading. - bool double_quote = true; + const bool double_quote = true; /// Whether escaping is used. Only relevant for reading. - bool escaping = false; + const bool escaping = false; /// Escaping character (if escaping is true). Only relevant for reading. - char escape_char = arrow::csv::kDefaultEscapeChar; + const char escape_char = arrow::csv::kDefaultEscapeChar; /// Whether values are allowed to contain CR (0x0d) and LF (0x0a) /// characters. Only relevant for reading. - bool newlines_in_values = false; + const bool newlines_in_values = false; /// Whether empty lines are ignored. /// If false, an empty line represents a single empty value (assuming a /// one-column CSV file). Only relevant for reading. - bool ignore_empty_lines = true; + const bool ignore_empty_lines = true; + RFL_SETTINGS_OPS(Settings) + + RFL_DEPRECATED_WITH(batch_size) Settings with_batch_size(const int32_t _batch_size) const noexcept { return replace(*this, make_field<"batch_size">(_batch_size)); } + RFL_DEPRECATED_WITH(delimiter) Settings with_delimiter(const char _delimiter) const noexcept { return replace(*this, make_field<"delimiter">(_delimiter)); } + RFL_DEPRECATED_WITH(quoting) Settings with_quoting(const bool _quoting) const noexcept { return replace(*this, make_field<"quoting">(_quoting)); } + RFL_DEPRECATED_WITH(quote_char) Settings with_quote_char(const char _quote_char) const noexcept { return replace(*this, make_field<"quote_char">(_quote_char)); } + RFL_DEPRECATED_WITH(null_string) Settings with_null_string(const std::string& _null_string) const noexcept { return replace(*this, make_field<"null_string">(_null_string)); } + RFL_DEPRECATED_WITH(double_quote) Settings with_double_quote(const bool _double_quote) const noexcept { return replace(*this, make_field<"double_quote">(_double_quote)); } + RFL_DEPRECATED_WITH(escaping) Settings with_escaping(const bool _escaping) const noexcept { return replace(*this, make_field<"escaping">(_escaping)); } + RFL_DEPRECATED_WITH(escape_char) Settings with_escape_char(const char _escape_char) const noexcept { return replace(*this, make_field<"escape_char">(_escape_char)); } + RFL_DEPRECATED_WITH(newlines_in_values) Settings with_newlines_in_values( const bool _newlines_in_values) const noexcept { return replace(*this, make_field<"newlines_in_values">(_newlines_in_values)); } + RFL_DEPRECATED_WITH(ignore_empty_lines) Settings with_ignore_empty_lines( const bool _ignore_empty_lines) const noexcept { return replace(*this, diff --git a/include/rfl/internal/deprecated_with.hpp b/include/rfl/internal/deprecated_with.hpp new file mode 100644 index 00000000..e4ebb739 --- /dev/null +++ b/include/rfl/internal/deprecated_with.hpp @@ -0,0 +1,12 @@ +#ifndef RFL_INTERNAL_DEPRECATED_WITH_HPP_ +#define RFL_INTERNAL_DEPRECATED_WITH_HPP_ + +/// Marks a legacy with_() method as deprecated, pointing at the +/// unified RFL_SETTINGS_OPS-generated with<&T::field>() / with<"field">() +/// API. Used in csv::Settings and parquet::Settings to keep the old +/// per-field setter names working for existing users. +#define RFL_DEPRECATED_WITH(field_name) \ + [[deprecated("Use .with<&Settings::" #field_name ">(value) or " \ + ".with<\"" #field_name "\">(value) instead.")]] + +#endif diff --git a/include/rfl/internal/field_index_by_name.hpp b/include/rfl/internal/field_index_by_name.hpp new file mode 100644 index 00000000..52609b51 --- /dev/null +++ b/include/rfl/internal/field_index_by_name.hpp @@ -0,0 +1,69 @@ +#ifndef RFL_INTERNAL_FIELD_INDEX_BY_NAME_HPP_ +#define RFL_INTERNAL_FIELD_INDEX_BY_NAME_HPP_ + +#include +#include +#include + +#include "StringLiteral.hpp" +#include "field_index_from_ptm.hpp" +#include "get_field_names.hpp" +#include "get_ith_field_from_fake_object.hpp" +#include "num_fields.hpp" + +namespace rfl::internal { + +/// Returns the name of the i-th field of T as a string_view, using the same +/// fake-object name-lookup path that to_named_tuple uses. +template +consteval std::string_view field_name_at() { + return get_field_name_str_view()>(); +} + +template +consteval std::size_t field_index_by_name_impl(std::index_sequence) { + std::size_t result = static_cast(-1); + ((field_name_at() == Name.string_view() + ? (result = Is, true) + : false) || + ...); + return result; +} + +/// Finds the index of the field with the given Name in T. Returns size_t(-1) +/// if no field with that name exists; callers must produce a static_assert. +template +inline constexpr std::size_t field_index_by_name_v = + field_index_by_name_impl( + std::make_index_sequence>{}); + +/// Helper: extracts the cv-stripped value type of the i-th field of T, +/// suitable as a function parameter or local variable type. Use this when +/// you need to declare an object of the field's type; do not use it to +/// inspect const-ness of the original field declaration (cv is removed). +template +struct field_value_type_at { + using type = std::remove_cv_t(I)>())>>; +}; + +/// Sentinel value type used when field_index_by_name_v fails to find a +/// matching field. The caller's static_assert then fires before any +/// real call site can use this type. +struct unresolved_field_type {}; + +/// When the index is the sentinel size_t(-1), return a placeholder type so +/// that a failing field_index_by_name_v lookup does not trip an out-of-range +/// instantiation before the caller's static_assert can fire. +template +struct field_value_type_at(-1)> { + using type = unresolved_field_type; +}; + +template +using field_value_type_at_t = typename field_value_type_at::type; + +} // namespace rfl::internal + +#endif diff --git a/include/rfl/internal/field_index_from_ptm.hpp b/include/rfl/internal/field_index_from_ptm.hpp new file mode 100644 index 00000000..22a7366a --- /dev/null +++ b/include/rfl/internal/field_index_from_ptm.hpp @@ -0,0 +1,83 @@ +#ifndef RFL_INTERNAL_FIELD_INDEX_FROM_PTM_HPP_ +#define RFL_INTERNAL_FIELD_INDEX_FROM_PTM_HPP_ + +#include +#include + +#ifdef _MSC_VER +#include +#endif + +#include "get_fake_object.hpp" +#include "get_field_names.hpp" +#include "get_ith_field_from_fake_object.hpp" +#include "num_fields.hpp" + +namespace rfl::internal { + +/// Finds the index of the data member designated by `FieldPtr` in T by +/// comparing the address of (obj.*FieldPtr) against the addresses of T's +/// structured-binding fields. All comparisons are between subobject pointers +/// of the same object, which is well-defined in a constant expression. +/// +/// On GCC/Clang we use the extern-const fake object — fields can be of any +/// type (including non-literal `std::string` on older standard libraries), +/// since the object is never actually instantiated, only address-folded +/// inside the `consteval` evaluation. +/// +/// MSVC refuses to constant-fold &(fake_object.*FieldPtr) on an extern-const +/// symbol, so on MSVC we construct a local default-initialised T instead. +/// This requires T (and the field type) to be a constexpr literal type, but +/// settings structs in practice satisfy this when targeting MSVC builds. +/// +/// Returns size_t(-1) if no match is found, in which case callers must +/// produce a static_assert. +template +consteval std::size_t field_index_from_ptm_impl(std::index_sequence) { +#ifdef _MSC_VER + static_assert(std::is_default_constructible_v, + "On MSVC, Settings structs used with " + "rfl::Settings::with<&T::field>() must be " + "default-constructible (constexpr literal type)."); + T obj{}; + const void* target = static_cast(&(obj.*FieldPtr)); + std::size_t result = static_cast(-1); + ((static_cast(get_ith_field_ptr(Is)>(obj)) == + target + ? (result = Is, true) + : false) || + ...); + return result; +#else + const void* target = + static_cast(&(get_fake_object().*FieldPtr)); + std::size_t result = static_cast(-1); + ((static_cast(get_ith_field_from_fake_object()) == target + ? (result = Is, true) + : false) || + ...); + return result; +#endif +} + +template +inline constexpr std::size_t field_index_v = + field_index_from_ptm_impl( + std::make_index_sequence>{}); + +/// Returns the i-th field pointer of the fake object of T in the form +/// `get_field_name_str_lit` expects on the current compiler. On clang the +/// pointer is wrapped in `Wrapper` because clang's parser of __PRETTY_FUNCTION__ +/// extracts the field name from the wrapper's type, not from the value. +template +consteval auto fake_field_ptr_for_name_lookup() { +#if defined(__clang__) + return wrap(get_ith_field_from_fake_object(I)>()); +#else + return get_ith_field_from_fake_object(I)>(); +#endif +} + +} // namespace rfl::internal + +#endif diff --git a/include/rfl/internal/get_ith_field_from_fake_object.hpp b/include/rfl/internal/get_ith_field_from_fake_object.hpp index 6707ae11..2820b6a6 100644 --- a/include/rfl/internal/get_ith_field_from_fake_object.hpp +++ b/include/rfl/internal/get_ith_field_from_fake_object.hpp @@ -12,9 +12,9 @@ namespace rfl::internal { template -struct fake_object_helper { - template - static consteval auto get_field() { +struct field_extractor { + template + static consteval auto get_ptr(U&) { static_assert( rfl::always_false_v, "\n\nThis error occurs for one of two reasons:\n\n" @@ -33,11 +33,11 @@ struct fake_object_helper { #define RFL_INTERNAL_FAKE_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS( \ n, ...) \ template \ - struct fake_object_helper { \ - template \ - static consteval auto get_field() { \ - const auto& [__VA_ARGS__] = get_fake_object>(); \ - const auto get_ptrs = [](const auto&... _refs) { \ + struct field_extractor { \ + template \ + static consteval auto get_ptr(U& _obj) { \ + auto& [__VA_ARGS__] = _obj; \ + const auto get_ptrs = [](auto&... _refs) { \ return nth_element<_i>(&_refs...); \ }; \ return get_ptrs(__VA_ARGS__); \ @@ -2815,10 +2815,35 @@ RFL_INTERNAL_FAKE_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS( #undef RFL_INTERNAL_FAKE_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS +template +consteval auto get_ith_field_ptr(U& _obj) { + return field_extractor>::template get_ptr<_i>(_obj); +} + template consteval auto get_ith_field_from_fake_object() { - return fake_object_helper>::template get_field<_i>(); + return get_ith_field_ptr(get_fake_object>()); +} + +/// Returns whether the i-th field of T is declared const. Implemented by +/// inspecting the type that get_ith_field_ptr deduces from a non-const T, +/// where structured-binding references inherit the field's cv-qualification. +/// +/// Only available on MSVC: the implementation requires constructing a local +/// T{} inside `consteval`, which forces T (and every field) to be a +/// constexpr literal type. Settings structs in reflect-cpp routinely contain +/// `std::string`, which is not a literal type on older standard libraries +/// (e.g. libstdc++ shipped with GCC 11), so on GCC/Clang this check is +/// skipped — the name-based with<"name">() path then falls through to +/// `rfl::replace`, which will fail to assign into a const member. +#ifdef _MSC_VER +template +consteval bool ith_field_is_const() { + std::remove_cvref_t _obj{}; + using FieldRef = decltype(*get_ith_field_ptr(_obj)); + return std::is_const_v>; } +#endif } // namespace rfl::internal diff --git a/include/rfl/parquet/Settings.hpp b/include/rfl/parquet/Settings.hpp index 2ba65def..026b7f7d 100644 --- a/include/rfl/parquet/Settings.hpp +++ b/include/rfl/parquet/Settings.hpp @@ -4,8 +4,8 @@ #include #include -#include "../Field.hpp" -#include "../replace.hpp" +#include "../Settings.hpp" +#include "../internal/deprecated_with.hpp" namespace rfl::parquet { @@ -13,15 +13,19 @@ using Compression = arrow::Compression::type; struct Settings { /// The size of the chunks of the parquet file. - size_t chunksize = 2000; + const size_t chunksize = 2000; /// The compression algorithm used to compress the parquet file. - Compression compression = Compression::SNAPPY; + const Compression compression = Compression::SNAPPY; + RFL_SETTINGS_OPS(Settings) + + RFL_DEPRECATED_WITH(chunksize) Settings with_chunksize(const size_t _chunksize) const noexcept { return replace(*this, make_field<"chunksize">(_chunksize)); } + RFL_DEPRECATED_WITH(compression) Settings with_compression(const Compression _compression) const noexcept { return replace(*this, make_field<"compression">(_compression)); } diff --git a/tests/cli/test_settings_macro.cpp b/tests/cli/test_settings_macro.cpp new file mode 100644 index 00000000..d30d28e2 --- /dev/null +++ b/tests/cli/test_settings_macro.cpp @@ -0,0 +1,75 @@ +#include +#include +#include + +namespace test_settings_macro { + +struct DemoSettings { + const int batch_size = 1024; + const char delimiter = ','; + const std::string null_string = "n/a"; + const bool quoting = true; + + RFL_SETTINGS_OPS(DemoSettings) +}; + +TEST(settings_macro, single_field_replace_returns_new_copy) { + const DemoSettings original{}; + const auto modified = + original.with<&DemoSettings::delimiter>(';'); + + EXPECT_EQ(original.delimiter, ','); + EXPECT_EQ(modified.delimiter, ';'); + EXPECT_EQ(modified.batch_size, original.batch_size); + EXPECT_EQ(modified.null_string, original.null_string); + EXPECT_EQ(modified.quoting, original.quoting); +} + +TEST(settings_macro, chained_with_calls_apply_all_replacements) { + const auto modified = DemoSettings{} + .with<&DemoSettings::batch_size>(2048) + .with<&DemoSettings::delimiter>('|') + .with<&DemoSettings::null_string>(std::string{"NA"}) + .with<&DemoSettings::quoting>(false); + + EXPECT_EQ(modified.batch_size, 2048); + EXPECT_EQ(modified.delimiter, '|'); + EXPECT_EQ(modified.null_string, "NA"); + EXPECT_FALSE(modified.quoting); +} + +TEST(settings_macro, replace_string_field_moves_value) { + // The with<> parameter is passed by value, so passing an rvalue lets the + // implementation move into make_field. Result must equal the source. + std::string moved_in = "moved-in-value"; + const auto modified = + DemoSettings{}.with<&DemoSettings::null_string>(std::move(moved_in)); + EXPECT_EQ(modified.null_string, "moved-in-value"); +} + +TEST(settings_macro, by_name_replace_works_for_each_field) { + const auto modified = DemoSettings{}.with<"delimiter">(';'); + EXPECT_EQ(modified.delimiter, ';'); + EXPECT_EQ(modified.batch_size, 1024); +} + +TEST(settings_macro, by_name_chained_calls_apply_all_replacements) { + const auto modified = DemoSettings{} + .with<"batch_size">(4096) + .with<"delimiter">('\t') + .with<"null_string">(std::string{"NULL"}) + .with<"quoting">(false); + EXPECT_EQ(modified.batch_size, 4096); + EXPECT_EQ(modified.delimiter, '\t'); + EXPECT_EQ(modified.null_string, "NULL"); + EXPECT_FALSE(modified.quoting); +} + +TEST(settings_macro, by_name_and_by_ptm_are_interchangeable) { + const auto by_ptm = DemoSettings{}.with<&DemoSettings::delimiter>(';'); + const auto by_name = DemoSettings{}.with<"delimiter">(';'); + EXPECT_EQ(by_ptm.delimiter, by_name.delimiter); + EXPECT_EQ(by_ptm.batch_size, by_name.batch_size); +} + +} // namespace test_settings_macro diff --git a/tests/csv/test_save_load.cpp b/tests/csv/test_save_load.cpp index 14ef4ff2..43abf314 100644 --- a/tests/csv/test_save_load.cpp +++ b/tests/csv/test_save_load.cpp @@ -39,7 +39,7 @@ TEST(csv, test_save_load) { .age = 45, .email = "homer@simpson.com"}}); - const auto settings = rfl::csv::Settings{}.with_delimiter(';'); + const auto settings = rfl::csv::Settings{}.with<&rfl::csv::Settings::delimiter>(';'); rfl::csv::save("people.csv", people1, settings); diff --git a/tests/csv/test_settings.cpp b/tests/csv/test_settings.cpp index ca3ddbb1..03ad0298 100644 --- a/tests/csv/test_settings.cpp +++ b/tests/csv/test_settings.cpp @@ -36,7 +36,7 @@ TEST(csv, test_settings) { .age = 45, .email = "homer@simpson.com"}}); - const auto settings = rfl::csv::Settings{}.with_delimiter(';'); + const auto settings = rfl::csv::Settings{}.with<&rfl::csv::Settings::delimiter>(';'); write_and_read(people, settings); } diff --git a/tests/parquet/test_gzip.cpp b/tests/parquet/test_gzip.cpp index 62f45f7f..cf600d89 100644 --- a/tests/parquet/test_gzip.cpp +++ b/tests/parquet/test_gzip.cpp @@ -36,7 +36,7 @@ TEST(parquet, test_gzip) { .age = 45, .email = "homer@simpson.com"}}); - const auto settings = rfl::parquet::Settings{}.with_compression( + const auto settings = rfl::parquet::Settings{}.with<&rfl::parquet::Settings::compression>( rfl::parquet::Compression::GZIP); write_and_read(people, settings);