1 /**
2 License:
3     Boost Software License - Version 1.0 - August 17th, 2003
4 
5     Permission is hereby granted, free of charge, to any person or organization
6     obtaining a copy of the software and accompanying documentation covered by
7     this license (the "Software") to use, reproduce, display, distribute,
8     execute, and transmit the Software, and to prepare derivative works of the
9     Software, and to permit third-parties to whom the Software is furnished to
10     do so, all subject to the following:
11 
12     The copyright notices in the Software and this entire statement, including
13     the above license grant, this restriction and the following disclaimer,
14     must be included in all copies of the Software, in whole or in part, and
15     all derivative works of the Software, unless such copies or derivative
16     works are solely in the form of machine-executable object code generated by
17     a source language processor.
18 
19     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20     IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21     FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
22     SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
23     FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
24     ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25     DEALINGS IN THE SOFTWARE.
26 
27 Authors:
28     Alexandru Ermicioi
29 **/
30 module aermicioi.aedi_property_reader.convertor.mapper;
31 
32 import aermicioi.aedi.configurer.annotation.annotation;
33 import aermicioi.aedi_property_reader.convertor.exception;
34 import aermicioi.aedi_property_reader.convertor.convertor :
35     CombinedConvertor,
36     ConvertsFromToMixin,
37     Convertor,
38     EqualToHashToStringOpCmpMixin;
39 import aermicioi.aedi_property_reader.convertor.inspector;
40 import aermicioi.aedi_property_reader.convertor.setter;
41 import aermicioi.aedi_property_reader.convertor.accessor;
42 import aermicioi.aedi_property_reader.convertor.placeholder;
43 import std.experimental.allocator;
44 import std.experimental.logger;
45 import std.exception;
46 import std.algorithm;
47 import std.conv;
48 import std.traits : fullyQualifiedName;
49 import std.typecons : Flag;
50 
51 /**
52 Interface for components that are able to map from one type to another one.
53 **/
54 interface Mapper(To, From = To) {
55 
56     /**
57     Map from component to component.
58 
59     Map from component to component, or transfer data from component to component
60     with optional conversion of data along the way.
61 
62     Params:
63         from = original component that has it's data transferred
64         to = destination component that receives transferred data
65         allocator = optional allocator that could be used by convertors when doing field conversion
66     **/
67     void map(From from, ref To to, RCIAllocator allocator = theAllocator) const;
68 
69     @property {
70         /**
71         Set convertors
72 
73         Params:
74             convertors = set a list of convertors to be used by mapper to automatically convert a mapped field to designated type
75         Returns:
76             typeof(this)
77         **/
78         typeof(this) convertor(Convertor convertor) @safe nothrow pure;
79 
80         /**
81         Get convertors
82 
83         Returns:
84             Convertor[]
85         **/
86         inout(Convertor) convertor() @safe nothrow pure inout;
87 
88         /**
89         Set force
90 
91         Params:
92             force = forces mapper to try and set a field even if it is not existent
93 
94         Returns:
95             typeof(this)
96         **/
97         typeof(this) force(Flag!"force" force) @safe nothrow pure;
98 
99         /**
100         Get force
101 
102         Returns:
103             Flag!"force"
104         **/
105         inout(Flag!"force") force() @safe nothrow pure inout;
106 
107         /**
108         Set conversion
109 
110         Params:
111             conversion = whether to enable or not automatic conversion of fields using convertors.
112 
113         Returns:
114             typeof(this)
115         **/
116         typeof(this) conversion(Flag!"conversion" conversion) @safe nothrow pure;
117 
118         /**
119         Get conversion
120 
121         Returns:
122             Flag!"conversion"
123         **/
124         inout(Flag!"conversion") conversion() @safe nothrow pure inout;
125 
126         /**
127         Set skip
128 
129         Params:
130             skip = whether to skip mapping of fields that have their type not identifiable in destination component
131 
132         Returns:
133             typeof(this)
134         **/
135         typeof(this) skip(Flag!"skip" skip) @safe nothrow pure;
136 
137         /**
138         Get skip
139 
140         Returns:
141             Flag!"skip"
142         **/
143         inout(Flag!"skip") skip() @safe nothrow pure inout;
144     }
145 }
146 
147 /**
148 An implementation of a mapper that specifically converts From component To component.
149 
150 An implementation of a mapper that specifically converts From component To component.
151 It will use inspectors for From and To component to get information about component fields
152 at runtime, and then use accessor and setter implementations to transfer data from one
153 component to another, with optional conversion of data using passed convertors.
154 **/
155 @component
156 class CompositeMapper(To, From) : Mapper!(To, From) {
157 
158     private {
159         Flag!"conversion" conversion_;
160         Flag!"force" force_;
161         Flag!"skip" skip_;
162 
163         Convertor convertor_;
164 
165         const PropertySetter!(To, Object) setter;
166         const PropertyAccessor!(From, Object) accessor;
167         const Inspector!From fromInspector;
168         const Inspector!To toInspector;
169     }
170 
171     public {
172 
173         @autowired
174         this(
175             PropertySetter!(To, Object) setter,
176             PropertyAccessor!(From, Object) accessor,
177             Inspector!From fromInspector,
178             Inspector!To toInspector
179         ) {
180             this.setter = setter;
181             this.accessor = accessor;
182             this.fromInspector = fromInspector;
183             this.toInspector = toInspector;
184         }
185 
186         @property {
187             /**
188             Set convertors
189 
190             Params:
191                 convertors = a list of optional convertors used to convert from one format to another one
192 
193             Returns:
194                 typeof(this)
195             **/
196             @autowired
197             typeof(this) convertor(Convertor convertor) @safe nothrow pure
198                 in (convertor !is null, "Expected convertor, not null value") {
199                 this.convertor_ = convertor;
200 
201                 return this;
202             }
203 
204             /**
205             Get convertors
206 
207             Returns:
208                 Convertor[]
209             **/
210             inout(Convertor) convertor() @safe nothrow pure inout
211                 out(conv; conv !is null, "Expected to have a proper convertor available not null") {
212                 return this.convertor_;
213             }
214 
215             /**
216             Set conversion
217 
218             Params:
219                 conversion = wether to convert or not values using convertors.
220 
221             Returns:
222                 typeof(this)
223             **/
224             @autowired
225             typeof(this) conversion(Flag!"conversion" conversion) @safe nothrow pure {
226                 this.conversion_ = conversion;
227 
228                 return this;
229             }
230 
231             /**
232             Get conversion
233 
234             Returns:
235                 Flag!"conversion"
236             **/
237             inout(Flag!"conversion") conversion() @safe nothrow pure inout {
238                 return this.conversion_;
239             }
240 
241             /**
242             Set force
243 
244             Params:
245                 force = whether to force attempt in setting a property in a mapped component
246 
247             Returns:
248                 typeof(this)
249             **/
250             @autowired
251             typeof(this) force(Flag!"force" force) @safe nothrow pure {
252                 this.force_ = force;
253 
254                 return this;
255             }
256 
257             /**
258             Get force
259 
260             Returns:
261                 Flag!"force"
262             **/
263             inout(Flag!"force") force() @safe nothrow pure inout {
264                 return this.force_;
265             }
266 
267             /**
268             Set skip
269 
270             Params:
271                 skip = whether to skip mapping of fields that have their type not identifiable in destination component
272 
273             Returns:
274                 typeof(this)
275             **/
276             @autowired
277             typeof(this) skip(Flag!"skip" skip) @safe nothrow pure {
278                 this.skip_ = skip;
279 
280                 return this;
281             }
282 
283             /**
284             Get skip
285 
286             Returns:
287                 Flag!"skip"
288             **/
289             inout(Flag!"skip") skip() @safe nothrow pure inout {
290                 return this.skip_;
291             }
292         }
293 
294         /**
295         Map from component to component.
296 
297         Map from component to component, or transfer data from component to component
298         with optional conversion of data along the way.
299 
300         Params:
301             from = original component that has it's data transferred
302             to = destination component that receives transferred data
303             allocator = optional allocator that could be used by convertors when doing field conversion
304         **/
305         void map(From from, ref To to, RCIAllocator allocator = theAllocator) const {
306 
307             debug(trace) trace(
308                 "Mapping ",
309                 this.fromInspector.properties(from),
310                 " of ",
311                 from.identify,
312                 " to ",
313                 to.identify
314             );
315 
316             foreach (property; this.fromInspector.properties(from)) {
317 
318                 debug(trace) trace("Migrating \"", property, "\" property ");
319                 if (this.toInspector.has(to, property) || this.force) {
320 
321                     Object value = this.accessor.access(from, property, allocator);
322 
323                     if (
324                         this.fromInspector.typeOf(from, property) != this.toInspector.typeOf(to, property)
325                     ) {
326                         if (this.conversion) {
327                             if (this.toInspector.typeOf(to, property) is typeid(void)) {
328                                 if (this.skip) {
329                                     debug(trace) trace(
330                                         "Skipping migration of \"",
331                                         property,
332                                         "\" due to missing type information in destination component"
333                                     );
334 
335                                     continue;
336                                 }
337 
338                                 throw new ConvertorException(text(
339                                         "Cannot identify type of \"", property,
340                                         "\" in destination component, probably missing"
341                                 ));
342                             }
343 
344                             debug(trace) trace("\"",
345                                 property,
346                                 "\"'s type differs in original component and destination component, ",
347                                 this.fromInspector.typeOf(from, property), " and ",
348                                 this.toInspector.typeOf(to, property)
349                             );
350 
351                             enforce!ConvertorException(
352                                 this.convertor.converts(this.fromInspector.typeOf(from, property), this.toInspector.typeOf(to, property)),
353                                 text(
354                                     "Could not convert \"", property, "\" from ",
355                                     this.fromInspector.typeOf(from, property), " to ", this.toInspector.typeOf(to, property), " ",
356                                     this.convertor, " does not support it"
357                                 )
358                             );
359 
360                             debug(trace) trace(
361                                 "Converting \"", property, "\" from ",
362                                 this.fromInspector.typeOf(from, property), " to ",
363                                 this.toInspector.typeOf(to, property)
364                             );
365 
366                             value = this.convertor.convert(value, this.toInspector.typeOf(to, property), allocator);
367                         } else {
368 
369                             throw new InvalidArgumentException(text(
370                                 "Invalid assignment \"", property, "\" has type of ",
371                                 this.fromInspector.typeOf(from, property),
372                                 " in from component while in to component it is ",
373                                 this.toInspector.typeOf(to, property)
374                             ));
375                         }
376                     }
377 
378                     try {
379 
380                         this.setter.set(
381                             to,
382                             value,
383                             property
384                         );
385 
386                         debug(trace) trace("Migrated \"", property, "\" from ", from.identify, " to ", to.identify);
387                     } catch (Exception e) {
388 
389                         debug(trace) trace(
390                             "Couldn't ",
391                             this.force ? "forcefully " : "",
392                             "set property \"",
393                             property,
394                             "\" to ",
395                             to.identify,
396                             " from ",
397                             from.identify,
398                             " due to ",
399                             e
400                         );
401                     }
402                 } else {
403 
404                     debug(trace) error(to.identify, " element does not have: ", property);
405                 }
406             }
407         }
408 
409     }
410 }
411 
412 /**
413 An implementation of convertor that is using a mapper to map from component to component.
414 **/
415 @component
416 class CompositeConvertor(To, From) : Convertor {
417     import std.algorithm : filter;
418     import std.array: array;
419 
420     private {
421         Mapper!(To, From) mapper_;
422     }
423 
424     public {
425         @property {
426             /**
427             Set mapper
428 
429             Params:
430                 mapper = mapper used to map from component to component
431 
432             Returns:
433                 typeof(this)
434             **/
435             @autowired
436             typeof(this) mapper(Mapper!(To, From) mapper) @safe nothrow pure {
437                 this.mapper_ = mapper;
438 
439                 return this;
440             }
441 
442             /**
443             Get mapper
444 
445             Returns:
446                 Mapper!(To, From)
447             **/
448             inout(Mapper!(To, From)) mapper() @safe nothrow pure inout {
449                 return this.mapper_;
450             }
451         }
452 
453         mixin ConvertsFromToMixin!(From, To);
454 
455         /**
456         Convert from component to component.
457 
458         Params:
459             from = original component that is to be converted.
460             to = destination object that will be constructed out for original one.
461             allocator = optional allocator that could be used to construct to component.
462         Throws:
463             ConvertorException when there is a converting error
464             InvalidArgumentException when arguments passed are not of right type or state
465         Returns:
466             Resulting converted component.
467         **/
468         Object convert(in Object from, const TypeInfo to, RCIAllocator allocator = theAllocator)  const {
469             enforce!InvalidArgumentException(
470                 this.convertsFrom(from),
471                 text(
472                     "Cannot convert ", from.identify, " to ", typeid(To),
473                     ", ", from.identify, " is not supported by ", typeid(this)
474                 )
475             );
476 
477             enforce!InvalidArgumentException(
478                 this.convertsTo(to),
479                 text(
480                     "Cannot convert ", from.identify, " to ", typeid(To),
481                     ", ", to, " is not supported by ", typeid(this)
482                 )
483             );
484 
485 
486             auto placeholder = To.init.pack(from, this, allocator);
487 
488             static if (is(To : Object)) {
489                 placeholder.value = allocator.make!To;
490             }
491 
492             this.mapper.map(from.unwrap!From, placeholder.value, allocator);
493 
494             return placeholder;
495         }
496 
497         /**
498         Destroy component created using this convertor.
499 
500         Destroy component created using this convertor.
501         Since convertor could potentially allocate memory for
502         converted component, only itself is containing history of allocation,
503         and therefore it is responsible as well to destroy and free allocated
504         memory with allocator.
505 
506         Params:
507             converted = component that should be destroyed.
508             allocator = allocator used to allocate converted component.
509         **/
510         void destruct(const TypeInfo from, ref Object converted, RCIAllocator allocator = theAllocator) const {
511             enforce!ConvertorException(this.destroys(from, converted), text(
512                 "Cannot destruct ", converted.identify, " not supported by ", typeid(this), " expected ", this.to
513             ));
514 
515             static if (is(To : Object)) {
516                 allocator.dispose(converted.unpack!To);
517             } else {
518                 converted.unpack!To;
519             }
520 
521             converted = Object.init;
522         }
523 
524         mixin EqualToHashToStringOpCmpMixin!();
525     }
526 }