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 }