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 }