1 module fakerjsgenerator; 2 3 import std.algorithm; 4 import std.array; 5 import std.stdio; 6 import std.process; 7 import std.format; 8 import std.conv : to; 9 import std.exception : enforce; 10 import std.file : dirEntries, SpanMode, isDir, readText, isFile, exists, 11 mkdirRecurse, read; 12 import std.algorithm; 13 import std.string; 14 import std.regex; 15 16 import jssplitter; 17 import generator; 18 import iban; 19 20 enum outputDir = "../source/faked/"; 21 22 void main() { 23 cloneFaker(); 24 FakerData[] locales = scrapeFakers(); 25 mkdirRecurse(outputDir); 26 27 /+ 28 auto list = [ 29 /*"az",*/ /*"ar",*/ /*"cz",*/ "de", "de_AT", "de_CH", "en_AU", "en_AU_ocker", 30 "en_BORK", "en_CA", "en_GB", "en_IE", "en_IND", "en_US", "en_ZA", "es", 31 /*"es_MX",*/ /*"fa",*/ "fi", "fr", "fr_CA", "fr_CH", "ge", "hy", "hr", "id_ID", 32 "it", "ja", "ko", "nb_NO", "ne", "nl", "nl_BE", "pl", "pt_BR", "pt_PT", 33 "ro", "ru", /*"sk",*/ "sv", "tr", "uk", "vi", "zh_CN", "zh_TW", "af_ZA", 34 "el", "lv", "zu_ZA" 35 36 ].sort.array; 37 +/ 38 39 auto list = 40 [ "af_ZA", "de_AT", "en_AU_ocker", "en_IE", "es", "fr_BE", "hr", "it" 41 , "nb_NO", "pt_BR", "sv", "zh_CN" , /*"ar",*/ "de_CH", "en_BORK", "en_IND" 42 , "es_MX", "fr_CA", "hu", "ja", "ne", "pt_PT", "tr", "zh_TW", "az" 43 , "el", "en_CA", "en_NG", /*"fa",*/ "fr_CH", "hy", "ko", "nl", "ro", "uk" 44 , "zu_ZA", "cz", /*"en",*/ "en_GB", "en_US", "fi", "ge", "id_ID", "lv" 45 , "nl_BE", "ru", /*"ur",*/ "de", "en_AU", "en_GH", "en_ZA", "fr", "he" 46 , "mk", "pl", "sk", "vi"].sort.array; 47 48 auto en = locales.find!(a => a.locale == "en").front; 49 string[] methods = buildFile("en", en, []); 50 methods ~= ["addressLatitude", "addressLongitude", "financeAccount", 51 "financeRoutingNumber", "financeMask", "financeBitcoinAddress", 52 /*"phoneNumber", "commerceProductName",*/ "companyCatchPhrase", 53 "companyBs", "internetUserName", "internetProtocol", 54 "internetDomainWord", "internetDomainName", "internetUrl", 55 "internetIPv4", "internetIPv6", "internetColor", "internetPassword", 56 "vehicle", "vehicleVin", "helperAlpha", "helperAlphaNum", 57 "helperHexaDecimal", 58 ]; 59 methods = methods.sort.uniq.array; 60 61 //writefln("%(%s\n%)", methods); 62 foreach(ll; list) { 63 auto f = locales.find!(a => a.locale == ll); 64 assert(!f.empty, ll); 65 buildFile(ll, f.front, methods); 66 } 67 68 buildInstantionTest(list ~ "en", methods); 69 70 writePackageFile(list); 71 } 72 73 void writePackageFile(string[] list) { 74 auto f = File(outputDir ~ "package.d", "w"); 75 auto ltw = f.lockingTextWriter(); 76 77 formattedWrite(ltw, "// generated by fakerjsgenerator\n"); 78 formattedWrite(ltw, "module faked;\n\n"); 79 formattedWrite(ltw, "///\npublic import faked.base;\n\n"); 80 foreach(l; list) { 81 formattedWrite(ltw, "///\npublic import faked.faker_%s;\n\n", toLower(l)); 82 } 83 } 84 85 void buildInstantionTest(string[] list, string[] methods) { 86 auto f = File(outputDir ~ "test.d", "w"); 87 auto ltw = f.lockingTextWriter(); 88 formattedWrite(ltw, "// generated by fakerjsgenerator\n"); 89 formattedWrite(ltw, "module faked.test;\n\nimport std.array : empty;\n"); 90 formattedWrite(ltw, "import std.stdio;\n"); 91 formattedWrite(ltw, "import std.format : format;\n\n"); 92 93 formattedWrite(ltw, ` 94 struct ToIgnore { 95 string lang; 96 string[] methods; 97 } 98 99 const allowedToFail = 100 [ ToIgnore("az" 101 , ["addressState", "companySuffix", "namePrefix", "nameSuffix"]) 102 , ToIgnore("cz" , ["addressState", "addressStateAbbr"]) 103 , ToIgnore("id_ID", ["namePrefix"]) 104 , ToIgnore("it", ["nameSuffix"]) 105 , ToIgnore("mk", ["nameSuffix"]) 106 , ToIgnore("pt_BR", ["addressCityPrefix"]) 107 , ToIgnore("pt_PT", ["addressCityPrefix", "addressCitySuffix", "nameSuffix"]) 108 , ToIgnore("ru", ["nameSuffix", "namePrefix"]) 109 , ToIgnore("sk", ["addressState", "addressStateAbbr"]) 110 ]; 111 112 void fakedAssert(D)(D fakeData, const string lang, const string method, const long round) { 113 foreach(ref l; allowedToFail) { 114 if(l.lang == lang) { 115 foreach(m; l.methods) { 116 if(m == method) { 117 if(fakeData.empty) { 118 //writefln("%%5s %%20s %%3d gives no data as expected", lang, method, round); 119 return; 120 } else { 121 assert(false, format("%%5s %%20s %%3d was expected to give no data", lang, method, round)); 122 } 123 } 124 } 125 } 126 } 127 assert(!fakeData.empty, format("%%5s %%20s %%3d failed to give data", lang 128 , method, round)); 129 } 130 131 `); 132 133 string ut = `unittest { 134 %s; 135 auto faker = new %s(%d); 136 %s} 137 138 `; 139 foreach(idx, l; list) { 140 string calls = "\tfor(int i = 0; i < 30; ++i) {\n" ~ 141 methods 142 .map!(a => format( 143 "\t\tfakedAssert(faker.%1$s(), \"%2$s\", \"%1$s\", i);\n", a, l)) 144 .joiner() 145 .to!string() 146 ~ "\n\t}\n"; 147 148 formattedWrite(ltw, ut, 149 l == "en" 150 ? "\timport faked.base" 151 : "\timport faked.faker_" ~ toLower(l), 152 l == "en" ? "Faker" : "Faker_" ~ toLower(l), 153 idx + 1, calls 154 ); 155 } 156 } 157 158 string[] buildFile(string ll, FakerData entry, string[] toOverride) { 159 //writeln(f.front); 160 string[] methods; 161 Generator gen = new Generator(ll, entry.fallback, toOverride); 162 foreach(string key, Entry value; entry.data) { 163 if(ignoreEntries.canFind(key)) { 164 continue; 165 } 166 foreach(string sub, Data svalue; value.subs) { 167 Direct d = cast(Direct)svalue; 168 if(d !is null) { 169 if(key == "name" && sub == "title") { 170 methods ~= gen.buildNameTitle(d.data); 171 continue; 172 } 173 if(key == "finance" && sub == "currency") { 174 methods ~= gen.buildFinanceCurrency(d.data); 175 continue; 176 } 177 if(key == "commerce" && sub == "product_name") { 178 methods ~= gen.buildCommerceProductName(d.data); 179 continue; 180 } 181 if(key == "name" && d.data.canFind("concat")) { 182 methods ~= gen.buildConcat(ll, key, sub, d.data); 183 continue; 184 } 185 if(d.data.canFind("concat(")) { 186 writefln("TODO %s %s %s TODO", ll, key, sub); 187 continue; 188 } 189 //write(key, ".", sub, " "); 190 //writefln("%s %s %s %s", __LINE__, ll, key, sub); 191 TypeLines tl = jssplit(d.data, ll ~ " " ~ key ~ " " ~ sub); 192 //writeln(tl.type); 193 if(tl.type == Type.strings) { 194 methods ~= gen.buildString(key, sub, tl.lines); 195 } else if(tl.type == Type.digit) { 196 methods ~= gen.buildDigits(key, sub, tl.lines); 197 } else if(tl.type == Type.call) { 198 methods ~= gen.buildCall(key, sub, tl.lines); 199 } else { 200 if(key == "address" && sub == "postcode_by_state") { 201 writefln("IGNORED HERE %s %s %s %s", __FILE__, __LINE__ 202 , key, sub); 203 continue; 204 } 205 if(tl.lines.empty) { 206 writefln("No lines for %s %s %s", ll, key, sub); 207 } 208 methods ~= gen.buildMustache(ll, key, sub, tl.lines); 209 //writefln("Not handled %s %s %s %s", ll, key, sub 210 // , tl.type); 211 } 212 } else { 213 Sub s = cast(Sub)svalue; 214 if(key == "finance" && sub == "credit_card" && s !is null) { 215 methods ~= gen.buildCreditCards(s); 216 } 217 } 218 } 219 } 220 221 string fname = (ll == "en") 222 ? "base.d" 223 : "faker_" ~ toLower(ll) ~ ".d"; 224 225 if(ll == "en") { 226 methods ~= gen.buildIbanAndBic(); 227 methods ~= ["addressLatitude", "addressLongitude", "financeAccount", 228 "financeRoutingNumber", "financeMask", "financeBitcoinAddress", 229 "loremSentance", "loremSentances", "loremParagraph", 230 "loremParagraphs", "loremText", "phoneNumber"]; 231 } 232 233 gen.finish(); 234 235 auto fout = File(outputDir ~ fname, "w"); 236 auto ltw = fout.lockingTextWriter(); 237 formattedWrite(ltw, "// generated by fakerjsgenerator\n"); 238 fout.write(gen.output); 239 240 //return methods.filter!(a => !a.empty).array; 241 return methods.sort.array; 242 } 243 244 const fakerFolder = "faker"; 245 246 const string[] ignoreListLocals = ["fa"]; 247 const string[] ignoreEntries = ["system"]; 248 249 class Data { 250 override string toString() const { 251 assert(false); 252 } 253 } 254 255 class Direct : Data { 256 string data; 257 258 this(string d) { 259 this.data = d; 260 } 261 262 override string toString() const { 263 return format(" Direct \n%s", this.data); 264 } 265 } 266 267 class Sub : Data { 268 string index; 269 Direct[string] subs; 270 271 override string toString() const { 272 string ret; 273 foreach(key, value; subs) { 274 ret ~= format("\n %s", key); 275 } 276 return ret; 277 } 278 } 279 280 struct Entry { 281 string index; 282 Data[string] subs; 283 284 void toString(scope void delegate(const(char)[]) sink) const { 285 foreach(key, const(Data) value; this.subs) { 286 formattedWrite(sink, " %s:%s\n", key, value.toString()); 287 } 288 } 289 } 290 291 class FakerData { 292 string locale; 293 string fallback; 294 Entry[string] data; 295 296 void toString(scope void delegate(const(char)[]) sink) const { 297 formattedWrite(sink, "%s fallback %s\n", this.locale, fallback); 298 foreach(key, value; this.data) { 299 formattedWrite(sink, " %s:\n", key); 300 value.toString(sink); 301 } 302 } 303 } 304 305 void cloneFaker() { 306 import std.file : exists, rmdirRecurse; 307 308 if(exists(fakerFolder)) { 309 return; 310 } 311 312 auto rslt = executeShell( 313 "git clone --depth=1 https://github.com/faker-js/faker.git" 314 ); 315 enforce(rslt.status == 0, 316 format("Failed to clone faker.js with message %s %s", rslt.status, 317 rslt.output) 318 ); 319 } 320 321 FakerData[] scrapeFakers() { 322 FakerData[] ret; 323 const fakerLocales = fakerFolder ~ "/src/locales/"; 324 325 outer: foreach(f; dirEntries(fakerLocales, SpanMode.shallow)) { 326 foreach(ig; ignoreListLocals) { 327 if(f.name.endsWith(ig)) { 328 continue outer; 329 } 330 } 331 if(isDir(f.name)) { 332 ret ~= scrapeFaker(f.name); 333 buildFallback(ret.back); 334 } 335 } 336 return ret; 337 } 338 339 FakerData scrapeFaker(string foldername) { 340 auto ret = new FakerData(); 341 string name = foldername[foldername.lastIndexOf("/") + 1 .. $]; 342 ret.locale = name; 343 344 foreach(f; dirEntries(foldername, SpanMode.shallow)) { 345 Entry entry; 346 if(f.name.endsWith("index.ts")) { 347 string iFile = "./" ~ f.name; 348 assert(isFile(iFile)); 349 entry.index = readText(iFile); 350 continue; 351 } 352 if(isDir(f.name)) { 353 foreach(g; dirEntries(f, SpanMode.shallow) 354 .filter!(a => !a.name.canFind("index.ts"))) 355 { 356 if(isFile(g.name)) { 357 string s = g.name; 358 s = s[s.lastIndexOf("/") + 1 .. $ - 3]; 359 auto t = cast(string)read(g.name); 360 entry.subs[s] = new Direct(t); 361 } else { 362 auto sub = new Sub(); 363 sub.index = readText(g.name ~ "/index.ts"); 364 foreach(h; dirEntries(g.name, SpanMode.shallow) 365 .filter!(a => !a.name.canFind("index.ts"))) 366 { 367 string sh = h.name; 368 sh = sh[sh.lastIndexOf("/") + 1 .. $ - 3]; 369 370 if(isFile(h)) { 371 sub.subs[sh] = new Direct(readText(h)); 372 } 373 } 374 entry.subs[g.name[g.name.lastIndexOf("/") + 1 .. $]] = sub; 375 } 376 } 377 } 378 ret.data[f.name[f.name.lastIndexOf("/") + 1 .. $]] = entry; 379 } 380 return ret; 381 } 382 383 enum r = r"\{\s*locale:\s*'([a-zA-Z_]\+)'\s*,\s*localeFallback\s*:'([a-zA-Z_]\+)'\s*\}"; 384 enum r2 = r"'(\w*)'"; 385 auto re = regex(r2); 386 387 void buildFallback(FakerData fd) { 388 string t = fakerFolder ~ "/locale/" ~ fd.locale ~ ".ts"; 389 if(!exists(t)) { 390 return; 391 } 392 393 string[] tt = readText(t).split("\n"); 394 foreach(l; tt) { 395 enum pre = "var faker = new Faker("; 396 if(l.startsWith(pre)) { 397 string narrow = l[pre.length .. $ - 2]; 398 auto m = matchAll(narrow, re); 399 if(!m.empty) { 400 string f = m.front.hit.strip("'"); 401 assert(f == fd.locale, 402 format("%s %s %s", fd.locale, f, t) 403 ); 404 m.popFront(); 405 f = m.front.hit.strip("'"); 406 if(!m.empty) { 407 fd.fallback = f; 408 } 409 } 410 } 411 } 412 }