1 /**
2  * Authors: Tomoya Tanjo
3  * Copyright: © 2021 Tomoya Tanjo
4  * License: Apache-2.0
5  */
6 module salad.canonicalizer;
7 
8 ///
9 mixin template genCanonicalizeBody(Base, FieldCanonicalizer...)
10 {
11     import dyaml : Node;
12 
13     import salad.context : LoadingContext;
14     import salad.meta : genIdentifier, genToString, id, isConstantMember;
15 
16     import std.algorithm : endsWith;
17     import std.format : format;
18     import std.meta : AliasSeq, Stride;
19     import std.traits : isCallable, FieldNameTuple, Fields, hasUDA, Parameters, ReturnType;
20 
21     alias FTypes = Fields!Base;
22     alias FNames = FieldNameTuple!Base;
23 
24     static assert(FieldCanonicalizer.length % 2 == 0);
25     static if (FieldCanonicalizer.length == 0)
26     {
27         alias ConvFuns = AliasSeq!();
28     }
29     else
30     {
31         alias ConvFuns = Stride!(2, FieldCanonicalizer[1..$]);
32     }
33 
34     static auto findIndex(string name)
35     {
36         import std.algorithm : find;
37         import std.range : enumerate;
38 
39         auto rng = (cast(string[])[Stride!(2, FieldCanonicalizer)]).enumerate.find!(e => e.value~"_" == name);
40         return rng.empty ? -1 : rng.front.index;
41     }
42 
43     static foreach(idx, fname; FNames)
44     {
45         static assert(fname.endsWith("_"),
46                       format!"Field name should end with `_` (%s.%s)"(Base.stringof, fname));
47         static if (isConstantMember!(Base, fname))
48         {
49             mixin("immutable string "~fname~" = \""~mixin("(new Base)."~fname)~"\";");
50         }
51         else static if (findIndex(fname) != -1)
52         {
53             static assert(isCallable!(ConvFuns[findIndex(fname)]),
54                           format!"Convert function for `%s` is not callable"(fname));
55             static assert(Parameters!(ConvFuns[findIndex(fname)]).length == 1,
56                           format!"Convert function for `%s` should have only one parameter"(fname));
57             static assert(is(Parameters!(ConvFuns[findIndex(fname)])[0] == FTypes[idx]),
58                           format!"A parameter of convert function for `%s` expects %s but actual: %s"(
59                                 fname, FTypes[idx], Parameters!(ConvFuns[findIndex(fname)])[0]
60                           ));
61             static if (hasUDA!(__traits(getMember, Base, fname), id))
62             {
63                 mixin("@id "~ReturnType!(ConvFuns[findIndex(fname)]).stringof ~ " " ~ fname ~ ";");
64             }
65             else
66             {
67                 mixin(ReturnType!(ConvFuns[findIndex(fname)]).stringof ~ " " ~ fname ~ ";");
68             }
69         }
70         else
71         {
72             static if (hasUDA!(__traits(getMember, Base, fname), id))
73             {
74                 mixin("@id "~FTypes[idx].stringof~" "~fname~";");
75             }
76             else
77             {
78                 mixin(FTypes[idx].stringof~" "~fname~";");
79             }
80         }
81     }
82 
83     this() {}
84 
85     this(Base base)
86     {
87         canonicalize(base);
88     }
89 
90     this(in Node node, in LoadingContext context = LoadingContext.init)
91     {
92         auto base = new Base(node, context);
93         canonicalize(base);
94     }
95 
96     mixin genToString;
97     mixin genIdentifier;
98 
99     final void canonicalize(Base base)
100     {
101         static foreach(fname; FNames)
102         {
103             static if (findIndex(fname) != -1)
104             {
105                 {
106                     alias conv = ConvFuns[findIndex(fname)];
107                     __traits(getMember, this, fname) = conv(__traits(getMember, base, fname));
108                 }
109             }
110             else static if (!isConstantMember!(Base, fname))
111             {
112                 __traits(getMember, this, fname) = __traits(getMember, base, fname);
113             }
114         }
115     }
116 }
117 
118 unittest
119 {
120     import salad.context : LoadingContext;
121     import std.conv : to;
122     import dyaml : Node, Loader;
123 
124     static class C
125     {
126         int foo_;
127         string str_;
128 
129         this(Node node, in LoadingContext context = LoadingContext.init)
130         {
131             foo_ = node["foo"].as!int;
132             str_ = node["str"].as!string;
133         }
134     }
135 
136     static class Foo
137     {
138         mixin genCanonicalizeBody!(
139             C,
140             "foo", (int i) => i.to!string,
141             "str", (string s) => 0,
142         );
143     }
144 
145     enum ymlStr = q"EOS
146 foo: 10
147 str: "string"
148 EOS";
149 
150     auto foo = Loader.fromString(ymlStr).load.as!Foo;
151     assert(foo.foo_ == "10");
152     assert(foo.str_ == 0);
153 }
154 
155 unittest
156 {
157     import salad.context : LoadingContext;
158     import std.conv : to;
159     import dyaml : Node, Loader;
160 
161     static class C
162     {
163         immutable class_ = "File";
164         int foo_;
165 
166         this() {}
167         this(Node node, in LoadingContext context = LoadingContext.init)
168         {
169             foo_ = node["foo"].as!int;
170         }
171     }
172 
173     static class Foo
174     {
175         mixin genCanonicalizeBody!(C, "foo", (int i) => i.to!string);
176     }
177 
178     enum ymlStr = q"EOS
179 foo: 10
180 EOS";
181 
182     auto foo = Loader.fromString(ymlStr).load.as!Foo;
183     assert(foo.foo_ == "10");
184     assert(foo.class_ == "File");
185 }