From 989ba63bf137bfcccd2f4a7c2d4ff5a486d3b2db Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Thu, 13 Jun 2024 19:43:02 +0900 Subject: [PATCH 01/10] Use fiveam instead of rove --- .github/workflows/test.yml | 29 +++++++++++++++++++++++++++++ ningle-fbr-test.asd | 7 +++++++ ningle-fbr-tests.asd | 9 --------- ningle-fbr.asd | 2 +- qlfile | 2 +- qlfile.lock | 8 ++++---- tests/.keep | 0 tests/main.lisp | 4 ---- 8 files changed, 42 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 ningle-fbr-test.asd delete mode 100644 ningle-fbr-tests.asd create mode 100644 tests/.keep delete mode 100644 tests/main.lisp diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2c2a54a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: 'test' + +on: + push: + branches: + - 'main' + pull_request: + +jobs: + tests: + runs-on: ubuntu-latest + + strategy: + matrix: + lisp: + - sbcl-bin + - ccl-bin + + env: + LISP: ${{ matrix.lisp }} + + steps: + - uses: actions/checkout@v4 + - uses: 40ants/setup-lisp@v4 + with: + asdf-system: ningle-fbr + - uses: 40ants/run-tests@v2 + with: + asdf-system: ningle-fbr diff --git a/ningle-fbr-test.asd b/ningle-fbr-test.asd new file mode 100644 index 0000000..32ff098 --- /dev/null +++ b/ningle-fbr-test.asd @@ -0,0 +1,7 @@ +(defsystem "ningle-fbr-test" + :defsystem-depends-on ("fiveam-asdf") + :class :package-inferred-fiveam-tester-system + :pathname "tests" + :depends-on () + :test-names () + :num-checks 0) diff --git a/ningle-fbr-tests.asd b/ningle-fbr-tests.asd deleted file mode 100644 index 8bdb5a1..0000000 --- a/ningle-fbr-tests.asd +++ /dev/null @@ -1,9 +0,0 @@ -(defsystem "ningle-fbr-tests" - :author "skyizwhite <paku@skyizwhite.dev>" - :license "MIT" - :class :package-inferred-system - :pathname "tests" - :depends-on ("rove" - "ningle-fbr-tests/main") - :perform (test-op (o c) - (symbol-call :rove '#:run c))) diff --git a/ningle-fbr.asd b/ningle-fbr.asd index 9b2737e..083f795 100644 --- a/ningle-fbr.asd +++ b/ningle-fbr.asd @@ -8,4 +8,4 @@ :class :package-inferred-system :pathname "src" :depends-on ("ningle-fbr/main") - :in-order-to ((test-op (test-op "ningle-fbr-tests")))) + :in-order-to ((test-op (test-op "ningle-fbr-test")))) diff --git a/qlfile b/qlfile index 087ffa1..567dbb8 100644 --- a/qlfile +++ b/qlfile @@ -1,3 +1,3 @@ +ql fiveam-asdf ql ningle ql cl-ppcre -ql rove diff --git a/qlfile.lock b/qlfile.lock index 5634b14..57bf0bf 100644 --- a/qlfile.lock +++ b/qlfile.lock @@ -2,6 +2,10 @@ (:class qlot/source/dist:source-dist :initargs (:distribution "https://beta.quicklisp.org/dist/quicklisp.txt" :%version :latest) :version "2023-10-21")) +("fiveam-asdf" . + (:class qlot/source/ql:source-ql + :initargs (:%version :latest) + :version "ql-2023-10-21")) ("ningle" . (:class qlot/source/ql:source-ql :initargs (:%version :latest) @@ -10,7 +14,3 @@ (:class qlot/source/ql:source-ql :initargs (:%version :latest) :version "ql-2023-10-21")) -("rove" . - (:class qlot/source/ql:source-ql - :initargs (:%version :latest) - :version "ql-2023-10-21")) diff --git a/tests/.keep b/tests/.keep new file mode 100644 index 0000000..e69de29 diff --git a/tests/main.lisp b/tests/main.lisp deleted file mode 100644 index 06f02eb..0000000 --- a/tests/main.lisp +++ /dev/null @@ -1,4 +0,0 @@ -(defpackage #:ningle-fbr-tests/main - (:use #:cl - #:rove)) -(in-package #:ningle-fbr-tests/main) From 7ab7284e88b82fdf31d55187f893e815a551ef38 Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Thu, 13 Jun 2024 21:01:22 +0900 Subject: [PATCH 02/10] Use absolute path instead --- src/main.lisp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.lisp b/src/main.lisp index 0c3632a..995b04d 100644 --- a/src/main.lisp +++ b/src/main.lisp @@ -47,7 +47,7 @@ (mapcar (lambda (pathname) (cons (pathname->url pathname dir-namestring) (pathname->package pathname system-path-namestring system-prefix))) - (dir->pathnames dir)))) + (dir->pathnames dir-namestring)))) (defparameter *http-request-methods* '(:GET :POST :PUT :DELETE :HEAD :CONNECT :OPTIONS :PATCH)) From b1dee071ff80d5b85555d23bceb51735cac21285 Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Fri, 20 Dec 2024 13:32:20 +0900 Subject: [PATCH 03/10] Refactor (#4) * Update system info * Update qlfile * Organize code * Add test * Implement uri-mapper and package-mapper * Change type of package from string to keyword * Implement router --- ningle-fbr-test.asd | 9 +++--- ningle-fbr.asd | 5 ++-- qlfile | 4 ++- qlfile.lock | 16 ++++++++--- src/main.lisp | 68 ++------------------------------------------- src/router.lisp | 67 ++++++++++++++++++++++++++++++++++++++++++++ tests/.keep | 0 tests/router.lisp | 34 +++++++++++++++++++++++ 8 files changed, 126 insertions(+), 77 deletions(-) create mode 100644 src/router.lisp delete mode 100644 tests/.keep create mode 100644 tests/router.lisp diff --git a/ningle-fbr-test.asd b/ningle-fbr-test.asd index 32ff098..47b744e 100644 --- a/ningle-fbr-test.asd +++ b/ningle-fbr-test.asd @@ -1,7 +1,6 @@ (defsystem "ningle-fbr-test" - :defsystem-depends-on ("fiveam-asdf") - :class :package-inferred-fiveam-tester-system + :class :package-inferred-system :pathname "tests" - :depends-on () - :test-names () - :num-checks 0) + :depends-on ("rove" + "ningle-fbr-test/router") + :perform (test-op (o c) (symbol-call :rove :run c :style :dot))) diff --git a/ningle-fbr.asd b/ningle-fbr.asd index 083f795..a567398 100644 --- a/ningle-fbr.asd +++ b/ningle-fbr.asd @@ -1,9 +1,10 @@ (defsystem "ningle-fbr" :version "0.1.0" - :description "Plugin for ningle to enable file-based routing" + :description "File-based router for Ningle" :long-description #.(uiop:read-file-string (uiop:subpathname *load-pathname* "README.md")) - :author "skyizwhite <paku@skyizwhite.dev>" + :author "skyizwhite" + :maintainer "skyizwhite <paku@skyizwhite.dev>" :license "MIT" :class :package-inferred-system :pathname "src" diff --git a/qlfile b/qlfile index 567dbb8..048c828 100644 --- a/qlfile +++ b/qlfile @@ -1,3 +1,5 @@ -ql fiveam-asdf ql ningle ql cl-ppcre +ql alexandria +github rove fukamachi/rove +github dissect shinmera/dissect diff --git a/qlfile.lock b/qlfile.lock index 57bf0bf..56722c8 100644 --- a/qlfile.lock +++ b/qlfile.lock @@ -2,10 +2,6 @@ (:class qlot/source/dist:source-dist :initargs (:distribution "https://beta.quicklisp.org/dist/quicklisp.txt" :%version :latest) :version "2023-10-21")) -("fiveam-asdf" . - (:class qlot/source/ql:source-ql - :initargs (:%version :latest) - :version "ql-2023-10-21")) ("ningle" . (:class qlot/source/ql:source-ql :initargs (:%version :latest) @@ -14,3 +10,15 @@ (:class qlot/source/ql:source-ql :initargs (:%version :latest) :version "ql-2023-10-21")) +("alexandria" . + (:class qlot/source/ql:source-ql + :initargs (:%version :latest) + :version "ql-2024-10-12")) +("rove" . + (:class qlot/source/github:source-github + :initargs (:repos "fukamachi/rove" :ref nil :branch nil :tag nil) + :version "github-cacea7331c10fe9d8398d104b2dfd579bf7ea353")) +("dissect" . + (:class qlot/source/github:source-github + :initargs (:repos "shinmera/dissect" :ref nil :branch nil :tag nil) + :version "github-a70cabcd748cf7c041196efd711e2dcca2bbbb2c")) diff --git a/src/main.lisp b/src/main.lisp index 995b04d..8236432 100644 --- a/src/main.lisp +++ b/src/main.lisp @@ -1,68 +1,6 @@ (uiop:define-package :ningle-fbr (:nicknames #:ningle-fbr/main) - (:use #:cl) - (:import-from #:cl-ppcre) - (:import-from #:ningle) - (:export #:assign-routes)) + (:use #:cl + #:ningle-fbr/router) + (:export #:set-routes)) (in-package :ningle-fbr) - -(defun remove-file-type (namestr) - (cl-ppcre:regex-replace ".lisp" namestr "")) - -(defun remove-index (url) - (if (string= url "/index") - "/" - (cl-ppcre:regex-replace "/index" url ""))) - -(defun replace-dynamic-annotation (url) - (cl-ppcre:regex-replace "=" url ":")) - -(defun format-url (url) - (replace-dynamic-annotation (remove-index url))) - -(defun pathname->url (pathname dir-namestring) - (format-url - (cl-ppcre:regex-replace dir-namestring - (remove-file-type (namestring pathname)) - ""))) - -(defun pathname->package (pathname system-path-namestring system-prefix) - (string-upcase - (cl-ppcre:regex-replace system-path-namestring - (remove-file-type (namestring pathname)) - system-prefix))) - -(defun dir->pathnames (dir) - (directory (concatenate 'string - dir - "/**/*.lisp"))) - -(defun dir->urls-and-packages (dir system) - (let ((dir-namestring (namestring - (asdf:system-relative-pathname system dir))) - (system-path-namestring (namestring - (asdf/component:component-relative-pathname - (asdf/find-system:find-system system)))) - (system-prefix (concatenate 'string system "/"))) - (mapcar (lambda (pathname) - (cons (pathname->url pathname dir-namestring) - (pathname->package pathname system-path-namestring system-prefix))) - (dir->pathnames dir-namestring)))) - -(defparameter *http-request-methods* - '(:GET :POST :PUT :DELETE :HEAD :CONNECT :OPTIONS :PATCH)) - -(defun assign-routes (app &key directory system) - (loop - :for (url . pkg) :in (dir->urls-and-packages directory system) - :do (ql:quickload pkg) - (if (string= url "/not-found") - (let ((handler (find-symbol "HANDLE-NOT-FOUND" pkg))) - (defmethod ningle:not-found ((app ningle:app)) - (funcall handler)))) - (loop - :for method :in *http-request-methods* - :do (let ((handler (find-symbol (concatenate 'string "HANDLE-" (string method)) - pkg))) - (when handler - (setf (ningle:route app url :method method) handler)))))) diff --git a/src/router.lisp b/src/router.lisp new file mode 100644 index 0000000..3c75566 --- /dev/null +++ b/src/router.lisp @@ -0,0 +1,67 @@ +(defpackage #:ningle-fbr/router + (:use #:cl) + (:import-from #:alexandria + #:make-keyword) + (:import-from #:cl-ppcre + #:quote-meta-chars + #:regex-replace + #:regex-replace-all) + (:import-from #:ningle) + (:export #:pathname->path + #:path->uri + #:path-package + #:set-routes)) +(in-package #:ningle-fbr/router) + +(defun pathname->path (pathname target-dir-pathname) + (let* ((full (namestring pathname)) + (prefix (quote-meta-chars (namestring target-dir-pathname)))) + (regex-replace (format nil "^~A(.*?).lisp$" prefix) full "/\\1"))) + +(defun detect-paths (system target-dir-path) + (let ((target-dir-pathname + (merge-pathnames (concatenate 'string + target-dir-path + "/") + (asdf:component-pathname (asdf:find-system system))))) + (mapcar (lambda (pathname) + (pathname->path pathname target-dir-pathname)) + (directory (merge-pathnames "**/*.lisp" target-dir-pathname))))) + +(defun remove-index (path) + (if (string= path "/index") + "/" + (regex-replace "/index$" path ""))) + +(defun bracket->colon (path) + (regex-replace-all "\\[(.*?)\\]" path ":\\1")) + +(defun path->uri (path) + (bracket->colon (remove-index path))) + +(defun path->package (path system target-dir-path) + (make-keyword (string-upcase (concatenate 'string + (string system) + "/" + target-dir-path + path)))) + +(defparameter *http-request-methods* + '(:GET :POST :PUT :DELETE :HEAD :CONNECT :OPTIONS :PATCH)) + +(defmethod set-routes ((app ningle:app) &key system target-dir-path) + (loop + :for path :in (detect-paths system target-dir-path) + :for uri := (path->uri path) + :for pkg := (path->package path system target-dir-path) + :do (ql:quickload pkg) + (if (string= uri "/not-found") + (let ((handler (find-symbol "HANDLE-NOT-FOUND" pkg))) + (defmethod ningle:not-found ((app ningle:app)) + (funcall handler)))) + (loop + :for method :in *http-request-methods* + :do (let ((handler (find-symbol (concatenate 'string "HANDLE-" (string method)) + pkg))) + (when handler + (setf (ningle:route app uri :method method) handler)))))) diff --git a/tests/.keep b/tests/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/router.lisp b/tests/router.lisp new file mode 100644 index 0000000..0196212 --- /dev/null +++ b/tests/router.lisp @@ -0,0 +1,34 @@ +(defpackage #:ningle-fbr-test/router + (:use #:cl + #:rove) + (:import-from #:ningle-fbr/router + #:pathname->path + #:path->uri + #:path->package)) +(in-package #:ningle-fbr-test/router) + +(deftest router-test + (testing "pathname->path" + (ok (string= (pathname->path #P"/home/app/src/routes/foo.lisp" + #P"/home/app/src/routes/") + "/foo")))) + +(deftest uri-test + (testing "normal path" + (ok (string= (path->uri "/foo") "/foo")) + (ok (string= (path->uri "/foo/bar") "/foo/bar"))) + + (testing "index path" + (ok (string= (path->uri "/index") "/")) + (ok (string= (path->uri "/nested/index") "/nested"))) + + (testing "dynamic path" + (ok (string= (path->uri "/user/[id]") "/user/:id")) + (ok (string= (path->uri "/location/[country]/[city]") "/location/:country/:city" )))) + +(deftest package-test + (testing "normal case" + (ok (eq (path->package "/foo" :app "routes") + :app/routes/foo)) + (ok (eq (path->package "/foo" :app "somedir/routes") + :app/somedir/routes/foo)))) From 09c5a126be62bb84d0619f7bc200f809df9633b7 Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Fri, 20 Dec 2024 15:09:37 +0900 Subject: [PATCH 04/10] Update README --- README.md | 48 ++++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index ee74370..fd8622c 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,22 @@ -# ningle-fbr (WIP) +# ningle-fbr -A utility for [ningle](https://github.com/fukamachi/ningle) and [jingle](https://github.com/dnaeon/cl-jingle) to enable file-based routing. +A file-based router for [ningle](https://github.com/fukamachi/ningle). + +## Warning + +This software is still in ALPHA quality. The APIs are likely to change. + +Please check the [release notes](https://github.com/skyizwhite/ningle-fbr/releases) for updates. ## What is File-Based Routing? -File-based routing is a concept commonly used in modern web frameworks such as [Next.js](https://nextjs.org/). Instead of explicitly defining routes through configuration or code, the framework automatically sets up routes based on the file hierarchy of a specific directory (usually the "pages" or "routes" directory). +File-based routing is a concept widely used in modern web frameworks like [Next.js](https://nextjs.org/). Instead of explicitly defining routes in code or configuration, routes are automatically generated based on the file and directory structure within a designated folder (often called "pages" or "routes"). ## Usage -To use ningle-fbr, you need to use [package-inferred-system](https://asdf.common-lisp.dev/asdf/The-package_002dinferred_002dsystem-extension.html). +To use ningle-fbr, you need to set up your project based on the [package-inferred-system](https://asdf.common-lisp.dev/asdf/The-package_002dinferred_002dsystem-extension.html). -`/example.asd` +`/example.asd`: ```lisp (defsystem "example" :class :package-inferred-system @@ -18,33 +24,31 @@ To use ningle-fbr, you need to use [package-inferred-system](https://asdf.common :depends-on ("example/app")) ``` -`/src/app.lisp` +`/src/app.lisp`: ```lisp (defpackage #:example (:nicknames #:example/app) (:use #:cl) (:import-from #:ningle) (:import-from #:ningle-fbr - #:assign-routes)) + #:set-routes)) (in-package #:example/app) (defparameter *app* (make-instance 'ningle:<app>)) -(assign-routes *app* - :directory "src/routes" - :system "example") +(set-routes *app* :system :example :target-dir-path "routes") ``` ### Static Routing -`/src/routes/index.lisp` → `/` +Routes are generated automatically from packages under `:example/routes`: -`/src/routes/hello.lisp` → `/hello` - -`/src/routes/users/index.lisp` → `/users` - -`/src/routes/nested/page.lisp` → `/nested/page` +- `:example/routes/index` → `/` +- `:example/routes/hello` → `/hello` +- `:example/routes/users/index` → `/users` +- `:example/routes/nested/page` → `/nested/page` +`/src/routes/index.lisp` example: ```lisp (defpackage #:example/routes/index (:use #:cl) @@ -69,22 +73,22 @@ To use ningle-fbr, you need to use [package-inferred-system](https://asdf.common ### Dynamic Routing -A file or directory name prefixed with '=' indicates a dynamic path. +Dynamic routes use square brackets to indicate parameters. For example: -In the example below, the parameter `id` can be obtained from the handler's params. +`/src/routes/user/[id].lisp` → `/user/[id]` -`/src/routes/user/=id.lisp` → `/user/:id` +In the handlers, you can access the value of `id` through the `params` argument. ### Not Found Error -`not-found.lisp` is a special file to handle 404 errors. Implement the `handle-not-found` function and export it. +`:example/routes/not-found` is a special package for handling 404 errors. Implement and export `handle-not-found`: ```lisp (defpackage #:example/routes/not-found (:use #:cl) (:export #:handle-not-found)) (in-package #:example/routes/not-found) - + (defun handle-not-found () ...) ``` @@ -93,4 +97,4 @@ In the example below, the parameter `id` can be obtained from the handler's para Licensed under the MIT License. -© 2024, skyizwhite. +© 2024, skyizwhite. \ No newline at end of file From d20dbe264911b7dfd6e6fade899db9b09bd2dd32 Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Fri, 20 Dec 2024 15:15:04 +0900 Subject: [PATCH 05/10] Fix --- README.md | 4 ++-- src/router.lisp | 2 +- tests/router.lisp | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fd8622c..43e8ae3 100644 --- a/README.md +++ b/README.md @@ -73,9 +73,9 @@ Routes are generated automatically from packages under `:example/routes`: ### Dynamic Routing -Dynamic routes use square brackets to indicate parameters. For example: +Dynamic routes use `< >` to indicate parameters. For example: -`/src/routes/user/[id].lisp` → `/user/[id]` +`/src/routes/user/<id>.lisp` → `/user/:id` In the handlers, you can access the value of `id` through the `params` argument. diff --git a/src/router.lisp b/src/router.lisp index 3c75566..9f974c3 100644 --- a/src/router.lisp +++ b/src/router.lisp @@ -34,7 +34,7 @@ (regex-replace "/index$" path ""))) (defun bracket->colon (path) - (regex-replace-all "\\[(.*?)\\]" path ":\\1")) + (regex-replace-all "<(.*?)>" path ":\\1")) (defun path->uri (path) (bracket->colon (remove-index path))) diff --git a/tests/router.lisp b/tests/router.lisp index 0196212..564c06d 100644 --- a/tests/router.lisp +++ b/tests/router.lisp @@ -23,8 +23,8 @@ (ok (string= (path->uri "/nested/index") "/nested"))) (testing "dynamic path" - (ok (string= (path->uri "/user/[id]") "/user/:id")) - (ok (string= (path->uri "/location/[country]/[city]") "/location/:country/:city" )))) + (ok (string= (path->uri "/user/<id>") "/user/:id")) + (ok (string= (path->uri "/location/<country>/<city>") "/location/:country/:city" )))) (deftest package-test (testing "normal case" From 234b2e50f38e04836c1f6d5123a1eec317149542 Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Fri, 20 Dec 2024 15:30:29 +0900 Subject: [PATCH 06/10] Update README --- README.md | 57 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 43e8ae3..b01bb22 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,27 @@ Please check the [release notes](https://github.com/skyizwhite/ningle-fbr/releas ## What is File-Based Routing? -File-based routing is a concept widely used in modern web frameworks like [Next.js](https://nextjs.org/). Instead of explicitly defining routes in code or configuration, routes are automatically generated based on the file and directory structure within a designated folder (often called "pages" or "routes"). +File-based routing automatically generates URL routes based on a project’s file and directory structure. Instead of manually configuring routes in a separate routing file, each file in a designated directory (e.g., `pages` or `routes`) becomes a route. This simplifies development and maintenance since adding, removing, or renaming a route is often just a matter of modifying a file’s name or location. ## Usage -To use ningle-fbr, you need to set up your project based on the [package-inferred-system](https://asdf.common-lisp.dev/asdf/The-package_002dinferred_002dsystem-extension.html). +To use ningle-fbr, set up your project in accordance with the [package-inferred-system](https://asdf.common-lisp.dev/asdf/The-package_002dinferred_002dsystem-extension.html) conventions. -`/example.asd`: +**Example directory structure**: +``` +example.asd +src/ + app.lisp + routes/ + index.lisp + hello.lisp + users/ + index.lisp + nested/ + page.lisp +``` + +**`example.asd`**: ```lisp (defsystem "example" :class :package-inferred-system @@ -24,14 +38,13 @@ To use ningle-fbr, you need to set up your project based on the [package-inferre :depends-on ("example/app")) ``` -`/src/app.lisp`: +**`/src/app.lisp`**: ```lisp (defpackage #:example (:nicknames #:example/app) (:use #:cl) (:import-from #:ningle) - (:import-from #:ningle-fbr - #:set-routes)) + (:import-from #:ningle-fbr #:set-routes)) (in-package #:example/app) (defparameter *app* (make-instance 'ningle:<app>)) @@ -41,14 +54,14 @@ To use ningle-fbr, you need to set up your project based on the [package-inferre ### Static Routing -Routes are generated automatically from packages under `:example/routes`: +Routes are derived from packages under `:example/routes`. The package’s name corresponds directly to a URL path: - `:example/routes/index` → `/` - `:example/routes/hello` → `/hello` - `:example/routes/users/index` → `/users` - `:example/routes/nested/page` → `/nested/page` -`/src/routes/index.lisp` example: +**`/src/routes/index.lisp`**: ```lisp (defpackage #:example/routes/index (:use #:cl) @@ -59,29 +72,36 @@ Routes are generated automatically from packages under `:example/routes`: (in-package #:example/routes/index) (defun handle-get (params) - ...) + ;; implement GET logic here + ) (defun handle-post (params) - ...) + ;; implement POST logic here + ) (defun handle-put (params) - ...) + ;; implement PUT logic here + ) (defun handle-delete (params) - ...) + ;; implement DELETE logic here + ) ``` +Handlers are chosen based on the HTTP method. If `handle-get` is exported, it will be called for `GET` requests on `/`. + ### Dynamic Routing -Dynamic routes use `< >` to indicate parameters. For example: +To define dynamic routes, use `<>` in the file name to indicate URL parameters. -`/src/routes/user/<id>.lisp` → `/user/:id` +For example: +`:example/routes/user/<id>` → `/user/:id` -In the handlers, you can access the value of `id` through the `params` argument. +If a request comes in at `/user/123`, `params` will include `:id "123"`. -### Not Found Error +### 404 Handling -`:example/routes/not-found` is a special package for handling 404 errors. Implement and export `handle-not-found`: +To handle 404 (Not Found) error, create a special package named `:example/routes/not-found` and define `handle-not-found`: ```lisp (defpackage #:example/routes/not-found @@ -90,7 +110,8 @@ In the handlers, you can access the value of `id` through the `params` argument. (in-package #:example/routes/not-found) (defun handle-not-found () - ...) + ;; Implement custom 404 logic here + ) ``` ## License From 7641ec5f6190a5b31540b874c3261c302dc3391b Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Sun, 22 Dec 2024 18:33:49 +0900 Subject: [PATCH 07/10] Add test for set-routes --- README.md | 1 + qlfile | 1 + qlfile.lock | 4 +++ tests/router.lisp | 67 ++++++++++++++++++++++++++++++----- tests/routes/hello.lisp | 8 +++++ tests/routes/index.lisp | 8 +++++ tests/routes/nested/page.lisp | 8 +++++ tests/routes/not-found.lisp | 11 ++++++ tests/routes/users/<id>.lisp | 8 +++++ tests/routes/users/index.lisp | 8 +++++ 10 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 tests/routes/hello.lisp create mode 100644 tests/routes/index.lisp create mode 100644 tests/routes/nested/page.lisp create mode 100644 tests/routes/not-found.lisp create mode 100644 tests/routes/users/<id>.lisp create mode 100644 tests/routes/users/index.lisp diff --git a/README.md b/README.md index b01bb22..ecb0a03 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ src/ hello.lisp users/ index.lisp + <id>.lisp nested/ page.lisp ``` diff --git a/qlfile b/qlfile index 048c828..548652c 100644 --- a/qlfile +++ b/qlfile @@ -1,4 +1,5 @@ ql ningle +ql lack ql cl-ppcre ql alexandria github rove fukamachi/rove diff --git a/qlfile.lock b/qlfile.lock index 56722c8..003a88d 100644 --- a/qlfile.lock +++ b/qlfile.lock @@ -6,6 +6,10 @@ (:class qlot/source/ql:source-ql :initargs (:%version :latest) :version "ql-2023-10-21")) +("lack" . + (:class qlot/source/ql:source-ql + :initargs (:%version :latest) + :version "ql-2024-10-12")) ("cl-ppcre" . (:class qlot/source/ql:source-ql :initargs (:%version :latest) diff --git a/tests/router.lisp b/tests/router.lisp index 564c06d..03724a0 100644 --- a/tests/router.lisp +++ b/tests/router.lisp @@ -1,18 +1,18 @@ (defpackage #:ningle-fbr-test/router (:use #:cl #:rove) + (:import-from #:ningle) + (:import-from #:lack) + (:import-from #:lack/test + #:testing-app + #:request) (:import-from #:ningle-fbr/router - #:pathname->path #:path->uri - #:path->package)) + #:path->package + #:pathname->path + #:set-routes)) (in-package #:ningle-fbr-test/router) -(deftest router-test - (testing "pathname->path" - (ok (string= (pathname->path #P"/home/app/src/routes/foo.lisp" - #P"/home/app/src/routes/") - "/foo")))) - (deftest uri-test (testing "normal path" (ok (string= (path->uri "/foo") "/foo")) @@ -24,7 +24,7 @@ (testing "dynamic path" (ok (string= (path->uri "/user/<id>") "/user/:id")) - (ok (string= (path->uri "/location/<country>/<city>") "/location/:country/:city" )))) + (ok (string= (path->uri "/location/<country>/<city>") "/location/:country/:city")))) (deftest package-test (testing "normal case" @@ -32,3 +32,52 @@ :app/routes/foo)) (ok (eq (path->package "/foo" :app "somedir/routes") :app/somedir/routes/foo)))) + +(deftest router-test + (testing "pathname->path" + (ok (string= (pathname->path #P"/home/app/src/routes/foo.lisp" + #P"/home/app/src/routes/") + "/foo"))) + + (testing "set-routes" + (testing-app (let ((app (make-instance 'ningle:app))) + (set-routes app + :system :ningle-fbr-test + :target-dir-path "routes") + (lack:builder app)) + (multiple-value-bind (body status headers) + (request "/") + (declare (ignore headers)) + (ok (string= body "ok")) + (ok (eql status 200))) + + (multiple-value-bind (body status headers) + (request "/hello") + (declare (ignore headers)) + (ok (string= body "ok")) + (ok (eql status 200))) + + (multiple-value-bind (body status headers) + (request "/nested/page") + (declare (ignore headers)) + (ok (string= body "ok")) + (ok (eql status 200))) + + (multiple-value-bind (body status headers) + (request "/users") + (declare (ignore headers)) + (ok (string= body "ok")) + (ok (eql status 200))) + + (multiple-value-bind (body status headers) + (request "/users/bob") + (declare (ignore headers)) + (ok (string= body "bob")) + (ok (eql status 200))) + + (multiple-value-bind (body status headers) + (request "/missing") + (declare (ignore headers)) + (ok (string= body "Not Found")) + (ok (eql status 404)))))) + diff --git a/tests/routes/hello.lisp b/tests/routes/hello.lisp new file mode 100644 index 0000000..18beefa --- /dev/null +++ b/tests/routes/hello.lisp @@ -0,0 +1,8 @@ +(defpackage #:ningle-fbr-test/routes/hello + (:use #:cl) + (:export #:handle-get)) +(in-package #:ningle-fbr-test/routes/hello) + +(defun handle-get (params) + (declare (ignore params)) + "ok") diff --git a/tests/routes/index.lisp b/tests/routes/index.lisp new file mode 100644 index 0000000..dab5d79 --- /dev/null +++ b/tests/routes/index.lisp @@ -0,0 +1,8 @@ +(defpackage #:ningle-fbr-test/routes/index + (:use #:cl) + (:export #:handle-get)) +(in-package #:ningle-fbr-test/routes/index) + +(defun handle-get (params) + (declare (ignore params)) + "ok") diff --git a/tests/routes/nested/page.lisp b/tests/routes/nested/page.lisp new file mode 100644 index 0000000..afc9f62 --- /dev/null +++ b/tests/routes/nested/page.lisp @@ -0,0 +1,8 @@ +(defpackage #:ningle-fbr-test/routes/nested/page + (:use #:cl) + (:export #:handle-get)) +(in-package #:ningle-fbr-test/routes/nested/page) + +(defun handle-get (params) + (declare (ignore params)) + "ok") diff --git a/tests/routes/not-found.lisp b/tests/routes/not-found.lisp new file mode 100644 index 0000000..5392fd8 --- /dev/null +++ b/tests/routes/not-found.lisp @@ -0,0 +1,11 @@ +(defpackage #:ningle-fbr-test/routes/not-found + (:use #:cl) + (:import-from #:lack/response) + (:import-from #:ningle) + (:export #:handle-not-found)) +(in-package #:ningle-fbr-test/routes/not-found) + +(defun handle-not-found () + (setf (lack/response:response-status ningle:*response*) + 404) + "Not Found") diff --git a/tests/routes/users/<id>.lisp b/tests/routes/users/<id>.lisp new file mode 100644 index 0000000..0eb385f --- /dev/null +++ b/tests/routes/users/<id>.lisp @@ -0,0 +1,8 @@ +(defpackage #:ningle-fbr-test/routes/users/<id> + (:use #:cl) + (:export #:handle-get)) +(in-package #:ningle-fbr-test/routes/users/<id>) + +(defun handle-get (params) + (let ((id (cdr (assoc :id params)))) + id)) diff --git a/tests/routes/users/index.lisp b/tests/routes/users/index.lisp new file mode 100644 index 0000000..5473c9b --- /dev/null +++ b/tests/routes/users/index.lisp @@ -0,0 +1,8 @@ +(defpackage #:ningle-fbr-test/routes/users/index + (:use #:cl) + (:export #:handle-get)) +(in-package #:ningle-fbr-test/routes/users/index) + +(defun handle-get (params) + (declare (ignore params)) + "ok") From 1151e4adacffa6b0be59fba136520a430cbdc704 Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Wed, 25 Dec 2024 02:47:46 +0900 Subject: [PATCH 08/10] Update README --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ecb0a03..0b2d284 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,19 @@ A file-based router for [ningle](https://github.com/fukamachi/ningle). ## Warning -This software is still in ALPHA quality. The APIs are likely to change. +This software is currently in ALPHA stage. Its APIs are subject to change. -Please check the [release notes](https://github.com/skyizwhite/ningle-fbr/releases) for updates. +Check the [release notes](https://github.com/skyizwhite/ningle-fbr/releases) for the latest updates. ## What is File-Based Routing? -File-based routing automatically generates URL routes based on a project’s file and directory structure. Instead of manually configuring routes in a separate routing file, each file in a designated directory (e.g., `pages` or `routes`) becomes a route. This simplifies development and maintenance since adding, removing, or renaming a route is often just a matter of modifying a file’s name or location. +File-based routing automatically creates URL routes based on a project’s file and directory structure. Instead of manually configuring routes in a separate routing file, each file in a designated directory (e.g., `pages` or `routes`) becomes a route. This simplifies development and maintenance since adding, removing, or renaming a route is often just a matter of modifying a file’s name or location. ## Usage -To use ningle-fbr, set up your project in accordance with the [package-inferred-system](https://asdf.common-lisp.dev/asdf/The-package_002dinferred_002dsystem-extension.html) conventions. +To use ningle-fbr, set up your project using the [package-inferred-system](https://asdf.common-lisp.dev/asdf/The-package_002dinferred_002dsystem-extension.html) style. -**Example directory structure**: +**Example directory structure**: ``` example.asd src/ @@ -55,7 +55,7 @@ src/ ### Static Routing -Routes are derived from packages under `:example/routes`. The package’s name corresponds directly to a URL path: +Routes are determined by packages located under `:example/routes`. The package’s name corresponds directly to a URL path: - `:example/routes/index` → `/` - `:example/routes/hello` → `/hello` @@ -73,19 +73,19 @@ Routes are derived from packages under `:example/routes`. The package’s name c (in-package #:example/routes/index) (defun handle-get (params) - ;; implement GET logic here + ;; Implement GET logic here ) (defun handle-post (params) - ;; implement POST logic here + ;; Implement POST logic here ) (defun handle-put (params) - ;; implement PUT logic here + ;; Implement PUT logic here ) (defun handle-delete (params) - ;; implement DELETE logic here + ;; Implement DELETE logic here ) ``` @@ -102,7 +102,7 @@ If a request comes in at `/user/123`, `params` will include `:id "123"`. ### 404 Handling -To handle 404 (Not Found) error, create a special package named `:example/routes/not-found` and define `handle-not-found`: +To handle 404 (Not Found) errors, create a special package named `:example/routes/not-found` and define `handle-not-found`: ```lisp (defpackage #:example/routes/not-found From 90ac8ee8f5258d18d2a02eb672bc4e9090dc44cc Mon Sep 17 00:00:00 2001 From: Anthony Green <green@moxielogic.com> Date: Sat, 28 Dec 2024 07:41:05 -0700 Subject: [PATCH 09/10] Remove dependency on quicklisp (#5) --- src/router.lisp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/router.lisp b/src/router.lisp index 9f974c3..fc780a9 100644 --- a/src/router.lisp +++ b/src/router.lisp @@ -7,6 +7,7 @@ #:regex-replace #:regex-replace-all) (:import-from #:ningle) + (:import-from #:trivial-system-loader) (:export #:pathname->path #:path->uri #:path-package @@ -54,7 +55,7 @@ :for path :in (detect-paths system target-dir-path) :for uri := (path->uri path) :for pkg := (path->package path system target-dir-path) - :do (ql:quickload pkg) + :do (load-system pkg) (if (string= uri "/not-found") (let ((handler (find-symbol "HANDLE-NOT-FOUND" pkg))) (defmethod ningle:not-found ((app ningle:app)) From 6cdf49965b2857e08454d29381cd96650e8f2614 Mon Sep 17 00:00:00 2001 From: paku <paku@skyizwhite.dev> Date: Sat, 28 Dec 2024 23:44:37 +0900 Subject: [PATCH 10/10] Add trivial-system-loader to qlot deps --- qlfile | 1 + qlfile.lock | 4 ++++ src/router.lisp | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/qlfile b/qlfile index 548652c..88e3eca 100644 --- a/qlfile +++ b/qlfile @@ -2,5 +2,6 @@ ql ningle ql lack ql cl-ppcre ql alexandria +ql trivial-system-loader github rove fukamachi/rove github dissect shinmera/dissect diff --git a/qlfile.lock b/qlfile.lock index 003a88d..251711d 100644 --- a/qlfile.lock +++ b/qlfile.lock @@ -18,6 +18,10 @@ (:class qlot/source/ql:source-ql :initargs (:%version :latest) :version "ql-2024-10-12")) +("trivial-system-loader" . + (:class qlot/source/ql:source-ql + :initargs (:%version :latest) + :version "ql-2024-10-12")) ("rove" . (:class qlot/source/github:source-github :initargs (:repos "fukamachi/rove" :ref nil :branch nil :tag nil) diff --git a/src/router.lisp b/src/router.lisp index fc780a9..de26076 100644 --- a/src/router.lisp +++ b/src/router.lisp @@ -7,7 +7,8 @@ #:regex-replace #:regex-replace-all) (:import-from #:ningle) - (:import-from #:trivial-system-loader) + (:import-from #:trivial-system-loader + #:load-system) (:export #:pathname->path #:path->uri #:path-package