Skip to content

Commit 7b2ffd8

Browse files
authored
custom optional support (#2390)
* custom optional support * Update reflect.hpp * is_custom_field_null * Update nullable_lambda_test.cpp
1 parent 50c57ba commit 7b2ffd8

File tree

5 files changed

+272
-1
lines changed

5 files changed

+272
-1
lines changed

include/glaze/beve/write.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,12 @@ namespace glz
13191319
}
13201320
}
13211321
}
1322+
else if constexpr (is_specialization_v<val_t, custom_t> && Options.skip_null_members &&
1323+
custom_getter_returns_nullable<val_t>()) {
1324+
if (!is_custom_field_null<T, I>(value, t, ctx)) {
1325+
++member_count;
1326+
}
1327+
}
13221328
else {
13231329
++member_count;
13241330
}
@@ -1385,6 +1391,12 @@ namespace glz
13851391
}
13861392
}
13871393
}
1394+
else if constexpr (is_specialization_v<val_t, custom_t> && Options.skip_null_members &&
1395+
custom_getter_returns_nullable<val_t>()) {
1396+
if (is_custom_field_null<T, I>(value, t, ctx)) {
1397+
return;
1398+
}
1399+
}
13881400

13891401
static constexpr sv key = reflect<T>::keys[I];
13901402
to<BEVE, std::remove_cvref_t<decltype(key)>>::template no_header_cx<key.size()>(key, ctx, b, ix);

include/glaze/cbor/write.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,12 @@ namespace glz
682682
}
683683
}
684684
}
685+
else if constexpr (is_specialization_v<val_t, custom_t> && Opts.skip_null_members &&
686+
custom_getter_returns_nullable<val_t>()) {
687+
if (!is_custom_field_null<T, I>(value, t, ctx)) {
688+
++member_count;
689+
}
690+
}
685691
else {
686692
++member_count;
687693
}
@@ -733,6 +739,12 @@ namespace glz
733739
}
734740
}
735741
}
742+
else if constexpr (is_specialization_v<val_t, custom_t> && Opts.skip_null_members &&
743+
custom_getter_returns_nullable<val_t>()) {
744+
if (is_custom_field_null<T, I>(value, t, ctx)) {
745+
return;
746+
}
747+
}
736748

737749
static constexpr sv key = reflect<T>::keys[I];
738750
// Write key as text string

include/glaze/core/reflect.hpp

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,54 @@ namespace glz
380380
template <class T, size_t I>
381381
using field_t = std::remove_cvref_t<refl_t<T, I>>;
382382

383+
// Check if a custom_t getter (To) returns a nullable type
384+
template <class V>
385+
consteval bool custom_getter_returns_nullable()
386+
{
387+
if constexpr (!is_specialization_v<V, custom_t>) {
388+
return false;
389+
}
390+
else {
391+
using To = typename V::to_t;
392+
using ParentT = std::remove_reference_t<decltype(std::declval<V&>().val)>;
393+
394+
if constexpr (std::is_member_pointer_v<To>) {
395+
if constexpr (std::is_member_function_pointer_v<To>) {
396+
using Ret = std::decay_t<typename return_type<To>::type>;
397+
return null_t<Ret>;
398+
}
399+
else if constexpr (std::is_member_object_pointer_v<To>) {
400+
using Value = std::decay_t<decltype(std::declval<ParentT&>().*(std::declval<To>()))>;
401+
if constexpr (is_specialization_v<Value, std::function>) {
402+
using Ret = std::decay_t<typename function_traits<Value>::result_type>;
403+
return null_t<Ret>;
404+
}
405+
else {
406+
return null_t<Value>;
407+
}
408+
}
409+
else {
410+
return false;
411+
}
412+
}
413+
else if constexpr (std::invocable<To, ParentT&>) {
414+
using Ret = std::decay_t<std::invoke_result_t<To, ParentT&>>;
415+
return null_t<Ret>;
416+
}
417+
else if constexpr (std::invocable<To, const ParentT&>) {
418+
using Ret = std::decay_t<std::invoke_result_t<To, const ParentT&>>;
419+
return null_t<Ret>;
420+
}
421+
else if constexpr (std::invocable<To, ParentT&, context&>) {
422+
using Ret = std::decay_t<std::invoke_result_t<To, ParentT&, context&>>;
423+
return null_t<Ret>;
424+
}
425+
else {
426+
return false;
427+
}
428+
}
429+
}
430+
383431
template <auto Opts, class T>
384432
inline constexpr bool maybe_skipped = [] {
385433
if constexpr (reflect<T>::size > 0) {
@@ -393,7 +441,9 @@ namespace glz
393441
return [&]<size_t... I>(std::index_sequence<I...>) {
394442
return ((always_skipped<field_t<T, I>> ||
395443
(!write_function_pointers && is_member_function_pointer<field_t<T, I>>) ||
396-
null_t<field_t<T, I>>) ||
444+
null_t<field_t<T, I>> ||
445+
(is_specialization_v<field_t<T, I>, custom_t> &&
446+
custom_getter_returns_nullable<field_t<T, I>>())) ||
397447
...);
398448
}(std::make_index_sequence<N>{});
399449
}
@@ -474,6 +524,68 @@ namespace glz
474524
return false;
475525
}
476526

527+
// Runtime check: invoke a custom_t getter and return whether the result is null
528+
template <class V>
529+
bool custom_getter_is_null(V&& custom_val, auto&& ctx)
530+
{
531+
using CV = std::remove_cvref_t<V>;
532+
using To = typename CV::to_t;
533+
534+
auto check_null = [](auto&& result) {
535+
using Ret = std::decay_t<decltype(result)>;
536+
if constexpr (nullable_value_t<Ret>) {
537+
return !result.has_value();
538+
}
539+
else {
540+
return !bool(result);
541+
}
542+
};
543+
544+
if constexpr (std::is_member_pointer_v<To>) {
545+
if constexpr (std::is_member_function_pointer_v<To>) {
546+
return check_null((custom_val.val.*(custom_val.to))());
547+
}
548+
else if constexpr (std::is_member_object_pointer_v<To>) {
549+
auto& to_val = custom_val.val.*(custom_val.to);
550+
using Func = std::decay_t<decltype(to_val)>;
551+
if constexpr (is_specialization_v<Func, std::function>) {
552+
return check_null(to_val());
553+
}
554+
else {
555+
return check_null(to_val);
556+
}
557+
}
558+
else {
559+
return false;
560+
}
561+
}
562+
else if constexpr (std::invocable<To, decltype(custom_val.val)>) {
563+
return check_null(std::invoke(custom_val.to, custom_val.val));
564+
}
565+
else if constexpr (std::invocable<To, decltype(custom_val.val), std::remove_reference_t<decltype(ctx)>&>) {
566+
return check_null(std::invoke(custom_val.to, custom_val.val, ctx));
567+
}
568+
else {
569+
return false;
570+
}
571+
}
572+
573+
// Check if a custom_t field at index I is null, given the parent value and tie.
574+
// Used by JSON/CBOR/BEVE write paths to skip null custom getter results.
575+
template <class T, size_t I, class Value, class Tie, class Ctx>
576+
bool is_custom_field_null(Value&& value, Tie&& t, Ctx&& ctx)
577+
{
578+
decltype(auto) custom_val = [&]() -> decltype(auto) {
579+
if constexpr (reflectable<T>) {
580+
return get_member(value, get<I>(t));
581+
}
582+
else {
583+
return get_member(value, get<I>(reflect<T>::values));
584+
}
585+
}();
586+
return custom_getter_is_null(custom_val, ctx);
587+
}
588+
477589
template <class T, auto Opts>
478590
constexpr auto required_fields()
479591
{

include/glaze/json/write.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2192,6 +2192,10 @@ namespace glz
21922192
if (is_null) return;
21932193
}
21942194
}
2195+
else if constexpr (is_specialization_v<val_t, custom_t> && Opts.skip_null_members &&
2196+
custom_getter_returns_nullable<val_t>()) {
2197+
if (is_custom_field_null<T, I>(value, t, ctx)) return;
2198+
}
21952199

21962200
if constexpr (Opts.prettify) {
21972201
if (!ensure_space(ctx, b, ix + padding + ctx.depth)) [[unlikely]] {

tests/json_test/nullable_lambda_test.cpp

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
// can be properly skipped when they return null values
33

44
#include "glaze/glaze.hpp"
5+
#include "glaze/beve/read.hpp"
6+
#include "glaze/beve/write.hpp"
7+
#include "glaze/cbor/read.hpp"
8+
#include "glaze/cbor/write.hpp"
59
#include "ut/ut.hpp"
610

711
using namespace ut;
@@ -109,4 +113,131 @@ suite nullable_lambda_optional_tests = [] {
109113
};
110114
};
111115

116+
// Test with glz::custom and optional return type (GitHub issue #2386)
117+
struct my_struct_custom_optional
118+
{
119+
int value = 42;
120+
};
121+
122+
template <>
123+
struct glz::meta<my_struct_custom_optional>
124+
{
125+
using T = my_struct_custom_optional;
126+
static constexpr auto set_status = [](T& s, std::optional<std::string> val) {
127+
if (val.has_value()) {
128+
s.value = 50;
129+
}
130+
else {
131+
s.value = 0;
132+
}
133+
};
134+
static constexpr auto get_status = [](auto& s) -> std::optional<std::string> {
135+
if (s.value > 50) {
136+
return "high";
137+
}
138+
else {
139+
return std::nullopt;
140+
}
141+
};
142+
static constexpr auto value = glz::object("value", &T::value, "status", glz::custom<set_status, get_status>);
143+
};
144+
145+
suite custom_nullable_optional_tests = [] {
146+
"custom getter returns nullopt - should skip field"_test = [] {
147+
my_struct_custom_optional obj{};
148+
obj.value = 42; // getter returns nullopt
149+
150+
std::string buffer;
151+
expect(not glz::write_json(obj, buffer));
152+
153+
// The "status" field should be omitted
154+
expect(buffer == R"({"value":42})") << "Got: " << buffer;
155+
};
156+
157+
"custom getter returns optional value - should write field"_test = [] {
158+
my_struct_custom_optional obj{};
159+
obj.value = 100; // getter returns "high"
160+
161+
std::string buffer;
162+
expect(not glz::write_json(obj, buffer));
163+
164+
// The "status" field should be present
165+
expect(buffer == R"({"value":100,"status":"high"})") << "Got: " << buffer;
166+
};
167+
168+
"custom getter with skip_null_members false"_test = [] {
169+
constexpr auto opts = glz::opts{.skip_null_members = false};
170+
my_struct_custom_optional obj{};
171+
obj.value = 42; // getter returns nullopt
172+
173+
std::string buffer;
174+
expect(not glz::write<opts>(obj, buffer));
175+
176+
// When skip_null_members is false, null should be written explicitly
177+
expect(buffer == R"({"value":42,"status":null})") << "Got: " << buffer;
178+
};
179+
};
180+
181+
// CBOR round-trip tests for custom nullable getter
182+
suite custom_nullable_cbor_tests = [] {
183+
"cbor custom getter nullopt - round trip"_test = [] {
184+
my_struct_custom_optional src{};
185+
src.value = 42; // getter returns nullopt
186+
187+
std::string buffer;
188+
expect(not glz::write_cbor(src, buffer));
189+
190+
my_struct_custom_optional dst{};
191+
dst.value = 99;
192+
expect(not glz::read_cbor(dst, buffer));
193+
194+
// value should survive round-trip; status was omitted (nullopt)
195+
expect(dst.value == 42);
196+
};
197+
198+
"cbor custom getter with value - round trip"_test = [] {
199+
my_struct_custom_optional src{};
200+
src.value = 100; // getter returns "high"
201+
202+
std::string buffer;
203+
expect(not glz::write_cbor(src, buffer));
204+
205+
my_struct_custom_optional dst{};
206+
expect(not glz::read_cbor(dst, buffer));
207+
208+
// setter receives "high" -> sets value to 50
209+
expect(dst.value == 50);
210+
};
211+
};
212+
213+
// BEVE round-trip tests for custom nullable getter
214+
suite custom_nullable_beve_tests = [] {
215+
"beve custom getter nullopt - round trip"_test = [] {
216+
my_struct_custom_optional src{};
217+
src.value = 42; // getter returns nullopt
218+
219+
std::string buffer;
220+
expect(not glz::write_beve(src, buffer));
221+
222+
my_struct_custom_optional dst{};
223+
dst.value = 99;
224+
expect(not glz::read_beve(dst, buffer));
225+
226+
expect(dst.value == 42);
227+
};
228+
229+
"beve custom getter with value - round trip"_test = [] {
230+
my_struct_custom_optional src{};
231+
src.value = 100; // getter returns "high"
232+
233+
std::string buffer;
234+
expect(not glz::write_beve(src, buffer));
235+
236+
my_struct_custom_optional dst{};
237+
expect(not glz::read_beve(dst, buffer));
238+
239+
expect(dst.value == 50);
240+
};
241+
};
242+
112243
int main() {}

0 commit comments

Comments
 (0)