1 /** 2 Helpers for working with user-defined attributes that can be attached to 3 function or method to modify its behavior. In some sense those are similar to 4 Python decorator. D does not support this feature natively but 5 it can be emulated within certain code generation framework. 6 7 Copyright: © 2013 RejectedSoftware e.K. 8 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 9 Authors: Михаил Страшун 10 */ 11 12 module vson.meta.funcattr; 13 14 import std.traits : isInstanceOf, ReturnType; 15 16 /// example 17 unittest 18 { 19 struct Context 20 { 21 int increment; 22 string token; 23 bool updated = false; 24 } 25 26 static int genID(Context* context) 27 { 28 static int id = 0; 29 return (id += context.increment); 30 } 31 32 static string update(string result, Context* context) 33 { 34 context.updated = true; 35 return result ~ context.token; 36 } 37 38 class API 39 { 40 @before!genID("id") @after!update() 41 string handler(int id, string name, string text) 42 { 43 import std..string : format; 44 45 return format("[%s] %s : %s", id, name, text); 46 } 47 } 48 49 auto api = new API(); 50 auto context = new Context(5, " | token"); 51 auto funcattr = createAttributedFunction!(API.handler)(context); 52 auto result = funcattr(&api.handler, "Developer", "Hello, World!"); 53 54 assert (result == "[5] Developer : Hello, World! | token"); 55 assert (context.updated); 56 } 57 58 /** 59 Marks function/method for usage with `AttributedFunction`. 60 61 Former will call a Hook before calling attributed function/method and 62 provide its return value as input parameter. 63 64 Params: 65 Hook = function/method symbol to run before attributed function/method 66 parameter_name = name in attributed function/method parameter list to bind result to 67 68 Returns: 69 internal attribute struct that embeds supplied information 70 */ 71 auto before(alias Hook)(string parameter_name) 72 { 73 return InputAttribute!Hook(parameter_name); 74 } 75 76 /// 77 unittest 78 { 79 int genID() { return 42; } 80 81 @before!genID("id") 82 void foo(int id, double something) {} 83 } 84 85 /** 86 Marks function/method for usage with `AttributedFunction`. 87 88 Former will call a Hook after calling attributed function/method and provide 89 its return value as a single input parameter for a Hook. 90 91 There can be only one "after"-attribute attached to a single symbol. 92 93 Params: 94 Hook = function/method symbol to run after attributed function/method 95 96 Returns: 97 internal attribute struct that embeds supplied information 98 */ 99 auto after(alias Function)() 100 { 101 return OutputAttribute!Function(); 102 } 103 104 /// 105 unittest 106 { 107 auto filter(int result) 108 { 109 return result; 110 } 111 112 @after!filter() 113 int foo() { return 42; } 114 } 115 /** 116 Checks if parameter is calculated by one of attached 117 functions. 118 119 Params: 120 Function = function symbol to query for attributes 121 name = parameter name to check 122 123 Returns: 124 `true` if it is calculated 125 */ 126 template IsAttributedParameter(alias Function, string name) 127 { 128 import std.traits : FunctionTypeOf; 129 130 static assert (is(FunctionTypeOf!Function)); 131 132 private { 133 alias Data = AttributedParameterMetadata!Function; 134 135 template Impl(T...) 136 { 137 static if (T.length == 0) { 138 enum Impl = false; 139 } 140 else { 141 static if (T[0].name == name) { 142 enum Impl = true; 143 } 144 else { 145 enum Impl = Impl!(T[1..$]); 146 } 147 } 148 } 149 } 150 151 enum IsAttributedParameter = Impl!Data; 152 } 153 154 /** 155 Computes the given attributed parameter using the corresponding @before modifier. 156 */ 157 auto computeAttributedParameter(alias FUNCTION, string NAME, ARGS...)(ARGS args) 158 { 159 import std.typetuple : Filter; 160 static assert(IsAttributedParameter!(FUNCTION, NAME), "Missing @before attribute for parameter "~NAME); 161 alias input_attributes = Filter!(isInputAttribute, __traits(getAttributes, FUNCTION)); 162 foreach (att; input_attributes) 163 static if (att.parameter == NAME) { 164 return att.evaluator(args); 165 } 166 assert(false); 167 } 168 169 170 /** 171 Computes the given attributed parameter using the corresponding @before modifier. 172 173 This overload tries to invoke the given function as a member of the $(D ctx) 174 parameter. It also supports accessing private member functions using the 175 $(D PrivateAccessProxy) mixin. 176 */ 177 auto computeAttributedParameterCtx(alias FUNCTION, string NAME, T, ARGS...)(T ctx, ARGS args) 178 { 179 import std.typetuple : Filter; 180 static assert(IsAttributedParameter!(FUNCTION, NAME), "Missing @before attribute for parameter "~NAME); 181 alias input_attributes = Filter!(isInputAttribute, __traits(getAttributes, FUNCTION)); 182 foreach (att; input_attributes) 183 static if (att.parameter == NAME) { 184 static if (is(typeof(__traits(parent, att.evaluator).init) == T)) { 185 static if (is(typeof(ctx.invokeProxy__!(att.evaluator)(args)))) 186 return ctx.invokeProxy__!(att.evaluator)(args); 187 else return __traits(getMember, ctx, __traits(identifier, att.evaluator))(args); 188 } else { 189 return att.evaluator(args); 190 } 191 } 192 assert(false); 193 } 194 195 196 /** 197 Helper mixin to support private member functions for $(D @before) attributes. 198 */ 199 mixin template PrivateAccessProxy() { 200 auto invokeProxy__(alias MEMBER, ARGS...)(ARGS args) { return MEMBER(args); } 201 } 202 /// 203 unittest { 204 class MyClass { 205 @before!computeParam("param") 206 void method(bool param) 207 { 208 assert(param == true); 209 } 210 211 private bool computeParam() 212 { 213 return true; 214 } 215 } 216 } 217 218 219 /** 220 Processes the function return value using all @after modifiers. 221 */ 222 ReturnType!FUNCTION evaluateOutputModifiers(alias FUNCTION)(ReturnType!FUNCTION result) 223 { 224 import std.typetuple : Filter; 225 alias output_attributes = Filter!(isOutputAttribute, __traits(getAttributes, FUNCTION)); 226 foreach (OA; output_attributes) { 227 import std.typetuple : TypeTuple; 228 229 static assert ( 230 Compare!( 231 Group!(ParameterTypeTuple!(OA.modificator)), 232 Group!(ReturnType!Function, StoredArgTypes.expand) 233 ), 234 format( 235 "Output attribute function '%s%s' argument list " ~ 236 "does not match provided argument list %s", 237 fullyQualifiedName!(OA.modificator), 238 ParameterTypeTuple!(OA.modificator).stringof, 239 TypeTuple!(ReturnType!Function, StoredArgTypes.expand).stringof 240 ) 241 ); 242 243 result = OA.modificator(result, m_storedArgs); 244 } 245 return result; 246 } 247 248 /// 249 unittest 250 { 251 int foo() 252 { 253 return 42; 254 } 255 256 @before!foo("name1") 257 void bar(int name1, double name2) 258 { 259 } 260 261 static assert (IsAttributedParameter!(bar, "name1")); 262 static assert (!IsAttributedParameter!(bar, "name2")); 263 static assert (!IsAttributedParameter!(bar, "oops")); 264 } 265 266 // internal attribute definitions 267 private { 268 269 struct InputAttribute(alias Function) 270 { 271 alias evaluator = Function; 272 string parameter; 273 } 274 275 struct OutputAttribute(alias Function) 276 { 277 alias modificator = Function; 278 } 279 280 template isInputAttribute(T...) 281 { 282 enum isInputAttribute = (T.length == 1) && isInstanceOf!(InputAttribute, typeof(T[0])); 283 } 284 285 unittest 286 { 287 void foo() {} 288 289 enum correct = InputAttribute!foo("name"); 290 enum wrong = OutputAttribute!foo(); 291 292 static assert (isInputAttribute!correct); 293 static assert (!isInputAttribute!wrong); 294 } 295 296 template isOutputAttribute(T...) 297 { 298 enum isOutputAttribute = (T.length == 1) && isInstanceOf!(OutputAttribute, typeof(T[0])); 299 } 300 301 unittest 302 { 303 void foo() {} 304 305 enum correct = OutputAttribute!foo(); 306 enum wrong = InputAttribute!foo("name"); 307 308 static assert (isOutputAttribute!correct); 309 static assert (!isOutputAttribute!wrong); 310 } 311 } 312 313 // tools to operate on InputAttribute tuple 314 private { 315 316 // stores metadata for single InputAttribute "effect" 317 struct Parameter 318 { 319 // evaluated parameter name 320 string name; 321 // that parameter index in attributed function parameter list 322 int index; 323 // fully qualified return type of attached function 324 string type; 325 // for non-basic types - module to import 326 string origin; 327 } 328 329 /** 330 Used to accumulate various parameter-related metadata in one 331 tuple in one go. 332 333 Params: 334 Function = attributed functon / method symbol 335 336 Returns: 337 TypeTuple of Parameter instances, one for every Function 338 parameter that will be evaluated from attributes. 339 */ 340 template AttributedParameterMetadata(alias Function) 341 { 342 import std.array : join; 343 import std.typetuple : Filter, staticMap, staticIndexOf; 344 import std.traits : ParameterIdentifierTuple, ReturnType, 345 fullyQualifiedName, moduleName; 346 347 private alias attributes = Filter!( 348 isInputAttribute, 349 __traits(getAttributes, Function) 350 ); 351 352 private alias parameter_names = ParameterIdentifierTuple!Function; 353 354 /* 355 Creates single Parameter instance. Used in pair with 356 staticMap. 357 */ 358 template BuildParameter(alias attribute) 359 { 360 enum name = attribute.parameter; 361 362 static assert ( 363 is (ReturnType!(attribute.evaluator)) && !(is(ReturnType!(attribute.evaluator) == void)), 364 "hook functions attached for usage with `AttributedFunction` " ~ 365 "must have a return type" 366 ); 367 368 static if (is(typeof(moduleName!(ReturnType!(attribute.evaluator))))) { 369 enum origin = moduleName!(ReturnType!(attribute.evaluator)); 370 } 371 else { 372 enum origin = ""; 373 } 374 375 enum BuildParameter = Parameter( 376 name, 377 staticIndexOf!(name, parameter_names), 378 fullyQualifiedName!(ReturnType!(attribute.evaluator)), 379 origin 380 ); 381 382 import std..string : format; 383 384 static assert ( 385 BuildParameter.index >= 0, 386 format( 387 "You are trying to attach function result to parameter '%s' " ~ 388 "but there is no such parameter for '%s(%s)'", 389 name, 390 fullyQualifiedName!Function, 391 join([ parameter_names ], ", ") 392 ) 393 ); 394 } 395 396 alias AttributedParameterMetadata = staticMap!(BuildParameter, attributes); 397 } 398 399 // no false attribute detection 400 unittest 401 { 402 @(42) void foo() {} 403 static assert (AttributedParameterMetadata!foo.length == 0); 404 } 405 406 // does not compile for wrong attribute data 407 unittest 408 { 409 int attached1() { return int.init; } 410 void attached2() {} 411 412 @before!attached1("doesnotexist") 413 void bar(int param) {} 414 415 @before!attached2("param") 416 void baz(int param) {} 417 418 // wrong name 419 static assert (!__traits(compiles, AttributedParameterMetadata!bar)); 420 // no return type 421 static assert (!__traits(compiles, AttributedParameterMetadata!baz)); 422 } 423 424 // generates expected tuple for valid input 425 unittest 426 { 427 int attached1() { return int.init; } 428 double attached2() { return double.init; } 429 430 @before!attached1("two") @before!attached2("three") 431 void foo(string one, int two, double three) {} 432 433 alias result = AttributedParameterMetadata!foo; 434 static assert (result.length == 2); 435 static assert (result[0] == Parameter("two", 1, "int")); 436 static assert (result[1] == Parameter("three", 2, "double")); 437 } 438 439 /** 440 Combines types from arguments of initial `AttributedFunction` call 441 with parameters (types) injected by attributes for that call. 442 443 Used to verify that resulting argument list can be passed to underlying 444 attributed function. 445 446 Params: 447 ParameterMeta = Group of Parameter instances for extra data to add into argument list 448 ParameterList = Group of types from initial argument list 449 450 Returns: 451 type tuple of expected combined function argument list 452 */ 453 template MergeParameterTypes(alias ParameterMeta, alias ParameterList) 454 { 455 import vson.meta.typetuple : isGroup, Group; 456 457 static assert (isGroup!ParameterMeta); 458 static assert (isGroup!ParameterList); 459 460 static if (ParameterMeta.expand.length) { 461 enum Parameter meta = ParameterMeta.expand[0]; 462 463 static assert (meta.index <= ParameterList.expand.length); 464 static if (meta.origin != "") { 465 mixin("static import " ~ meta.origin ~ ";"); 466 } 467 mixin("alias type = " ~ meta.type ~ ";"); 468 469 alias PartialResult = Group!( 470 ParameterList.expand[0..meta.index], 471 type, 472 ParameterList.expand[meta.index..$] 473 ); 474 475 alias MergeParameterTypes = MergeParameterTypes!( 476 Group!(ParameterMeta.expand[1..$]), 477 PartialResult 478 ); 479 } 480 else { 481 alias MergeParameterTypes = ParameterList.expand; 482 } 483 } 484 485 // normal 486 unittest 487 { 488 import vson.meta.typetuple : Group, Compare; 489 490 alias meta = Group!( 491 Parameter("one", 2, "int"), 492 Parameter("two", 3, "string") 493 ); 494 495 alias initial = Group!( double, double, double ); 496 497 alias merged = Group!(MergeParameterTypes!(meta, initial)); 498 499 static assert ( 500 Compare!(merged, Group!(double, double, int, string, double)) 501 ); 502 } 503 504 // edge 505 unittest 506 { 507 import vson.meta.typetuple : Group, Compare; 508 509 alias meta = Group!( 510 Parameter("one", 3, "int"), 511 Parameter("two", 4, "string") 512 ); 513 514 alias initial = Group!( double, double, double ); 515 516 alias merged = Group!(MergeParameterTypes!(meta, initial)); 517 518 static assert ( 519 Compare!(merged, Group!(double, double, double, int, string)) 520 ); 521 } 522 523 // out-of-index 524 unittest 525 { 526 import vson.meta.typetuple : Group; 527 528 alias meta = Group!( 529 Parameter("one", 20, "int"), 530 ); 531 532 alias initial = Group!( double ); 533 534 static assert ( 535 !__traits(compiles, MergeParameterTypes!(meta, initial)) 536 ); 537 } 538 539 } 540 541 /** 542 Entry point for `funcattr` API. 543 544 Helper struct that takes care of calling given Function in a such 545 way that part of its arguments are evalutated by attached input attributes 546 (see `before`) and output gets post-processed by output attribute 547 (see `after`). 548 549 One such structure embeds single attributed function to call and 550 specific argument type list that can be passed to attached functions. 551 552 Params: 553 Function = attributed function 554 StoredArgTypes = Group of argument types for attached functions 555 556 */ 557 struct AttributedFunction(alias Function, alias StoredArgTypes) 558 { 559 import std.traits : isSomeFunction, ReturnType, FunctionTypeOf, 560 ParameterTypeTuple, ParameterIdentifierTuple; 561 import vson.meta.typetuple : Group, isGroup, Compare; 562 import std.functional : toDelegate; 563 import std.typetuple : Filter; 564 565 static assert (isGroup!StoredArgTypes); 566 static assert (is(FunctionTypeOf!Function)); 567 568 /** 569 Stores argument tuple for attached function calls 570 571 Params: 572 args = tuple of actual argument values 573 */ 574 void storeArgs(StoredArgTypes.expand args) 575 { 576 m_storedArgs = args; 577 } 578 579 /** 580 Used to invoke configured function/method with 581 all attached attribute functions. 582 583 As aliased method symbols can't be called without 584 the context, explicit providing of delegate to call 585 is required 586 587 Params: 588 dg = delegated created from function / method to call 589 args = list of arguments to dg not provided by attached attribute function 590 591 Return: 592 proxies return value of dg 593 */ 594 ReturnType!Function opCall(T...)(FunctionDg dg, T args) 595 { 596 import std.traits : fullyQualifiedName; 597 import std..string : format; 598 599 enum hasReturnType = is(ReturnType!Function) && !is(ReturnType!Function == void); 600 601 static if (hasReturnType) { 602 ReturnType!Function result; 603 } 604 605 // check that all attached functions have conforming argument lists 606 foreach (uda; input_attributes) { 607 static assert ( 608 Compare!( 609 Group!(ParameterTypeTuple!(uda.evaluator)), 610 StoredArgTypes 611 ), 612 format( 613 "Input attribute function '%s%s' argument list " ~ 614 "does not match provided argument list %s", 615 fullyQualifiedName!(uda.evaluator), 616 ParameterTypeTuple!(uda.evaluator).stringof, 617 StoredArgTypes.expand.stringof 618 ) 619 ); 620 } 621 622 static if (hasReturnType) { 623 result = prepareInputAndCall(dg, args); 624 } 625 else { 626 prepareInputAndCall(dg, args); 627 } 628 629 static assert ( 630 output_attributes.length <= 1, 631 "Only one output attribute (@after) is currently allowed" 632 ); 633 634 static if (output_attributes.length) { 635 import std.typetuple : TypeTuple; 636 637 static assert ( 638 Compare!( 639 Group!(ParameterTypeTuple!(output_attributes[0].modificator)), 640 Group!(ReturnType!Function, StoredArgTypes.expand) 641 ), 642 format( 643 "Output attribute function '%s%s' argument list " ~ 644 "does not match provided argument list %s", 645 fullyQualifiedName!(output_attributes[0].modificator), 646 ParameterTypeTuple!(output_attributes[0].modificator).stringof, 647 TypeTuple!(ReturnType!Function, StoredArgTypes.expand).stringof 648 ) 649 ); 650 651 static if (hasReturnType) { 652 result = output_attributes[0].modificator(result, m_storedArgs); 653 } 654 else { 655 output_attributes[0].modificator(m_storedArgs); 656 } 657 } 658 659 static if (hasReturnType) { 660 return result; 661 } 662 } 663 664 /** 665 Convenience wrapper tha creates stub delegate for free functions. 666 667 As those do not require context, passing delegate explicitly is not 668 required. 669 */ 670 ReturnType!Function opCall(T...)(T args) 671 if (!is(T[0] == delegate)) 672 { 673 return this.opCall(toDelegate(&Function), args); 674 } 675 676 private { 677 // used as an argument tuple when function attached 678 // to InputAttribute is called 679 StoredArgTypes.expand m_storedArgs; 680 681 // used as input type for actual function pointer so 682 // that both free functions and methods can be supplied 683 alias FunctionDg = typeof(toDelegate(&Function)); 684 685 // information about attributed function arguments 686 alias ParameterTypes = ParameterTypeTuple!Function; 687 alias parameter_names = ParameterIdentifierTuple!Function; 688 689 // filtered UDA lists 690 alias input_attributes = Filter!(isInputAttribute, __traits(getAttributes, Function)); 691 alias output_attributes = Filter!(isOutputAttribute, __traits(getAttributes, Function)); 692 } 693 694 private { 695 696 /** 697 Does all the magic necessary to prepare argument list for attributed 698 function based on `input_attributes` and `opCall` argument list. 699 700 Catches all name / type / size mismatch erros in that domain via 701 static asserts. 702 703 Params: 704 dg = delegate for attributed function / method 705 args = argument list from `opCall` 706 707 Returns: 708 proxies return value of dg 709 */ 710 ReturnType!Function prepareInputAndCall(T...)(FunctionDg dg, T args) 711 if (!Compare!(Group!T, Group!(ParameterTypeTuple!Function))) 712 { 713 alias attributed_parameters = AttributedParameterMetadata!Function; 714 // calculated combined input type list 715 alias Input = MergeParameterTypes!( 716 Group!attributed_parameters, 717 Group!T 718 ); 719 720 import std.traits : fullyQualifiedName; 721 import std..string : format; 722 723 static assert ( 724 Compare!(Group!Input, Group!ParameterTypes), 725 format( 726 "Calculated input parameter type tuple %s does not match " ~ 727 "%s%s", 728 Input.stringof, 729 fullyQualifiedName!Function, 730 ParameterTypes.stringof 731 ) 732 ); 733 734 // this value tuple will be used to assemble argument list 735 Input input; 736 737 foreach (i, uda; input_attributes) { 738 // each iteration cycle is responsible for initialising `input` 739 // tuple from previous spot to current attributed parameter index 740 // (including) 741 742 enum index = attributed_parameters[i].index; 743 744 static if (i == 0) { 745 enum lStart = 0; 746 enum lEnd = index; 747 enum rStart = 0; 748 enum rEnd = index; 749 } 750 else { 751 enum previousIndex = attributed_parameters[i - 1].index; 752 enum lStart = previousIndex + 1; 753 enum lEnd = index; 754 enum rStart = previousIndex + 1 - i; 755 enum rEnd = index - i; 756 } 757 758 static if (lStart != lEnd) { 759 input[lStart..lEnd] = args[rStart..rEnd]; 760 } 761 762 // during last iteration cycle remaining tail is initialised 763 // too (if any) 764 765 static if ((i == input_attributes.length - 1) && (index != input.length - 1)) { 766 input[(index + 1)..$] = args[(index - i)..$]; 767 } 768 769 input[index] = uda.evaluator(m_storedArgs); 770 } 771 772 // handle degraded case with no attributes separately 773 static if (!input_attributes.length) { 774 input[] = args[]; 775 } 776 777 return dg(input); 778 } 779 780 /** 781 `prepareInputAndCall` overload that operates on argument tuple that exactly 782 matches attributed function argument list and thus gets updated by 783 attached function instead of being merged with it 784 */ 785 ReturnType!Function prepareInputAndCall(T...)(FunctionDg dg, T args) 786 if (Compare!(Group!T, Group!(ParameterTypeTuple!Function))) 787 { 788 alias attributed_parameters = AttributedParameterMetadata!Function; 789 790 foreach (i, uda; input_attributes) { 791 enum index = attributed_parameters[i].index; 792 args[index] = uda.evaluator(m_storedArgs); 793 } 794 795 return dg(args); 796 } 797 } 798 } 799 800 /// example 801 unittest 802 { 803 import std.conv; 804 805 static string evaluator(string left, string right) 806 { 807 return left ~ right; 808 } 809 810 // all attribute function must accept same stored parameters 811 static int modificator(int result, string unused1, string unused2) 812 { 813 return result * 2; 814 } 815 816 @before!evaluator("a") @before!evaluator("c") @after!modificator() 817 static int sum(string a, int b, string c, double d) 818 { 819 return to!int(a) + to!int(b) + to!int(c) + to!int(d); 820 } 821 822 // ("10", "20") - stored arguments for evaluator() 823 auto funcattr = createAttributedFunction!sum("10", "20"); 824 825 // `b` and `d` are unattributed, thus `42` and `13.5` will be 826 // used as their values 827 int result = funcattr(42, 13.5); 828 829 assert(result == (1020 + 42 + 1020 + to!int(13.5)) * 2); 830 } 831 832 // testing other prepareInputAndCall overload 833 unittest 834 { 835 import std.conv; 836 837 static string evaluator(string left, string right) 838 { 839 return left ~ right; 840 } 841 842 // all attribute function must accept same stored parameters 843 static int modificator(int result, string unused1, string unused2) 844 { 845 return result * 2; 846 } 847 848 @before!evaluator("a") @before!evaluator("c") @after!modificator() 849 static int sum(string a, int b, string c, double d) 850 { 851 return to!int(a) + to!int(b) + to!int(c) + to!int(d); 852 } 853 854 auto funcattr = createAttributedFunction!sum("10", "20"); 855 856 // `a` and `c` are expected to be simply overwritten 857 int result = funcattr("1000", 42, "1000", 13.5); 858 859 assert(result == (1020 + 42 + 1020 + to!int(13.5)) * 2); 860 } 861 862 /** 863 Syntax sugar in top of AttributedFunction 864 865 Creates AttributedFunction with stored argument types that 866 match `T` and stores `args` there before returning. 867 */ 868 auto createAttributedFunction(alias Function, T...)(T args) 869 { 870 import vson.meta.typetuple : Group; 871 872 AttributedFunction!(Function, Group!T) result; 873 result.storeArgs(args); 874 return result; 875 } 876 877 /// 878 unittest 879 { 880 void foo() {} 881 882 auto funcattr = createAttributedFunction!foo(1, "2", 3.0); 883 884 import std.typecons : tuple; 885 assert (tuple(funcattr.m_storedArgs) == tuple(1, "2", 3.0)); 886 }