1 /** 2 Automatic compile-time generated object mapper. 3 4 This module aims to provide a compile-time generated way to 5 create object mapper with no runtime overhead. 6 7 Features: 8 9 $(UL 10 $(LI Object and struct automatic mapping: See `AutoMapper`) 11 $(LI Type converter: See `automapper.type.converter`) 12 $(LI Value transformer: See `automapper.value.transformer`) 13 ) 14 15 To get an AutoMapper instance you need to provide a `automapper.config.MapperConfiguration`. 16 17 */ 18 module automapper; 19 20 import automapper.meta; 21 import automapper.value.transformer; 22 23 public import automapper.api; 24 public import automapper.type.converter; 25 public import automapper.mapper; 26 public import automapper.naming; 27 28 29 /** 30 AutoMapper entry point. 31 32 Used to create low runtime overhead mapper for object or struct. 33 34 Mappings are compile-time checked. 35 */ 36 class AutoMapper(MC) if (is(MC : MapperConfiguration!(C), C)) 37 { 38 import std.format : format; 39 40 private: 41 alias TypesConverters = MC.TypesConverters; 42 alias ValueTransformers = MC.ValueTransformers; 43 44 template completeMapperConfiguration() 45 { 46 private template completeMapperConfigurationImpl(size_t idx) { 47 static if (idx < MC.ObjectMappersConfig.length) { 48 alias CurrentConfig = MC.ObjectMappersConfig[idx]; 49 50 // trick to get config specialization 51 static if (is(CurrentConfig : ObjectMapperConfig!(TSource, TDest, TSourceConv, 52 TDestConv, Reverse, Mappings), 53 TSource, TDest, TSourceConv, TDestConv, bool Reverse, Mappings...)) { 54 55 alias NewConfig = tryAutoMapUnmappedMembers!(TSource, TDest, TSourceConv, TDestConv, Mappings); 56 alias completeMapperConfigurationImpl = AliasSeq!( 57 ObjectMapperConfig!(TSource, TDest, TSourceConv, TDestConv, Reverse, NewConfig), 58 completeMapperConfigurationImpl!(idx + 1)); 59 } 60 } 61 else 62 alias completeMapperConfigurationImpl = AliasSeq!(); 63 } 64 65 alias completeMapperConfiguration = completeMapperConfigurationImpl!0; 66 } 67 68 alias CompletedConfig = completeMapperConfiguration!(); 69 alias FullMapperConfigs = AliasSeq!(CompletedConfig, generateReversedMapperConfig!(CompletedConfig)); 70 71 template buildMapper() 72 { 73 private template buildMapperImpl(size_t idx) { 74 static if (idx < FullMapperConfigs.length) { 75 alias MPC = FullMapperConfigs[idx]; 76 77 alias buildMapperImpl = AliasSeq!( 78 ObjectMapper!MPC, 79 buildMapperImpl!(idx + 1)); 80 } 81 else 82 alias buildMapperImpl = AliasSeq!(); 83 } 84 alias buildMapper = buildMapperImpl!0; 85 } 86 87 88 alias FullMappers = buildMapper!(); 89 90 91 // run-time 92 string runtimeUniqueMapperIdentifier(TypeInfo a, TypeInfo b) 93 { 94 import std..string : replace; 95 96 return ("mapper_" ~ a.toString() ~ "_" ~ b.toString()).replace(".", "_"); 97 } 98 99 /// compile-time 100 template uniqueConverterIdentifier(T) 101 { 102 import std..string : replace; 103 static if (is(T : ITypeConverter!(A, B), A, B)) 104 enum string uniqueConverterIdentifier = ("conv_" ~ fullyQualifiedName!A ~ "_" ~ fullyQualifiedName!B) 105 .replace(".", "_"); 106 } 107 108 // declare private registered ITypeConverter 109 static foreach (Conv; TypesConverters) { 110 static if (is(Conv : ITypeConverter!(A, B), A, B)) { 111 mixin(q{private ITypeConverter!(%s, %s) %s; }.format(fullyQualifiedName!A, fullyQualifiedName!B, 112 uniqueConverterIdentifier!Conv)); 113 } 114 } 115 116 /// compile-time 117 template uniqueTransformerIdentifier(A) 118 { 119 import std..string : replace; 120 enum string uniqueTransformerIdentifier = ("trans" ~ fullyQualifiedName!A).replace(".", "_"); 121 } 122 123 // declare private registered IValueTransformer 124 static foreach (Trans; ValueTransformers) { 125 static if (is(Trans : IValueTransformer!TValue, TValue)) { 126 mixin(q{private IValueTransformer!TValue %s; }.format( 127 uniqueTransformerIdentifier!TValue)); 128 } 129 } 130 131 public: 132 /// 133 this() 134 { 135 // instanciate registered ITypeConverter 136 static foreach (Conv; TypesConverters) 137 mixin(q{%s = new Conv(); }.format(uniqueConverterIdentifier!Conv)); 138 139 // instanciate registered IValueTransformer 140 static foreach (Trans; ValueTransformers) 141 static if (is(Trans : IValueTransformer!TValue, TValue)) 142 mixin(q{%s = new Trans(); }.format(uniqueTransformerIdentifier!TValue)); 143 } 144 145 /// It create a RuntimeAutoMapper that is an AutoMapper more suitable for runtime operation. 146 /// It means that errors are thrown at runtime. 147 RuntimeAutoMapper createRuntimeContext() 148 { 149 auto context = new RuntimeAutoMapper(); 150 151 // fill mapper 152 static foreach (Mapper; FullMappers) 153 context.runtimeMappers[typeid(IMapper!(Mapper.TSource, Mapper.TDest))] = new Mapper(context); 154 155 // fill converters 156 static foreach (Conv; TypesConverters) 157 static if (is(Conv : ITypeConverter!(TSource, TDest), TSource, TDest)) 158 context.converters[typeid(ITypeConverter!(TSource, TDest))] = new Conv(); 159 160 // fill transformers 161 static foreach (Trans; ValueTransformers) 162 static if (is(Trans : IValueTransformer!TValue, TValue)) 163 context.transformers[typeid(IValueTransformer!TValue)] = new Trans(); 164 165 return context; 166 } 167 168 /** 169 Map an object to another. 170 Params: 171 source = The type to map 172 Retuns: 173 The mapped object 174 */ 175 TDest map(TDest, TSource)(TSource source) if (isClassOrStruct!TSource && isClassOrStruct!TDest) 176 { 177 template isRightMapper(T) { 178 enum bool isRightMapper = (isInstanceOf!(ObjectMapper, T) && is(T.TSource : TSource) && 179 is(T.TDest : TDest)); 180 } 181 182 alias M = Filter!(isRightMapper, FullMappers); 183 184 static if (M.length is 0) 185 static assert(false, "No mapper found for mapping from " ~ TSource.stringof ~ " to " ~ TDest.stringof); 186 else 187 return M[0].map(source, this); 188 } 189 190 /// ditto 191 TDest map(TDest, TSource)(TSource source) if (isArray!TSource && isArray!TDest) 192 { 193 TDest ret = TDest.init; 194 195 foreach(ForeachType!TSource elem; source) { 196 static if (is(ForeachType!TSource == ForeachType!TDest)) 197 ret ~= elem; // same array type, just copy 198 else 199 ret ~= this.map!(ForeachType!TDest)(elem); // else map 200 } 201 202 return ret; 203 } 204 205 /// ditto 206 TDest map(TDest, TSource)(TSource source) if 207 (!isArray!TSource && !isArray!TDest && (!isClassOrStruct!TSource || !isClassOrStruct!TDest)) 208 { 209 template isRightConverter(T) { 210 enum bool isRightConverter = is(T : ITypeConverter!(TSource, TDest)); 211 } 212 213 alias M = Filter!(isRightConverter, TypesConverters); 214 215 static if (M.length is 0) 216 static assert(false, "No type converter found for mapping from " ~ TSource.stringof ~ " to " ~ 217 TDest.stringof); 218 else 219 return __traits(getMember, this, uniqueConverterIdentifier!M).convert(source); 220 } 221 222 /// 223 TValue transform(TValue)(TValue value) 224 { 225 template isRightTransformer(T) { 226 enum bool isRightTransformer = is(T : IValueTransformer!(TValue)); 227 } 228 229 alias Transformer = Filter!(isRightTransformer, ValueTransformers); 230 231 static if (Transformer.length is 0) { 232 pragma(inline, true); 233 return value; 234 } 235 else 236 return __traits(getMember, this, uniqueTransformerIdentifier!TValue).transform(value); 237 } 238 } 239 240 /** 241 This is a non templated slower version of AutoMapper. 242 */ 243 class RuntimeAutoMapper 244 { 245 package Object[TypeInfo] runtimeMappers; 246 package Object[TypeInfo] transformers; 247 package Object[TypeInfo] converters; 248 249 public: 250 /// Retrive a mapper instance. 251 IMapper!(TSource, TDest) getMapper(TDest, TSource)() 252 { 253 alias I = IMapper!(TSource, TDest); 254 auto info = typeid(I); 255 256 Object* mapper = (info in runtimeMappers); 257 258 if (mapper !is null) { 259 return (cast(I) *mapper); 260 } 261 else { 262 return null; 263 } 264 } 265 266 /// Map a source object to a dest object. 267 TDest map(TDest, TSource)(TSource value) if (isClassOrStruct!TSource && isClassOrStruct!TDest) 268 { 269 // enum id = uniquePairIdentifier!(TSource, TDest); 270 alias I = IMapper!(TSource, TDest); 271 auto info = typeid(I); 272 273 Object* mapper = (info in runtimeMappers); 274 275 if (mapper !is null) { 276 return (cast(I) *mapper).map(value); 277 } 278 else { 279 throw new Exception("No mapper found for mapping from " ~ TSource.stringof ~ " to " ~ TDest.stringof); 280 } 281 } 282 283 /// ditto 284 TDest map(TDest, TSource)(TSource source) if (isArray!TSource && isArray!TDest) 285 { 286 TDest ret = TDest.init; 287 288 foreach(ForeachType!TSource elem; source) { 289 static if (is(ForeachType!TSource == ForeachType!TDest)) 290 ret ~= elem; // same array type, just copy 291 else 292 ret ~= this.map!(ForeachType!TDest)(elem); // else map 293 } 294 295 return ret; 296 } 297 298 /// ditto 299 TDest map(TDest, TSource)(TSource source) 300 if (!isArray!TDest && (!isClassOrStruct!TSource || !isClassOrStruct!TDest)) 301 { 302 alias I = ITypeConverter!(TSource, TDest); 303 auto info = typeid(I); 304 305 Object* converter = (info in converters); 306 307 if (converters !is null) { 308 return (cast(I) *converter).convert(source); 309 } 310 else { 311 throw new Exception("No converter found for converting from " ~ TSource.stringof ~ " to " ~ TDest.stringof); 312 } 313 } 314 315 /// Transform a value using registered value transformers. 316 TValue transform(TValue)(TValue value) 317 { 318 // enum id = uniqueTypeIdentifier!TValue; 319 alias I = IValueTransformer!TValue; 320 auto info = typeid(I); 321 322 Object* transformer = (info in transformers); 323 324 if (transformer !is null) { 325 return (cast(I) *transformer).transform(value); 326 } 327 else { 328 return value; 329 } 330 } 331 } 332 333 /// 334 unittest 335 { 336 import std.datetime : SysTime; 337 import automapper; 338 339 static class A { 340 string foo = "foo"; 341 long timestamp; 342 } 343 344 static class B { 345 string bar; 346 SysTime timestamp; 347 } 348 349 RuntimeAutoMapper am = MapperConfiguration!( 350 CreateMap!(long, SysTime) 351 .ConvertUsing!((long ts) => SysTime(ts)), 352 CreateMap!(A, B) 353 .ForMember!("bar", "foo")) 354 .createMapper().createRuntimeContext(); 355 356 am.map!B(new A()); 357 } 358 359 360 /// 361 unittest 362 { 363 import std.datetime : SysTime; 364 365 static class Address { 366 long zipcode = 42_420; 367 string city = "London"; 368 } 369 370 static class User { 371 Address address = new Address(); 372 string name = "Foo"; 373 string lastName = "Bar"; 374 string mail = "foo.bar@baz.fr"; 375 long timestamp; 376 } 377 378 static class UserDTO { 379 string fullName; 380 string email; 381 string addressCity; 382 long addressZipcode; 383 SysTime timestamp; 384 int context; 385 } 386 387 // we would like to map from User to UserDTO 388 auto am = MapperConfiguration!( 389 // create a type converter for a long to SysTime 390 CreateMap!(long, SysTime) 391 .ConvertUsing!((long ts) => SysTime(ts)), 392 // create a mapping for User to UserDTO 393 CreateMap!(User, UserDTO) 394 // map member using a delegate 395 .ForMember!("fullName", (User a) => a.name ~ " " ~ a.lastName ) 396 // map UserDTO.email to User.mail 397 .ForMember!("email", "mail") 398 // ignore UserDTO.context 399 .Ignore!"context") 400 // other member are automatically mapped 401 .createMapper(); 402 403 auto user = new User(); 404 const UserDTO dto = am.map!UserDTO(user); 405 406 assert(dto.fullName == user.name ~ " " ~ user.lastName); 407 assert(dto.addressCity == user.address.city); 408 assert(dto.addressZipcode == user.address.zipcode); 409 } 410 411 /// Naming conventions 412 unittest 413 { 414 static class A { 415 int foo_bar_baz = 42; 416 string data_processor = "42"; 417 } 418 419 static class B { 420 int fooBarBaz; 421 string dataProcessor; 422 } 423 424 auto am = MapperConfiguration!( 425 CreateMap!(A, B) 426 .SourceMemberNaming!LowerUnderscoreNamingConvention 427 .DestMemberNaming!CamelCaseNamingConvention) 428 .createMapper(); 429 430 A a = new A(); 431 const B b = am.map!B(a); 432 assert(a.foo_bar_baz == b.fooBarBaz); 433 assert(a.data_processor == b.dataProcessor); 434 } 435 436 /// Type converters 437 unittest 438 { 439 import std.datetime: SysTime; 440 441 static class A { 442 long timestamp = 1_542_873_605; 443 } 444 445 static class B { 446 SysTime timestamp; 447 } 448 449 auto am_delegate = MapperConfiguration!( 450 CreateMap!(long, SysTime).ConvertUsing!((long ts) => SysTime(ts)), 451 CreateMap!(A, B)) 452 .createMapper(); 453 454 A a = new A(); 455 B b = am_delegate.map!B(a); 456 assert(SysTime(a.timestamp) == b.timestamp); 457 458 459 static class TimestampToSystime : ITypeConverter!(long, SysTime) { 460 override SysTime convert(long ts) { 461 return SysTime(ts); 462 } 463 } 464 465 auto am_class = MapperConfiguration!( 466 CreateMap!(long, SysTime).ConvertUsing!TimestampToSystime, 467 CreateMap!(A, B)) 468 .createMapper(); 469 470 a = new A(); 471 b = am_class.map!B(a); 472 assert(SysTime(a.timestamp) == b.timestamp); 473 } 474 475 /// struct 476 unittest 477 { 478 static struct A { 479 int foo; 480 } 481 482 static struct B { 483 int foo; 484 } 485 486 auto am = MapperConfiguration!( 487 CreateMap!(A, B)) 488 .createMapper(); 489 490 A a; 491 const B b = am.map!B(a); 492 assert(b.foo == a.foo); 493 } 494 495 /// reverse flattening 496 unittest 497 { 498 static class Address { 499 int zipcode; 500 } 501 502 static class A { 503 Address address; 504 } 505 506 static class B { 507 int addressZipcode = 74_000; 508 } 509 510 auto am = MapperConfiguration!( 511 CreateMap!(A, B) 512 .ReverseMap!()) 513 .createMapper(); 514 515 B b = new B(); 516 const A a = am.map!A(b); 517 assert(b.addressZipcode == a.address.zipcode); 518 } 519 520 /// array 521 unittest 522 { 523 import std.algorithm.comparison : equal; 524 525 static class Data { 526 this() {} 527 this(string id) { this.id = id; } 528 string id; 529 } 530 531 static class DataDTO { 532 string id; 533 } 534 535 static class A { 536 int[] foo = [1, 2, 4, 8]; 537 Data[] data = [new Data("baz"), new Data("foz")]; 538 } 539 540 static class B { 541 int[] foo; 542 DataDTO[] data; 543 } 544 545 auto am = MapperConfiguration!( 546 CreateMap!(Data, DataDTO), 547 CreateMap!(A, B)) 548 .createMapper(); 549 550 MapperConfiguration!( 551 CreateMap!(Data, DataDTO), 552 CreateMap!(A, B)) 553 .createMapper(); 554 555 A a = new A(); 556 B b = am.map!B(a); 557 558 assert(b.foo.equal(a.foo)); 559 assert(b.data.length == 2); 560 assert(b.data[0].id == "baz"); 561 assert(b.data[1].id == "foz"); 562 } 563 564 /// auto 565 unittest 566 { 567 static class Address { 568 long zipcode = 74_000; 569 string city = "unknown"; 570 } 571 572 static class User { 573 Address address = new Address(); 574 string name = "Eliott"; 575 string lastName = "Dumeix"; 576 } 577 578 static class UserDTO { 579 string fullName; 580 string addressCity; 581 long addressZipcode; 582 } 583 584 auto am = MapperConfiguration!( 585 CreateMap!(User, UserDTO) 586 .ForMember!("fullName", (User a) => a.name ~ " " ~ a.lastName )) 587 .createMapper(); 588 589 auto user = new User(); 590 const UserDTO dto = am.map!UserDTO(user); 591 592 assert(dto.fullName == user.name ~ " " ~ user.lastName); 593 assert(dto.addressCity == user.address.city); 594 assert(dto.addressZipcode == user.address.zipcode); 595 596 } 597 598 /// flattening 599 unittest 600 { 601 static class Address { 602 int zipcode = 74_000; 603 } 604 605 static class A { 606 Address address = new Address(); 607 } 608 609 static class B { 610 int addressZipcode; 611 } 612 613 auto am = MapperConfiguration!( 614 CreateMap!(A, B) 615 .ForMember!("addressZipcode", "address.zipcode")) 616 .createMapper(); 617 618 A a = new A(); 619 const B b = am.map!B(a); 620 assert(b.addressZipcode == a.address.zipcode); 621 } 622 623 /// nest 624 unittest 625 { 626 static class Address { 627 int zipcode = 74_000; 628 } 629 630 static class AddressDTO { 631 int zipcode; 632 } 633 634 static class A { 635 Address address = new Address(); 636 } 637 638 static class B { 639 AddressDTO address; 640 } 641 642 auto am = MapperConfiguration!( 643 CreateMap!(Address, AddressDTO) 644 .ForMember!("zipcode", "zipcode") 645 .ReverseMap!(), 646 CreateMap!(A, B) 647 .ForMember!("address", "address") 648 .ReverseMap!()) 649 .createMapper(); 650 651 A a = new A(); 652 const B b = am.map!B(a); 653 assert(b.address.zipcode == a.address.zipcode); 654 655 // test reversed mapper 656 am.map!Address(new AddressDTO()); 657 } 658 659 unittest 660 { 661 static class A { 662 string str = "foo"; 663 int foo = 42; 664 } 665 666 static class B { 667 string str; 668 int foo; 669 long bar; 670 string mod; 671 } 672 673 auto am = MapperConfiguration!( 674 CreateMap!(A, B) 675 .ForMember!("str", "str") 676 .ForMember!("foo", "foo") 677 .Ignore!"bar" 678 .ForMember!("mod", (A a) { 679 return "modified"; 680 })) 681 .createMapper(); 682 683 684 A a = new A(); 685 const B b = am.map!B(a); 686 assert(b.str == a.str); 687 assert(a.foo == a.foo); 688 assert(b.mod == "modified"); 689 }