Closed TheNitesWhoSay closed 3 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
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)
};
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.
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.
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.
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.
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.
Finished as of #75
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,