5.0. RareMapper - TheNitesWhoSay/RareCpp GitHub Wiki

RareMapper is a tool for easily mapping between different types, taking advantage of reflection, standard-language conversion mechanics, and RareMapper-specific customizations.

Any two reflected objects containing fields of the same name and compatible types can be mapped to eachother with zero additional setup, e.g. Run

struct PersonDao
{
    int id;
    std::string firstName;
    std::string lastName;

    REFLECT(PersonDao, id, firstName, lastName)
};

struct PersonDto
{
    std::string firstName;
    std::string lastName;

    REFLECT(PersonDto, firstName, lastName)
};

int main()
{
    PersonDao dao{ 1337, "Leeroy", "Jenkins" };
    PersonDto dto{};
    RareMapper::map(dto, dao);
    std::cout << "dto: " << Json::pretty(dto) << std::endl;
}
dto: {
  "firstName": "Leeroy",
  "lastName": "Jenkins"
}

The above also could have been compressed to one line with the single-argument helper function:

PersonDto dto = RareMapper::map<PersonDto>(dao);

Automated Mapping Algorithm

When calling RareMapper::map, the order in which various mappings are attempted is as follows:

  • Specializations of RareMapper::map<To, From>(To &, const From &)
  • A matching map_from method in the destination/To class
  • A matching map_to method in the source/From class
  • An ObjectMappings note exists in the "To" or "From" class e.g. NOTE(ObjectMappings, RareMapper::createMapping<To, From>() ... )
  • If the statement "to = from" is valid, then it is ran (user defined assignment or conversion operators, converting constructors)
  • If the statement "to = static_cast(from)" is valid, then it is ran (user defined explicit conversion operators)

Not finding any such (or if the above make explicit calls to RareMapper::mapDefault), the following is then considered in order:

  • If To and From are compatible shared_ptrs, the source value is shared to target
  • If To or From is a non-null pointer, it is dereferenced
  • If To is a null unique or shared pointer, it is allocated then mapped to
  • If to and from are both pairs or tuples, the individual values within are mapped to eachother
  • If to and from are both STL containers or static arrays, the container "to" is cleared, then all the elements from "from" are constructed, recursively mapped, then added to it
  • If to and from are both reflected, then a mapping is performed on all matching field names

If none of the above are satisfied, no mapping is performed; no exception is raised nor is any other indication given that there was no mapping found

Defining Mappings / Best Practices

1.) If there are pre-existing user-defined assignment operators, user-defined conversion operators, or converting constructors appropriate for your mapping, do nothing! RareMapper will find them and use them automatically; consider adding assignment/conversion operators or constructors where they make sense in your codebase as these are more standard (albeit a little more work).

2.) When it is reasonable to do so, name source and target fields the same and add reflection to both objects, no additional code will be needed to perform mappings.

3.) When one or both objects cannot be reflected within their own class, but the field names are the same and the fields are public or protected, consider adding a reflection proxy, e.g.

template <> struct RareTs::Proxy<PersonDao> : public PersonDao
{
    REFLECT(RareTs::Proxy<PersonDao>, id, firstName, lastName)
};

4.) When one or both objects cannot be reflected or the field names differ, but the types are compatible for mapping, consider using an ObjectMappings note to declare a uni-directional or bi-directional mapping between the desired fields e.g.

struct ObjDao
{
    int a;
    int b;
    int c;
    std::map<int, int> d;

    REFLECT(ObjDao, a, b, c, d)
};

struct ObjModel
{
    int a;
    int b;
    std::map<int, int> c;

    REFLECT(ObjModel, a, b, c)

    NOTE(ObjectMappings, RareMapper::createMapping<ObjModel, ObjDao>()
        .a->a()
        .b->b()
        .c->d()
        .bidirectional())
};

5.) If one or more of your types are incompatible and need special handling, consider specifying a map_from(source) method in your target class or a map_to(target) method in your source class. A user-defined assignment operator, user defined-conversion operator, or converting constructor is highly preferable in most cases, only use these when you cannot use the former, or when the former is specified and you need unique behavior for object mappings. If you use RareMapper inside your map_to/map_from methods, be sure to use RareMapper::mapDefault rather than RareMapper::map to avoid infinite recursion.

struct UnownedEncapsulator
{
    UnownedEncapsulator(int a) : a(a) {}
    int getA() const { return a; }
    void setA(int a) { this->a = a; }

private:
    int a;
};

struct OwnedObject
{
    int a = 0;
    int c = 0;
    REFLECT(OwnedObject, a)

    void map_to(UnownedEncapsulator & o) const { o.setA(this->a); }
    void map_from(const UnownedEncapsulator & o) { this->a = o.getA(); }
};

6.) As a last resort for objects (or when customizing mappings to/from more generic types), specialize the two-argument RareMapper::map method (avoid specializing the one-arg method as that method simply calls the two-arg method, and the one-arg method will NOT be called when recursing down various parts of an object), example valid specialization:

template <>
void RareMapper::map(SpecializationMapping & to, const OwnedObject & from)
{
    to.a = from.a;
}

Constructors & Operator Overloads

You may define various constructors and operator overloads (such as copy constructors, converting constructors, assignment operators, and conversion operators) using RareMapper. If you choose to do so, be sure to use the RareMapper::mapDefault method(s) rather than the RareMapper::map method(s) to avoid infinite recursion.

struct Src
{
    int a = 0;
    int b = 0;

    REFLECT(Src, a, b)
};

struct Dest
{
    int a = 0;
    int b = 0;

    REFLECT(Dest, a, b)

    void operator=(const Src & src) { RareMapper::mapDefault(*this, src); }
};

Annotations (Serializer Instructions)

RareMapper provides annotations that can instruct serializers to use another type to which your type has a mapping for I/O. This is perhaps most useful for objects coming from libraries included in your code which are not reflected/reflectable or are otherwise not well-suited for serialization (e.g. DAOs that you need to convert to DTOs). Having a default mapping should tell a given serializer to behave akin to:

using D = RareMapper::default_mapping_t<T, typename Member::Notes, OpAnnotations>;
D d = RareMapper::map<D>(value);
output << d;

Or deserializer to behave akin to:

using D = RareMapper::default_mapping_t<T, typename Member::Notes, OpAnnotations>;
D d {};
input >> d;
return RareMapper::map<T>(d);

You can specify a default mapping (or mapping for particular operation) with...

1.) A class-level annotation

NOTE(Dao, RareMapper::MappedBy<Dto>)
struct Dao {
    std::string a;
    REFLECT_NOTED(Dao, a)
};

2.) A field-level annotation (for a given sub-object)

struct MyObj
{
    NOTE(Dao, RareMapper::MappedBy<Dto>)
    Dao dao;

    REFLECT(MyObj, dao)
};

3.) Using the macro at global scope:

SET_DEFAULT_OBJECT_MAPPING(Dao, Dto)

4.) An op-level annotation, e.g.

Json::out<Json::Statics::Included, Json::OpNotes<RareMapping::UseMapping<Dao, Dto>>>(objectContainingDaos);

Two helpers are provided to assist in using defined mappings:

  • bool trait RareMapper::has_default_mapping
  • type trait RareMapper::default_mapping

e.g.

if constexpr ( RareMapper::has_default_mapping_v<T> )
    using D = RareMapper::default_mapping_t<T>;
⚠️ **GitHub.com Fallback** ⚠️