1 module cucumber.server;
2 
3 public import cucumber.keywords;
4 public import cucumber.feature;
5 
6 import cucumber.reflection;
7 import cucumber.feature: PendingException;
8 import vibe.d;
9 import std.stdio;
10 import std.conv;
11 public import std.typecons: Flag, Yes, No;
12 
13 alias DetailsFlag = Flag!"details";
14 MatchResult[int] gMatches;
15 
16 
17 void runCucumberServer(ModuleNames...)(in ushort port, in DetailsFlag details = No.details) {
18     debug writeln("Running the Cucumber server on port ", port, " details ", details);
19     listenTCP(54321, (tcpConnection) { accept!ModuleNames(tcpConnection, details); });
20 }
21 
22 
23 private void accept(ModuleNames...)(TCPConnection tcpConnection, in DetailsFlag details) {
24     debug writeln("Accepting a connection");
25     while(tcpConnection.connected) {
26         auto bytes = tcpConnection.readLine(size_t.max, "\n");
27         handleTcpRequest!ModuleNames(tcpConnection, (cast(string)bytes).strip(), details);
28     }
29 
30     if(tcpConnection.connected) tcpConnection.close();
31 }
32 
33 private void send(TCPConnection tcpConnection, in string str) {
34     tcpConnection.write(str ~ "\n"); //I don't know why writeln doesn't work
35 }
36 
37 private void handleTcpRequest(ModuleNames...)(TCPConnection tcpConnection, in string request,
38                                               in Flag!"details" details) {
39     const reply = handleRequest!ModuleNames(request, details);
40     debug writeln("Reply: ", reply);
41     tcpConnection.send(reply);
42 }
43 
44 string handleRequest(ModuleNames...)(string request, in DetailsFlag details = No.details) {
45     import cucumber.feature: sanitize;
46 
47     debug writeln("Request: ", request);
48     const fail = `["fail"]`;
49 
50     try {
51         const json = parseJson(request);
52         const command = json[0].get!string;
53         switch(command) {
54         case "begin_scenario": return `["success"]`;
55         case "end_scenario": return `["success"]`;
56         case "step_matches": return handleStepMatches!ModuleNames(json, details);
57         case "invoke": return handleInvoke(json);
58         default: return fail;
59         }
60     } catch(Throwable ex) {
61         stderr.writeln("Error processing request: ", request);
62         stderr.writeln("Exception: ", ex.toString().sanitize());
63         return fail;
64     }
65 }
66 
67 private string handleStepMatches(ModuleNames...)(in Json json, in DetailsFlag details) {
68     const nameToMatch = json[1]["name_to_match"].get!string;
69     auto func = findMatch!ModuleNames(nameToMatch);
70     if(!func) return `["success",[]]`;
71     gMatches[func.id] = func;
72 
73     auto infoElem = Json.emptyObject;
74     infoElem["id"] = func.id.to!string;
75     infoElem["args"] = Json.emptyArray;
76 
77     if(details) {
78         infoElem["regexp"] = func.regex;
79         infoElem["source"] = func.source;
80     }
81 
82     auto info = Json.emptyArray;
83     info ~= infoElem;
84 
85     return `["success",` ~ info.toString ~ `]`;
86 }
87 
88 
89 private string handleInvoke (in Json json) {
90     debug writeln("handleInvoke for json ", json);
91     const invokeArgs = json[1];
92     const id = invokeArgs["id"].to!int;
93     if(id !in gMatches) throw new Exception(text("Could not find match for id ", id));
94     try {
95         gMatches[id]();
96     } catch(PendingException ex) {
97         return `["pending", "` ~ ex.msg ~ `"]`;
98     } catch(Throwable ex) {
99         return `["fail",{"message":"` ~ ex.msg ~ `", "exception": "` ~ ex.classinfo.name ~ `"}]`;
100     }
101     return `["success"]`;
102 }