1 
2 /*******************************************************************************
3  * Decoder for STM of CSV
4  * 
5  * Copyright: © 2019, SHOO
6  * License: [BSL-1.0](http://boost.org/LICENSE_1_0.txt).
7  * Author: SHOO
8  */
9 module cushion.csvdecoder;
10 
11 import std.csv, std.array, std.algorithm, std.range, std.format;
12 import cushion.core, cushion._internal.misc;
13 
14 /*******************************************************************************
15  * Decode to D language code from STM of CSV
16  * 
17  * In the first argument, STM in CSV format is passed as a string.
18  * And in the second argument, replacement map in CSV format is passed as a string.
19  * This CSV pair will be decoded into the code in D language.
20  * 
21  * 
22  * STM by CSV is converted according to the following rules.
23  * 
24  * $(UL
25  *   $(LI The cell at most top-left is described name of the STM.)
26  *   $(LI In the first row cells except the leftmost column, "$(B state)" names are described.)
27  *   $(LI Cells of leftmost column in rows 2 and 3 are ignored in program code.)
28  *   $(LI "$(B Event)" names are described on the leftmost 4th row and beyond.)
29  *   $(LI The 1st line of CSV describes "$(B states)")
30  *   $(LI "$(B States)" is always specified a string beginning with "∇" (default character) or `stateKey` that is user defined key character.)
31  *   $(LI The 2nd line of CSV describes "$(B start activity)")
32  *   $(LI The 3rd line of CSV describes "$(B end activity)")
33  *   $(LI "$(B start activity)" and "$(B end activity)" are described by some "$(B processes)")
34  *   $(LI When the "$(B state transitions)", the "$(B process)" described in the "$(B start activity)" of the "$(B states)" of after the transition are performed.)
35  *   $(LI In the other hand, the "$(B processes)" in the "$(B end activity)" are performed at before the transition.)
36  *   $(LI In cell where "$(B state)" and "$(B event)" intersect, "$(B processes)" are described)
37  *   $(LI For state transition, specify the state name starting with `stateKey` in the first line of the "$(B processes)" described in the cell)
38  *   $(LI A blank cell does not process the event and means to ignore it.)
39  *   $(LI Cells written with only `x` assert forbidden event handling.)
40  * )
41  * 
42  * The replacement map is described according to the following specifications.
43  * 
44  * $(UL
45  *   $(LI The replacement target of the replacement map CSV is the "$(B state)", "$(B event)", "$(B state transition)", and "$(B process)" of the STM)
46  *   $(LI The first column of CSV describes the string before conversion.)
47  *   $(LI The second column of CSV describes the string after conversion.)
48  *   $(LI The string in the left column is simply replaced by the string in the right column.)
49  *   $(LI After substitution, "$(B process)" must be complete D language code.)
50  *   $(LI "$(B event)" must be replaced to enum member of "$(B Event)".)
51  *   $(LI "$(B state)" must be replaced to enum member of "$(B State)".)
52  *   $(LI And also, "$(B state transition)" that are described together "$(B process)" must be replaced to enum member of "$(B State)".)
53  * )
54  * 
55  * Since the string generated by this function is the source code of the D language, it can be embedded in the actual code after saving it in the file, or it can be used directly by mixin().
56  * The "$(B states)" are generated as an enum type named `State`, and the "$(B events)" are generated as an enum type named `Event`.
57  * STM instance is generated by executing the generated factory function that name can be specified by `factoryName` with makeStm as its default name.
58  */
59 string decodeStmFromCsv(
60 	string stmCsvContents, string mapCsvContents, string mapFileName = null, string stmFileName = null,
61 	string stateKey = "▽", string factoryName = "makeStm")
62 {
63 	import cushion.stmgen;
64 	string[][] mat;
65 	string[string] map;
66 	static struct MapLayout
67 	{
68 		string key;
69 		string val;
70 	}
71 	foreach (data; csvReader!string(stmCsvContents))
72 		mat ~= data.array;
73 	foreach (data; csvReader!MapLayout(mapCsvContents))
74 		map[data.key] = data.val;
75 	
76 	StmGenerator stmgen;
77 	stmgen.stmFileName = stmFileName;
78 	stmgen.mapFileName = mapFileName;
79 	stmgen.stateKey    = stateKey;
80 	stmgen.map         = map;
81 	stmgen.nameRaw     = mat[0][0];
82 	stmgen.statesRaw   = mat[0][1..$];
83 	stmgen.stactsRaw   = mat[1][1..$];
84 	stmgen.edactsRaw   = mat[2][1..$];
85 	stmgen.eventsRaw.length = cast(size_t)(cast(int)mat.length-3);
86 	stmgen.cellsRaw.length = cast(size_t)(cast(int)mat.length-3);
87 	foreach (i, r; mat[3..$])
88 	{
89 		stmgen.eventsRaw[i] = r[0];
90 		stmgen.cellsRaw[i]  = r[1..$];
91 	}
92 	
93 	return stmgen.genCode();
94 }
95 
96 /*******************************************************************************
97  * Example is following.
98  * 
99  * This case explains how to operate the music player with the start button and the stop button.
100  * The players play music when the start button is pressed while stopping.
101  * And when you press the start button during playing, the behavior will change and music playback will pause.
102  * When the stop button is pressed, the player stops music playback and returns to the initial state.
103  * 
104  * When this specification is made to STM, the following table can be created.
105  * 
106  * stmcsv:
107  * $(TABLE
108  *   $(TR $(TH *MusicPlayer* )$(TH #>stop                   )$(TH #>play                                          )$(TH #>pause                 ) )
109  *   $(TR $(TH StartAct.     )$(TD                          )$(TD                                                 )$(TD                         ) )
110  *   $(TR $(TH EndAct.       )$(TD                          )$(TD                                                 )$(TD                         ) )
111  *   $(TR $(TH onStart       )$(TD #>play$(BR)- Start music )$(TD #>pause$(BR)- Stop music                        )$(TD #>play$(BR)- Start music) )
112  *   $(TR $(TH onStop        )$(TD                          )$(TD  #>stop$(BR)- Stop music$(BR)- Return to first  )$(TD #>stop$(BR)- Return to first) )
113  * )
114  * 
115  * In each cell of the table, the transition destination and processing are described in natural language.
116  * Representations in natural language are replaced by the following map table and converted into a program expression in D.
117  * One line in each cell is subject to replacement. However, those that do not exist in the replacement map are not replaced.
118  * 
119  * mapcsv:
120  * $(TABLE
121  *   $(TR $(TD #>stop            )$(TD stop           ) )
122  *   $(TR $(TD #>play            )$(TD play           ) )
123  *   $(TR $(TD #>pause           )$(TD pause          ) )
124  *   $(TR $(TD - Start music     )$(TD startMusic();  ) )
125  *   $(TR $(TD - Stop music      )$(TD stopMusic();   ) )
126  *   $(TR $(TD - Return to first )$(TD resetMusic();  ) )
127  * )
128  * 
129  * To execute the pair of STM and replacement map as code, see the following code:
130  */
131 @safe unittest
132 {
133 	import std..string, std.datetime.stopwatch;
134 	// STM
135 	enum stmcsv = `
136 	*MusicPlayer*,#>stop,#>play,#>pause
137 	StartAct,,,
138 	EndAct,,,
139 	onStart,"#>play\n- Start music","#>pause\n- Stop music","#>play\n- Start music"
140 	onStop,,"#>stop\n- Stop music\n- Return to first","#>stop\n- Return to first"`
141 	.strip("\n").outdent.replace(`\n`,"\n");
142 	
143 	// replacement mapping data
144 	enum mapcsv = `
145 	#>stop,stop
146 	#>play,play
147 	#>pause,pause
148 	- Start music,startMusic();
149 	- Stop music,stopMusic();
150 	- Return to first,resetMusic();`
151 	.strip("\n").outdent.replace(`\n`,"\n");
152 	
153 	// Programs to be driven by STM
154 	string status = "stopped";
155 	StopWatch playTime;
156 	void startMusic() { playTime.start(); status = "playing"; }
157 	void stopMusic()  { playTime.stop();  status = "stopped"; }
158 	void resetMusic() { playTime.reset(); }
159 	
160 	// Generate code from STM(csv data) and mapping data
161 	enum stmcode = decodeStmFromCsv(stmcsv, mapcsv, null, null, "#>", "makeStm");
162 	// string mixin. Here the code is expanded.
163 	// The code contains the enum of State and Event,
164 	// activity functions and proccess when transtition.
165 	mixin(stmcode);
166 	
167 	// Create StateTransitor instance
168 	// By executing this function, construction of StateTransitor,
169 	// registration of various handlers, name setting of the matrix and states / events are performed.
170 	auto stm = makeStm();
171 	
172 	// Initial state is "stop" that most left state.
173 	assert(stm.currentState == State.stop);
174 	// At run time, display names of the state are gettable
175 	assert(stm.getStateName(stm.currentState) == "#>stop");
176 	// Likewise, event names are also gettable
177 	// In this case, event names are not replaced by mapcsv datas,
178 	// this code in following line gets the same string as the enum member of Event.
179 	assert(stm.getEventName(Event.onStart) == "onStart");
180 	
181 	// When the onStart event occurs, based on the STM,
182 	// it transit to the "#>play" state and play music.
183 	stm.put(Event.onStart);
184 	assert(stm.currentState == State.play);
185 	assert(playTime.running);
186 	() @trusted { import core.thread; Thread.sleep(10.msecs); }(); // progress in playing...
187 	assert(playTime.peek != 0.msecs);
188 	
189 	// If push the play button again during playing, it pauses.
190 	stm.put(Event.onStart);
191 	assert(stm.currentState == State.pause);
192 	assert(!playTime.running);
193 	
194 	// When you press the stop button, the player stops and returns to the first stop state
195 	stm.put(Event.onStop);
196 	assert(stm.currentState == State.stop);
197 	assert(!playTime.running);
198 	() @trusted { import core.thread; Thread.sleep(10.msecs); }(); // progress in stopped...
199 	assert(playTime.peek == 0.msecs);
200 }
201 
202 /// Following example is case of network communication in Japanese.
203 @safe unittest
204 {
205 	import std..string;
206 	enum stmcsv = `
207 	,▽初期,▽接続中,▽通信中,▽切断中
208 	スタートアクティビティ,,接続要求を開始,,切断要求を開始
209 	エンドアクティビティ,,接続要求を停止,,切断要求を停止
210 	接続の開始指示を受けたら,▽接続中,,x,x
211 	接続の停止指示を受けたら,,▽切断中,▽切断中,
212 	通信が開始されたら,▽切断中,▽通信中,x,x
213 	通信が切断されたら,x,▽初期,▽初期,▽初期`
214 	.strip("\n").outdent.replace(`\n`,"\n");
215 
216 	enum replaceData = `
217 	▽初期,init
218 	▽接続中,connectBeginning
219 	▽通信中,connecting
220 	▽切断中,connectClosing
221 	通信が開始されたら,openedConnection
222 	通信が切断されたら,closedConnection
223 	接続の開始指示を受けたら,openConnection
224 	接続の停止指示を受けたら,closeConnection
225 	接続要求を開始,startBeginConnect();
226 	接続要求を停止,endBeginConnect();
227 	切断要求を開始,startCloseConnect();
228 	切断要求を停止,endCloseConnect();`
229 	.strip("\n").outdent.replace(`\n`,"\n");
230 
231 	enum stmcode = decodeStmFromCsv(stmcsv, replaceData);
232 	int x;
233 	void startBeginConnect()
234 	{
235 		x = 1;
236 	}
237 	void endBeginConnect()
238 	{
239 		x = 2;
240 	}
241 	void startCloseConnect()
242 	{
243 		x = 3;
244 	}
245 	void endCloseConnect()
246 	{
247 		x = 4;
248 	}
249 	
250 	mixin(stmcode);
251 	auto stm = makeStm();
252 	assert(stm.getStateName(State.init) == "▽初期");
253 	assert(stm.getStateName(stm.currentState) == "▽初期");
254 	assert(x == 0);
255 	stm.put(Event.openConnection);
256 	assert(x == 1);
257 	assert(stm.getStateName(stm.currentState) == "▽接続中");
258 	assert(stm.currentState == State.connectBeginning);
259 	stm.put(Event.openedConnection);
260 	assert(x == 2);
261 	assert(stm.getStateName(stm.currentState) == "▽通信中");
262 	assert(stm.currentState == State.connecting);
263 	stm.put(Event.closeConnection);
264 	assert(x == 3);
265 	assert(stm.getStateName(stm.currentState) == "▽切断中");
266 	assert(stm.currentState == State.connectClosing);
267 	stm.put(Event.closedConnection);
268 	assert(x == 4);
269 	assert(stm.getStateName(stm.currentState) == "▽初期");
270 	assert(stm.currentState == State.init);
271 }
272 
273 // External State, Event
274 @safe unittest
275 {
276 	import std..string, std.datetime.stopwatch;
277 	// STM
278 	enum stmcsv = `
279 	*MusicPlayer*,#>stop,#>play,#>pause
280 	StartAct,,,
281 	EndAct,,,
282 	onStart,"#>play\n- Start music","#>pause\n- Stop music","#>play\n- Start music"
283 	onStop,,"#>stop\n- Stop music\n- Return to first","#>stop\n- Return to first"`
284 	.strip("\n").outdent.replace(`\n`,"\n");
285 	
286 	// replacement mapping data
287 	enum mapcsv = `
288 	#>stop,stop
289 	#>play,play
290 	#>pause,pause
291 	- Start music,startMusic();
292 	- Stop music,stopMusic();
293 	- Return to first,resetMusic();`
294 	.strip("\n").outdent.replace(`\n`,"\n");
295 	
296 	enum State
297 	{
298 		stop, play, pause
299 	}
300 	enum Event
301 	{
302 		onStart, onStop
303 	}
304 	// The code that has been generated mixin uses the name "StateTransitor".
305 	// This name must be defined separately from the one specified in the template parameter.
306 	alias StateTransitor = .StateTransitor!(State, Event);
307 	
308 	// Programs to be driven by STM
309 	string status = "stopped";
310 	StopWatch playTime;
311 	void startMusic() { playTime.start(); status = "playing"; }
312 	void stopMusic()  { playTime.stop();  status = "stopped"; }
313 	void resetMusic() { playTime.reset(); }
314 	
315 	// Generate code from STM(csv data) and mapping data
316 	// The generated code differs depending on whether the `StateTransitor`
317 	// specified in the template parameter is a template or
318 	// a full-specialized template instance.
319 	enum stmcode = decodeStmFromCsv(stmcsv, mapcsv, null, null, "#>", "makeStm");
320 	// string mixin. Here the code is expanded.
321 	// The code contains the enum of State and Event,
322 	// activity functions and proccess when transtition.
323 	mixin(stmcode);
324 	
325 	// Create StateTransitor instance
326 	// By executing this function, construction of StateTransitor,
327 	// registration of various handlers, name setting of the matrix and states / events are performed.
328 	auto stm = makeStm();
329 	
330 	// Initial state is "stop" that most left state.
331 	assert(stm.currentState == State.stop);
332 	// At run time, display names of the state are gettable
333 	assert(stm.getStateName(stm.currentState) == "#>stop");
334 	// Likewise, event names are also gettable
335 	// In this case, event names are not replaced by mapcsv datas,
336 	// this code in following line gets the same string as the enum member of Event.
337 	assert(stm.getEventName(Event.onStart) == "onStart");
338 	
339 	// When the onStart event occurs, based on the STM,
340 	// it transit to the "#>play" state and play music.
341 	stm.put(Event.onStart);
342 	assert(stm.currentState == State.play);
343 	assert(playTime.running);
344 	() @trusted { import core.thread; Thread.sleep(10.msecs); }(); // progress in playing...
345 	assert(playTime.peek != 0.msecs);
346 	
347 	// If push the play button again during playing, it pauses.
348 	stm.put(Event.onStart);
349 	assert(stm.currentState == State.pause);
350 	assert(!playTime.running);
351 	
352 	// When you press the stop button, the player stops and returns to the first stop state
353 	stm.put(Event.onStop);
354 	assert(stm.currentState == State.stop);
355 	assert(!playTime.running);
356 	() @trusted { import core.thread; Thread.sleep(10.msecs); }(); // progress in stopped...
357 	assert(playTime.peek == 0.msecs);
358 }
359 
360 /*******************************************************************************
361  * Load CSV file of STM and decode to D code.
362  */
363 string loadStmFromCsvFilePair(string stmFileName, string mapFileName)(
364 	string stateKey = "▽", string factoryName = "makeStm")
365 {
366 	static if (__traits(compiles, import(mapFileName)))
367 	{
368 		return decodeStmFromCsv(import(stmFileName), import(mapFileName),
369 			stmFileName, mapFileName, stateKey, factoryName);
370 	}
371 	else
372 	{
373 		return decodeStmFromCsv(import(stmFileName), null,
374 			stmFileName, mapFileName, stateKey, factoryName);
375 	}
376 }
377 
378 /// ditto
379 string loadStmFromCsv(string name)(
380 	string stateKey = "▽", string factoryName = "makeStm")
381 {
382 	return loadStmFromCsvFilePair!(name ~ ".stm.csv", name ~ ".map.csv")(stateKey, factoryName);
383 }
384 
385 
386 /*******************************************************************************
387  * Load CSV file of STM and decode to D code.
388  */
389 template CreateStmPolicy(
390 	string name_,
391 	alias ST = cushion.core.StateTransitor,
392 	string stateKey_ = "▽",
393 	string factoryName_ = "makeStm")
394 {
395 	enum string name        = name_;
396 	enum string stateKey    = stateKey_;
397 	enum string factoryName = factoryName_;
398 	alias StateTransitor    = ST;
399 }
400 
401 
402 /*******************************************************************************
403  * Load CSV file of STM and decode to D code.
404  */
405 auto createStm(string name, ALIASES...)()
406 {
407 	return createStm!(CreateStmPolicy!name, ALIASES)();
408 }
409 
410 /// ditto
411 auto createStm(alias basePolicy, ALIASES...)()
412 	if (__traits(hasMember, basePolicy, "name"))
413 {
414 	alias policy = CreateStmPolicy!(
415 		basePolicy.name,
416 		getMemberAlias!(basePolicy, "StateTransitor", cushion.core.StateTransitor),
417 		getMemberValue!(basePolicy, "stateKey",       "▽"),
418 		getMemberValue!(basePolicy, "factoryName",    "makeStm"));
419 	
420 	static if (__traits(identifier, policy.StateTransitor) != "StateTransitor")
421 		mixin(`alias `~__traits(identifier, policy.StateTransitor)~` = policy.StateTransitor;`);
422 	alias StateTransitor = policy.StateTransitor;
423 	
424 	auto obj = new class
425 	{
426 		static foreach (ALIAS; ALIASES)
427 			mixin(`alias ` ~ __traits(identifier, ALIAS) ~ ` = ALIAS;`);
428 		pragma(msg, "Compiling STM " ~ policy.name ~ "...");
429 		mixin(loadStmFromCsv!(policy.name)(policy.stateKey, policy.factoryName));
430 	};
431 	return __traits(getMember, obj, policy.factoryName)();
432 }