In order to support your own serialization format, you need to implement a reader and a writer on top of whatever serialization libary
you have chosen.
The reader and writer constitute a thin layer between the serialization library of your choice and reflect-cpp.
The requirements are laid down in the concepts IsReader and
IsWriter and also documented below.
Using the reader and the writer you can define your parser like this:
This can then be used to implement a read function and a write function:
usingInputVarType=typenameYourReader::InputVarType;usingOutputVarType=typenameYourWriter::OutputVarType;template<classT>rfl::Result<T>read(conststd::string&_str){// This should be supported by whatever library you are// using for your format.constInputVarTyperoot=str_to_input_var(_str);// You can pass variables to the constructor, if necessaryconstautor=Reader(...);returnYourParser<T>::read(r,root);}template<classT>std::stringwrite(constT&_obj){// You can pass variables to the constructor, if necessaryautow=Writer(...);OutputVarTypevar=Parser<T>::write(w,_obj);// This should be supported by whatever library you are// using for your format.returnoutput_var_to_str(var);}
In the following two sections, we will provide templates for your Reader and Writer.
You should probably just copy + paste this into your own code and fill in the blanks.
As a reference, you can take a look at how this is done for JSON: https://github.com/getml/reflect-cpp/tree/main/include/rfl/json
Because writers are somewhat simpler, we will start with them.
Any Writer needs to define the following types:
1) An OutputArrayType, which must be an array-like data structure.
2) An OutputObjectType, which must contain key-value pairs.
3) An OutputVarType, which must be able to represent either
OutputArrayType, OutputObjectType or a basic type (bool, integral,
floating point, std::string). We hesitate to call these "primitive types",
because primitive types in C++ are defined as a slightly different group
of types.
It also needs to support the following methods:
structWriter{usingOutputArrayType=...;usingOutputObjectType=...;usingOutputVarType=...;/// Sets an empty array as the root element of the document./// Some serialization formats require you to pass the expected size in/// advance. If you are not working with such a format, you can ignore the/// parameter `_size`. Returns the new array for further modification.OutputArrayTypearray_as_root(constsize_t_size)constnoexcept;/// Sets an empty object as the root element of the document./// Some serialization formats require you to pass the expected size in/// advance. If you are not working with such a format, you can ignore the/// parameter `_size`./// Returns the new object for further modification.OutputObjectTypeobject_as_root(constsize_t_size)constnoexcept;/// Sets a null as the root element of the document. Returns OutputVarType/// containing the null value.OutputVarTypenull_as_root()constnoexcept;/// Sets a basic value (bool, numeric, string) as the root element of the/// document. Returns an OutputVarType containing the new value.template<classT>OutputVarTypevalue_as_root(constT&_var)constnoexcept;/// Adds an empty array to an existing array. Returns the new/// array for further modification.OutputArrayTypeadd_array_to_array(constsize_t_size,OutputArrayType*_parent)constnoexcept;/// Adds an empty array to an existing object. The key or name of the field is/// signified by `_name`. Returns the new array for further modification.OutputArrayTypeadd_array_to_object(conststd::string_view&_name,constsize_t_size,OutputObjectType*_parent)constnoexcept;/// Adds an empty object to an existing array. Returns the new/// object for further modification.OutputObjectTypeadd_object_to_array(constsize_t_size,OutputArrayType*_parent)constnoexcept;/// Adds an empty object to an existing object. The key or name of the field/// is signified by `_name`. Returns the new object for further modification.OutputObjectTypeadd_object_to_object(conststd::string_view&_name,constsize_t_size,OutputObjectType*_parent)constnoexcept;/// Adds a basic value (bool, numeric, string) to an array. Returns an/// OutputVarType containing the new value.template<classT>OutputVarTypeadd_value_to_array(constT&_var,OutputArrayType*_parent)constnoexcept;/// Adds a basic value (bool, numeric, string) to an existing object. The key/// or name of the field is signified by `name`. Returns an/// OutputVarType containing the new value.template<classT>OutputVarTypeadd_value_to_object(conststd::string_view&_name,constT&_var,OutputObjectType*_parent)constnoexcept;/// Adds a null value to an array. Returns an/// OutputVarType containing the null value.OutputVarTypeadd_null_to_array(OutputArrayType*_parent)constnoexcept;/// Adds a null value to an existing object. The key/// or name of the field is signified by `name`. Returns an/// OutputVarType containing the null value.OutputVarTypeadd_null_to_object(conststd::string_view&_name,OutputObjectType*_parent)constnoexcept;/// Signifies to the writer that we do not want to add any further elements to/// this array. Some serialization formats require this. If you are working/// with a serialization format that doesn't, just leave the function empty.voidend_array(OutputArrayType*_arr)constnoexcept;/// Signifies to the writer that we do not want to add any further elements to/// this object. Some serialization formats require this. If you are working/// with a serialization format that doesn't, just leave the function empty.voidend_object(OutputObjectType*_obj)constnoexcept;};
1) An InputArrayType, which must be an array-like data structure.
2) An InputObjectType, which must contain key-value pairs.
3) An InputVarType, which must be able to represent either
InputArrayType, InputObjectType or a basic type (bool, integral,
floating point, std::string).
4) A static constexpr bool has_custom_constructor<T>, that determines
whether the class in question as a custom constructor, which might
be called something like from_json_obj(...). If you do not want to
support this functionality, just set it to false.
It also needs to support the following methods:
structReader{usingInputArrayType=...;usingInputObjectType=...;usingInputVarType=...;/// If you do not want to support custom constructors,/// just set this to false.template<classT>staticconstexprboolhas_custom_constructor=false;/// Retrieves a particular field from an array./// Returns an rfl::Error if the index is out of bounds./// If your format is schemaful, you do not need this.rfl::Result<InputVarType>get_field_from_array(constsize_t_idx,constInputArrayType_arr)constnoexcept{...}/// Retrieves a particular field from an object./// Returns an rfl::Error if the field cannot be found./// If your format is schemaful, you do not need this.rfl::Result<InputVarType>get_field_from_object(conststd::string&_name,constInputObjectType&_obj)constnoexcept{...}/// Determines whether a variable is empty (the NULL type).boolis_empty(constInputVarType&_var)constnoexcept{...}/// Cast _var as a basic type (bool, integral,/// floating point, std::string)./// Returns an rfl::Error if it cannot be cast/// as that typetemplate<classT>rfl::Result<T>to_basic_type(constInputVarType&_var)constnoexcept{...}/// Casts _var as an InputArrayType./// Returns an rfl::Error if `_var` cannot be cast as an array.rfl::Result<InputArrayType>to_array(constInputVarType&_var)constnoexcept{...}/// Casts _var as an InputObjectType./// Returns an rfl::Error if `_var` cannot be cast as an object.rfl::Result<InputObjectType>to_object(constInputVarType&_var)constnoexcept{...}/// Iterates through an array and inserts the values into the array/// reader. See below for a more detailed explanation.template<classArrayReader>std::optional<Error>read_array(constArrayReader&_array_reader,constInputArrayType&_arr)constnoexcept{...}/// Iterates through an object and inserts the key-value pairs into the object /// reader. See below for a more detailed explanation.template<classObjectReader>std::optional<Error>read_object(constObjectReader&_object_reader,constInputObjectType&_obj)constnoexcept{...}/// Constructs T using its custom constructor. This will only be triggered if/// T was determined to have a custom constructor by/// static constexpr bool has_custom_constructor, as defined above./// Returns an rfl::Error, if the custom constructor throws an exception.template<classT>rfl::Result<T>use_custom_constructor(constInputVarType&_var)constnoexcept{// If you do not want to support this functionality,// just return this.returnrfl::error("Not supported.");}};
Of these methods, read_array and read_object probably require further explanation.
Within your implementation of read_array, you must iterate through the array passed
to the function and then insert the resulting values into array_reader.read. If
array_reader.read returns an error, then you must return that error immediately.
Within your implementation of read_object, you must iterate through the object passed
to the function and then insert the resulting key-value-pairs into object_reader.read.
Schemaful formats, like Apache Avro or Cap'n Proto,
are somewhat more complicated than schemaless ones. There are additional factors
to consider which do not apply schemaless formats:
Schemaful formats needs to differentiate between objects, for which
the field names are known at compile time and maps, for which the
field names are not known at compile time. In schemaless formats, there
is no differentiation.
Schemaful formats needs an explicit union types. This also means that
many of the problems we have with serializing std::variant which
requires us to develop concepts like rfl::TaggedUnion simply do not
apply to schemaful formats - the problem is already solved.
Any schemaful reader additionally needs to define the following:
1) An OutputMapType, which must contain key-value pairs.
2) An OutputUnionType, which represents an explicit union.
structWriter{usingOutputArrayType=...;usingOutputMapType=...;usingOutputObjectType=...;usingOutputUnionType=...;usingOutputVarType=...;/// Sets an empty map as the root element of the document./// Some serialization formats require you to pass the expected size in/// advance. If you are not working with such a format, you can ignore the/// parameter `size`. Returns the new array for further modification.OutputMapTypemap_as_root(constsize_t_size)constnoexcept;/// Sets an empty union as the root element of the document.OutputUnionTypeunion_as_root()constnoexcept;/// Adds an empty array to an existing map. Returns the new/// array for further modification.OutputArrayTypeadd_array_to_map(conststd::string_view&_name,constsize_t_size,OutputMapType*_parent)constnoexcept;/// Adds an empty array to an existing union./// The index refers to the index of the element in the union./// Returns the new array for further modification.OutputArrayTypeadd_array_to_union(constsize_t_index,constsize_t_size,OutputUnionType*_parent)constnoexcept;/// Adds an empty map to an existing array. Returns the new/// map for further modification.OutputMapTypeadd_map_to_array(constsize_t_size,OutputArrayType*_parent)constnoexcept;/// Adds an empty map to an existing map. The key or name of the field/// is signified by `name`. Returns the new map for further modification.OutputMapTypeadd_map_to_map(conststd::string_view&_name,constsize_t_size,OutputMapType*_parent)constnoexcept;/// Adds an empty map to an existing object. The key or name of the field/// is signified by `name`. Returns the new map for further modification.OutputMapTypeadd_map_to_object(conststd::string_view&_name,constsize_t_size,OutputObjectType*_parent)constnoexcept;/// Adds an empty map to an existing union./// The index refers to the index of the element in the union./// Returns the new map for further modification.OutputMapTypeadd_map_to_union(constsize_t_index,constsize_t_size,OutputUnionType*_parent)constnoexcept;/// Adds an empty object to an existing map. The key or name of the field/// is signified by `name`. Returns the new object for further modification.OutputObjectTypeadd_object_to_map(conststd::string_view&_name,constsize_t_size,OutputMapType*_parent)constnoexcept;/// Adds an empty object to an existing union./// The index refers to the index of the element in the union./// Returns the new object for further modification.OutputObjectTypeadd_object_to_union(constsize_t_index,constsize_t_size,OutputUnionType*_parent)constnoexcept;/// Adds an empty union to an existing array. Returns the new/// union for further modification.OutputUnionTypeadd_union_to_array(OutputArrayType*_parent)constnoexcept;/// Adds an empty union to an existing map. The key or name of the field/// is signified by `name`. Returns the new union for further modification.OutputUnionTypeadd_union_to_map(conststd::string_view&_name,OutputMapType*_parent)constnoexcept;/// Adds an empty union to an existing object. The key or name of the field/// is signified by `name`. Returns the new union for further modification.OutputUnionTypeadd_union_to_object(conststd::string_view&_name,OutputObjectType*_parent)constnoexcept;/// Adds an empty union to an existing union./// The index refers to the index of the element in the union./// Returns the new union for further modification.OutputUnionTypeadd_union_to_union(constsize_t_index,OutputUnionType*_parent)constnoexcept;/// Adds a null value to a map. Returns an/// OutputVarType containing the null value.OutputVarTypeadd_null_to_map(conststd::string_view&_name,OutputMapType*_parent)constnoexcept;/// Adds a null value to a union. Returns an/// OutputVarType containing the null value.OutputVarTypeadd_null_to_union(constsize_t_index,OutputUnionType*_parent)constnoexcept;/// Adds a basic value (bool, numeric, string) to an existing map. The key/// or name of the field is signified by `name`. Returns an/// OutputVarType containing the new value.template<classT>OutputVarTypeadd_value_to_map(conststd::string_view&_name,constT&_var,OutputMapType*_parent)constnoexcept;/// Adds a basic value (bool, numeric, string) to an existing union. The key/// or name of the field is signified by `name`. Returns an/// OutputVarType containing the new value.template<classT>OutputVarTypeadd_value_to_union(constsize_t_index,constT&_var,OutputUnionType*_parent)constnoexcept;/// Signifies to the writer that we do not want to add any further elements to/// this map. Some serialization formats require this. If you are working/// with a serialization format that doesn't, just leave the function empty.voidend_map(OutputMapType*_obj)constnoexcept;};
Any schemaful reader additionally needs to define the following:
1) An InputMapType, which must contain key-value pairs.
2) An InputUnionType, which represents an explicit union.
structReader{usingInputArrayType=...;usingInputObjectType=...;usingInputMapType=...;usingInputUnionType=...;usingInputVarType=...;/// A schemaful reader needs to differentiate between objects, for which/// the field names are known at compile time and maps, for which the/// field names are not known at compile time.rfl::Result<InputMapType>to_map(constInputVarType&_var)constnoexcept;/// read_map works exactly the same as read_object in schemaless formats.template<classMapReader>std::optional<Error>read_map(constMapReader&_map_reader,constInputMapType&_map)constnoexcept;/// A schemaful reader needs an explicit union type.rfl::Result<InputUnionType>to_union(constInputVarType&_var)constnoexcept;/// read_union needs to be able to take an InputUnionType and return the corresponding/// variant (like std::variant or rfl::Variant). template<classVariantType,classUnionReaderType>rfl::Result<VariantType>read_union(constInputUnionType&_union)constnoexcept;};