Skip to content

Latest commit

 

History

History
249 lines (191 loc) · 9.08 KB

05-optional-data-members.md

File metadata and controls

249 lines (191 loc) · 9.08 KB

Optional Data Members

This tutorial introduces an RTTR feature: metadata. The reflection library uses that feature to codify the idea of optional members and members whose value should determine if it is treated as optional. Testing in this section will involve printing our results into the test log (i.e., visual verification).

The files you will be editing are:

Referred to as Location
tutorial header lib/include/tutorial.hpp
tutorial source lib/src/tutorial.cpp
tutorial test tests/src/tutorial-test.cpp

A Foreword on the Converters

This reflection library's converters use of optional members is nuanced since some members are value types whereas other members have the ability to be empty or otherwise appear not set (null). It is important to keep those distinctions in mind when extending this tutorial's design with other container types.

Moreover the concept of optional is applied bidirectionally. In the conversion away from the C++ type, optional provides a way to not represent a member in the target type if the source meets some condition(s) like emptiness. Conversely, restoring to the C++ type, optional provides a way to not fail deserialization (return false) if the source type is missing the member entirely.

Then there is the concept of optional with a default. This metadata feature means almost nothing on the restoration side since if the source type contains the member, the converter will make an attempt to apply it to the target C++ type. On the other hand, in conversions from the C++ type to a target type, comparing the named default (via ==) to the stored value of the source type provides the path to not represent that member in the target type. If the value does not match the default, it is treated as required.

This tutorial will provide some examples of these statements; the tests in the reflection library provide several others.

Getting Started

Let us begin with a structure having value members, one of which has the concept of being empty. To the tutorial header, add:

struct Sometimes {
  Sometimes() {}
  virtual ~Sometimes() {}

  long number;
  std::string text;

  RTTR_ENABLE();
};

In the tutorial source, register the new type:

::rttr::registration::class_<Sometimes>("sometimes")
  .property("number", &Sometimes::number)
  .property("text", &Sometimes::text)
  ;

Both members are now required for serializing both to and from the target type, JsonNode. Let's add a test and look at the log output. Add the following to the tutorial test:

TEST(Optional, PrintSometimes) {
  ::tutorial::Sometimes sometimes;
  JsonNode* temp = NULL;

  ASSERT_TRUE((temp = converters::to_json_glib(sometimes)));
  auto out = json_to_string(temp, true);

  std::cout << out << std::endl;

  g_free(out);
  json_node_unref(temp);
}

Compile and run the test, then open the log. You should see something like the following since std::string would have initialized empty, and the value is just whatever happened to be in memory (depending on what compiler you are using):

[----------] 1 test from Optional
[ RUN      ] Optional.PrintSometimes
{
  "number" : 94635773910520,
  "text" : ""
}
[       OK ] Optional.PrintSometimes (0 ms)
[----------] 1 test from Optional (0 ms total)

Great. Both are required, therefore both are represented in the target type.

Behavior of Optional

Now, let's go make text optional. Open the tutorial source and make that change:

::rttr::registration::class_<Sometimes>("sometimes")
  .property("number", &Sometimes::number)
  .property("text", &Sometimes::text) (
    ::lldc::reflection::metadata::set_is_optional()
  )
  ;

Compile, run the test again, and check the log. It should again have number printed, but now text, because it's optional and empty-like, is omitted.

[----------] 1 test from Optional
[ RUN      ] Optional.PrintSometimes
{
  "number" : 94404035604360
}
[       OK ] Optional.PrintSometimes (0 ms)
[----------] 1 test from Optional (0 ms total)

Repeat that procedure for number back in the tutorial source:

::rttr::registration::class_<Sometimes>("sometimes")
  .property("number", &Sometimes::number) (
    ::lldc::reflection::metadata::set_is_optional()
  )
  .property("text", &Sometimes::text) (
    ::lldc::reflection::metadata::set_is_optional()
  )
  ;

Compile and re-run the test.

You'll observe the number member is still printed despite being optional. There is no way to check for emptiness on this type, so as far as it pertains to the converter, optional in this context really only means that if the incoming JSON blob is missing the member, that's fine, "please continue." For the impacted member, you will get whatever the class-initialized value of that member happened to be once the conversion is finished. If you want a value -type member to be skipped in the conversion to the intermediate type, you need to provide a default reference value.

If you want a value -type member to be skipped in the conversion to the intermediate type, you need to provide a default reference value.

As an exercise left to the learner, what would you expect for marking a pointer -like member as optional? (Answer: if it's null, it will be skipped.)

Behavior of Optional with Default

Now, how can we omit number from the JSON object? This is where the optional with default behavior becomes useful:

// tutorial source
::rttr::registration::class_<Sometimes>("sometimes")
  .property("number", &Sometimes::number) (
    ::lldc::reflection::metadata::set_is_optional_with_default(1234)
  )
  .property("text", &Sometimes::text) (
    ::lldc::reflection::metadata::set_is_optional()
  )
  ;

Modify the test to run the procedure twice, once with the number member set to the magic 1234 value:

TEST(Optional, PrintSometimes) {
  ::tutorial::Sometimes sometimes;
  JsonNode* temp = NULL;
  char* out = NULL;

  sometimes.number = 1233;
  ASSERT_TRUE((temp = converters::to_json_glib(sometimes)));
  ASSERT_TRUE((out = json_to_string(temp, true)));
  std::cout << out << std::endl;
  g_free(out);
  json_node_unref(temp);

  sometimes.number = 1234;
  ASSERT_TRUE((temp = converters::to_json_glib(sometimes)));
  ASSERT_TRUE((out = json_to_string(temp, true)));
  std::cout << out << std::endl;
  g_free(out);
  json_node_unref(temp);
}

Compile and re-run the test, the log output should show an object with number set to 1233 (non-default value) and the second object will be empty, since both members now can meet the optionality criteria.

[----------] 1 test from Optional
[ RUN      ] Optional.PrintSometimes
{
  "number" : 1233
}
{}
[       OK ] Optional.PrintSometimes (0 ms)
[----------] 1 test from Optional (0 ms total)

How does this work for object members that can support empty-like checks? Let's find out.

// tutorial source
::rttr::registration::class_<Sometimes>("sometimes")
  .property("number", &Sometimes::number)
    (::lldc::reflection::metadata::set_is_optional_with_default(1234))
  .property("text", &Sometimes::text)
    (::lldc::reflection::metadata::set_is_optional_with_default("default"))
  ;

Now, update the tutorial test to validate that text will be skipped if it matches "default", but will be present if it's empty (since that is not the default):

TEST(Optional, PrintSometimes) {
  ::tutorial::Sometimes sometimes;
  JsonNode* temp = NULL;
  char* out = NULL;

  // If it matches the default value, it should not be present
  // in the JSON blob.
  sometimes.number = 0;
  sometimes.text = "default";
  ASSERT_TRUE((temp = converters::to_json_glib(sometimes)));

  auto temp_obj = json_node_get_object(temp);
  EXPECT_FALSE(json_object_has_member(temp_obj, "text"));

  ASSERT_TRUE((out = json_to_string(temp, true)));
  std::cout << out << std::endl;
  g_free(out);
  json_node_unref(temp);

  // If it is empty-like (i.e., empty string), then it should
  // be represented as an empty string in the JSON blob since
  // it does not match the default value.
  sometimes.text.clear();
  ASSERT_TRUE((temp = converters::to_json_glib(sometimes)));

  temp_obj = json_node_get_object(temp);
  EXPECT_TRUE(json_object_has_member(temp_obj, "text"));

  ASSERT_TRUE((out = json_to_string(temp, true)));
  std::cout << out << std::endl;
  g_free(out);
  json_node_unref(temp);
}

Compile and run the test. Your results should be:

[----------] 1 test from Optional
[ RUN      ] Optional.PrintSometimes
{
  "number" : 0
}
{
  "number" : 0,
  "text" : ""
}
[       OK ] Optional.PrintSometimes (0 ms)
[----------] 1 test from Optional (0 ms total)

The first check is now skipping the text member because it was set to the default, and the second check included text because it no longer matched the default.

Conclusions

This tutorial discussed the Optional and Optional with Default metadata features provided by this library. You learned how to declare members as optional, explored the behavior of value vs. empty-like members, and then explored the impact of specifying a default value.