1 module libwasm.router;
2 
3 import memutils.hashmap;
4 import libwasm.promise;
5 import libwasm.types;
6 import libwasm.bindings.MouseEvent;
7 import libwasm.bindings.Event;
8 import std.traits : isPointer, isAggregateType;
9 import memutils.vector;
10 import memutils.scoped;
11 import optional;
12 
13 // Match setup in compile
14 
15 @safe nothrow:
16 
17 enum Direction
18 {
19     Entering,
20     Leaving,
21     Always
22 }
23 
24 struct RouterEvent
25 {
26     HashMap!(string, string) parameters;
27     string prevURL;
28     string newURL;
29 }
30 /*
31 @entering!"/this/path/:id/" Promise!Any routeHandler(RouterEvent ev) {
32 
33 }
34 
35 todo: Write Promise on ThreadMemAllocator
36 
37 
38 
39 https://github.com/CyberShadow/ae/blob/master/utils/promise/package.d
40 
41 */
42 
43 private enum maxRouteParameters = 64;
44 
45 private class Route
46 {
47 @safe nothrow:
48 
49     Array!char pattern;
50     Array!char active_url;
51     Optional!(Promise!void) delegate(ref RouterEvent ev) entering_cb;
52     Optional!(Promise!void) delegate(ref RouterEvent ev) leaving_cb;
53     Optional!(Promise!void) delegate(ref RouterEvent ev) always_cb;
54     this(Array!char _pattern) { pattern = _pattern; }
55     ~this() {
56         //console.log("Route destroyed for pattern");
57         //console.log(pattern[]);
58     }
59 
60     bool matches(string url, ref HashMap!(string, string) params) @trusted
61     {
62         size_t i, j;
63         //console.log("Matches");
64         //console.log(url);
65         // store parameters until a full match is confirmed
66         import memutils.ct;
67 
68         Tuple!(string, string)[maxRouteParameters] tmpparams;
69         size_t tmppparams_length = 0;
70 
71         for (i = 0, j = 0; i < url.length && j < pattern.length;)
72         {
73             if (pattern[j] == '*')
74             {
75                 foreach (t; tmpparams[0 .. tmppparams_length])
76                     params[cast(string) t[0]] = cast(string) t[1];
77                 return true;
78             }
79             if (url[i] == pattern[j])
80             {
81                 i++;
82                 j++;
83             }
84             else if (pattern[j] == ':')
85             {
86                 j++;
87                 string name = skipPathNode(pattern[], j);
88                 string match = skipPathNode(url, i);
89                 assert(tmppparams_length < maxRouteParameters, "Maximum number of route parameters exceeded.");
90                 tmpparams[tmppparams_length++] = tuple(name, match);
91             }
92             else
93                 return false;
94         }
95 
96         if ((j < pattern.length && pattern[j] == '*') || (i == url.length && j == pattern.length))
97         {
98             foreach (t; tmpparams[0 .. tmppparams_length])
99                 params[cast(string) t[0]] = cast(string) t[1];
100             return true;
101         }
102 
103         return false;
104     }
105 
106     bool matches(string url)
107     {
108         //console.log("matches");
109         //console.log(url);
110         size_t i, j;
111 
112         for (i = 0, j = 0; i < url.length && j < pattern.length;)
113         {
114             if (pattern[j] == '*')
115             {
116                 return true;
117             }
118             if (url[i] == pattern[j])
119             {
120                 i++;
121                 j++;
122             }
123             else if (pattern[j] == ':')
124             {
125                 j++;
126                 while (i < url.length && url[i] != '/')
127                     i++;
128                 while (j < pattern.length && pattern[j] != '/')
129                     j++;
130             }
131             else
132                 return false;
133         }
134 
135         if ((j < pattern.length && pattern[j] == '*') || (i == url.length && j == pattern.length))
136         {
137             return true;
138         }
139 
140         return false;
141     }
142 
143 static:
144     private string skipPathNode()(string str, ref size_t idx)
145     {
146         size_t start = idx;
147         while (idx < str.length && str[idx] != '/')
148             idx++;
149         return str[start .. idx];
150     }
151 
152     private string skipPathNode()(ref string str)
153     {
154         size_t idx = 0;
155         auto ret = skipPathNode(str, idx);
156         str = str[idx .. $];
157         return ret;
158     }
159 }
160 
161 class URLRouter
162 {
163 @safe nothrow:
164 
165     private
166     {
167         bool m_is_setup;
168         Array!char m_base_path;
169         Array!Route m_routes;
170         // Array!Route m_routesForAlways;
171 
172         Array!Route m_activeRoutes;
173         Array!char m_currentURL;
174 
175         // defined when concurrent URL changes occur and promises are still pending
176         bool m_busy;
177         Array!char m_pendingURL;
178 
179         Array!char m_title;
180         ManagedPool m_pool;
181     }
182 
183     this() {
184         m_pool = ManagedPool(64*1024);
185         setupRouter();
186     }
187 
188     ~this()
189     {
190 
191         console.error("Destroying router");
192 
193     }
194 
195     void setTitle()(string title) @trusted
196     {
197         m_title = Array!char(title);
198     }
199 
200     string getTitle()() @trusted
201     {
202         return m_title[];
203     }
204 
205     void setBasePath()(string base_url)
206     {
207         m_base_path = Array!char(base_url);
208     }
209 
210     string getBasePath()() @trusted
211     {
212         return m_base_path[];
213     }
214 
215     void delegate(Handle) getDelegate() {
216         return (Handle hndl) {
217                 //console.log("In delegate");
218                 string path = libwasm_get__string(hndl);
219                 //console.log("Got path");
220                 //console.log(path);
221                 navigateTo(path);
222                 //console.log("Done");
223                 libwasm_removeObject(hndl);
224             };
225     }
226 
227     private void setupRouter()
228     {
229         if (!m_is_setup)
230         {
231             import libwasm.dom : document, window;
232             import libwasm.bindings.Location;
233             import libwasm.bindings.Window;
234             import libwasm.bindings.EventHandler;
235             //console.log("Allocating delegate");
236             exportDelegate("navigate_to", getDelegate());
237             //console.log("Allocated delegate");
238 
239             auto onpopstate = EventHandler(cast(EventHandlerNonNull)(&onPopState));
240             window().onpopstate(onpopstate);
241             m_is_setup = true;
242         }
243     }
244 
245     // struct PromiseIterator {
246     private
247     {
248     @safe nothrow:
249 
250         Array!Route leaving_candidates;
251         Array!Route entering_candidates;
252         Array!char newPath;
253 
254         void setupIterator(Array!Route _leaving_candidates, Array!Route _entering_candidates, Array!char _newPath)
255         {
256             leaving_candidates = _leaving_candidates;
257             //console.log("Setting up iterator with n entering candidates");
258             //console.log(_entering_candidates.length);
259             entering_candidates = _entering_candidates;
260             newPath = _newPath;
261         }
262 
263         void iterate()() @trusted
264         {
265             bool still_busy;
266             import libwasm.bindings.Console;
267 
268             auto scoped = ScopedPool(m_pool);
269             //console.log("Iterate: ");
270             //console.log(m_title[]);
271             //console.log(newPath[]);
272             //console.log(newPath[] == "/home");
273             if (!leaving_candidates.empty)
274             {
275                 //console.log("Leaving candidates n:");
276                 //console.log(leaving_candidates.length);
277                 auto r = leaving_candidates.back;
278                 leaving_candidates.removeBack();
279                 if (!r.matches(newPath[]))
280                 {
281                     RouterEvent ev;
282                     ev.newURL = newPath[];
283                     ev.prevURL = m_currentURL[];
284                     r.matches(r.active_url[], ev.parameters);
285 
286                     Optional!(Promise!void) promise;
287                     if (r.leaving_cb) promise = r.leaving_cb(ev);
288                     r.active_url.clear();
289                     // remove from activeRoutes
290                     if (!promise.empty)
291                     {
292                         promise.front.then(&iterate);
293                         still_busy = true;
294                     }
295                 }
296             }
297             else if (!entering_candidates.empty)
298             {
299                 //console.log("We have entering candidates");
300                 RouterEvent ev;
301                 auto r = entering_candidates.front;
302                 entering_candidates.removeFront();
303 
304                 //console.log(newPath[]);
305                 bool found;
306                 foreach (route; m_activeRoutes[]) {
307                     if (route.pattern[] == r.pattern[]) {
308                         found = true;
309                         break;
310                     }
311                 }
312                 if (!found)
313                 {
314                     //console.log("We cannot find this active route");
315                     //console.log(newPath[]);
316                     if (r.matches(newPath[], ev.parameters))
317                     {
318                         //console.log(newPath[]);
319                         ev.newURL = newPath[];
320                         ev.prevURL = m_currentURL[];
321                         //console.log(newPath[]);
322 
323                         Optional!(Promise!void) promise;
324                         if (r.entering_cb) promise = r.entering_cb(ev);
325                         r.active_url[] = cast(char[]) newPath[];
326                         //console.log("Added to active url");
327                         //console.log(newPath[]);
328                         m_activeRoutes ~= r;
329                         if (!promise.empty)
330                         {
331                             //console.log("Promise was not empty");
332                             promise.front.then(&iterate);
333                             still_busy = true;
334                         }
335                     }
336                 } else {
337                     
338                     //console.log("We found this active route");
339                 }
340             }
341             else
342             {
343                     //console.log("Both entering and leaving candidates were empty");
344                 m_busy = false;
345 
346                 import libwasm.bindings.Window;
347                 import libwasm.bindings.History;
348                 import libwasm.dom : window;
349 
350                 //console.log("Pushing state: ");
351                 //console.log(m_title[]);
352                 //console.log(newPath[]);
353                 if (m_is_setup)
354                     window().history().pushState(null, m_title[], Optional!string(newPath[]));
355 
356                 // we finished iteration
357                 if (!m_pendingURL.empty())
358                 {
359                     auto url = m_pendingURL;
360                     m_pendingURL.clear();
361                     navigateTo(url[]);
362                 }
363                 return;
364             }
365 
366             if (!still_busy)
367                 iterate();
368 
369             // todo: detect finished iteration and start over for the pendingURL if need be
370 
371         }
372         //}
373     }
374 
375     /// Adds a new route for requests matching the specified HTTP method and pattern.
376     void register(string path)(Optional!(Promise!void) delegate(
377             ref RouterEvent ev) nothrow @safe cb, Direction direction)
378     {
379         auto scoped = ScopedPool(m_pool);
380         //console.log("Registering");
381         //console.log(path);
382         //static assert(path.count(':') <= maxRouteParameters, "Too many route parameters");
383         bool found;
384         foreach (route; m_routes[])
385         {
386             if (route.pattern[] == path)
387             {
388                 final switch (direction)
389                 {
390                 case Direction.Entering:
391                      //console.log("Is entering cb");
392                     route.entering_cb = cb;
393                     break;
394                 case Direction.Leaving:
395                     route.leaving_cb = cb;
396                     break;
397                 case Direction.Always:
398                     route.always_cb = cb;
399                     break;
400                 }
401                 found = true;
402                 break;
403             }
404         }
405         if (!found)
406         {
407             auto route = new Route(Array!char(path));
408             final switch (direction)
409             {
410             case Direction.Entering:
411                      //console.log("Is entering cb");
412                 route.entering_cb = cb;
413                 break;
414             case Direction.Leaving:
415                      //console.log("Is leaving cb");
416                 route.leaving_cb = cb;
417                 break;
418             case Direction.Always:
419                 route.always_cb = cb;
420                 break;
421             }
422             m_routes ~= route;
423         }
424 
425     }
426 
427     /// Handles a HTTP request by dispatching it to the registered route handlers.
428     void navigateTo(string new_url)
429     {
430         //console.log("navigating to1");
431         auto scoped = ScopedPool(m_pool);
432         //console.log("navigating to");
433         //console.log(new_url);
434         //console.log(m_base_path.length);
435         if (new_url.length < m_base_path.length || (m_base_path.length > 0 && new_url[0 .. m_base_path.length] != m_base_path[]))
436             return;
437         new_url = new_url[m_base_path.length .. $];
438 
439         // queue path changes due to promises
440         if (m_busy)
441         {
442             m_pendingURL = Array!char(new_url);
443             return;
444         }
445 
446         m_busy = true;
447 
448         //console.log("Setup Iterator");
449         //console.log(m_routes.length);
450         setupIterator(Array!Route(m_activeRoutes[]), Array!Route(m_routes[]), Array!char(new_url));
451         //console.log("Iterate");
452         iterate();
453 
454     }
455 
456     void handleLinkEvent()(MouseEvent ev)
457     {
458         auto scoped = ScopedPool(m_pool);
459         import libwasm.bindings.HTMLLinkElement;
460         import libwasm.bindings.Node;
461 
462         auto pool = ScopedPool();
463         auto target = Node(ev.target().front);
464         if (target.nodeName() == "A")
465         {
466             auto el = target.as!HTMLLinkElement;
467             if (!el.getAttribute("internal").empty())
468             {
469                 string link_url = el.href();
470                 navigateTo(link_url);
471                 ev.preventDefault();
472             }
473         }
474     }
475 
476     Any onPopState(Event ev)
477     {
478         import libwasm.dom : document;
479         import libwasm.bindings.Location;
480         import libwasm.bindings.Window;
481 
482         auto scoped = ScopedPool(m_pool);
483         navigateTo(document().location().front.pathname());
484         return Any.init;
485     }
486 
487 }
488 
489 void registerRoutes(T)(auto ref T t) @trusted
490 {
491     import std.meta : AliasSeq;
492     import std.traits : hasUDA, isCallable, getUDAs;
493     import libwasm.dom : compile;
494 
495     static foreach (i; __traits(allMembers, T))
496     {
497         {
498             alias sym = __traits(getMember, t, i);
499             static if (isPointer!(typeof(sym)))
500                 alias ChildType = PointerTarget!(typeof(sym));
501             else
502                 alias ChildType = typeof(sym);
503             enum isPublic = __traits(getProtection, sym) == "public";
504             static if (hasUDA!(sym, entering))
505             {
506                 alias udas = getUDAs!(sym, entering);
507                 static foreach (uda; udas)
508                 {
509                     static if (is(uda : entering!path, string path))
510                     {
511                         // callMember behind the scenes
512                         g_router.register!path(cast(Optional!(Promise!void) delegate(
513                                 ref RouterEvent ev) nothrow @safe)&__traits(getMember, t, i), Direction
514                                 .Entering);
515                     }
516                 }
517             }
518             else static if (hasUDA!(sym, leaving))
519             {
520                 alias udas = getUDAs!(sym, leaving);
521                 static foreach (uda; udas)
522                 {
523                     static if (is(uda : leaving!path, string path))
524                     {
525                         // callMember behind the scenes
526                         g_router.register!path(cast(Optional!(Promise!void) delegate(
527                                 ref RouterEvent ev) nothrow @safe)&__traits(getMember, t, i), Direction
528                                 .Leaving);
529                     }
530                 }
531             }
532             /*
533         else static if (hasUDA!(sym, always)) {
534             alias udas = getUDAs!(field, always);
535             static foreach (uda; udas) {
536                 static if (is(uda : always!path, string path)) {
537                     // callMember behind the scenes
538                     g_router.register!path(__traits(getMember, t, sym), Direction.Always);
539                 }
540             }
541         }*/
542             else static if (isPublic && isAggregateType!ChildType && hasUDA!(sym, child) && !isCallable!(
543                     typeof(sym)))
544             {
545                 static if (isPointer!(typeof(sym)))
546                     registerRoutes(*__traits(getMember, t, i));
547                 else
548                     registerRoutes(__traits(getMember, t, i));
549             }
550         }
551     }
552 }
553 
554 void setupRouter()()
555 {
556     g_router = new URLRouter();
557 }
558 
559 URLRouter router()
560 {
561     return g_router;
562 }
563 
564 private URLRouter g_router;