From b64a9b7dc7ceb40db5a268041ce5e83d8e847076 Mon Sep 17 00:00:00 2001 From: tamura shingo Date: Sun, 26 Apr 2026 01:00:38 +0000 Subject: [PATCH 1/4] Add action-based routing for RESTful controller dispatch Enable routing by action name and HTTP method, allowing a single controller to handle multiple actions (index, show, new, etc.) instead of requiring separate controllers per path. Routes without :action/:method continue to use do-get/do-post dispatch for backward compatibility. --- src/controller/base-controller.lisp | 42 ++++++++---- src/middleware/clails-middleware.lisp | 98 ++++++++++++++++++--------- test/controller/base-controller.lisp | 18 ++--- 3 files changed, 106 insertions(+), 52 deletions(-) diff --git a/src/controller/base-controller.lisp b/src/controller/base-controller.lisp index 3095934..e036e11 100644 --- a/src/controller/base-controller.lisp +++ b/src/controller/base-controller.lisp @@ -55,7 +55,10 @@ :documentation "HTTP response headers as plist") (params :initform (make-hash-table :test 'equal) :reader params - :documentation "Request parameters hash table")) + :documentation "Request parameters hash table") + (action :initform nil + :reader action + :documentation "Action name to be called (e.g., \"index\", \"show\")")) (:documentation "Base controller class for handling HTTP requests.")) (defmethod initialize-instance :after ((c ) &rest initargs) @@ -274,22 +277,37 @@ "Global routing table holding compiled route scanners.") -(defun path-controller (path) - "Find matching controller for the given request path. +(defun path-controller (path http-method) + "Find matching controller for the given request path and HTTP method. - Iterates through *router* and returns the first route that matches the path, - including captured URL parameters. + Matches routes based on: + 1. Path and HTTP method (highest priority) + 2. Path only (when :method not specified in route) @param path [string] Request path to match - @return [plist] Route information with :scanner, :controller, :parameters, etc. + @param http-method [keyword] HTTP method (:get, :post, :put, :delete) + @return [plist] Route information with :scanner, :controller, :action, :parameters, etc. @return [nil] NIL if no matching route found " - (loop for r in *router* - do (multiple-value-bind (match regs) - (ppcre:scan-to-strings (getf r :scanner) path) - (when match - (return (append r - `(:parameters ,regs))))))) + (let ((exact-match nil) + (path-only-match nil)) + + (loop for r in *router* + do (multiple-value-bind (match regs) + (ppcre:scan-to-strings (getf r :scanner) path) + (when match + (let ((route-method (getf r :method))) + (cond + ;; Exact match: path and method + ((and route-method (eq route-method http-method)) + (setf exact-match (append r `(:parameters ,regs))) + (return)) + ;; Path-only match: no method specified + ((null route-method) + (unless path-only-match + (setf path-only-match (append r `(:parameters ,regs)))))))))) + + (or exact-match path-only-match))) (defun initialize-routing-tables () diff --git a/src/middleware/clails-middleware.lisp b/src/middleware/clails-middleware.lisp index e489cd6..fb9a666 100644 --- a/src/middleware/clails-middleware.lisp +++ b/src/middleware/clails-middleware.lisp @@ -2,7 +2,8 @@ (defpackage #:clails/middleware/clails-middleware (:use #:cl) (:import-from #:lack/request - #:request-method) + #:request-method + #:request-path-info) (:import-from #:clails/controller/base-controller #:path-controller #:do-get @@ -36,47 +37,76 @@ (lambda (env) (handler-case (let* ((controller (make-controller env)) - (method (request-method (request controller)))) + (method (request-method (request controller))) + (action (slot-value controller 'clails/controller/base-controller::action))) (when (log-level-enabled-p :trace) (log-package.trace (format nil "Request path: ~A" (getf env :path-info)))) - (cond ((eq method :get) - (when (log-level-enabled-p :trace) - (log-package.trace "Dispatching to do-get")) - (do-get controller)) - ((or (eq method :put) - (and (eq method :post) - (string-equal "put" - (gethash "_method" (params controller))))) - (when (log-level-enabled-p :trace) - (log-package.trace "Dispatching to do-put")) - (do-put controller)) - ((or (eq method :delete) - (and (eq method :post) - (string-equal "delete" - (gethash "_method" (params controller))))) - (when (log-level-enabled-p :trace) - (log-package.trace "Dispatching to do-delete")) - (do-delete controller)) - ((eq method :post) - (when (log-level-enabled-p :trace) - (log-package.trace "Dispatching to do-post")) - (do-post controller)) - (t - (when (log-level-enabled-p :trace) - (log-package.trace (format nil "Unsupported method: ~A" method))) - nil)) + + ;; If action is specified, call the action method + (if action + (progn + (when (log-level-enabled-p :trace) + (log-package.trace (format nil "Dispatching to action: ~A" action))) + (call-action controller action)) + ;; Otherwise, dispatch to do-* methods (backward compatibility) + (progn + (cond ((eq method :get) + (when (log-level-enabled-p :trace) + (log-package.trace "Dispatching to do-get")) + (do-get controller)) + ((or (eq method :put) + (and (eq method :post) + (string-equal "put" + (gethash "_method" (params controller))))) + (when (log-level-enabled-p :trace) + (log-package.trace "Dispatching to do-put")) + (do-put controller)) + ((or (eq method :delete) + (and (eq method :post) + (string-equal "delete" + (gethash "_method" (params controller))))) + (when (log-level-enabled-p :trace) + (log-package.trace "Dispatching to do-delete")) + (do-delete controller)) + ((eq method :post) + (when (log-level-enabled-p :trace) + (log-package.trace "Dispatching to do-post")) + (do-post controller)) + (t + (when (log-level-enabled-p :trace) + (log-package.trace (format nil "Unsupported method: ~A" method))) + nil)))) (resolve-view controller)) (404/not-found (c) (funcall app env))))) "Lack middleware function for Clails controller routing and dispatching.") +(defun call-action (controller action-name) + "Call the specified action method on the controller. + + @param controller [] Controller instance + @param action-name [string] Action name (e.g., \"index\", \"show\") + @condition 404/not-found When action method does not exist + " + (let* ((action-symbol (intern (string-upcase action-name) + (symbol-package (class-name (class-of controller))))) + (method (find-method (symbol-function action-symbol) + nil + (list (class-of controller)) + nil))) + (unless method + (error '404/not-found + :path (lack/request:request-path-info (request controller)))) + (funcall action-symbol controller))) + + (defun make-controller (env) "Create and initialize controller instance for the request. - Finds the appropriate controller based on the request path, + Finds the appropriate controller based on the request path and HTTP method, initializes it with request parameters (both from URL and path), - and sets up the request and environment. + and sets up the request, environment, and action. @param env [plist] Lack environment containing request information @return [] Initialized controller instance @@ -84,7 +114,8 @@ " (let* ((path-info (getf env :path-info)) (req (lack/request:make-request env)) - (controller-info (path-controller path-info))) + (http-method (lack/request:request-method req)) + (controller-info (path-controller path-info http-method))) (when (null controller-info) (error '404/not-found :path path-info)) @@ -100,6 +131,11 @@ for idx from 0 do (setf (gethash path-param param-hash) (aref (getf controller-info :parameters) idx))) + ;; Set action if specified in route + (let ((action (getf controller-info :action))) + (when action + (setf (slot-value controller 'clails/controller/base-controller::action) action))) + (setf (slot-value controller 'clails/controller/base-controller::request) req) (setf (slot-value controller 'clails/controller/base-controller::env) env) diff --git a/test/controller/base-controller.lisp b/test/controller/base-controller.lisp index 9165293..87ec0cd 100644 --- a/test/controller/base-controller.lisp +++ b/test/controller/base-controller.lisp @@ -56,13 +56,13 @@ (deftest path-controller-test - (let ((result (clails/controller/base-controller::path-controller "/"))) + (let ((result (clails/controller/base-controller::path-controller "/" :get))) (ok result) (ok (string= (getf result :controller) "clails-test/controller/base-controller::")) (ok (= (length (getf result :parameters)) 0))) - (let ((result (clails/controller/base-controller::path-controller "/blog/123"))) + (let ((result (clails/controller/base-controller::path-controller "/blog/123" :get))) (ok result) (ok (string= (getf result :controller) "clails-test/controller/base-controller::")) @@ -71,7 +71,7 @@ (ok (string= (aref (getf result :parameters) 0) "123"))) - (let ((result (clails/controller/base-controller::path-controller "/blog/abc/comment/def"))) + (let ((result (clails/controller/base-controller::path-controller "/blog/abc/comment/def" :get))) (ok result) (ok (string= (getf result :controller) "clails-test/controller/base-controller::")) @@ -254,9 +254,9 @@ :scanner "^/spa/.*$"))) (*routing-tables* test-tables)) (initialize-routing-tables) - (let ((result1 (clails/controller/base-controller::path-controller "/spa/")) - (result2 (clails/controller/base-controller::path-controller "/spa/home")) - (result3 (clails/controller/base-controller::path-controller "/spa/users/123"))) + (let ((result1 (clails/controller/base-controller::path-controller "/spa/" :get)) + (result2 (clails/controller/base-controller::path-controller "/spa/home" :get)) + (result3 (clails/controller/base-controller::path-controller "/spa/users/123" :get))) (ok result1) (ok (string= (getf result1 :controller) "clails-test/controller/base-controller::")) @@ -274,7 +274,7 @@ :keys ("filepath")))) (*routing-tables* test-tables)) (initialize-routing-tables) - (let ((result (clails/controller/base-controller::path-controller "/static/images/logo.png"))) + (let ((result (clails/controller/base-controller::path-controller "/static/images/logo.png" :get))) (ok result) (ok (string= (getf result :controller) "clails-test/controller/base-controller::")) @@ -287,8 +287,8 @@ :generate-scanner ,#'generate-numeric-id-scanner))) (*routing-tables* test-tables)) (initialize-routing-tables) - (let ((result1 (clails/controller/base-controller::path-controller "/users/123")) - (result2 (clails/controller/base-controller::path-controller "/users/abc"))) + (let ((result1 (clails/controller/base-controller::path-controller "/users/123" :get)) + (result2 (clails/controller/base-controller::path-controller "/users/abc" :get))) (ok result1) (ok (string= (getf result1 :controller) "clails-test/controller/base-controller::")) From 2400b03121dd5a12c470d3f22b63ed8a3a21fba4 Mon Sep 17 00:00:00 2001 From: tamura shingo Date: Wed, 13 May 2026 21:45:49 +0000 Subject: [PATCH 2/4] Add resources function for RESTful route generation Introduce a resources helper that generates 7 standard RESTful routes (index, new, create, show, edit, update, destroy) from a resource name and controller, enabling concise route definitions like (resources "todos" "myapp/controllers/todo-controller:"). Routes are ordered so /new is matched before /:id. --- src/controller/base-controller.lisp | 24 ++++++- test/controller/base-controller.lisp | 100 +++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/controller/base-controller.lisp b/src/controller/base-controller.lisp index e036e11..47539ba 100644 --- a/src/controller/base-controller.lisp +++ b/src/controller/base-controller.lisp @@ -37,7 +37,8 @@ #:param #:initialize-routing-tables #:create-scanner-from-uri-path - #:path-controller)) + #:path-controller + #:resources)) (in-package #:clails/controller/base-controller) @@ -440,3 +441,24 @@ keys) (%create-scanner-normal path (1+ pos) len scanner keys)))))) + +(defun resources (name controller) + "Generate 7 standard RESTful route entries for a resource. + + @param name [string] Resource name (e.g., \"todos\") + @param controller [string] Fully qualified controller class (e.g., \"myapp/controllers/todo-controller:\") + @return [list] List of 7 route plists for index, new, create, show, edit, update, destroy + " + (let ((collection-path (format nil "/~A" name)) + (new-path (format nil "/~A/new" name)) + (member-path (format nil "/~A/:id" name)) + (edit-path (format nil "/~A/:id/edit" name))) + (list + (list :path collection-path :controller controller :action "index" :method :get) + (list :path new-path :controller controller :action "new" :method :get) + (list :path collection-path :controller controller :action "create" :method :post) + (list :path member-path :controller controller :action "show" :method :get) + (list :path edit-path :controller controller :action "edit" :method :get) + (list :path member-path :controller controller :action "update" :method :put) + (list :path member-path :controller controller :action "destroy" :method :delete)))) + diff --git a/test/controller/base-controller.lisp b/test/controller/base-controller.lisp index 87ec0cd..46c96f7 100644 --- a/test/controller/base-controller.lisp +++ b/test/controller/base-controller.lisp @@ -297,3 +297,103 @@ (ok (null result2)))))) +(deftest resources-test + (testing "generates 7 RESTful routes" + (let ((routes (resources "todos" "test::"))) + (ok (= (length routes) 7)))) + + (testing "routes have correct path, action, and method" + (let ((routes (resources "todos" "test::"))) + ;; index + (let ((r (nth 0 routes))) + (ok (string= (getf r :path) "/todos")) + (ok (string= (getf r :action) "index")) + (ok (eq (getf r :method) :get))) + ;; new (before :id routes) + (let ((r (nth 1 routes))) + (ok (string= (getf r :path) "/todos/new")) + (ok (string= (getf r :action) "new")) + (ok (eq (getf r :method) :get))) + ;; create + (let ((r (nth 2 routes))) + (ok (string= (getf r :path) "/todos")) + (ok (string= (getf r :action) "create")) + (ok (eq (getf r :method) :post))) + ;; show + (let ((r (nth 3 routes))) + (ok (string= (getf r :path) "/todos/:id")) + (ok (string= (getf r :action) "show")) + (ok (eq (getf r :method) :get))) + ;; edit + (let ((r (nth 4 routes))) + (ok (string= (getf r :path) "/todos/:id/edit")) + (ok (string= (getf r :action) "edit")) + (ok (eq (getf r :method) :get))) + ;; update + (let ((r (nth 5 routes))) + (ok (string= (getf r :path) "/todos/:id")) + (ok (string= (getf r :action) "update")) + (ok (eq (getf r :method) :put))) + ;; destroy + (let ((r (nth 6 routes))) + (ok (string= (getf r :path) "/todos/:id")) + (ok (string= (getf r :action) "destroy")) + (ok (eq (getf r :method) :delete))))) + + (testing "all routes reference the same controller" + (let ((routes (resources "blogs" "myapp::"))) + (dolist (r routes) + (ok (string= (getf r :controller) "myapp::")))))) + + +(defclass () + ()) + +(deftest resources-routing-integration-test + (testing "resources routes are correctly matched by path-controller" + (let* ((*routing-tables* (resources "todos" "clails-test/controller/base-controller::")) + (clails/controller/base-controller::*router* nil)) + (initialize-routing-tables) + + ;; GET /todos -> index + (let ((result (clails/controller/base-controller::path-controller "/todos" :get))) + (ok result) + (ok (string= (getf result :action) "index"))) + + ;; GET /todos/new -> new (not matched as :id) + (let ((result (clails/controller/base-controller::path-controller "/todos/new" :get))) + (ok result) + (ok (string= (getf result :action) "new"))) + + ;; POST /todos -> create + (let ((result (clails/controller/base-controller::path-controller "/todos" :post))) + (ok result) + (ok (string= (getf result :action) "create"))) + + ;; GET /todos/123 -> show with params + (let ((result (clails/controller/base-controller::path-controller "/todos/123" :get))) + (ok result) + (ok (string= (getf result :action) "show")) + (ok (= (length (getf result :parameters)) 1)) + (ok (string= (aref (getf result :parameters) 0) "123"))) + + ;; GET /todos/123/edit -> edit + (let ((result (clails/controller/base-controller::path-controller "/todos/123/edit" :get))) + (ok result) + (ok (string= (getf result :action) "edit")) + (ok (string= (aref (getf result :parameters) 0) "123"))) + + ;; PUT /todos/123 -> update + (let ((result (clails/controller/base-controller::path-controller "/todos/123" :put))) + (ok result) + (ok (string= (getf result :action) "update")) + (ok (string= (aref (getf result :parameters) 0) "123"))) + + ;; DELETE /todos/123 -> destroy + (let ((result (clails/controller/base-controller::path-controller "/todos/123" :delete))) + (ok result) + (ok (string= (getf result :action) "destroy")) + (ok (string= (aref (getf result :parameters) 0) "123"))) + + ;; GET /nonexistent -> nil + (ok (null (clails/controller/base-controller::path-controller "/nonexistent" :get)))))) From 366e6626a7d10611b5d34f9210a8150336c2ff37 Mon Sep 17 00:00:00 2001 From: tamura shingo Date: Wed, 13 May 2026 21:52:54 +0000 Subject: [PATCH 3/4] Add :only and :except options to resources function Allow selective route generation by specifying which actions to include or exclude, similar to Rails' resources routing options. Example: (resources todos controller :except '(:destroy)) --- src/controller/base-controller.lisp | 44 ++++++++++++++++++---------- test/controller/base-controller.lisp | 23 ++++++++++++++- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/src/controller/base-controller.lisp b/src/controller/base-controller.lisp index 47539ba..c7fb32a 100644 --- a/src/controller/base-controller.lisp +++ b/src/controller/base-controller.lisp @@ -442,23 +442,37 @@ (%create-scanner-normal path (1+ pos) len scanner keys)))))) -(defun resources (name controller) - "Generate 7 standard RESTful route entries for a resource. +(defun resources (name controller &key only except) + "Generate RESTful route entries for a resource. + + By default, generates 7 standard routes: index, new, create, show, edit, update, destroy. + Use :only to include specific actions, or :except to exclude specific actions. @param name [string] Resource name (e.g., \"todos\") @param controller [string] Fully qualified controller class (e.g., \"myapp/controllers/todo-controller:\") - @return [list] List of 7 route plists for index, new, create, show, edit, update, destroy + @param only [list] List of action keywords to include (e.g., '(:index :show)) + @param except [list] List of action keywords to exclude (e.g., '(:destroy)) + @return [list] List of route plists " - (let ((collection-path (format nil "/~A" name)) - (new-path (format nil "/~A/new" name)) - (member-path (format nil "/~A/:id" name)) - (edit-path (format nil "/~A/:id/edit" name))) - (list - (list :path collection-path :controller controller :action "index" :method :get) - (list :path new-path :controller controller :action "new" :method :get) - (list :path collection-path :controller controller :action "create" :method :post) - (list :path member-path :controller controller :action "show" :method :get) - (list :path edit-path :controller controller :action "edit" :method :get) - (list :path member-path :controller controller :action "update" :method :put) - (list :path member-path :controller controller :action "destroy" :method :delete)))) + (let* ((collection-path (format nil "/~A" name)) + (new-path (format nil "/~A/new" name)) + (member-path (format nil "/~A/:id" name)) + (edit-path (format nil "/~A/:id/edit" name)) + (all-routes + (list + (list :name :index :path collection-path :controller controller :action "index" :method :get) + (list :name :new :path new-path :controller controller :action "new" :method :get) + (list :name :create :path collection-path :controller controller :action "create" :method :post) + (list :name :show :path member-path :controller controller :action "show" :method :get) + (list :name :edit :path edit-path :controller controller :action "edit" :method :get) + (list :name :update :path member-path :controller controller :action "update" :method :put) + (list :name :destroy :path member-path :controller controller :action "destroy" :method :delete)))) + (mapcar (lambda (route) + (let ((r (copy-list route))) + (remf r :name) + r)) + (cond + (only (remove-if-not (lambda (r) (member (getf r :name) only)) all-routes)) + (except (remove-if (lambda (r) (member (getf r :name) except)) all-routes)) + (t all-routes))))) diff --git a/test/controller/base-controller.lisp b/test/controller/base-controller.lisp index 46c96f7..3b20663 100644 --- a/test/controller/base-controller.lisp +++ b/test/controller/base-controller.lisp @@ -343,7 +343,28 @@ (testing "all routes reference the same controller" (let ((routes (resources "blogs" "myapp::"))) (dolist (r routes) - (ok (string= (getf r :controller) "myapp::")))))) + (ok (string= (getf r :controller) "myapp::"))))) + + (testing ":only filters to specified actions" + (let ((routes (resources "todos" "test::" :only '(:index :show)))) + (ok (= (length routes) 2)) + (ok (string= (getf (nth 0 routes) :action) "index")) + (ok (string= (getf (nth 1 routes) :action) "show")))) + + (testing ":except excludes specified actions" + (let ((routes (resources "todos" "test::" :except '(:destroy :update)))) + (ok (= (length routes) 5)) + (ok (every (lambda (r) (not (member (getf r :action) '("destroy" "update") :test #'string=))) routes)))) + + (testing ":only with single action" + (let ((routes (resources "todos" "test::" :only '(:index)))) + (ok (= (length routes) 1)) + (ok (string= (getf (nth 0 routes) :action) "index")))) + + (testing "generated routes do not contain :name key" + (let ((routes (resources "todos" "test::"))) + (dolist (r routes) + (ok (null (getf r :name))))))) (defclass () From 26a3c33d552830907a9801607d592314828a6d87 Mon Sep 17 00:00:00 2001 From: tamura shingo Date: Wed, 13 May 2026 22:00:51 +0000 Subject: [PATCH 4/4] update document --- document/controller.md | 132 +++++++++++++++++++++++++++++++++-- document/controller_ja.md | 140 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 261 insertions(+), 11 deletions(-) diff --git a/document/controller.md b/document/controller.md index 11c010d..2288a0a 100644 --- a/document/controller.md +++ b/document/controller.md @@ -8,6 +8,8 @@ Controllers receive HTTP requests, process data using Models, and pass data to V ## Basic Concepts - Controllers define processing for each HTTP method (GET, POST, PUT, DELETE) +- Action-based routing allows defining multiple actions (index, show, new, edit, etc.) in a single Controller +- The `resources` function provides concise RESTful route definitions - There are `` for web applications and `` for REST APIs - Routing tables bind URL paths to Controllers - URL parameters are automatically extracted and accessible from the Controller @@ -173,6 +175,126 @@ Define routes in `config/routes.lisp` or similar. )) ``` +### Action-Based Routing + +By specifying `:action` and `:method` in a route, you can define multiple action methods in a single Controller. + +```common-lisp +(setf clails/environment:*routing-tables* + '((:path "/todos" + :controller "your-app/controllers/todo-controller::" + :action "index" + :method :get) + (:path "/todos/:id" + :controller "your-app/controllers/todo-controller::" + :action "show" + :method :get) + (:path "/todos/new" + :controller "your-app/controllers/todo-controller::" + :action "new" + :method :get))) +``` + +In the corresponding Controller, define action-named methods instead of `do-get`. + +```common-lisp +(defclass () + ()) + +(defmethod index ((controller )) + (set-view controller "todos/index.html")) + +(defmethod show ((controller )) + (let ((id (param controller "id"))) + (set-view controller "todos/show.html" `(:id ,id)))) + +(defmethod new ((controller )) + (set-view controller "todos/new.html")) +``` + +**Route matching priority:** + +1. Routes where both path and HTTP method match (highest priority) +2. Routes where only the path matches and `:method` is not specified +3. No match → 404 + +**Backward compatibility:** Routes without `:action` continue to dispatch to `do-get`, `do-post`, etc. as before. + +### RESTful Routing with the `resources` Function + +The `resources` function lets you define 7 standard RESTful routes in a single line. + +```common-lisp +(setf clails/environment:*routing-tables* + `(,@(resources "todos" "your-app/controllers/todo-controller::") + ,@(resources "blogs" "your-app/controllers/blog-controller::"))) +``` + +`(resources "todos" "controller")` generates the following 7 routes: + +| HTTP Method | Path | Action | Purpose | +|---|---|---|---| +| GET | /todos | index | List | +| GET | /todos/new | new | New form | +| POST | /todos | create | Create | +| GET | /todos/:id | show | Detail | +| GET | /todos/:id/edit | edit | Edit form | +| PUT | /todos/:id | update | Update | +| DELETE | /todos/:id | destroy | Delete | + +`/todos/new` is placed before `/todos/:id`, so "new" will never be matched as an `:id` parameter. + +#### `:only` Option — Generate Only Specific Actions + +```common-lisp +;; Generate only index and show +(resources "todos" "controller" :only '(:index :show)) +``` + +#### `:except` Option — Exclude Specific Actions + +```common-lisp +;; Exclude destroy and update +(resources "todos" "controller" :except '(:destroy :update)) +``` + +#### Controller Implementation Example with `resources` + +```common-lisp +(defclass () + ()) + +(defmethod index ((controller )) + (let ((todos (get-all-todos))) + (set-view controller "todos/index.html" `(:todos ,todos)))) + +(defmethod show ((controller )) + (let ((todo (find-todo (param controller "id")))) + (set-view controller "todos/show.html" `(:todo ,todo)))) + +(defmethod new ((controller )) + (set-view controller "todos/new.html")) + +(defmethod create ((controller )) + (let ((title (param controller "title"))) + (create-todo title) + (set-redirect controller "/todos"))) + +(defmethod edit ((controller )) + (let ((todo (find-todo (param controller "id")))) + (set-view controller "todos/edit.html" `(:todo ,todo)))) + +(defmethod update ((controller )) + (let ((id (param controller "id")) + (title (param controller "title"))) + (update-todo id title) + (set-redirect controller (format nil "/todos/~A" id)))) + +(defmethod destroy ((controller )) + (destroy-todo (param controller "id")) + (set-redirect controller "/todos")) +``` + --- ## 4. Retrieving Request Parameters @@ -656,9 +778,11 @@ REST APIs should follow RESTful design principles. clails Controllers have the following features: 1. **Simple Design**: Just define methods for each HTTP method -2. **Flexible Routing**: Automatic URL parameter extraction and pattern matching -3. **View Integration**: Easy view rendering with `set-view` -4. **REST API Support**: Return JSON responses with `` -5. **Transaction Support**: Transaction management in coordination with Models +2. **RESTful Routing**: Rails-like route definitions with the `resources` function (supports `:only` / `:except` options) +3. **Flexible Routing**: Automatic URL parameter extraction and pattern matching +4. **View Integration**: Easy view rendering with `set-view` +5. **REST API Support**: Return JSON responses with `` +6. **Transaction Support**: Transaction management in coordination with Models +7. **Backward Compatibility**: Traditional `do-get` / `do-post` style and new action-based style can coexist For detailed API reference, please refer to the docstring of each function. diff --git a/document/controller_ja.md b/document/controller_ja.md index 38d5457..bea6910 100644 --- a/document/controller_ja.md +++ b/document/controller_ja.md @@ -8,6 +8,8 @@ Controller は HTTP リクエストを受け取り、Model を使ってデータ ## 基本概念 - Controller は HTTP メソッド(GET、POST、PUT、DELETE)ごとに処理を定義します +- アクションベースのルーティングにより、1つの Controller で複数のアクション(index, show, new, edit など)を定義できます +- `resources` 関数を使って、RESTful なルーティングを簡潔に定義できます - Web アプリケーション用の `` と REST API 用の `` があります - ルーティングテーブルで URL パスと Controller を紐付けます - URL パラメータは自動的に抽出され、Controller からアクセスできます @@ -173,6 +175,126 @@ URL パス内の `:parameter-name` は自動的に抽出され、`param` 関数 )) ``` +### アクションベースのルーティング + +ルートに `:action` と `:method` を指定することで、1つの Controller に複数のアクションメソッドを定義できます。 + +```common-lisp +(setf clails/environment:*routing-tables* + '((:path "/todos" + :controller "your-app/controllers/todo-controller::" + :action "index" + :method :get) + (:path "/todos/:id" + :controller "your-app/controllers/todo-controller::" + :action "show" + :method :get) + (:path "/todos/new" + :controller "your-app/controllers/todo-controller::" + :action "new" + :method :get))) +``` + +対応する Controller では、`do-get` の代わりにアクション名のメソッドを定義します。 + +```common-lisp +(defclass () + ()) + +(defmethod index ((controller )) + (set-view controller "todos/index.html")) + +(defmethod show ((controller )) + (let ((id (param controller "id"))) + (set-view controller "todos/show.html" `(:id ,id)))) + +(defmethod new ((controller )) + (set-view controller "todos/new.html")) +``` + +**ルートマッチングの優先順位:** + +1. パスとHTTPメソッドが両方一致するルート(最優先) +2. パスのみ一致し、`:method` が指定されていないルート +3. マッチなし → 404 + +**後方互換性:** `:action` が指定されていないルートは、従来通り `do-get`、`do-post` 等にディスパッチされます。 + +### `resources` 関数による RESTful ルーティング + +`resources` 関数を使うと、RESTful な7つの標準ルートを1行で定義できます。 + +```common-lisp +(setf clails/environment:*routing-tables* + `(,@(resources "todos" "your-app/controllers/todo-controller::") + ,@(resources "blogs" "your-app/controllers/blog-controller::"))) +``` + +`(resources "todos" "controller")` は以下の7ルートを生成します。 + +| HTTP メソッド | パス | アクション | 用途 | +|---|---|---|---| +| GET | /todos | index | 一覧表示 | +| GET | /todos/new | new | 作成フォーム表示 | +| POST | /todos | create | 作成実行 | +| GET | /todos/:id | show | 詳細表示 | +| GET | /todos/:id/edit | edit | 編集フォーム表示 | +| PUT | /todos/:id | update | 更新実行 | +| DELETE | /todos/:id | destroy | 削除実行 | + +`/todos/new` は `/todos/:id` より前に配置されるため、"new" が `:id` パラメータとしてマッチすることはありません。 + +#### `:only` オプション — 特定のアクションのみ生成 + +```common-lisp +;; index と show のみ生成 +(resources "todos" "controller" :only '(:index :show)) +``` + +#### `:except` オプション — 特定のアクションを除外 + +```common-lisp +;; destroy と update を除外 +(resources "todos" "controller" :except '(:destroy :update)) +``` + +#### `resources` を使った Controller の実装例 + +```common-lisp +(defclass () + ()) + +(defmethod index ((controller )) + (let ((todos (get-all-todos))) + (set-view controller "todos/index.html" `(:todos ,todos)))) + +(defmethod show ((controller )) + (let ((todo (find-todo (param controller "id")))) + (set-view controller "todos/show.html" `(:todo ,todo)))) + +(defmethod new ((controller )) + (set-view controller "todos/new.html")) + +(defmethod create ((controller )) + (let ((title (param controller "title"))) + (create-todo title) + (set-redirect controller "/todos"))) + +(defmethod edit ((controller )) + (let ((todo (find-todo (param controller "id")))) + (set-view controller "todos/edit.html" `(:todo ,todo)))) + +(defmethod update ((controller )) + (let ((id (param controller "id")) + (title (param controller "title"))) + (update-todo id title) + (set-redirect controller (format nil "/todos/~A" id)))) + +(defmethod destroy ((controller )) + (destroy-todo (param controller "id")) + (set-redirect controller "/todos")) +``` + --- ## 4. リクエストパラメータの取得 @@ -367,11 +489,12 @@ View パスから自動的にパッケージ名が解決されます。 ### リクエスト処理の流れ -1. **ルーティング**: URL パスから対応する Controller を検索 +1. **ルーティング**: URL パスと HTTP メソッドから対応する Controller を検索 2. **インスタンス化**: Controller のインスタンスを作成 3. **パラメータ設定**: URL パラメータ、クエリパラメータ、POST データを設定 -4. **メソッド呼び出し**: HTTP メソッドに応じて `do-get`、`do-post` などを呼び出し -5. **レスポンス生成**: View のレンダリング、またはレスポンスデータの返却 +4. **アクション設定**: ルートに `:action` が指定されていれば、Controller の `action` スロットにセット +5. **メソッド呼び出し**: アクションが指定されていればアクションメソッドを、そうでなければ `do-get`、`do-post` などを呼び出し +6. **レスポンス生成**: View のレンダリング、またはレスポンスデータの返却 ### Controller のスロット @@ -384,6 +507,7 @@ Controller インスタンスには以下のスロットがあります。 - `code` - HTTP ステータスコード(デフォルト: 200) - `header` - HTTP レスポンスヘッダー(plist) - `params` - リクエストパラメータ(ハッシュテーブル) +- `action` - アクション名(例: "index"、"show")。ルートに `:action` が指定された場合にセットされる #### `` の追加スロット @@ -662,9 +786,11 @@ REST API は RESTful な設計原則に従います。 clails の Controller は以下の特徴を持ちます。 1. **シンプルな設計**: HTTP メソッドごとにメソッドを定義するだけ -2. **柔軟なルーティング**: URL パラメータの自動抽出とパターンマッチング -3. **View の統合**: `set-view` による簡単な View レンダリング -4. **REST API サポート**: `` による JSON レスポンスの返却 -5. **トランザクション対応**: Model と連携したトランザクション管理 +2. **RESTful ルーティング**: `resources` 関数による Rails ライクなルート定義(`:only` / `:except` オプション対応) +3. **柔軟なルーティング**: URL パラメータの自動抽出とパターンマッチング +4. **View の統合**: `set-view` による簡単な View レンダリング +5. **REST API サポート**: `` による JSON レスポンスの返却 +6. **トランザクション対応**: Model と連携したトランザクション管理 +7. **後方互換性**: 従来の `do-get` / `do-post` 方式と新しいアクション方式が共存可能 詳細な API リファレンスについては、各関数の docstring を参照してください。