Commits

Robert Clipsham committed f0a87ce

Routing for actions now works.
Action lookup now happens during initialisation rather than for each request,
bringing speed back to how it was originally for all cases.

Comments (0)

Files changed (4)

 
     /// Set up routing
     Router.addRoutes([
-                        "/"[]                           : "example/Home"[],
-                        "/[plugin]"                     : "[plugin]/Default",
-                        "/[plugin]/[controller]"        : "[plugin]/[controller]",
-                        "/[plugin]/[controller]/[args]" : "[plugin]/[controller]/[args]"
+                        "/"[]                                    : "example/Home/Default"[],
+                        "/[plugin]"                              : "[plugin]/Default/Default",
+                        "/[plugin]/[controller]"                 : "[plugin]/[controller]/Default",
+                        "/[plugin]/[controller]/[action]"        : "[plugin]/[controller]/[action]",
+                        "/[plugin]/[controller]/[action]/[args]" : "[plugin]/[controller]/[action]/[args]"
                     ]);
     Router.errorRoute("example", "Error");
 
 
 $DC -unittest $(echo $ARGS | perl -pe 's/-c//g') bootstrap.d controllers.d $OD.obj ${OF}bin/serenity.fcgi -L-Llib -L-lserenity-example -L-lserenity -L-lfcgid -L-lfcgi -Ifcgi || exit 1
 # Speed up symbol look up for actions when not debugging
-expr match "$ARGS" -g
+expr match "$ARGS" -g &>/dev/null
 if (( $? != 0 )); then
     strip -w '-K_D*controllers*view*MFZC8serenity12HtmlDocument12HtmlDocument' bin/serenity.fcgi
 fi

serenity/Controller.d

 import serenity.Response : Headers;
 
 import tango.core.tools.StackTrace : nameOfFunctionAt;
+import tango.text.Unicode : toLower;
+import tango.text.Util : containsPattern;
 
 public import serenity.HtmlDocument;
 
  */
 abstract class Controller
 {
-    private static ClassInfo[char[]] mControllers;
+    private struct Registered
+    {
+        ClassInfo ci;
+        void*[char[]] methods;
+    }
+    private static Registered[char[]] mControllers;
     protected char[][] mArguments;
     private Headers mHeaders;
     private Logger mLog;
     private ushort mResponseCode = 200;
-    private char[] mViewMethod = "viewDefault";
+    private char[] mViewMethod = "viewdefault";
 
     /**
      * Create an instance of the given controller
      * Returns:
      *  An instance of the requested controller
      */
-    public static Controller create(char[] plugin, char[] subClass, char[][] args, ushort code=200)
+    public static Controller create(char[] plugin, char[] subClass, char[] action=null, char[][] args=null, ushort code=200)
     {
         char[] cname = plugin ~ ".controllers." ~ subClass ~ '.' ~ subClass;
-        auto ci = cname in mControllers;
-        if (ci is null)
+        auto registered = cname in mControllers;
+        if (registered is null)
         {
             throw new ControllerNotFoundException("Controller not found: " ~ cname);
         }
-        auto controller = cast(typeof(this))ci.create();
+        auto controller = cast(typeof(this))registered.ci.create();
         controller.mLog = Log.getLogger(cname);
         controller.mArguments = args;
         controller.mHeaders = new Headers;
         controller.mHeaders["Content-Type"] = "text/html; charset=utf-8";
+        controller.mViewMethod = "view" ~ toLower(action);
         controller.setResponseCode(code);
         return controller;
     }
      * Params:
      *  plugin   = Name of the plugin containing the controller
      *  subClass = Name of the controller
+     *  action   = Name of the action in the given controller
      * Returns:
      *  true if the controller exists
      */
-    public static bool exists(char[] plugin, char[] subClass)
+    public static bool exists(char[] plugin, char[] subClass, char[] action="default")
     {
         char[] cname = plugin ~ ".controllers." ~ subClass ~ '.' ~ subClass;
-        auto ci = cname in mControllers;
-        return ci !is null;
+        auto registered = cname in mControllers;
+        if (registered is null)
+        {
+            return false;
+        }
+        foreach (name, ptr; registered.methods)
+        {
+            if (name[$-41-action.length .. $-41] == action)
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Get a list of all the view methods in a controller
+     *
+     * Params:
+     *  ci = ClassInfo for the given class
+     * Throws:
+     *  InvalidControllerException if no methods match
+     * Returns:
+     *  An array of all valid view methods
+     */
+    private static void*[char[]] getViewMethods(ClassInfo ci)
+    {
+        void*[char[]] fns;
+        foreach (ptr; ci.vtbl[6..$])
+        {
+            auto name = nameOfFunctionAt(ptr);
+            // TODO This should be case insensitive and support other Document derivatives
+            if (name.length > 64 &&
+                name[$-41..$] == "MFZC8serenity12HtmlDocument12HtmlDocument" &&
+                name.containsPattern("view"))
+            {
+                fns[toLower(name)] = ptr;
+            }
+        }
+        if (fns.length == 0)
+        {
+            throw new InvalidControllerException("Invalid controller: " ~ ci.name);
+        }
+        return fns;
     }
 
     /**
         {
             throw new InvalidControllerException("Invalid controller: " ~ ci.name);
         }
-        mControllers[ci.name] = ci;
+        mControllers[ci.name] = Registered(ci, getViewMethods(ci));
     }
 
     /**
      *
      * Methods must be named view* (where * is a wildcard), and the binary
      * must not have the relevant symbols stripped. The methods must also not
-     * be final, as this method will fail to function. Note that not stripping
-     * the binary can lead to poor performance of this method, so you can
-     * partially strip the binary by using:
+     * be final, as this method depends on the method existing in the vtable.
+     * You can improve start up time by running the following command:
      * ----
      * $ strip -w \
      * > '-K_D*controllers*view*MFZC8serenity12HtmlDocument12HtmlDocument' \
      * >  bin/serenity.fcgi
      * ----
      * Note that this should not be done for debug builds as stack traces
-     * will be completely useless - for debug builds the performance difference
-     * is negligible anyway.
+     * will be completely useless. You should not run a full strip on the binary
+     * as this will break actions (and thus most of the MVC).
      *
      * Examples:
      * ----
      */
     final public Document view()
     {
-        foreach (i, ptr; this.classinfo.vtbl[6..$])
+        auto registered = *(this.classinfo.name in mControllers);
+        foreach (name, ptr; registered.methods)
         {
-            auto name = nameOfFunctionAt(ptr);
-            if (name.length > 64 &&
-                name[$-41..$] == "MFZC8serenity12HtmlDocument12HtmlDocument" &&
-                name[$-41-mViewMethod.length .. $-41] == mViewMethod)
+            if (name[$-41-mViewMethod.length .. $-41] == mViewMethod)
             {
                 if (log.info) log.info("Calling method HtmlDocument {}() @ {:x#}", mViewMethod, ptr);
-                HtmlDocument delegate() dg;
+                Document delegate() dg;
                 dg.ptr = cast(void*)this;
                 dg.funcptr = cast(typeof(dg.funcptr))ptr;
-                return cast(Document)dg();
+                return dg();
             }
         }
+        throw new ControllerNotFoundException("Action not found: " ~ mViewMethod[4..$]);
     }
 }

serenity/Router.d

         Text,
         Plugin,
         Controller,
+        Action,
         Arguments
     }
     private struct Route
     {
         char[] plugin;
         char[] controller;
+        char[] action;
         char[][] args;
     }
     Part[] mPattern;
                     case "[controller]":
                         mPattern ~= Part.Controller;
                         break;
+                    case "[action]":
+                        mPattern ~= Part.Action;
+                        break;
                     case "[args]":
                         mPattern ~= Part.Arguments;
-                        if (j != pattern.length )
+                        if (j != pattern.length)
                         {
                             throw new RouterException("Invalid router pattern: [args] can only be used at the end of a URL pattern");
                         }
             mPattern = typeof(mPattern).init;
             mText = typeof(mText).init;
 
-            parsePattern("/[plugin]/[controller]/[args]");
-            assert(mPattern.length == 6);
+            parsePattern("/[plugin]/[controller]/[action]/[args]");
+            assert(mPattern.length == 8);
             assert(mPattern[0] == Part.Text);
             assert(mPattern[1] == Part.Plugin);
             assert(mPattern[2] == Part.Text);
             assert(mPattern[3] == Part.Controller);
             assert(mPattern[4] == Part.Text);
-            assert(mPattern[5] == Part.Arguments);
-            assert(mText.length == 3);
-            assert(mText == ["/", "/", "/"]);
+            assert(mPattern[5] == Part.Action);
+            assert(mPattern[6] == Part.Text);
+            assert(mPattern[7] == Part.Arguments);
+            assert(mText.length == 4);
+            assert(mText == ["/", "/", "/", "/"]);
 
             mPattern = typeof(mPattern).init;
             mText = typeof(mText).init;
      * Split the given route into its component parts
      *
      * Params:
-     *  route = Route in the form plugin/controller[/args]
+     *  route = Route in the form plugin/controller/action/[args]
      * TODO:
      *  Some sort of validation? If [foo] is used, is it in the pattern?
      */
     private void parseRoute(char[] route)
     {
         auto arr = split(route, "/");
-        assert(arr.length == 2 || arr.length == 3);
+        assert(arr.length >= 2 && arr.length <= 4);
         mRoute.plugin = mPath.plugin = arr[0];
         mRoute.controller = mPath.controller = arr[1];
+        if (arr.length >= 3)
+        {
+            mRoute.action = mPath.action = arr[2];
+        }
     }
 
     /**
                     i = j;
                     quality += 2;
                     break;
+                case Part.Action:
+                    size_t j = i;
+                    // BUG Needs to support all valid identifiers
+                    while (j < url.length &&
+                           ((url[j] >= 'A' && url[j] <= 'Z') ||
+                            (url[j] >= 'a' && url[j] <= 'z')))
+                    {
+                        j++;
+                    }
+                    if ((partNo != mPattern.length - 1 && j >= url.length) || (j < url.length && partNo == mPattern.length - 1))
+                    {
+                        return 0;
+                    }
+                    if (mRoute.action == "[action]")
+                    {
+                        mPath.action = toLower(url[i..j]);
+                    }
+                    i = j;
+                    quality += 2;
+                    break;
                 case Part.Arguments:
                     mPath.args = split(url[i..$], "/");
                     i++;
             assert(mPath.controller == "World");
             assert(mPath.args == typeof(mPath.args).init);
         }
+        with (new .Route("/hello-[controller]/[action]", "foo/[controller]/[action]"))
+        {
+            assert(!match("/"));
+            assert(!match("/plugin"));
+            assert(!match("/plugin/controller/my/args"));
+            assert(match("/hello-world/moo") == 6);
+            assert(mPath.plugin == "foo");
+            assert(mPath.controller == "World");
+            assert(mPath.action == "moo", mPath.action);
+            assert(mPath.args == typeof(mPath.args).init);
+        }
     }
 }
 
      */
     public static Controller getErrorController(ushort code, char[] error)
     {
-        return Controller.create(mErrorPlugin, mErrorController, [error], code);
+        return Controller.create(mErrorPlugin, mErrorController, "default", [error], code);
     }
 
     /**
         {
             throw new RouterException("No route matching url found: " ~ url);
         }
-        return Controller.create(bestRoute.mPath.plugin, bestRoute.mPath.controller, bestRoute.mPath.args);
+        return Controller.create(bestRoute.mPath.plugin, bestRoute.mPath.controller, bestRoute.mPath.action, bestRoute.mPath.args);
     }
 }