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