1 module aermicioi.aedi_property_reader.convertor.chaining_convertor;
2 
3 import std.algorithm;
4 import std.experimental.allocator;
5 import std.experimental.logger;
6 import std.conv;
7 import std.array;
8 import std.range : drop;
9 import std.range.primitives : isInputRange, ElementType;
10 import std.typecons : No;
11 import aermicioi.aedi_property_reader.convertor.convertor;
12 import aermicioi.aedi_property_reader.convertor.exception;
13 import aermicioi.aedi_property_reader.convertor.placeholder;
14 import aermicioi.aedi_property_reader.convertor.traits : n;
15 
16 @safe interface PricingStrategy {
17 
18     size_t rate(const TypeInfo from, const TypeInfo to) const nothrow;
19 }
20 
21 private bool passable(size_t score) @safe nothrow {
22     return score !is size_t.max;
23 }
24 
25 @safe class NumericPricingStrategy : PricingStrategy {
26 
27     private const TypeInfo[] identifications = [typeid(ulong), typeid(uint), typeid(ushort), typeid(ubyte), typeid(long), typeid(int), typeid(short), typeid(byte), typeid(real), typeid(double), typeid(float)];
28     private const size_t[][] prices = [
29         [0, 1, 2, 3, 1, 2, 3, 4, 5, 6, 7], // typeid(ulong),
30         [0, 0, 1, 2, 0, 1, 2, 3, 5, 6, 7], // typeid(uint),
31         [0, 0, 0, 1, 0, 0, 1, 2, 5, 6, 7], // typeid(ushort),
32         [0, 0, 0, 0, 0, 0, 0, 1, 5, 6, 7], // typeid(ubyte),
33         [1, 2, 3, 4, 0, 1, 2, 3, 5, 6, 7], // typeid(long),
34         [1, 1, 2, 3, 0, 0, 1, 2, 5, 6, 7], // typeid(int),
35         [1, 1, 1, 2, 0, 0, 0, 1, 5, 6, 7], // typeid(short),
36         [1, 1, 1, 1, 0, 0, 0, 0, 5, 6, 7], // typeid(byte),
37         [5, 6, 7, 8, 4, 5, 6, 7, 0, 1, 2], // typeid(real),
38         [5, 6, 7, 8, 4, 5, 6, 7, 0, 0, 1], // typeid(double),
39         [5, 6, 7, 8, 4, 5, 6, 7, 0, 0, 0], // typeid(float)
40     ];
41 
42     size_t rate(const TypeInfo from, const TypeInfo to) const nothrow {
43         import std.math;
44         import std.algorithm : canFind;
45 
46         if (!identifications.canFind!(tested => tested is from) || !identifications.canFind!(tested => tested is to)) {
47             return size_t.max;
48         }
49 
50         size_t fromIndex = identifications.countUntil!(tested => tested is from);
51         size_t toIndex = identifications.countUntil!(tested => tested is to);
52 
53         return prices[fromIndex][toIndex];
54     }
55 }
56 
57 @safe class DefaultPricingStrategy : PricingStrategy {
58     private size_t price = 1000;
59 
60     this() {
61 
62     }
63 
64     this(size_t price) {
65         this.price = price;
66     }
67 
68     size_t rate(const TypeInfo from, const TypeInfo to) const nothrow {
69         return price;
70     }
71 }
72 
73 @safe class MinimalBidPricingStrategy : PricingStrategy {
74 
75     private const(PricingStrategy)[] strategies;
76 
77     this(PricingStrategy[] strategies...)
78         in (!strategies.empty, "Cannot select minimal bidding strategy when none provided.") {
79         this.strategies = strategies.dup;
80     }
81 
82     typeof(this) add(const PricingStrategy strategy) nothrow {
83         import std.algorithm : canFind;
84         if (!this.strategies.canFind!(candidate => candidate is strategy)) {
85 
86             this.strategies ~= strategy;
87         }
88 
89         return this;
90     }
91 
92     typeof(this) remove(const PricingStrategy strategy) @trusted nothrow {
93         import std.algorithm : filter;
94         this.strategies = this.strategies.filter!(candidate => candidate !is strategy).array;
95 
96         return this;
97     }
98 
99     size_t rate(const TypeInfo from, const TypeInfo to) const nothrow {
100         return strategies.map!(strategy => strategy.rate(from, to)).minElement(size_t.max);
101     }
102 }
103 
104 @safe class IdenticalTypePriceStrategy : PricingStrategy {
105 
106     private size_t price;
107 
108     this(size_t price = 0) {
109         this.price = price;
110     }
111 
112     size_t rate(const TypeInfo from, const TypeInfo to) const nothrow {
113         if (from is to) {
114             return price;
115         }
116 
117         return size_t.max;
118     }
119 }
120 
121 @safe class OffsettingPriceStrategy : PricingStrategy {
122 
123     private size_t offset;
124     private const PricingStrategy strategy;
125 
126     this(const PricingStrategy strategy, size_t price = 1)
127         in (strategy !is null, "Cannot offset a price when decorated pricing is missing.") {
128         this.offset = price;
129         this.strategy = strategy;
130     }
131 
132     size_t rate(const TypeInfo from, const TypeInfo to) const nothrow {
133         size_t result = strategy.rate(from, to);
134 
135         if (result.passable) {
136             result += this.offset;
137         }
138 
139         return result;
140     }
141 }
142 
143 @safe class OverridablePricingStrategy : PricingStrategy {
144     private {
145         const PricingStrategy strategy;
146 
147         @safe static struct Relation {
148             const TypeInfo from;
149             const TypeInfo to;
150             size_t price;
151 
152             ptrdiff_t opEquals(in ref Relation relation) const nothrow {
153                 return (this.from is relation.from) && (this.to is relation.to);
154             }
155 
156             ref typeof(this) opAssign(in ref Relation relation) nothrow
157                 in (this == relation, "Cannot assign to a relation with different identity") {
158                 this.price = relation.price;
159 
160                 return this;
161             }
162         }
163 
164         Relation[] relations;
165     }
166 
167     this(const PricingStrategy strategy) nothrow
168         in (strategy !is null, "Cannot override a price when no underlying strategy is provided") {
169         this.strategy = strategy;
170 
171     }
172 
173     typeof(this) modify(const TypeInfo from, const TypeInfo to, size_t price) nothrow {
174         auto fresh = Relation(from, to, price);
175 
176         foreach (ref relation; relations) {
177 
178             if (relation == fresh) {
179                 relation = fresh;
180                 return this;
181             }
182         }
183 
184         relations ~= fresh;
185 
186         return this;
187     }
188 
189     typeof(this) modify(const TypeInfo from, const TypeInfo to) nothrow {
190         this.modify(from, to, size_t.max);
191 
192         return this;
193     }
194 
195     typeof(this) clear() {
196         this.relations = null;
197 
198         return this;
199     }
200 
201     size_t rate(const TypeInfo from, const TypeInfo to) const nothrow {
202         auto modified = this.relations[].filter!(relation => (relation.from is from) && (relation.to is to));
203 
204         if (!modified.empty) {
205             return modified.front.price;
206         }
207 
208         return this.strategy.rate(from, to);
209     }
210 }
211 
212 class ByTypePricingStrategy : PricingStrategy {
213     struct Entry {
214         const TypeInfo type;
215         size_t price;
216     }
217 
218     private const(Entry)[] entries;
219     private const(size_t delegate(size_t from, size_t to) @safe scope nothrow) adjuster;
220 
221     this() {
222         import std.functional : toDelegate;
223         this((size_t from, size_t to) scope nothrow => cast(size_t) ((from + to) / 2));
224     }
225 
226     this(size_t delegate(size_t from, size_t to) @safe scope nothrow adjuster) {
227         this.adjuster = adjuster;
228     }
229 
230     /**
231     Set entry
232 
233     Params:
234         entries = an overriding entry;
235 
236     Returns:
237         typeof(this)
238     **/
239     typeof(this) add(Entry[] entries...) @safe nothrow pure {
240         foreach (entry; entries) {
241             if (!this.entries.canFind!(candidate => candidate.type is entry.type)) {
242                 this.entries ~= entry;
243             }
244         }
245 
246         return this;
247     }
248 
249     /**
250     ditto
251     **/
252     typeof(this) add(const TypeInfo type, size_t price) @safe nothrow pure {
253         return this.add(Entry(type, price));
254     }
255 
256     size_t rate(const TypeInfo from, const TypeInfo to) const nothrow {
257         auto fromCandidate = this.entries.filter!(entry => entry.type is from);
258         auto toCandidate = this.entries.filter!(entry => entry.type is to);
259 
260 
261         if (fromCandidate.empty || toCandidate.empty) {
262             return size_t.max;
263         }
264 
265         return adjuster(fromCandidate.front.price, toCandidate.front.price);
266     }
267 }
268 
269 /**
270 A convertor tries to build a chain of convertors from source to destination.
271 **/
272 class ChainingConvertor : CombinedConvertorImpl {
273     import std.algorithm : canFind, find;
274 
275     private {
276         @safe struct Node {
277             import std.typecons : Rebindable;
278             size_t price = size_t.max;
279             bool visited;
280             Rebindable!(const(TypeInfo)) type;
281             Rebindable!(const(TypeInfo)) next;
282 
283             this(const(TypeInfo) type, size_t price, bool visited, const(TypeInfo) next = null) @trusted {
284                 this(type);
285                 this.price = price;
286                 this.visited = visited;
287                 this.next = next;
288             }
289 
290             this(const(TypeInfo) type) @trusted
291                 in (type !is null, "Cannot create node for a null type") {
292                 this.type = type;
293             }
294 
295             bool opEquals(in ref Node node) const {
296                 return this.type.get is node.type.get;
297             }
298 
299             size_t toHash() const nothrow {
300                 try {
301 
302                     return this.type.toHash;
303                 } catch (Exception e) {
304 
305                     assert(false, "Something completely wrong went during hashing");
306                 }
307             }
308         }
309 
310         @safe static struct Path {
311             import std.typecons : Rebindable;
312             private Node[const TypeInfo] graph;
313             private Rebindable!(const TypeInfo) current;
314             private Rebindable!(const TypeInfo) next;
315 
316             this(Node[const TypeInfo] graph, const TypeInfo from) nothrow {
317                 this.graph = graph;
318                 this.current = this.graph[from].type;
319                 this.next = this.graph[from].next;
320             }
321 
322             const(TypeInfo) front() inout nothrow {
323                 return current;
324             }
325 
326             bool empty() nothrow const {
327                 return current.get is null;
328             }
329 
330             void popFront() nothrow {
331                 current = next;
332 
333                 if (next !is null) {
334                     next = graph[next].next;
335                 }
336             }
337 
338             Path save() nothrow {
339                 return Path(graph, current);
340             }
341         }
342 
343         Convertor[] convertors_;
344         PricingStrategy appraiser;
345     }
346 
347     public {
348 
349         /**
350         Default constructor for AggregateConvertor
351         **/
352         this(PricingStrategy appraiser, Convertor[] convertors...) {
353             this.convertors = convertors.dup;
354             this.appraiser = appraiser;
355         }
356 
357         /**
358         Check whether convertor is able to convert from type to type.
359 
360         Check whether convertor is able to convert from type to type.
361         This set of methods should be the most precise way of determining
362         whether convertor is able to convert from type to type, since it
363         provides both components to the decision logic implemented by convertor
364         compared to the case with $(D_INLINECODE convertsTo) and $(D_INLINECODE convertsFrom).
365         Note that those methods are still useful when categorization or other
366         logic should be applied per original or destination type.
367 
368         Implementation:
369             This is default implementation of converts methods which delegate
370             the decision to $(D_INLINECODE convertsTo) and $(D_INLINECODE convertsFrom).
371 
372         Params:
373             from = the original component or it's type to convert from
374             to = the destination component or it's type to convert to
375 
376         Returns:
377             true if it is able to convert from component to destination component
378         **/
379         override bool converts(const TypeInfo from, const TypeInfo to) @safe const nothrow {
380             debug(trace) trace("Checking if ", from, " is convertable to ", to).n;
381             return !this.convertors.filter!(convertor => convertor.converts(from, to)).empty || !this.search(from, to).empty;
382         }
383 
384         /**
385         ditto
386         **/
387         override bool converts(const TypeInfo from, in Object to) @safe const nothrow {
388             debug(trace) trace("Checking if ", from, " is convertable to ", to.identify).n;
389             return !this.convertors.filter!(convertor => convertor.converts(from, to)).empty || !this.search(from, to.identify).empty;
390         }
391 
392         /**
393         ditto
394         **/
395         override bool converts(in Object from, const TypeInfo to) @safe const nothrow {
396             debug(trace) trace("Checking if ", from.identify, " is convertable to ", to).n;
397             return !this.convertors.filter!(convertor => convertor.converts(from, to)).empty || !this.search(from, to).empty;
398         }
399 
400         /**
401         ditto
402         **/
403         override bool converts(in Object from, in Object to) @safe const nothrow {
404             debug(trace) trace("Checking if ", from.identify, " is convertable to ", to.identify).n;
405             return !this.convertors.filter!(convertor => convertor.converts(from, to)).empty || !this.search(from, to.identify).empty;
406         }
407 
408         /**
409         Convert from component to component.
410 
411         Finds a right convertor from component to component and uses it
412         to execute conversion from component to component.
413 
414         Params:
415             from = typeal component that is to be converted.
416             to = destination object that will be constructed out for typeal one.
417             allocator = optional allocator that could be used to construct to component.
418         Throws:
419             ConvertorException when convertor is not able to convert from, or to component.
420         Returns:
421             Resulting converted component.
422         **/
423         override Object convert(in Object from, const TypeInfo to, RCIAllocator allocator = theAllocator)  const
424         {
425             static union Context {
426                 const Object constant;
427                 Object mutable;
428             }
429 
430             import std.range : slide, take, drop;
431 
432             if (super.converts(from, to)) {
433                 return super.convert(from, to, allocator);
434             }
435 
436             Path path = this.search(from, to);
437 
438             if (path.empty) {
439                 throw new ConvertorException(text("Could not find a way to convert from ", from.identify, " to ", to, " type"));
440             }
441 
442             debug(trace) trace("Found best conversion path commencing conversion ", path, ".");
443             Context value = Context(from);
444 
445             foreach (conversion; path.slide(2).take(2)) {
446                 value.mutable = this.convertors.filter!(c => c.converts(conversion.front, conversion.drop(1).front)).front
447                     .convert(value.mutable, conversion.drop(1).front, allocator);
448             }
449 
450             path = path.drop(1);
451 
452             foreach (conversion; path.slide!(No.withPartial)(3)) {
453                 Object temporary = this.convertors.filter!(c => c.converts(conversion.drop(1).front, conversion.drop(2).front)).front
454                     .convert(value.mutable, conversion.drop(2).front, allocator);
455                 this.convertors.filter!(c => c.destroys(conversion.front, conversion.drop(1).front)).front
456                     .destruct(conversion.front, value.mutable, allocator);
457 
458                 value.mutable = temporary;
459             }
460 
461             return value.mutable;
462         }
463 
464         mixin ToStringMixin!();
465         mixin OpCmpMixin!();
466 
467         override size_t toHash() @trusted {
468             import std.range : only;
469             size_t result = super.toHash();
470 
471             foreach (convertor; convertors) {
472                 Object hashable = cast(Object) convertor;
473 
474                 size_t hash = (hashable !is null) ? hashable.toHash() : typeid(convertor).getHash(cast(void*) convertor);
475 
476                 result = result * 31 + hash;
477             }
478 
479             return result;
480         }
481 
482         override bool opEquals(Object o) {
483             return super.opEquals(o) || ((this.classinfo is o.classinfo) && this.opEquals(cast(ChainingConvertor) o));
484         }
485 
486         bool opEquals(ChainingConvertor convertor) {
487             import std.algorithm : equal;
488             return (convertor !is null) && (this.convertors.equal(convertor.convertors));
489         }
490     }
491 
492     private Path search(T)(T from, const TypeInfo to) @safe const nothrow
493         if (is(T : const Object) || is(T : const TypeInfo)) {
494         import std.range : slide, take, generate, chain;
495         import aermicioi.aedi_property_reader.convertor.traits : n;
496 
497         debug(trace) trace("Searching for a way to convert ", from.identify, " to ", to).n;
498 
499         OverridablePricingStrategy appraiser = new OverridablePricingStrategy(this.appraiser);
500 
501         foreach (path; this.range(from.identify, to, appraiser)) {
502 
503             debug(trace) trace("Found a way to convert ", from.identify, " to ", to, " using a chain of ", path, " convertors.").n;
504 
505             bool found = false;
506             auto step = path.take(2);
507             if (!this.convertors.filter!(c => c.converts(from, step.drop(1).front)).empty) {
508                 auto remainder = path.drop(1).slide(2).find!(
509                     (conversion) => this.convertors.filter!(c => c.converts(conversion.front, conversion.drop(1).front)).empty
510                 );
511 
512                 found = remainder.empty;
513                 if (!remainder.empty) {
514                     step = remainder.front;
515                 }
516             }
517 
518             if (found) {
519 
520                 return path;
521             }
522 
523             debug(trace) trace("Found chain of convertors broke at ", step, " marking it to avoid in next chain.").n;
524             appraiser.modify(step.front, step.drop(1).front);
525         }
526 
527         debug(trace) trace("Could not find a way to convert ", from.identify, " to ", to).n;
528         return Path.init;
529     }
530 
531     private Path pave(const TypeInfo from, const TypeInfo to, PricingStrategy appraiser) @safe const nothrow {
532         import std.algorithm;
533         import std.range;
534 
535         try {
536 
537             Node[const TypeInfo] nodes;
538             foreach (type; this.convertors.map!((c) @safe => chain(c.to, c.from)).joiner.filter!((type) @safe => type !is typeid(void))) {
539                 nodes[type] = Node(type, size_t.max, false);
540             }
541 
542             Node current = Node(to, 0, true);
543             nodes[to] = current;
544             Node[] unvisited;
545 
546             do {
547                 debug(ChainingConvertorTraceGraph) trace("Processing neighbors of ", current.type).n;
548 
549                 auto candidates = this.convertors.filter!(c => c.convertsTo(current.type));
550 
551                 foreach (convertor; candidates) {
552                     foreach (fromType; convertor.from) {
553                         size_t score = appraiser.rate(fromType, current.type);
554 
555                         debug(ChainingConvertorTraceGraph) trace("Checking convertion price to ", current.type, " from ", fromType, " of ", score).n;
556                         if (score.passable && ((current.price + score) < nodes[fromType].price)) {
557 
558                             debug(ChainingConvertorTraceGraph) trace(score, " less than one of ", nodes[fromType].price, " setting new one.").n;
559                             nodes[fromType].price = current.price + score;
560                             nodes[fromType].next = current.type;
561                         }
562                     }
563                 }
564 
565                 unvisited = unvisited
566                     .filter!(candidate => !nodes[candidate.type].visited)
567                     .chain(candidates.map!(convertor => convertor.from).joiner.map!(from => nodes[from]).filter!(node => !node.visited))
568                     .array;
569                 unvisited.sort!((f, s) => f.price < s.price);
570 
571                 nodes[current.type] = current;
572 
573                 if (!unvisited.empty) {
574                     debug(ChainingConvertorTraceGraph) trace("Selecting new unvisited node of ", unvisited.front.type).n;
575                     current = unvisited.front;
576                     current.visited = true;
577 
578                     unvisited.popFront;
579                 }
580             } while (!unvisited.empty && (current.type !is from));
581 
582             if (current.type !is from) {
583                 return Path.init;
584             }
585 
586             return Path(nodes, current.type);
587         } catch (Exception e) {
588 
589             throw new Error("Encountered unexpected exception during paving", e);
590         }
591     }
592 
593     auto range(const TypeInfo from, const TypeInfo to, PricingStrategy appraiser) const nothrow @safe {
594         import std.range : generate, chain, only;
595         import std.algorithm : until;
596         auto searcher = generate!(() => this.pave(from, to, appraiser)).until!(path => path.empty || path.drop(1).empty);
597 
598         return searcher;
599     }
600 }
601 
602 /**
603 A convertor that will run a chain of conversions.
604 **/
605 class ChainedConvertor : Convertor {
606     import std.algorithm : canFind, find;
607 
608     private {
609         Convertor[] convertors;
610     }
611 
612     public {
613 
614         /**
615         Default constructor for AggregateConvertor
616         **/
617         this(Convertor[] convertors...)
618             in (convertors.length > 0, "Chained convertor expects at least one convertor in chain to operate properly.")
619             in (ChainedConvertor.valid(convertors), "Chained convertor expects a unbroken chain of convertors, one provided is broken at some point.") {
620             this.convertors = convertors.dup;
621         }
622 
623         @property {
624 
625             /**
626             Get the type info of component that convertor can convert from.
627 
628             Get the type info of component that convertor can convert from.
629             The method is returning the default type that it is able to convert,
630             though it is not necessarily limited to this type only. More generalistic
631             checks should be done by convertsFrom method.
632 
633             Returns:
634                 type info of component that convertor is able to convert.
635             **/
636             const(TypeInfo)[] from() @safe const nothrow pure {
637                 return this.convertors[0].from;
638             }
639 
640             /**
641             Get the type info of component that convertor is able to convert to.
642 
643             Get the type info of component that convertor is able to convert to.
644             The method is returning the default type that is able to convert,
645             though it is not necessarily limited to this type only. More generalistic
646             checks should be done by convertsTo method.
647 
648             Returns:
649                 type info of component that can be converted to.
650             **/
651             const(TypeInfo)[] to() @safe const nothrow pure {
652                 return this.convertors[$ - 1].to;
653             }
654         }
655 
656         mixin ConvertsFromToMixin DefaultImplementation;
657 
658         /**
659         Check whether this convertor is able to destroy to component.
660 
661         The destroys family of methods are designed purposely for identification
662         whether convertor was able to convert from type to destination to, and
663         is eligible for destruction of converted components.
664 
665         Params:
666             from = original component which was converted.
667             to = converted component that should be destroyed by convertor.
668 
669         Returns:
670             true if convertor is eligible for destroying to, or false otherwise.
671         **/
672         bool destroys(const TypeInfo from, const TypeInfo to) @safe const nothrow {
673             return this.convertors.back.converts(from, to);
674         }
675 
676         /**
677         ditto
678         **/
679         bool destroys(in Object from, const TypeInfo to) @safe const nothrow {
680             return this.convertors.back.converts(from, to);
681         }
682 
683         /**
684         ditto
685         **/
686         bool destroys(const TypeInfo from, in Object to) @safe const nothrow {
687             return this.convertors.back.converts(from, to);
688         }
689 
690         /**
691         ditto
692         **/
693         bool destroys(in Object from, in Object to) @safe const nothrow {
694             return this.convertors.back.converts(from, to);
695         }
696 
697         /**
698         Convert from component to component.
699 
700         Finds a right convertor from component to component and uses it
701         to execute conversion from component to component.
702 
703         Params:
704             from = typeal component that is to be converted.
705             to = destination object that will be constructed out for typeal one.
706             allocator = optional allocator that could be used to construct to component.
707         Throws:
708             ConvertorException when convertor is not able to convert from, or to component.
709         Returns:
710             Resulting converted component.
711         **/
712         override Object convert(in Object from, const TypeInfo to, RCIAllocator allocator = theAllocator)  const
713         {
714             import std.math;
715 
716             union Context {
717                 const Object constant;
718                 Object mutable;
719             }
720 
721             Context value = Context(from);
722 
723             foreach (convertor; convertors[0 .. $.min(2)]) {
724                 value.mutable = convertor.convert(value.mutable, convertor.to[0], allocator);
725             }
726 
727             if (convertors.length > 2)
728             foreach (index, convertor; convertors[2 .. $]) {
729                 Object temporary = convertor.convert(value.mutable, convertor.to[0], allocator);
730                 convertors[index - 1].destruct(convertors[index - 1].from[0], value.mutable, allocator);
731 
732                 value.mutable = temporary;
733             }
734 
735             return value.mutable;
736         }
737 
738         /**
739         Destroy component created using this convertor.
740 
741         Find a suitable convertor for destruction and use it to execute destruction.
742 
743         Params:
744             converted = component that should be destroyed.
745             allocator = allocator used to allocate converted component.
746         **/
747         void destruct(const TypeInfo from, ref Object converted, RCIAllocator allocator = theAllocator) const {
748             import std.exception : enforce;
749             enforce!ConvertorException(this.destroys(from, converted), text(
750                 "Cannot destroy ", converted.identify, " which was not converted from ", from, ".",
751                 " Expected destroyable type of ", this.to, " from origin of ", this.from
752             ));
753 
754             this.convertors.back.destruct(from, converted, allocator);
755         }
756 
757         mixin ToStringMixin!();
758         mixin OpCmpMixin!();
759 
760         override size_t toHash() @trusted {
761             import std.range : only;
762             size_t result = super.toHash();
763 
764             foreach (convertor; convertors) {
765                 Object hashable = cast(Object) convertor;
766 
767                 size_t hash = (hashable !is null) ? hashable.toHash() : typeid(convertor).getHash(cast(void*) convertor);
768 
769                 result = result * 31 + hash;
770             }
771 
772             return result;
773         }
774 
775         override bool opEquals(Object o) {
776             return super.opEquals(o) || ((this.classinfo is o.classinfo) && this.opEquals(cast(ChainedConvertor) o));
777         }
778 
779         bool opEquals(ChainedConvertor convertor) {
780             import std.algorithm : equal;
781             return (convertor !is null) && (this.convertors.equal(convertor.convertors));
782         }
783     }
784 
785     private static valid(Convertor[] convertors) {
786         import std.range : slide;
787         import std.algorithm : any;
788 
789         if (convertors.length == 1) {
790             return true;
791         }
792 
793         foreach (window; convertors.slide(2)) {
794             if (!window[0].to.any!(to => window[1].from.any!(from => from is to))) {
795                 return false;
796             }
797         }
798 
799         return true;
800     }
801 }