1 /** 2 * Authors: Tomoya Tanjo 3 * Copyright: © 2021 Tomoya Tanjo 4 * License: Apache-2.0 5 */ 6 module salad.meta; 7 8 import salad.type; 9 10 import dyaml; 11 12 /// 13 mixin template genCtor() 14 { 15 import dyaml : Node, NodeType; 16 import salad.context : LoadingContext; 17 18 this() {} 19 this(in Node node, in LoadingContext context = LoadingContext.init) @trusted 20 { 21 import std.algorithm : endsWith; 22 import std.traits : FieldNameTuple; 23 24 alias This = typeof(this); 25 26 static foreach(field; FieldNameTuple!This) 27 { 28 static if (field.endsWith("_") && !isConstantMember!(This, field)) 29 { 30 // static if (This.stringof == "RecordField") 31 // pragma(msg, Assign!(node, mixin(field))); 32 mixin("mixin(Assign!(node, "~field~"));"); 33 } 34 } 35 } 36 } 37 38 /** 39 * Bugs: It does not work with self recursive classes 40 */ 41 mixin template genToString() 42 { 43 override string toString() const @trusted 44 { 45 import salad.type : isEither, isOptional, match; 46 import std.array : join; 47 import std.format : format; 48 import std.traits : FieldNameTuple; 49 50 string[] fstrs; 51 52 alias This = typeof(this); 53 54 static foreach(field; FieldNameTuple!This) 55 { 56 static if (isOptional!(typeof(mixin(field)))) 57 { 58 mixin(field).match!((None _) { }, 59 (rest) { fstrs ~= format!"%s: %s"(field, rest); }); 60 } 61 else static if (isEither!(typeof(mixin(field)))) 62 { 63 mixin(field).match!(f => fstrs ~= format!"%s: %s"(field, f)); 64 } 65 else 66 { 67 fstrs ~= format!"%s: %s"(field, mixin(field)); 68 } 69 } 70 return format!"%s(%s)"(This.stringof, fstrs.join(", ")); 71 } 72 } 73 74 /// 75 mixin template genIdentifier() 76 { 77 import std.traits : getSymbolsByUDA; 78 79 static if (getSymbolsByUDA!(typeof(this), id).length == 1) 80 { 81 auto identifier() const @nogc nothrow pure @safe 82 { 83 import std.traits : Unqual; 84 auto i = getSymbolsByUDA!(typeof(this), id)[0]; 85 alias idType = Unqual!(typeof(i)); 86 static assert(is(idType == string) || is(idType == Optional!string)); 87 static if (is(idType == string)) 88 { 89 return i; 90 } 91 else 92 { 93 return i.match!( 94 (string s) => s, 95 none => "", 96 ); 97 } 98 } 99 } 100 } 101 102 /** 103 * UDA for identifier maps 104 * See_Also: https://www.commonwl.org/v1.2/SchemaSalad.html#Identifier_maps 105 */ 106 struct idMap { string subject; string predicate = ""; } 107 108 /** 109 * UDA for DSL for types 110 * See_Also: https://www.commonwl.org/v1.2/SchemaSalad.html#Domain_Specific_Language_for_types 111 */ 112 struct typeDSL{} 113 114 /** 115 * UDA for documentRoot 116 * See_Also: https://www.commonwl.org/v1.2/SchemaSalad.html#SaladRecordSchema 117 */ 118 struct documentRoot{} 119 120 /** 121 * UDA for identifier 122 * See_Also: https://www.commonwl.org/v1.2/SchemaSalad.html#Record_field_annotations 123 */ 124 struct id{} 125 126 enum hasIdentifier(T) = __traits(compiles, { auto id = T.init.identifier(); }); 127 128 /// 129 template DocumentRootType(alias module_) 130 { 131 import std.meta : allSatisfy, ApplyRight, Filter, staticMap; 132 import std.traits : fullyQualifiedName, hasUDA; 133 134 alias StrToType(string T) = __traits(getMember, module_, T); 135 alias syms = staticMap!(StrToType, __traits(allMembers, module_)); 136 alias RootTypes = Filter!(ApplyRight!(hasUDA, documentRoot), syms); 137 static if (RootTypes.length > 0) 138 { 139 static assert(allSatisfy!(hasIdentifier, RootTypes)); 140 alias DocumentRootType = SumType!RootTypes; 141 } 142 else 143 { 144 import std.format : format; 145 import std.traits : moduleName; 146 static assert(false, format!"No schemas with `documentRoot: true` in module `%s`"(moduleName!module_)); 147 } 148 } 149 150 /// 151 template IdentifierType(alias module_) 152 { 153 import std.meta : allSatisfy, Filter, staticMap; 154 import std.traits : fullyQualifiedName; 155 156 alias StrToType(string T) = __traits(getMember, module_, T); 157 alias syms = staticMap!(StrToType, __traits(allMembers, module_)); 158 alias IDTypes = Filter!(hasIdentifier, syms); 159 160 static if (IDTypes.length > 0) 161 { 162 alias IdentifierType = SumType!IDTypes; 163 } 164 else 165 { 166 static assert(false, "No schemas with identifier field"); 167 } 168 } 169 170 /** 171 Returns: a string to construct `T` with a parameter whose variable name is `param` 172 Note: Use this function instead of `param.as!T` to prevent circular references 173 */ 174 string ctorStr(T)(string param) 175 { 176 import std.format : format; 177 static if (is(T == class)) 178 { 179 return format!"new %1$s(%2$s, context)"(T.stringof, param); 180 } 181 else 182 { 183 return format!"%2$s.as!%1$s"(T.stringof, param); 184 } 185 } 186 187 enum isConstantMember(T, string M) = is(typeof(__traits(getMember, T, M)) == immutable string); 188 189 /// 190 template Assign(alias node, alias field) 191 { 192 import std.format : format; 193 import std.traits : getUDAs, hasUDA, select; 194 195 static if (hasUDA!(field, idMap)) 196 { 197 enum idMap_ = getUDAs!(field, idMap)[0]; 198 } 199 else 200 { 201 enum idMap_ = idMap.init; 202 } 203 204 alias T = typeof(field); 205 206 enum param = field.stringof[0..$-1]; 207 208 enum ImportList = q"EOS 209 import salad.util : edig; 210 import std.algorithm : map; 211 import std.array : array; 212 EOS"; 213 214 static if (isOptional!T) 215 { 216 enum Assign = ImportList~format!q"EOS 217 if (auto f = "%s" in %s) 218 { 219 %s 220 } 221 EOS"(param, node.stringof, Assign_!("(*f)", field.stringof, T, hasUDA!(field, typeDSL), idMap_)); 222 } 223 else static if (isEither!T) 224 { 225 enum Assign = ImportList~Assign_!(format!`%s.edig("%s")`(node.stringof, param), 226 field.stringof, T, hasUDA!(field, typeDSL), idMap_); 227 } 228 else 229 { 230 enum Assign = ImportList~Assign_!(format!`%s.edig("%s")`(node.stringof, param), 231 field.stringof, T, hasUDA!(field, typeDSL), idMap_); 232 } 233 } 234 235 version(unittest) 236 { 237 auto stripLeftAll(string str) @safe 238 { 239 import std.algorithm : joiner, map; 240 import std.array : array; 241 import std..string : split, stripLeft; 242 return str.split.map!stripLeft.joiner("\n").array; 243 } 244 } 245 246 /// 247 @safe unittest 248 { 249 import salad.util : edig; 250 251 enum fieldName = "strVariable"; 252 Node n = [ fieldName: "string value" ]; 253 string strVariable_; 254 enum exp = Assign!(n, strVariable_).stripLeftAll; 255 static assert(exp == q"EOS 256 import salad.util : edig; 257 import std.algorithm : map; 258 import std.array : array; 259 strVariable_ = n.edig("strVariable").as!string; 260 EOS".stripLeftAll, exp); 261 262 mixin(exp); 263 assert(strVariable_ == "string value"); 264 } 265 266 /// optional of non-array type 267 @safe unittest 268 { 269 import std.exception : assertNotThrown; 270 271 enum fieldName = "param"; 272 Node n = [ fieldName: true ]; 273 Optional!bool param_; 274 enum exp = Assign!(n, param_).stripLeftAll; 275 static assert(exp == q"EOS 276 import salad.util : edig; 277 import std.algorithm : map; 278 import std.array : array; 279 if (auto f = "param" in n) 280 { 281 param_ = (*f).as!bool; 282 } 283 EOS".stripLeftAll, exp); 284 285 mixin(exp); 286 assert(param_.tryMatch!((bool b) => b) 287 .assertNotThrown); 288 } 289 290 /// optional of array type 291 unittest 292 { 293 import std.algorithm : map; 294 import std.array : array; 295 import std.exception : assertNotThrown; 296 297 enum fieldName = "params"; 298 Node n = [ fieldName: [1, 2, 3] ]; 299 Optional!(int[]) params_; 300 enum exp = Assign!(n, params_).stripLeftAll; 301 static assert(exp == q"EOS 302 import salad.util : edig; 303 import std.algorithm : map; 304 import std.array : array; 305 if (auto f = "params" in n) 306 { 307 params_ = (*f).sequence.map!((a) 308 { 309 int ret; 310 ret = a.as!int; 311 return ret; 312 }).array; 313 } 314 EOS".stripLeftAll, exp); 315 316 mixin(exp); 317 assert(params_.tryMatch!((int[] arr) => arr) 318 .assertNotThrown == [1, 2, 3]); 319 } 320 321 template Assign_(string node, string field, T, bool typeDSL = false, idMap idMap_ = idMap.init) 322 if (!isSumType!T) 323 { 324 import std.format : format; 325 import std.traits : isArray, isSomeString; 326 327 static if (!isSomeString!T && isArray!T) 328 { 329 import std.range : ElementType, empty; 330 import std..string : chomp; 331 332 enum AssignBase = format!q"EOS 333 %s = %s.sequence.map!((a) { 334 %s ret; 335 %s 336 return ret; 337 }).array; 338 EOS"(field, node, (ElementType!T).stringof, Assign_!("a", "ret", ElementType!T)).chomp; 339 340 static if (idMap_.subject.empty) 341 { 342 enum Assign_ = AssignBase; 343 } 344 else 345 { 346 static if (idMap_.predicate.empty) 347 { 348 enum Trans = format!q"EOS 349 Node a_ = a.value; 350 a_.add("%s", a.key); 351 %s ret; 352 %s 353 return ret; 354 EOS"(idMap_.subject, (ElementType!T).stringof, Assign_!("a_", "ret", ElementType!T)); 355 } 356 else 357 { 358 enum Trans = format!q"EOS 359 Node a_; 360 a_.add("%1$s", a.key); 361 if (a.value.type == NodeType.mapping && "%2$s" in a.value) 362 { 363 foreach(kv; a.value.mapping) 364 { 365 a_.add(kv.key, kv.value); 366 } 367 } 368 else 369 { 370 a_.add("%2$s", a.value); 371 } 372 return %3$s; 373 EOS"(idMap_.subject, idMap_.predicate, ctorStr!(ElementType!T)("a_")); 374 } 375 376 enum Assign_ = format!q"EOS 377 if (%2$s.type == NodeType.sequence) 378 { 379 %3$s 380 } 381 else 382 { 383 %1$s = %2$s.mapping.map!((a) { 384 %4$s 385 }).array; 386 } 387 EOS"(field, node, AssignBase, Trans); 388 } 389 } 390 else 391 { 392 static assert(idMap_ == idMap.init); 393 enum Assign_ = format!"%s = %s;"(field, ctorStr!T(node)); 394 } 395 } 396 397 template Assign_(string node, string field, T, bool typeDSL = false, idMap idMap_ = idMap.init) 398 if (isSumType!T) 399 { 400 import std.format : format; 401 static if (isOptional!T && T.Types.length == 2) 402 { 403 enum Assign_ = Assign_!(node, field, T.Types[1], typeDSL, idMap_); 404 } 405 else static if (isEither!T && T.Types.length == 1) 406 { 407 enum Assign_ = Assign_!(node, field, T[0], typeDSL, idMap_); 408 } 409 else 410 { 411 import std.traits : isSomeString; 412 import std.meta : Filter; 413 414 static if (isOptional!T) 415 { 416 alias Types = T.Types[1..$]; 417 } 418 else static if (isEither!T) 419 { 420 alias Types = T.Types; 421 } 422 static if (typeDSL && Filter!(isSomeString, Types).length > 0) 423 { 424 enum Pre = format!q"EOS 425 Node n; 426 if (%1$s.type == NodeType.string) 427 { 428 import std.algorithm : endsWith; 429 auto s = %1$s.as!string; 430 if (s.endsWith("[]?")) 431 { 432 n.add("null"); 433 n.add([ 434 "type": "array", 435 "items": s[0..$-3], 436 ]); 437 } 438 else if (s.endsWith("[]")) 439 { 440 n.add([ 441 "type": "array", 442 "items": s[0..$-2], 443 ]); 444 } 445 else if (s.endsWith("?")) 446 { 447 n.add("null"); 448 n.add(s[0..$-1]); 449 } 450 else 451 { 452 n = Node(s); 453 } 454 } 455 else 456 { 457 n = %1$s; 458 } 459 EOS"(node); 460 } 461 else 462 { 463 enum Pre = format!q"EOS 464 Node n = %s; 465 EOS"(node); 466 } 467 enum Assign_ = format!q"EOS 468 { 469 %s 470 %s = (%s)(n); 471 } 472 EOS"(Pre, field, DispatchFun!(T, Types)); 473 } 474 } 475 476 template DispatchFun(RetType, Types...) 477 { 478 import std.format : format; 479 import std.meta : anySatisfy, Filter, staticMap; 480 import std.traits : isArray, isIntegral, isSomeString; 481 482 enum isNonStringArray(T) = !isSomeString!T && isArray!T; 483 alias ArrayTypes = Filter!(isNonStringArray, Types); 484 static if (ArrayTypes.length == 0) 485 { 486 enum ArrayStatement = ""; 487 } 488 else 489 { 490 enum ArrayStatement = ArrayDispatchStatement!(RetType, ArrayTypes); 491 } 492 493 enum isRecord(T) = is(T == class) && !__traits(compiles, T.Types); 494 alias RecordTypes = Filter!(isRecord, Types); 495 static if (RecordTypes.length == 0) 496 { 497 enum RecordStatement = ""; 498 } 499 else 500 { 501 enum RecordStatement = RecordDispatchStatement!(RetType, RecordTypes); 502 } 503 504 enum isEnum(T) = is(T == class) && is(T.Types == enum); 505 alias EnumTypes = Filter!(isEnum, Types); 506 enum hasString = anySatisfy!(isSomeString, Types); 507 static if (EnumTypes.length == 0) 508 { 509 static if (hasString) 510 { 511 enum EnumStatement = format!q"EOS 512 if (a.type == NodeType.string) 513 { 514 return %s(a.as!string); 515 } 516 EOS"(RetType.stringof); 517 } 518 else 519 { 520 enum EnumStatement = ""; 521 } 522 } 523 else 524 { 525 enum EnumStatement = EnumDispatchStatement!(RetType, hasString, EnumTypes); 526 } 527 528 static if (Filter!(isIntegral, Types).length == 0) 529 { 530 enum NumStatement = ""; 531 } 532 else 533 { 534 enum NumStatement = format!q"EOS 535 if (a.type == NodeType.integer) 536 { 537 return %s(a.as!int); 538 } 539 EOS"(RetType.stringof); 540 } 541 542 static assert(Types.length == 543 ArrayTypes.length + RecordTypes.length + EnumTypes.length + (hasString ? 1 : 0) + Filter!(isIntegral, Types).length, 544 format!"Internal error: Params: %s (%s) but Array: %s, Record: %s, Enum: %s, hasString: %s, Integer: %s"( 545 Types.stringof, Types.length, ArrayTypes.stringof, RecordTypes.stringof, EnumTypes.stringof, 546 hasString, Filter!(isIntegral, Types).stringof 547 )); 548 549 import std.algorithm : filter, joiner; 550 import std.array : array; 551 import std.functional : not; 552 import std.range : empty; 553 enum FunBody = [ 554 ArrayStatement, 555 RecordStatement, 556 EnumStatement, 557 NumStatement, 558 `throw new DocumentException("Unknown node type in DispatchFun", a);` 559 ].filter!(not!empty).joiner("else ").array; 560 561 enum DispatchFun = format!q"EOS 562 (a) { %s } 563 EOS"(FunBody); 564 } 565 566 template ArrayDispatchStatement(RetType, ArrayTypes...) 567 { 568 static if (ArrayTypes.length == 1) 569 { 570 import std.format : format; 571 import std.range : ElementType; 572 alias T = ElementType!(ArrayTypes[0]); 573 static if (isEither!T) 574 { 575 enum ArrayDispatchStatement = format!q"EOS 576 if (a.type == NodeType.sequence) 577 { 578 return %s(a.sequence.map!( 579 %s 580 ).array); 581 } 582 EOS"(RetType.stringof, DispatchFun!(T, T.Types)); 583 } 584 else 585 { 586 enum ArrayDispatchStatement = format!q"EOS 587 if (a.type == NodeType.sequence) 588 { 589 return %s(a.sequence.map!(a => %s).array); 590 } 591 EOS"(RetType.stringof, ctorStr!T("a")); 592 } 593 } 594 else 595 { 596 // It is not used in CWL 597 static assert(false, "It is not supported"); 598 } 599 } 600 601 template RecordDispatchStatement(RetType, RecordTypes...) 602 { 603 import std.format : format; 604 605 static if (RecordTypes.length == 1) 606 { 607 enum RecordDispatchStatement = format!q"EOS 608 if (a.type == NodeType.mapping) 609 { 610 return %s(%s); 611 } 612 EOS"(RetType.stringof, ctorStr!(RecordTypes[0])("a")); 613 } 614 else 615 { 616 import std.algorithm : joiner; 617 import std.array : array; 618 import std.meta : ApplyLeft, Filter, staticMap, templateNot; 619 import std.traits : FieldNameTuple; 620 621 enum ConstantMembersOf(T) = Filter!(ApplyLeft!(isConstantMember, T), FieldNameTuple!T); 622 enum RecordTypeName = ConstantMembersOf!(RecordTypes[0])[0]; 623 enum isDispatchable(T) = ConstantMembersOf!T.length != 0 && ConstantMembersOf!T[0] == RecordTypeName; 624 alias NonDispatchableRecords = Filter!(templateNot!isDispatchable, RecordTypes); 625 static assert(NonDispatchableRecords.length <= 1, 626 "There are too many non-dispatchable record candidates: "~NonDispatchableRecords.stringof); 627 628 static if (NonDispatchableRecords.length == 0) 629 { 630 enum DefaultCaseStr = format!q"EOS 631 default: throw new DocumentException("Unknown record type: "~a.edig("%1$s").as!string, a.edig("%1$s")); 632 EOS"(RecordTypeName[0..$-1]); 633 } 634 else 635 { 636 enum DefaultCaseStr = format!q"EOS 637 default: return %s(%s); 638 EOS"(RetType.stringof, ctorStr!(NonDispatchableRecords[0])("a")); 639 } 640 641 enum RecordCaseStr(T) = format!q"EOS 642 case "%s": return %s(%s); 643 EOS"(mixin("(new T)."~RecordTypeName), RetType.stringof, ctorStr!T("a")); 644 645 enum RecordDispatchStatement = format!q"EOS 646 if (a.type == NodeType.mapping) 647 { 648 switch(a.edig("%1$s").as!string) 649 { 650 %2$s 651 %3$s 652 } 653 } 654 EOS"(RecordTypeName[0..$-1], 655 [staticMap!(RecordCaseStr, Filter!(isDispatchable, RecordTypes))].joiner("").array, 656 DefaultCaseStr); 657 } 658 } 659 660 template EnumDispatchStatement(RetType, bool hasString, EnumTypes...) 661 { 662 import std.algorithm : joiner, map; 663 import std.array : array; 664 import std.format : format; 665 import std.meta : staticMap; 666 import std.traits : EnumMembers; 667 668 enum EnumCaseStr(T) = format!q"EOS 669 case %s: return %s(a.as!%s); 670 EOS"([EnumMembers!(T.Types)].map!(m => format!`"%s"`(cast(string)m)) 671 .joiner(", ") 672 .array, 673 RetType.stringof, T.stringof); 674 static if (hasString) 675 { 676 enum DefaultStr = format!q"EOS 677 return %s(value); 678 EOS"(RetType.stringof); 679 } 680 else 681 { 682 enum DefaultStr = `throw new DocumentException("Unknown symbol value: "~a.as!string, a);`; 683 } 684 enum EnumDispatchStatement = format!q"EOS 685 if (a.type == NodeType.string) 686 { 687 auto value = a.as!string; 688 switch(value) 689 { 690 %1$s 691 default: 692 %2$s 693 } 694 } 695 EOS"([staticMap!(EnumCaseStr, EnumTypes)].joiner("").array, DefaultStr); 696 }