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 }