TheNitesWhoSay / RareCpp

Creating a simpler, more intuitive means of C++ reflection
MIT License
124 stars 6 forks source link

RareObjectMapper #68

Closed TheNitesWhoSay closed 3 years ago

TheNitesWhoSay commented 4 years ago

An ObjectMapper simply maps between two similar objects - if the fields are all the same between two reflected objects it should be able to assign one to the other in a single statement without mentioning any fields explicitly and without any assignment overloads.

If field names differ but the types do not, it should be trivially easy to define a mapping between said fields.

If a function is needed to map between two objects it should be easy to provide one, and take advantage (or disable) automatic mapping between fields with the same name/type,

TheNitesWhoSay commented 4 years ago

Part of the motivation with object mapper is to be able to define an object mapping, and then other reflection extensions like RareJson can automatically take advantage of a mapping.

Say you have a type which due to being from a 3rd party library or for security concerns or whatnot cannot be reflected directly; if you had such a type annotated with NOTE(MyType, Json::MappedBy), then to serialize/deserialize you need only define how to programmatically transform to/from MyType/MyProxyType - you don't need to make a json reader/writer customization which would need to deal with the intricacies of JSON.

If the transformation between two such types is trivial (meaning all the fields are public and of the same name/type), then I think it best that the user use (and RareCpp support) reflection proxies which enable constructs to work on a class which does not contain the REFLECT macro directly, e.g.

struct UnownedObject
{
    int a;
    int b;
};

template <> struct Reflect::Proxy<UnownedObject> : public UnownedObject
{
    REFLECT(Reflect::Proxy<UnownedObject>, a, b)
};
TheNitesWhoSay commented 3 years ago

Only "direct" mappings should be done by default (that is, assignable types of the same name in both classes), auto-mapping to getters/setters/constructors can be done, or even preferred over direct mappings, if set as such via annotations in the reflected class or proxy...

Reason being that C++ has no widespread concept of a "bean" the fact that a "getX" or "setX" method exists in a class is a very weak hint that there's actually a variable called "x" in both objects which said methods are appropriate for getting or setting; and even if they do get or set a variable called "x" there's no telling whether these should only be done as part of a sequence of events, whether some ownership changes, or some destructive move occurs.

While some of these risks are shared by auto-mapping raw fields users will expect these fields to be automatically assigned to each other by the mapper where compatible by the default behavior (which will of course be overridable) - whereas I'm sure invocation of auto-detected methods will occasionally surprise - if instead client code must put a "Bean" annotation on the class/proxy, then they're being explicit that they've setup their getters and setters (or that the proxied class has setup getters and setters) in a manner appropriate for automatic use.

TheNitesWhoSay commented 3 years ago

In addition to mapping between reflected objects... containers of primitives, reflected objects, and assignables should also be auto-mappable where compatible

anyiterable<T> <--> anyiterable<T>
anymap<K,T> <--> anymap<K,T>
anymap<K,T> <--> anyiterable<pair<K,T>>
anymap<K,T> <--> anyiterable<tuple<K,T>>

Special care needs to be given to unique_ptrs, they could be destructively moved, but that's not necessarily intuitive, probably best is similar to what was done for json: set null if null in source, assign value if already allocated, allocate and assign value otherwise.

TheNitesWhoSay commented 3 years ago

Something I neglected to take seriously early: assignment operators and constructors taking the "from" value as an argument and assigning/building the "to" value should be auto-detected and used to the extent possible; I think this may result in a lot of rework to the object mapper as when available there's no reason the user should have to add another method to the object mapper namespace to do the mapping; these should just be picked up and used.

Another point I've neglected is that adding mappings in the way I'm currently doing them (a full specialization of a function in the object mapper namespace) is poor in an organizational sense - it could force code for a particular object to be located very far away from the object. Using SFINAE I could detect whether particular method names were added to an object and use those for mappings (for cases where assignment operators/constructors do not suffice). As assignment operators/constructors/methods within the objects themselves would most often be preferable having such options would make it unnecessary to use function full-specialization (which is not reliably detectable in the library and has serious limitations to what you can template) and preferable to partial template specialization on a struct (which is reliably detectable in the library code and allows for incredibly flexible templates, especially when done in headers).

Before I go off coding neglecting yet another point I'm going to research whether there's some reasonably standard conversion methodology I'm missing and could take advantage of besides assignment operators/constructors/library specific methods.

TheNitesWhoSay commented 3 years ago

Another thing I'd like to explore is defining bi-directional mappings; via reflection we have automatic bi-directional mappings for fields of the same name; but constructors, assignment operators, and even my custom library functions so far are all uni-directional and to do a custom mapping two ways requires two function definitions; there might be no good way to have a custom bi-directional mapping via a single definition but I do think it's worth taking a stab at.

TheNitesWhoSay commented 3 years ago

With respect to the earlier investigation, besides assignment operators and constructors the only other standard conversion methodology was user-defined conversion operators; I'm able to capture all of these by detecting whether "lhs = rhs" is valid or "lhs = static_cast<decltype(lhs)> rhs" is valid.

One thing I'm going to put a hold on/not have in the first draft implementation is default construction points - in a few places the destinations are required to be default constructable for now (if not manually specialized), I'd like to add something to check for different paths to get a thing constructed/copied/converted which may also be valid for the object mapper to use.

TheNitesWhoSay commented 3 years ago

Finished as of #75