1 module tagion.behaviour.Emendation;
2 
3 import std.algorithm;
4 import std.algorithm.iteration : cache, joiner, map;
5 import std.algorithm.searching : any;
6 import std.algorithm.sorting : sort;
7 import std.array : array, split;
8 import std.ascii : isAlphaNum, isWhite, toLower, toUpper;
9 import std.meta : Filter;
10 import std.range : empty;
11 import std.string : join, strip;
12 import std.traits : Fields;
13 import std.typecons : Flag, No, Yes;
14 import std.uni : isNumber;
15 import tagion.behaviour.BehaviourFeature;
16 
17 /**
18 This function tries to add functions name to a feature group for the action description
19 Params:
20 feature_group = Is the feature which have an emendation with function name
21 module_name = Will add the module name to the feature group if it's not already given
22 */
23 @safe
24 void emendation(ref FeatureGroup feature_group, string module_name = null) {
25     if (module_name && feature_group.info.name.length is 0) {
26         feature_group.info.name = module_name;
27     }
28     alias ScenarioActionGroups = Filter!(isActionGroup, Fields!ScenarioGroup);
29     static void emendation(ref ScenarioGroup scenario_group) {
30         size_t countActionInfos() { //nothrow {
31             size_t result;
32             static foreach (i, Type; Fields!ScenarioGroup) {
33                 static if (isActionGroup!Type) {
34                     result += scenario_group.tupleof[i].infos.length;
35                 }
36             }
37             return result;
38         }
39 
40         auto names = new string[countActionInfos];
41 
42         // Collects all the action function name and if name hasn't been defined, a name will be suggested
43         void collectNames() {
44             uint name_index;
45             static foreach (i, Type; Fields!ScenarioGroup) {
46                 static if (isActionGroup!Type) {
47                     with (scenario_group.tupleof[i]) {
48                         foreach (ref info; infos) {
49                             if (info.name.length) {
50                                 names[name_index] = info.name;
51                             }
52                             else {
53                                 takeName(names[name_index], info.property.description);
54                             }
55                             name_index++;
56                         }
57                     }
58                 }
59             }
60         }
61 
62         void setCollectNames() {
63             uint name_index;
64             static foreach (i, Type; Fields!ScenarioGroup) {
65                 static if (isActionGroup!Type) {
66                     with (scenario_group.tupleof[i]) {
67                         foreach (ref info; infos) {
68                             if (!info.name.length) {
69                                 info.name = names[name_index].camelName;
70                             }
71                             name_index++;
72                         }
73                     }
74                 }
75             }
76         }
77 
78         scenario_group.info.name = scenario_group.info.property.description.camelName(Yes.BigCamel);
79         collectNames;
80         int bail_out = 6;
81         while ((!names.isUnique && bail_out > 0) || names.any!(a => !a.isValidName)) {
82 
83             collectNames;
84             bail_out--;
85         }
86         setCollectNames;
87     }
88 
89     foreach (ref scenario_group; feature_group.scenarios) {
90         emendation(scenario_group);
91     }
92 }
93 
94 // Test emendation on a BDD with none function names
95 unittest {
96     enum bddfile_proto = "ProtoBDD_nofunc_name".unitfile;
97     immutable bdd_filename = bddfile_proto.setExtension(FileExtension.markdown);
98 
99     auto feature_byline = (() @trusted => File(bdd_filename).byLine)();
100 
101     string[] errors;
102     auto feature = parser(feature_byline, errors);
103     feature.emendation("test.emendation");
104     version (behaviour_unitdata)
105         "/tmp/feature_with_emendation".setExtension("hibon").fwrite(feature);
106 
107     const expected_feature = bdd_filename.setExtension(FileExtension.hibon).fread!FeatureGroup;
108     assert(feature.toDoc == expected_feature.toDoc);
109 }
110 
111 /** 
112 * This function add a word in reverse order from the description
113 * Params:
114 *   action_name = names which alreay was take
115 *   description = description of the action or scenario
116 * Returns: The camel case name
117 */
118 @safe
119 void takeName(ref string action_name, string description) {
120     import std.algorithm.iteration : splitter;
121     import std.ascii : isWhite;
122     import std.range : retro, take;
123     import std.range.primitives : walkLength;
124 
125     const action_subwords = action_name
126         .split!isWhite.walkLength;
127     action_name = description
128         .split!isWhite
129         .retro
130         .take(action_subwords + 1)
131         .map!(name => name.filterName)
132         .retro
133         .join(" ");
134 }
135 
136 @safe
137 bool isValidName(const string name) pure nothrow @nogc {
138     return !name.empty && !name[0].isNumber;
139 }
140 
141 @safe
142 unittest {
143     assert(!isValidName(""));
144     assert(!isValidName("1not_valid_name"));
145     assert(!isValidName("1"));
146     assert(isValidName("valid_name"));
147 }
148 /++
149 + 
150 + Params:
151 +   names_with_space = list of name separated with white-space
152 +   flag = No means function camel case and Yes means object camel case
153 + Returns: the a camel case name 
154 +/
155 @safe
156 string camelName(string names_with_space, const Flag!"BigCamel" flag = No.BigCamel) {
157     string camelCase(string name, ref bool not_first) {
158         if (name.length) {
159             if (not_first) {
160                 return toUpper(name[0]) ~ name[1 .. $];
161             }
162             not_first = true;
163             return (flag is Yes.BigCamel ? toUpper(name[0]) : toLower(name[0])) ~ name[1 .. $];
164         }
165         return null;
166     }
167 
168     bool not_first = false;
169     return names_with_space
170         .strip
171         .splitter!isWhite
172         .map!(a => camelCase(a, not_first))
173         .join
174         .filter!isAlphaNum
175         .map!(c => cast(immutable(char)) c)
176         .array;
177 }
178 
179 /// Examples: takeName and camelName
180 @safe
181 unittest {
182     string name;
183     auto some_description = "This is some description.";
184     takeName(name, some_description);
185     assert(name == "description");
186     assert(name.camelName == "description");
187     assert(name.camelName(Yes.BigCamel) == "Description");
188     takeName(name, some_description);
189     assert(name == "some description");
190     assert(name.camelName == "someDescription");
191     assert(name.camelName(Yes.BigCamel) == "SomeDescription");
192     takeName(name, some_description);
193     assert(name == "is some description");
194     assert(name.camelName == "isSomeDescription");
195     assert(name.camelName(Yes.BigCamel) == "IsSomeDescription");
196     takeName(name, some_description);
197     assert(name == "This is some description");
198     assert(name.camelName == "thisIsSomeDescription");
199     assert(name.camelName(Yes.BigCamel) == "ThisIsSomeDescription");
200 }
201 
202 /// Test of camelName with traling white white space
203 @safe
204 unittest {
205     string name = "  This is some description ";
206     assert(name.camelName == "thisIsSomeDescription");
207     assert(name.camelName(Yes.BigCamel) == "ThisIsSomeDescription");
208 
209     name = "  This is some description . ";
210     assert(name.camelName == "thisIsSomeDescription");
211     assert(name.camelName(Yes.BigCamel) == "ThisIsSomeDescription");
212     name = " the client is connected success fully ";
213     assert(name.camelName == "theClientIsConnectedSuccessFully");
214     assert(name.camelName(Yes.BigCamel) == "TheClientIsConnectedSuccessFully");
215 
216 }
217 
218 /** 
219  * 
220  * Params:
221  *   list_of_names = list of names which is going to be checked
222  * Returns: true if all the names in the list is unique and not empty
223  */
224 @safe
225 bool isUnique(string[] list_of_names) nothrow {
226     import std.algorithm.iteration : cache;
227     import std.algorithm.searching : all;
228     import std.algorithm.sorting : isStrictlyMonotonic;
229     import std.array : array;
230 
231     return (list_of_names.length == 0) ||
232         (list_of_names
233                 .all!(name => name.length != 0) &&
234                 list_of_names
235                     .array
236                     .sort
237                     .isStrictlyMonotonic);
238 }
239 
240 ///Examples:  Test of the isUnique
241 @safe
242 unittest {
243     string[] names;
244     assert(names.isUnique);
245     names = [null, "test"];
246     assert(!names.isUnique);
247     names = ["test", "test"];
248     assert(!names.isUnique);
249     names = ["test", "test1"];
250     assert(names.isUnique);
251     names = ["test", "test1", "test"];
252     assert(!names.isUnique);
253 }
254 
255 /** 
256  * Suggest a module name from the paths and the filename
257  * Params:
258  *   paths = list of search paths
259  *   filename = name of the file to be mapped to module name
260  * Returns: return a suggestion of a module name
261  */
262 @safe
263 string suggestModuleName(string filename, const(string)[] paths) {
264     import std.path : absolutePath, pathSplitter, stripExtension;
265     import std.range : drop, take;
266     import std.range.primitives : walkLength;
267 
268     auto filename_path = filename.stripExtension.absolutePath.pathSplitter;
269     foreach (path; paths) {
270         auto path_split = path.absolutePath.pathSplitter;
271         if (equal(path_split, filename_path.take(path_split.walkLength))) {
272             return filename_path.drop(path_split.walkLength).join(".");
273         }
274     }
275     return null;
276 }
277 
278 /// Example: suggestModuleName
279 @safe
280 unittest {
281     auto paths = [
282         buildPath(["some", "path", "to", "modules"]),
283         buildPath(["another", "path", "to"])
284     ];
285     const filename = buildPath(["another", "path", "to", "some", "module", "path", "ModuleName"])
286         .setExtension(FileExtension.dsrc);
287     assert(filename.suggestModuleName(paths) == "some.module.path.ModuleName");
288 }
289 
290 @safe
291 string filterName(const(char[]) name) pure {
292     return name
293         .filter!(a => a.isWhite || a.isAlphaNum)
294         .map!(a => cast(immutable char) a)
295         .array;
296 }
297 
298 @safe
299 unittest {
300     assert("#label.".filterName == "label");
301 }
302 
303 version (unittest) {
304     //    import io = std.stdio;
305     import std.exception;
306     import std.file : fwrite = write;
307     import std.path;
308     import std.stdio : File;
309     import tagion.basic.Types : FileExtension;
310     import tagion.basic.basic : unitfile;
311     import tagion.behaviour.BehaviourParser;
312     import tagion.hibon.HiBONFile : fread, fwrite;
313     import tagion.hibon.HiBONJSON;
314 }