1 /** 2 * This module converts a package containing D Model definitions to 3 * $(REF SerializedModels, dorm,declarative) 4 */ 5 module dorm.declarative.conversion; 6 7 import dorm.annotations; 8 import dorm.declarative; 9 import dorm.exception; 10 import dorm.model; 11 import dorm.types; 12 13 import std.algorithm : remove; 14 import std.conv; 15 import std.datetime; 16 import std.meta; 17 import std.traits; 18 import std.typecons; 19 20 version (unittest) import dorm.model; 21 22 /** 23 * Entry point to the Model (class) to SerializedModels (declarative) conversion 24 * code. Manually calling this should not be neccessary as the 25 * $(REF RegisterModels, dorm,declarative,entrypoint) mixin will call this instead. 26 */ 27 SerializedModels processModelsToDeclarations(alias mod)() 28 { 29 SerializedModels ret; 30 31 static foreach (member; __traits(allMembers, mod)) 32 { 33 static if (__traits(compiles, is(__traits(getMember, mod, member) : Model)) 34 && is(__traits(getMember, mod, member) : Model) 35 && !__traits(isAbstractClass, __traits(getMember, mod, member))) 36 { 37 processModel!( 38 __traits(getMember, mod, member), 39 SourceLocation(__traits(getLocation, __traits(getMember, mod, member))) 40 )(ret); 41 } 42 } 43 44 return ret; 45 } 46 47 private void processModel(TModel : Model, SourceLocation loc)( 48 ref SerializedModels models) 49 { 50 ModelFormat serialized = DormLayout!TModel; 51 serialized.definedAt = loc; 52 models.models ~= serialized; 53 } 54 55 private enum DormLayoutImpl(TModel : Model) = function() { 56 ModelFormat serialized; 57 serialized.tableName = DormModelTableName!TModel; 58 59 alias attributes = __traits(getAttributes, TModel); 60 61 static foreach (attribute; attributes) 62 { 63 static if (isDormModelAttribute!attribute) 64 { 65 static assert(false, "Unsupported attribute " ~ attribute.stringof ~ " on " ~ TModel.stringof ~ "." ~ member); 66 } 67 } 68 69 string errors; 70 71 static foreach (field; LogicalFields!(TModel, 2)) 72 { 73 try 74 { 75 processField!(TModel, field, field)(serialized); 76 } 77 catch (Exception e) 78 { 79 errors ~= "\n\t" ~ e.msg; 80 } 81 } 82 83 static if (!__traits(isAbstractClass, TModel)) 84 errors ~= serialized.lint("\n\t"); 85 86 struct Ret 87 { 88 string errors; 89 ModelFormat ret; 90 } 91 92 return Ret(errors, serialized); 93 }(); 94 95 template DormLayout(TModel : Model) 96 { 97 private enum Impl = DormLayoutImpl!TModel; 98 static if (Impl.errors.length) 99 static assert(false, "Model Definition of `" 100 ~ TModel.stringof ~ "` contains errors:" ~ Impl.errors); 101 else 102 enum ModelFormat DormLayout = Impl.ret; 103 } 104 105 /// Equivalent to `DormLayout!TModel.tableName` 106 enum DormModelTableName(TModel : Model) = TModel.stringof.toSnakeCase; 107 108 enum DormFields(TModel : Model) = DormLayout!TModel.fields; 109 enum DormForeignKeys(TModel : Model) = (() { 110 auto all = DormFields!TModel; 111 foreach_reverse (i, field; all) 112 if (!field.isForeignKey) 113 all = all.remove(i); 114 return all; 115 })(); 116 117 enum DormFieldIndex(TModel : Model, string sourceName) = findFieldIdx(DormFields!TModel, sourceName); 118 enum hasDormField(TModel : Model, string sourceName) = DormFieldIndex!(TModel, sourceName) != -1; 119 enum DormField(TModel : Model, string sourceName) = DormFields!TModel[DormFieldIndex!(TModel, sourceName)]; 120 121 enum DormPrimaryKeyIndex(TModel : Model) = findPrimaryFieldIdx(DormFields!TModel); 122 enum DormPrimaryKey(TModel : Model) = DormFields!TModel[DormPrimaryKeyIndex!TModel]; 123 124 /// Equivalent to DormPrimaryKey!TModel.sourceColumn, but does not analyze the 125 /// model, thus avoiding cyclic lookups. Note: does not do any sanity checks, 126 /// like checking if there are two primary keys. 127 template DormPrimaryKeyName(TModel : Model) 128 { 129 static foreach (field; LogicalFields!TModel) 130 static if (hasUDA!(__traits(getMember, TModel, field), primaryKey)) 131 enum DormPrimaryKeyName = field; 132 } 133 134 /// Used internally, basically converts the field name to snake case or returns 135 /// the @columnName value if the field has been annotated with it. 136 static template DormColumnNameImpl(alias field) 137 { 138 enum DormColumnNameImpl = { 139 string ret = __traits(identifier, field).toSnakeCase; 140 141 alias attributes = __traits(getAttributes, field); 142 static foreach (attribute; attributes) 143 static if (is(typeof(attribute) == columnName)) 144 ret = attribute.name; 145 return ret; 146 }(); 147 } 148 149 /// Returns all fields with `@constructValue` annotations. Used internally in 150 /// Model to determine which fields to look at in the constructor. 151 enum string[] DormModelConstructors(TModel : Model) = { 152 string[] ret; 153 static foreach (field; LogicalFields!(TModel, 1)) 154 {{ 155 alias fieldAlias = __traits(getMember, TModel, field); 156 157 alias attributes = __traits(getAttributes, fieldAlias); 158 static foreach (attribute; attributes) 159 {{ 160 static if (is(attribute == constructValue!fn, alias fn)) 161 ret ~= field; 162 }} 163 }} 164 return ret; 165 }(); 166 167 enum DormListFieldsForError(TModel : Model) = formatFieldList(DormFields!TModel); 168 169 private auto findFieldIdx(ModelFormat.Field[] fields, string name) 170 { 171 foreach (i, ref field; fields) 172 if (field.sourceColumn == name) 173 return i; 174 return -1; 175 } 176 177 private auto findPrimaryFieldIdx(ModelFormat.Field[] fields) 178 { 179 foreach (i, ref field; fields) 180 if (field.isPrimaryKey) 181 return i; 182 return -1; 183 } 184 185 private auto formatFieldList(ModelFormat.Field[] fields) 186 { 187 string ret; 188 foreach (field; fields) 189 ret ~= "\n- " ~ field.toPrettySourceString; 190 return ret; 191 } 192 193 /// Returns the name of all public member fields, through all base classes (i.e. 194 /// returns identifiers of public .tupleof members) 195 template LogicalFields(TModel, int cacheHack = 0) 196 { 197 // cacheHack exists because DormLayout needs a different cache due to cyclic things 198 static if (is(TModel : Model)) 199 alias Classes = AliasSeq!(TModel, BaseClassesTuple!TModel); 200 else 201 alias Classes = AliasSeq!(TModel); 202 alias LogicalFields = AliasSeq!(); 203 204 static foreach_reverse (Class; Classes) 205 { 206 static foreach (field; __traits(derivedMembers, Class)) 207 { 208 static if ( 209 // isTemplate does not compile for circular references, but those are assumed to be fields 210 !__traits(compiles, __traits(isTemplate, __traits(getMember, Class, field))) 211 || (is(typeof(__traits(getMember, Class, field))) 212 && !is(typeof(&__traits(getMember, Class, field))) 213 && !__traits(isTemplate, __traits(getMember, Class, field)) 214 && __traits(getProtection, __traits(getMember, Class, field)) == "public")) 215 LogicalFields = AliasSeq!(LogicalFields, field); 216 } 217 } 218 } 219 220 private struct ProcessFieldResult 221 { 222 bool include, checkForOtherPrimaryKey; 223 ModelFormat.Field field; 224 } 225 226 private void processField(TModel, string fieldName, string directFieldName)(ref ModelFormat serialized) 227 { 228 alias fieldAlias = __traits(getMember, TModel, directFieldName); 229 230 ProcessFieldResult result = processFieldImpl!(TModel, fieldName, directFieldName)(); 231 232 static if (hasUDA!(fieldAlias, embedded)) 233 { 234 serialized.embeddedStructs ~= fieldName; 235 alias TSubModel = typeof(fieldAlias); 236 static foreach (subfield; LogicalFields!(TSubModel, 2)) 237 { 238 processField!(TSubModel, fieldName ~ "." ~ subfield, subfield)(serialized); 239 } 240 } 241 242 with (result) if (include) 243 { 244 if (checkForOtherPrimaryKey) 245 { 246 foreach (other; serialized.fields) 247 { 248 if (other.isPrimaryKey) 249 throw new DormModelException("Duplicate primary key found in Model " ~ TModel.stringof 250 ~ ":\n- first defined here:\n" 251 ~ other.sourceColumn ~ " in " ~ other.definedAt.toString 252 ~ "\n- then attempted to redefine here:\n" 253 ~ typeof(fieldAlias).stringof ~ " " ~ fieldName ~ " in " ~ field.definedAt.toString 254 ~ "\nMaybe you wanted to define a `TODO: compositeKey`?"); 255 } 256 } 257 258 // https://github.com/myOmikron/drorm/issues/8 259 if (field.hasFlag(AnnotationFlag.autoIncrement) && !field.hasFlag(AnnotationFlag.primaryKey)) 260 throw new DormModelException(field.sourceReferenceName(TModel.stringof) 261 ~ " has @autoIncrement annotation, but is missing required @primaryKey annotation."); 262 263 if (field.type == InvalidDBType) 264 throw new DormModelException(SourceLocation(__traits(getLocation, fieldAlias)).toErrorString 265 ~ "Cannot resolve DORM Model DBType from " ~ typeof(fieldAlias).stringof 266 ~ " `" ~ directFieldName ~ "` in " ~ TModel.stringof); 267 268 foreach (ai, lhs; field.annotations) 269 { 270 foreach (bi, rhs; field.annotations) 271 { 272 if (ai == bi) continue; 273 if (!lhs.isCompatibleWith(rhs)) 274 throw new DormModelException("Incompatible annotation: " 275 ~ lhs.to!string ~ " conflicts with " ~ rhs.to!string 276 ~ " on " ~ field.sourceReferenceName(TModel.stringof)); 277 } 278 } 279 280 serialized.fields ~= field; 281 } 282 } 283 284 private ProcessFieldResult processFieldImpl(TModel, string fieldName, string directFieldName)() 285 { 286 import uda = dorm.annotations; 287 288 alias fieldAlias = __traits(getMember, TModel, directFieldName); 289 290 alias attributes = __traits(getAttributes, fieldAlias); 291 292 bool include = true; 293 ModelFormat.Field field; 294 295 field.definedAt = SourceLocation(__traits(getLocation, fieldAlias)); 296 field.columnName = directFieldName.toSnakeCase; 297 field.sourceType = typeof(fieldAlias).stringof; 298 field.sourceColumn = fieldName; 299 field.type = guessDBType!(typeof(fieldAlias)); 300 301 static if (is(typeof(fieldAlias) == Nullable!T, T)) 302 { 303 alias UnnullType = T; 304 enum isNullable = true; 305 } 306 else 307 { 308 alias UnnullType = typeof(fieldAlias); 309 enum isNullable = false; 310 } 311 312 bool checkForOtherPrimaryKey = false; 313 314 bool explicitNotNull = false; 315 bool nullable = false; 316 bool mustBeNullable = false; 317 bool hasNonNullDefaultValue = false; 318 static if (isNullable || is(typeof(fieldAlias) : Model)) 319 nullable = true; 320 321 static if (is(UnnullType == enum)) 322 field.annotations ~= DBAnnotation(Choices([ 323 __traits(allMembers, UnnullType) 324 ])); 325 else static if (is(UnnullType : ModelRefImpl!(_id, _TModel, _TSelect), 326 alias _id, _TModel, _TSelect)) 327 { 328 ForeignKeyImpl foreignKeyImpl; 329 auto foreignKeyInfo = processFieldImpl!(_TModel, UnnullType.primaryKeySourceName, UnnullType.primaryKeySourceName); 330 assert(foreignKeyInfo.include, "Refering to a foreign key that's not included in the result!"); 331 field.type = foreignKeyInfo.field.type; 332 field.annotations ~= foreignKeyInfo.field.foreignKeyAnnotations; 333 foreignKeyImpl.column = foreignKeyInfo.field.columnName; 334 foreignKeyImpl.table = DormModelTableName!_TModel; 335 } 336 337 enum bool isForeignKey = is(typeof(foreignKeyImpl)); 338 339 void setDefaultValue(T)(T value) 340 { 341 static if (__traits(compiles, value !is null)) 342 { 343 if (value !is null) 344 hasNonNullDefaultValue = true; 345 } 346 else 347 hasNonNullDefaultValue = true; 348 349 field.annotations ~= DBAnnotation(defaultValue(value)); 350 } 351 352 static foreach (attribute; attributes) 353 {{ 354 static if (__traits(isSame, attribute, uda.autoCreateTime)) 355 { 356 field.type = ModelFormat.Field.DBType.datetime; 357 field.annotations ~= DBAnnotation(AnnotationFlag.autoCreateTime); 358 hasNonNullDefaultValue = true; 359 } 360 else static if (__traits(isSame, attribute, uda.autoUpdateTime)) 361 { 362 field.type = ModelFormat.Field.DBType.datetime; 363 field.annotations ~= DBAnnotation(AnnotationFlag.autoUpdateTime); 364 mustBeNullable = true; 365 } 366 else static if (__traits(isSame, attribute, uda.timestamp)) 367 { 368 field.type = ModelFormat.Field.DBType.datetime; 369 mustBeNullable = true; 370 } 371 else static if (__traits(isSame, attribute, uda.primaryKey)) 372 { 373 nullable = true; 374 field.annotations ~= DBAnnotation(AnnotationFlag.primaryKey); 375 checkForOtherPrimaryKey = true; 376 } 377 else static if (__traits(isSame, attribute, uda.autoIncrement)) 378 { 379 field.annotations ~= DBAnnotation(AnnotationFlag.autoIncrement); 380 hasNonNullDefaultValue = true; 381 } 382 else static if (__traits(isSame, attribute, uda.unique)) 383 { 384 field.annotations ~= DBAnnotation(AnnotationFlag.unique); 385 } 386 else static if (__traits(isSame, attribute, uda.embedded)) 387 { 388 static if (!is(typeof(fieldAlias) == struct)) 389 static assert(false, "@embedded is only supported on structs"); 390 include = false; 391 } 392 else static if (__traits(isSame, attribute, uda.ignored)) 393 { 394 include = false; 395 } 396 else static if (__traits(isSame, attribute, uda.notNull)) 397 { 398 explicitNotNull = true; 399 } 400 else static if (is(attribute == constructValue!fn, alias fn)) 401 { 402 field.internalAnnotations ~= InternalAnnotation(ConstructValueRef(fieldName)); 403 } 404 else static if (is(attribute == validator!fn, alias fn)) 405 { 406 field.internalAnnotations ~= InternalAnnotation(ValidatorRef(fieldName)); 407 } 408 else static if (__traits(isSame, attribute, defaultFromInit)) 409 { 410 static if (is(TModel == struct)) 411 { 412 setDefaultValue(__traits(getMember, TModel.init, directFieldName)); 413 } 414 else static if (__traits(compiles, __traits(getMember, new TModel(), directFieldName))) 415 { 416 setDefaultValue(__traits(getMember, new TModel(), directFieldName)); 417 } 418 else 419 { 420 if (include) 421 { 422 throw new DormModelException(field.sourceReferenceName(TModel.stringof) 423 ~ " is annotated with `@defaultFromInit`, but failed construction. " 424 ~ "Consider moving this field into a struct and using `@embedded` to use it " 425 ~ "or remove incompatible attributes. (Implicit Patches work as well)"); 426 } 427 } 428 } 429 else static if (is(typeof(attribute) == maxLength) 430 || is(typeof(attribute) == DefaultValue!T, T) 431 || is(typeof(attribute) == index)) 432 { 433 static if (is(typeof(attribute) == DefaultValue!U, U) 434 && !is(U == typeof(null))) 435 { 436 hasNonNullDefaultValue = true; 437 } 438 field.annotations ~= DBAnnotation(attribute); 439 } 440 else static if (is(typeof(attribute) == Choices)) 441 { 442 field.type = ModelFormat.Field.DBType.choices; 443 field.annotations ~= DBAnnotation(attribute); 444 } 445 else static if (is(typeof(attribute) == columnName)) 446 { 447 field.columnName = attribute.name; 448 } 449 else static if (is(typeof(attribute) == uda.onUpdate) 450 && isForeignKey) 451 { 452 foreignKeyImpl.onUpdate = attribute.type; 453 } 454 else static if (is(typeof(attribute) == uda.onDelete) 455 && isForeignKey) 456 { 457 foreignKeyImpl.onDelete = attribute.type; 458 } 459 else static if (isDormFieldAttribute!attribute) 460 { 461 static assert(false, "Unsupported attribute " ~ attribute.stringof ~ " on " ~ TModel.stringof ~ "." ~ fieldName); 462 } 463 }} 464 465 static if (isForeignKey) 466 field.annotations ~= DBAnnotation(foreignKeyImpl); 467 468 if (include) 469 { 470 if (!nullable || explicitNotNull) 471 { 472 if (mustBeNullable && !hasNonNullDefaultValue) 473 { 474 throw new DormModelException(field.sourceReferenceName(TModel.stringof) 475 ~ " may be null. Change it to Nullable!(" ~ typeof(fieldAlias).stringof 476 ~ ") or annotate with defaultValue, autoIncrement or autoCreateTime"); 477 } 478 field.annotations = DBAnnotation(AnnotationFlag.notNull) ~ field.annotations; 479 } 480 } 481 482 return ProcessFieldResult( 483 include, 484 checkForOtherPrimaryKey, 485 field 486 ); 487 } 488 489 private string toSnakeCase(string s) 490 { 491 import std.array; 492 import std.ascii; 493 494 auto ret = appender!(char[]); 495 int upperCount; 496 char lastChar; 497 foreach (char c; s) 498 { 499 scope (exit) 500 lastChar = c; 501 if (upperCount) 502 { 503 if (isUpper(c)) 504 { 505 ret ~= toLower(c); 506 upperCount++; 507 } 508 else 509 { 510 if (isDigit(c)) 511 { 512 ret ~= '_'; 513 ret ~= c; 514 } 515 else if (isLower(c) && upperCount > 1 && ret.data.length) 516 { 517 auto last = ret.data[$ - 1]; 518 ret.shrinkTo(ret.data.length - 1); 519 ret ~= '_'; 520 ret ~= last; 521 ret ~= c; 522 } 523 else 524 { 525 ret ~= toLower(c); 526 } 527 upperCount = 0; 528 } 529 } 530 else if (isUpper(c)) 531 { 532 if (ret.data.length 533 && ret.data[$ - 1] != '_' 534 && !lastChar.isUpper) 535 { 536 ret ~= '_'; 537 ret ~= toLower(c); 538 upperCount++; 539 } 540 else 541 { 542 if (ret.data.length) 543 upperCount++; 544 ret ~= toLower(c); 545 } 546 } 547 else if (c == '_') 548 { 549 if (ret.data.length) 550 ret ~= '_'; 551 } 552 else if (isDigit(c)) 553 { 554 if (ret.data.length && !ret.data[$ - 1].isDigit && ret.data[$ - 1] != '_') 555 ret ~= '_'; 556 ret ~= c; 557 } 558 else 559 { 560 if (ret.data.length && ret.data[$ - 1].isDigit && ret.data[$ - 1] != '_') 561 ret ~= '_'; 562 ret ~= c; 563 } 564 } 565 566 auto slice = ret.data; 567 while (slice.length && slice[$ - 1] == '_') 568 slice = slice[0 .. $ - 1]; 569 return slice.idup; 570 } 571 572 unittest 573 { 574 assert("".toSnakeCase == ""); 575 assert("a".toSnakeCase == "a"); 576 assert("A".toSnakeCase == "a"); 577 assert("AB".toSnakeCase == "ab"); 578 assert("JsonValue".toSnakeCase == "json_value"); 579 assert("HTTPHandler".toSnakeCase == "http_handler"); 580 assert("Test123".toSnakeCase == "test_123"); 581 assert("foo_bar".toSnakeCase == "foo_bar"); 582 assert("foo_Bar".toSnakeCase == "foo_bar"); 583 assert("Foo_Bar".toSnakeCase == "foo_bar"); 584 assert("FOO_bar".toSnakeCase == "foo_bar"); 585 assert("FOO__bar".toSnakeCase == "foo__bar"); 586 assert("do_".toSnakeCase == "do"); 587 assert("fooBar_".toSnakeCase == "foo_bar"); 588 assert("_do".toSnakeCase == "do"); 589 assert("_fooBar".toSnakeCase == "foo_bar"); 590 assert("_FooBar".toSnakeCase == "foo_bar"); 591 assert("HTTP2".toSnakeCase == "http_2"); 592 assert("HTTP2Foo".toSnakeCase == "http_2_foo"); 593 assert("HTTP2foo".toSnakeCase == "http_2_foo"); 594 } 595 596 private enum InvalidDBType = cast(ModelFormat.Field.DBType)int.max; 597 598 private template guessDBType(T) 599 { 600 static if (is(T == enum)) 601 enum guessDBType = ModelFormat.Field.DBType.choices; 602 else static if (is(T == BitFlags!U, U)) 603 enum guessDBType = ModelFormat.Field.DBType.set; 604 else static if (is(T == Nullable!U, U)) 605 { 606 static if (__traits(compiles, guessDBType!U)) 607 enum guessDBType = guessDBType!U; 608 else 609 static assert(false, "cannot resolve DBType from nullable " ~ U.stringof); 610 } 611 else static if (__traits(compiles, guessDBTypeBase!T)) 612 enum guessDBType = guessDBTypeBase!T; 613 else 614 enum guessDBType = InvalidDBType; 615 } 616 617 private enum guessDBTypeBase(T : const(char)[]) = ModelFormat.Field.DBType.varchar; 618 private enum guessDBTypeBase(T : const(ubyte)[]) = ModelFormat.Field.DBType.varbinary; 619 private enum guessDBTypeBase(T : byte) = ModelFormat.Field.DBType.int8; 620 private enum guessDBTypeBase(T : short) = ModelFormat.Field.DBType.int16; 621 private enum guessDBTypeBase(T : int) = ModelFormat.Field.DBType.int32; 622 private enum guessDBTypeBase(T : long) = ModelFormat.Field.DBType.int64; 623 private enum guessDBTypeBase(T : ubyte) = ModelFormat.Field.DBType.int16; 624 private enum guessDBTypeBase(T : ushort) = ModelFormat.Field.DBType.int32; 625 private enum guessDBTypeBase(T : uint) = ModelFormat.Field.DBType.int64; 626 private enum guessDBTypeBase(T : float) = ModelFormat.Field.DBType.floatNumber; 627 private enum guessDBTypeBase(T : double) = ModelFormat.Field.DBType.doubleNumber; 628 private enum guessDBTypeBase(T : bool) = ModelFormat.Field.DBType.boolean; 629 private enum guessDBTypeBase(T : Date) = ModelFormat.Field.DBType.date; 630 private enum guessDBTypeBase(T : DateTime) = ModelFormat.Field.DBType.datetime; 631 private enum guessDBTypeBase(T : SysTime) = ModelFormat.Field.DBType.datetime; 632 private enum guessDBTypeBase(T : TimeOfDay) = ModelFormat.Field.DBType.time; 633 634 unittest 635 { 636 import std.sumtype; 637 638 struct Mod 639 { 640 import std.datetime; 641 import std.typecons; 642 643 enum State : string 644 { 645 ok = "ok", 646 warn = "warn", 647 critical = "critical", 648 unknown = "unknown" 649 } 650 651 class User : Model 652 { 653 @maxLength(255) 654 string username; 655 656 @maxLength(255) 657 string password; 658 659 @maxLength(255) 660 Nullable!string email; 661 662 ubyte age; 663 664 Nullable!DateTime birthday; 665 666 @autoCreateTime 667 SysTime createdAt; 668 669 @autoUpdateTime 670 Nullable!SysTime updatedAt; 671 672 @autoCreateTime 673 ulong createdAt2; 674 675 @autoUpdateTime 676 Nullable!ulong updatedAt2; 677 678 State state; 679 680 @choices("ok", "warn", "critical", "unknown") 681 string state2; 682 683 @columnName("admin") 684 bool isAdmin; 685 686 @constructValue!(() => Clock.currTime + 4.hours) 687 SysTime validUntil; 688 689 @maxLength(255) 690 @defaultValue("") 691 string comment; 692 693 @defaultValue(1337) 694 int counter; 695 696 @primaryKey 697 long ownPrimaryKey; 698 699 @timestamp 700 Nullable!ulong someTimestamp; 701 702 @unique 703 int uuid; 704 705 @validator!(x => x >= 18) 706 int someInt; 707 708 @ignored 709 int imNotIncluded; 710 } 711 } 712 713 auto mod = processModelsToDeclarations!Mod; 714 assert(mod.models.length == 1); 715 716 auto m = mod.models[0]; 717 718 // Length is always len(m.fields + 1) as dorm.model.Model adds the id field, 719 // unless you define your own primary key field. 720 assert(m.fields.length == 19); 721 722 int i = 0; 723 724 assert(m.fields[i].columnName == "username"); 725 assert(m.fields[i].type == ModelFormat.Field.DBType.varchar); 726 assert(m.fields[i].annotations == [DBAnnotation(AnnotationFlag.notNull), DBAnnotation(maxLength(255))]); 727 728 assert(m.fields[++i].columnName == "password"); 729 assert(m.fields[i].type == ModelFormat.Field.DBType.varchar); 730 assert(m.fields[i].annotations == [DBAnnotation(AnnotationFlag.notNull), DBAnnotation(maxLength(255))]); 731 732 assert(m.fields[++i].columnName == "email"); 733 assert(m.fields[i].type == ModelFormat.Field.DBType.varchar); 734 assert(m.fields[i].annotations == [DBAnnotation(maxLength(255))]); 735 736 assert(m.fields[++i].columnName == "age"); 737 assert(m.fields[i].type == ModelFormat.Field.DBType.int16); 738 assert(m.fields[i].annotations == [DBAnnotation(AnnotationFlag.notNull)]); 739 740 assert(m.fields[++i].columnName == "birthday"); 741 assert(m.fields[i].type == ModelFormat.Field.DBType.datetime); 742 assert(m.fields[i].annotations == []); 743 744 assert(m.fields[++i].columnName == "created_at"); 745 assert(m.fields[i].type == ModelFormat.Field.DBType.datetime); 746 assert(m.fields[i].annotations == [ 747 DBAnnotation(AnnotationFlag.notNull), 748 DBAnnotation(AnnotationFlag.autoCreateTime), 749 ]); 750 751 assert(m.fields[++i].columnName == "updated_at"); 752 assert(m.fields[i].type == ModelFormat.Field.DBType.datetime); 753 assert(m.fields[i].annotations == [ 754 DBAnnotation(AnnotationFlag.autoUpdateTime) 755 ]); 756 757 assert(m.fields[++i].columnName == "created_at_2"); 758 assert(m.fields[i].type == ModelFormat.Field.DBType.datetime); 759 assert(m.fields[i].annotations == [ 760 DBAnnotation(AnnotationFlag.notNull), 761 DBAnnotation(AnnotationFlag.autoCreateTime), 762 ]); 763 764 assert(m.fields[++i].columnName == "updated_at_2"); 765 assert(m.fields[i].type == ModelFormat.Field.DBType.datetime); 766 assert(m.fields[i].annotations == [ 767 DBAnnotation(AnnotationFlag.autoUpdateTime) 768 ]); 769 770 assert(m.fields[++i].columnName == "state"); 771 assert(m.fields[i].type == ModelFormat.Field.DBType.choices); 772 assert(m.fields[i].annotations == [ 773 DBAnnotation(AnnotationFlag.notNull), 774 DBAnnotation(Choices(["ok", "warn", "critical", "unknown"])), 775 ]); 776 777 assert(m.fields[++i].columnName == "state_2"); 778 assert(m.fields[i].type == ModelFormat.Field.DBType.choices); 779 assert(m.fields[i].annotations == [ 780 DBAnnotation(AnnotationFlag.notNull), 781 DBAnnotation(Choices(["ok", "warn", "critical", "unknown"])), 782 ]); 783 784 assert(m.fields[++i].columnName == "admin"); 785 assert(m.fields[i].type == ModelFormat.Field.DBType.boolean); 786 assert(m.fields[i].annotations == [DBAnnotation(AnnotationFlag.notNull)]); 787 788 assert(m.fields[++i].columnName == "valid_until"); 789 assert(m.fields[i].type == ModelFormat.Field.DBType.datetime); 790 assert(m.fields[i].annotations == [ 791 DBAnnotation(AnnotationFlag.notNull) 792 ]); 793 assert(m.fields[i].internalAnnotations.length == 1); 794 assert(m.fields[i].internalAnnotations[0].match!((ConstructValueRef r) => true, _ => false)); 795 796 assert(m.fields[++i].columnName == "comment"); 797 assert(m.fields[i].type == ModelFormat.Field.DBType.varchar); 798 assert(m.fields[i].annotations == [ 799 DBAnnotation(AnnotationFlag.notNull), 800 DBAnnotation(maxLength(255)), 801 DBAnnotation(defaultValue("")), 802 ]); 803 804 assert(m.fields[++i].columnName == "counter"); 805 assert(m.fields[i].type == ModelFormat.Field.DBType.int32); 806 assert(m.fields[i].annotations == [ 807 DBAnnotation(AnnotationFlag.notNull), 808 DBAnnotation(defaultValue(1337)), 809 ]); 810 811 assert(m.fields[++i].columnName == "own_primary_key"); 812 assert(m.fields[i].type == ModelFormat.Field.DBType.int64); 813 assert(m.fields[i].annotations == [ 814 DBAnnotation(AnnotationFlag.primaryKey), 815 ]); 816 817 assert(m.fields[++i].columnName == "some_timestamp"); 818 assert(m.fields[i].type == ModelFormat.Field.DBType.datetime); 819 assert(m.fields[i].annotations == []); 820 821 assert(m.fields[++i].columnName == "uuid"); 822 assert(m.fields[i].type == ModelFormat.Field.DBType.int32); 823 assert(m.fields[i].annotations == [ 824 DBAnnotation(AnnotationFlag.notNull), 825 DBAnnotation(AnnotationFlag.unique), 826 ]); 827 828 assert(m.fields[++i].columnName == "some_int"); 829 assert(m.fields[i].type == ModelFormat.Field.DBType.int32); 830 assert(m.fields[i].annotations == [ 831 DBAnnotation(AnnotationFlag.notNull) 832 ]); 833 assert(m.fields[i].internalAnnotations.length == 1); 834 assert(m.fields[i].internalAnnotations[0].match!((ValidatorRef r) => true, _ => false)); 835 } 836 837 unittest 838 { 839 struct Mod 840 { 841 abstract class NamedThing : Model 842 { 843 @Id long id; 844 845 @maxLength(255) 846 string name; 847 } 848 849 class User : NamedThing 850 { 851 int age; 852 } 853 } 854 855 auto mod = processModelsToDeclarations!Mod; 856 assert(mod.models.length == 1); 857 858 auto m = mod.models[0]; 859 assert(m.tableName == "user"); 860 assert(m.fields.length == 3); 861 assert(m.fields[0].columnName == "id"); 862 assert(m.fields[1].columnName == "name"); 863 assert(m.fields[2].columnName == "age"); 864 } 865 866 unittest 867 { 868 struct Mod 869 { 870 struct SuperCommon 871 { 872 int superCommonField; 873 } 874 875 struct Common 876 { 877 string commonName; 878 @embedded 879 SuperCommon superCommon; 880 } 881 882 class NamedThing : Model 883 { 884 @Id long id; 885 886 @embedded 887 Common common; 888 889 @maxLength(255) 890 string name; 891 } 892 } 893 894 auto mod = processModelsToDeclarations!Mod; 895 assert(mod.models.length == 1); 896 auto m = mod.models[0]; 897 assert(m.tableName == "named_thing"); 898 assert(m.fields.length == 4); 899 assert(m.fields[1].columnName == "common_name"); 900 assert(m.fields[1].sourceColumn == "common.commonName"); 901 assert(m.fields[2].columnName == "super_common_field"); 902 assert(m.fields[2].sourceColumn == "common.superCommon.superCommonField"); 903 assert(m.fields[3].columnName == "name"); 904 assert(m.fields[3].sourceColumn == "name"); 905 assert(m.embeddedStructs == ["common", "common.superCommon"]); 906 } 907 908 // https://github.com/myOmikron/drorm/issues/6 909 unittest 910 { 911 struct Mod 912 { 913 class NamedThing : Model 914 { 915 @Id long id; 916 917 @timestamp 918 Nullable!long timestamp1; 919 920 @autoUpdateTime 921 Nullable!long timestamp2; 922 923 @autoCreateTime 924 long timestamp3; 925 926 @autoUpdateTime @autoCreateTime 927 long timestamp4; 928 } 929 } 930 931 auto mod = processModelsToDeclarations!Mod; 932 assert(mod.models.length == 1); 933 auto m = mod.models[0]; 934 assert(m.tableName == "named_thing"); 935 assert(m.fields.length == 5); 936 937 assert(m.fields[1].columnName == "timestamp_1"); 938 assert(m.fields[1].isNullable); 939 assert(m.fields[2].isNullable); 940 assert(!m.fields[3].isNullable); 941 assert(!m.fields[4].isNullable); 942 } 943 944 unittest 945 { 946 struct Mod 947 { 948 class DefaultValues : Model 949 { 950 @Id long id; 951 952 @defaultValue(10) 953 int f1; 954 955 @defaultValue(2) 956 int f2 = 1337; 957 958 @defaultFromInit 959 int f3 = 1337; 960 } 961 } 962 963 auto mod = processModelsToDeclarations!Mod; 964 assert(mod.models.length == 1); 965 auto m = mod.models[0]; 966 assert(m.fields.length == 4); 967 968 assert(m.fields[1].columnName == "f_1"); 969 assert(m.fields[1].annotations == [ 970 DBAnnotation(AnnotationFlag.notNull), 971 DBAnnotation(defaultValue(10)) 972 ]); 973 974 assert(m.fields[2].columnName == "f_2"); 975 assert(m.fields[2].annotations == [ 976 DBAnnotation(AnnotationFlag.notNull), 977 DBAnnotation(defaultValue(2)) 978 ]); 979 980 assert(m.fields[3].columnName == "f_3"); 981 assert(m.fields[3].annotations == [ 982 DBAnnotation(AnnotationFlag.notNull), 983 DBAnnotation(defaultValue(1337)) 984 ]); 985 } 986 987 unittest 988 { 989 struct Mod 990 { 991 class User : Model 992 { 993 @maxLength(255) @primaryKey 994 string username; 995 } 996 997 class Toot : Model 998 { 999 @Id long id; 1000 1001 ModelRef!(User.username) author; 1002 } 1003 } 1004 1005 static assert(DormForeignKeys!(Mod.Toot).length == 1); 1006 static assert(DormForeignKeys!(Mod.Toot)[0].columnName == "author"); 1007 1008 auto mod = processModelsToDeclarations!Mod; 1009 assert(mod.models.length == 2); 1010 auto m = mod.models[0]; 1011 assert(m.fields.length == 1); 1012 1013 assert(m.fields[0].columnName == "username"); 1014 assert(m.fields[0].annotations == [ 1015 DBAnnotation(maxLength(255)), 1016 DBAnnotation(AnnotationFlag.primaryKey) 1017 ]); 1018 1019 m = mod.models[1]; 1020 assert(m.fields.length == 2); 1021 1022 assert(m.fields[1].columnName == "author"); 1023 assert(m.fields[1].annotations == [ 1024 DBAnnotation(AnnotationFlag.notNull), 1025 DBAnnotation(maxLength(255)), 1026 DBAnnotation(ForeignKeyImpl( 1027 "user", "username", 1028 restrict, restrict 1029 )) 1030 ]); 1031 } 1032 1033 unittest 1034 { 1035 // cyclic ref 1036 struct Mod 1037 { 1038 class User : Model 1039 { 1040 @Id long id; 1041 1042 ModelRef!(Project.otherId) favoriteProject; 1043 } 1044 1045 class Project : Model 1046 { 1047 @primaryKey @maxLength(123) string otherId; 1048 1049 ModelRef!(User.id) author; 1050 } 1051 } 1052 1053 auto mod = processModelsToDeclarations!Mod; 1054 assert(mod.models.length == 2); 1055 auto m = mod.models[0]; 1056 assert(m.fields.length == 2); 1057 1058 assert(m.fields[0].columnName == "id"); 1059 assert(m.fields[0].annotations == [ 1060 DBAnnotation(AnnotationFlag.primaryKey), 1061 DBAnnotation(AnnotationFlag.autoIncrement), 1062 ]); 1063 1064 assert(m.fields[1].columnName == "favorite_project"); 1065 assert(m.fields[1].annotations == [ 1066 DBAnnotation(AnnotationFlag.notNull), 1067 DBAnnotation(maxLength(123)), 1068 DBAnnotation(ForeignKeyImpl( 1069 "project", "other_id", 1070 restrict, restrict 1071 )) 1072 ]); 1073 1074 m = mod.models[1]; 1075 assert(m.fields.length == 2); 1076 1077 assert(m.fields[0].columnName == "other_id"); 1078 assert(m.fields[0].annotations == [ 1079 DBAnnotation(AnnotationFlag.primaryKey), 1080 DBAnnotation(maxLength(123)), 1081 ]); 1082 1083 assert(m.fields[1].columnName == "author"); 1084 assert(m.fields[1].annotations == [ 1085 DBAnnotation(AnnotationFlag.notNull), 1086 DBAnnotation(ForeignKeyImpl( 1087 "user", "id", 1088 restrict, restrict 1089 )) 1090 ]); 1091 }