Git Product home page Git Product logo

ningle's Introduction

ningle

Build Status

"ningle" is a lightweight web application framework for Common Lisp.

Usage

(defvar *app* (make-instance 'ningle:app))

(setf (ningle:route *app* "/")
      "Welcome to ningle!")

(setf (ningle:route *app* "/login" :method :POST)
      #'(lambda (params)
          (if (authorize (cdr (assoc "username" params :test #'string=))
                         (cdr (assoc "password" params :test #'string=)))
              "Authorized!"
              "Failed...Try again.")))

(clack:clackup *app*)

Now you can access to http://localhost:5000/ and then ningle should show you "Welcome to ningle!".

Installation

(ql:quickload :ningle)

Description

ningle is a fork project of Caveman. ningle doesn't require you to generate a project skeleton.

As this is a thin framework, you need to have subtle knowledge about Clack. It is a server interface ningle bases on.

Getting started

Routing

ningle has the Sinatra-like routing system.

;; GET request (default)
(setf (ningle:route *app* "/" :method :GET) ...)

;; POST request
(setf (ningle:route *app* "/" :method :POST) ...)

;; PUT request
(setf (ningle:route *app* "/" :method :PUT) ...)

;; DELETE request
(setf (ningle:route *app* "/" :method :DELETE) ...)

;; OPTIONS request
(setf (ningle:route *app* "/" :method :OPTIONS) ...)

Route pattern may contain "keyword" to put the value into the argument.

(setf (ningle:route *app* "/hello/:name")
      #'(lambda (params)
          (format nil "Hello, ~A" (cdr (assoc :name params)))))

The above controller will be invoked when you access to "/hello/Eitaro" or "/hello/Tomohiro", and then (cdr (assoc :name params)) will be "Eitaro" and "Tomohiro".

Route patterns may also contain "wildcard" parameters. They are accessible by (assoc :splat params).

(setf (ningle:route *app* "/say/*/to/*")
      #'(lambda (params)
          ; matches /say/hello/to/world
          (cdr (assoc :splat params)) ;=> ("hello" "world")
          ))

(setf (ningle:route *app* "/download/*.*")
      #'(lambda (params)
          ; matches /download/path/to/file.xml
          (cdr (assoc :splat params)) ;=> ("path/to/file" "xml")
          ))

Route matching with Regular Expressions:

(setf (ningle:route *app* "/hello/([\\w]+)" :regexp t)
      #'(lambda (params)
          (format nil "Hello, ~A!" (first (cdr (assoc :captures params))))))

Requirements

Routes may include a variety of matching conditions, such as the Accept:

(setf (ningle:route *app* "/" :accept '("text/html" "text/xml"))
      #'(lambda (params)
          (declare (ignore params))
          "<html><body>Hello, World!</body></html>"))

(setf (ningle:route *app* "/" :accept "text/plain")
      #'(lambda (params)
          (declare (ignore params))
          "Hello, World!"))

You can easily define your own conditions:

(setf (ningle:requirement *app* :probability)
      #'(lambda (value)
          (<= (random 100) value)))

(setf (ningle:route *app* "/win_a_car" :probability 10)
      #'(lambda (params)
          (declare (ignore params))
          "You won!"))

(setf (ningle:route *app* "/win_a_car")
      #'(lambda (params)
          (declare (ignore params))
          "Sorry, you lost."))

Request & Response

ningle provides two special variables named *request* and *response*. They will be bound to an instance Lack.Request and Lack.Response for each request.

For example, by using them, you can change the response status code, Content-Type or something like that in each controllers.

(setf (lack.response:response-headers *response*)
      (append (lack.response:response-headers *response*)
              (list :content-type "application/json")))

(setf (lack.response:response-headers *response*)
      (append (lack.response:response-headers *response*)
              (list :access-control-allow-origin "*")))

(setf (lack.response:response-status *response*) 201)

Context

ningle provides a useful function named context. It is an accessor to an internal hash table.

(setf (context :database)
      (dbi:connect :mysql
                   :database-name "test-db"
                   :username "nobody"
                   :password "nobody"))

(context :database)
;;=> #<DBD.MYSQL:<DBD-MYSQL-CONNECTION> #x3020013D1C6D>

Using Session

ningle doesn't provide Session system in the core, but recommends to use Lack.Middleware.Session with Lack.Builder.

(import 'lack.builder:builder)

(clack:clackup
  (builder
    :session
    *app*))

Of course, you can use other Lack Middlewares with ningle.

See Also

Author

Copyright

Copyright (c) 2012-2014 Eitaro Fukamachi ([email protected])

License

Licensed under the LLGPL License.

ningle's People

Contributors

anquegi avatar dan-passaro avatar fukamachi avatar jackcarrozzo avatar jgarte avatar kilianmh avatar mtstickney avatar rudolph-miller avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ningle's Issues

Params list is truncated

I think the commit 3e60411 affects the params list not only for multipart/form-data but also for other requests.

Test case:

(defun handle (params)
  (format t "~&~a~%" params))
(setf (ningle:route *app* "/test" :method :post)
      #'handle)
curl -H "Content-Type: application/json" -X POST -d '{"args":[1,2,3]}' localhost:8083/test

prints: ((args . 1)) instead of expected ((args 1 2 3)). It works fine before that commit.

Regards,
Michał

Upload file size limit

Hi, is there a limit on file size by default in ningle (or (c)lack?)?

Flask has a feature like this. I'm wondering if I can configure something similar in ningle or (c)lack.

I tried uploading a relatively large file (334.2MB) but I got this error:

#<PACKAGE "NINGLE-UPLOAD">
NINGLE-UPLOAD(5): (start :port 8899)

Hunchentoot server is started.
Listening on localhost:8899.
#S(CLACK.HANDLER::HANDLER
   :SERVER :HUNCHENTOOT
   :ACCEPTOR #<SB-THREAD:THREAD "clack-handler-hunchentoot" RUNNING
                {10047AC0F3}>)
NINGLE-UPLOAD(6): Heap exhausted during allocation: 293339136 bytes available, 334184704 requested.
Gen  Boxed    Raw   Code  Mixed  LgRaw LgCode  LgMix  Pin       Alloc     Waste        Trig   Dirty GCs Mem-age
 0       0      0      0    318      0      0      0    0     5052672    157440    10737418       -   0  0.0000
 1    1070    573      7    210  40869      0     95 40852   700446352   1182064   711183770   41480   2  0.0000
 2       0      0      0      0      0      0      0    0           0         0     2000000       0   0  0.0000
 3       0      0      0      0      0      0      0    0           0         0     2000000       0   0  0.0000
 4       0      0      0      0      0      0      0    0           0         0     2000000       0   0  0.0000
 5       0      0      0      0      0      0      0    0           0         0     2000000       0   0  0.0000
 6     789    325      3     41     53      0    123    0    21262448    593808     2000000     143   0  0.0000
           Total bytes allocated    =     726761472
           Dynamic-space-size bytes =    1073741824
GC control variables:
   *GC-INHIBIT* = false
   *GC-PENDING* = true
   *STOP-FOR-GC-PENDING* = false
[2022-02-16 00:14:31 [ERROR]] Error while processing connection: The condition The condition Heap exhausted (no more space for allocation).
293339136 bytes available, 334184704 requested.

PROCEED WITH CAUTION. occurred with errno: 0. occurred with errno: 0.
Heap exhausted during allocation: 293339136 bytes available, 334184720 requested.
Gen  Boxed    Raw   Code  Mixed  LgRaw LgCode  LgMix  Pin       Alloc     Waste        Trig   Dirty GCs Mem-age
 0       0      0      0    103      0      0      0    0     1596592     90960    10737418       -   0  0.0000
 1    1054    568      7    208  40869      0     95 40852   700139456   1112128   710876874   41509   3  0.0000
 2       0      0      0      0      0      0      0    0           0         0     2000000       0   0  0.0000
 3       0      0      0      0      0      0      0    0           0         0     2000000       0   0  0.0000
 4       0      0      0      0      0      0      0    0           0         0     2000000       0   0  0.0000
 5       0      0      0      0      0      0      0    0           0         0     2000000       0   0  0.0000
 6     789    325      3     41     53      0    123    0    21262448    593808     2000000     144   0  0.0000
           Total bytes allocated    =     722998496
           Dynamic-space-size bytes =    1073741824
GC control variables:
   *GC-INHIBIT* = false
   *GC-PENDING* = true
   *STOP-FOR-GC-PENDING* = false
[2022-02-16 00:14:32 [ERROR]] Error while processing connection: The condition The condition Heap exhausted (no more space for allocation).
293339136 bytes available, 334184720 requested.

PROCEED WITH CAUTION. occurred with errno: 0. occurred with errno: 0.

Here is the upload form code:

;; License: MIT

(uiop:define-package #:ningle-upload
  (:use #:cl)
  (:import-from #:cl-fad)
  (:import-from #:ningle)
  (:import-from #:spinneret)
  (:import-from #:log4cl))

(in-package #:ningle-upload)


(defvar *app* (make-instance 'ningle:app))

(defvar *server* nil)


(defparameter *downloads-dir* #P"/tmp/downloads/")


(setf (ningle:route *app* "/")
      (lambda (params)
        (declare (ignore params))
        (spinneret:with-html-string
          (:h1 "Ningle Uploader Example")
         
          (:h2 "Already Uploaded")
          (let ((files
                  (when (probe-file *downloads-dir*)
                    (cl-fad:list-directory *downloads-dir*))))
            (if files
                (:ul
                 (loop for file in files
                       do (:li (:p (file-namestring file)))))
                (:p "There is no files yet.")))
         
          (:h2 "Add More")
          (:form :method "POST"
                 :action "/"
                 :enctype "multipart/form-data"
                 (:input :type "file"
                         :multiple t
                         :name "upload")
                 (:input :type "submit")))))


(setf (ningle:route *app* "/" :method :POST)
      (lambda (params)
        ;; In case of multiple files, params will have a multiple "upload"
        ;; entries:
        ;;
        ;; (("upload" #<FLEXI-STREAMS::VECTOR-INPUT-STREAM {700821F9F3}>
        ;;            "file1.png" "image/png")
        ;;  ("upload" #<FLEXI-STREAMS::VECTOR-INPUT-STREAM {7008223413}>
        ;;            "file2.png" "image/png")
        ;;  ("upload" #<FLEXI-STREAMS::VECTOR-INPUT-STREAM {7008223F23}>
        ;;            "file3.png" "image/png"))
        ;; 
        ;; And we need to process them one by one:
        (loop for row in params
              for name = (first row)
              when (string= name "upload")
                ;; Here stream will have type FLEXI-STREAMS::VECTOR-INPUT-STREAM
                do (destructuring-bind (stream filename content-type)
                       (rest row)
                     (when stream
                       (log:info "Accepting upload." filename content-type)
                       (let ((full-path (merge-pathnames filename *downloads-dir*)))
                         (ensure-directories-exist full-path)
                         (uiop:slurp-input-stream full-path stream))))
                   ;; read-line
                   ;; read-stream function
                   ;; will give a single stream ended by newline

              finally
                 ;; Redirecting user back to the list of files:
                 (setf (lack.response:response-status ningle:*response*)
                       302
                       (lack.response:response-headers ningle:*response*)
                       (append (lack.response:response-headers ningle:*response*)
                               (list :location "/"))))))


(defun start (&key (interface "localhost")
                (port 8080))
  (setf *server*
        (clack:clackup *app*
                       :address interface
                       :port port)))

How to access *request* and *response*

Hi I'm just starting out with your library. Thanks for all the hard work!

the documentation says

ningle provides two special variables named *request* and *response*. They will be bound to an instance Clack.Request and Clack.Response for each request.

can you provide an example of how to access these variables? I am trying to set the Content-Type to json

Thanks for the help

clack/lack dependency

Is there a reason for removing clack as a dependency? http://8arrow.org/ningle/ shows a call to clack:clackup in the first example already, but it is nowhere said that clack has to be installed separately. Does it even make sense to use ningle without clack?

The same is true for lack: If I want to use lack:builder, I have to load lack manually.

I would suggest to add both as a dependency since you usually want to use them.

Can't access POST parameters

Using the following code:

(ql:quickload :ningle)

(defvar *app* (make-instance 'ningle:<app>))

(setf (ningle:route *app* "/")
      "<form action='/form' method='post'><input type='text' id='text'><input type='submit' value='Submit'></form>")

(setf (ningle:route *app* "/query" :method :GET)
      #'(lambda (params)
          (princ-to-string params)))

(setf (ningle:route *app* "/form" :method :POST)
      #'(lambda (params)
          (princ-to-string params)))

(clack:clackup *app*)

If you point your browser to http://localhost:5000/query?derp=herp, you'll see (derp herp). If you send a request with the form in the index page, you'll see nil. I'm not sure what I'm doing wrong here.

POST body-parameters are being lost such as the filenames when uploading files

3e60411

I think this commit is causing lost to access of relevant body parameters in the route definition's lexical environment. For example if we want to know the file name of a file a user uploads via a POST Form, the hash-tables that used to contain the name are no longer available. I tested with these routes before/after reverting the commit.

(setf (ningle:route *app* "/")
      (cl-markup:html5
       (:form :id "post-form" :action "/" :method "POST" :enctype "multipart/form-data"
              (:input :type "file" :name "myfile")
              (:input :type "text" :name "sup")
              (:input :type "submit" :value "Post"))
       ))

(setf (ningle:route *app* "/" :method :POST)
      #'(lambda (params)
          (let ((bp (getf (lack.request:request-env *request*) :body-parameters)))
            (format nil "<b>Parameters:</b> ~a~%<b>Request Env:</b> ~a~%" params bp))))

Setting up routes for static content

How can I serve static content with ningle?
I have a static folder under my project root and keep the relevant static files in sub folders like css, js, images, etc.,
And I want to access them like:

<link rel="stylesheet" href="/css/main/css">
<script src="/js/main.js"></script>
<img src="/images/logo.png"/>

Is it possible to separate asdf:load-op for ningle-test and runnig the test suite?

Hi.

In cl-test-grid project we are running tests of CL libraries on various lisps implementation and OSes. The tests include ql:quickload for every ASDF system in quicklisp and also running testsuites of some libraries.

You may find your library test results at http://common-lisp.net/project/cl-test-grid/library/[LIBNAME].html. For example http://common-lisp.net/project/cl-test-grid/library/ningle.html

When we do (ql:quickload :ningle-test) it runs the testsuite, including starting hunchentoot.
It sometimes fails due to port being busy and just hangs on some lisps.

For example, sbcl --eval "(ql:quickload :ningle-test)" --eval "(quit)" hangs forever.

Is it possible to separate the test running from system loading?
I.e. to run testsuite on could do
common-lisp (ql:quickload :ningle-test) (ningle-test:run-tests)
But if you just want to load ASDF system you can (ql:quickload :ningle-test)

Body parameters containing an object with an array only keeps the first item.

Create a simple Ningle server -

(defvar *app* (make-instance 'ningle:<app>))

(setf (ningle:route *app* "/" :method :POST)
      (lambda (params)
        (format t "handler: ~A" ningle:*request*)))

(clack:clackup *app*)

Then send some json to it -

curl -H "Content-Type: application/json" -X POST -d '{"items":[{"id": 0, "shnorp": "incplork"}, {"id": 1, "shnorp": "ookwonk"}, {"id": 3, "shnorp": "erk"}]}' http://localhost:5000/

The only item that comes into BODY-PARAMETERS is the first item in that array

:BODY-PARAMETERS ((items (shnorp . incplork) (id . 0)))

The parameters passed into lack.request:make-request does contain the full array, so somewhere between there and my handler being called it gets truncated. I cannot work out where.

Context not accessible

Hello,

context seems not working in ningle:

CL-USER> (ql:quickload :ningle)
To load "ningle":
  Load 1 ASDF system:
    ningle
; Loading "ningle"
.........................
(:NINGLE)
CL-USER> (defvar *app* (make-instance 'ningle:<app>))
*APP*
CL-USER> 
(setf (ningle:route *app* "/")
      "Welcome to ningle!")
"Welcome to ningle!"
CL-USER> (clack:clackup (lack:builder *app*))
To load "lack-middleware-backtrace":
  Load 1 ASDF system:
    lack-middleware-backtrace
; Loading "lack-middleware-backtrace"

To load "clack-handler-hunchentoot":
  Load 1 ASDF system:
    clack-handler-hunchentoot
; Loading "clack-handler-hunchentoot"

Hunchentoot server is started.
Listening on localhost:5000.
#S(CLACK.HANDLER::HANDLER
   :SERVER :HUNCHENTOOT
   :ACCEPTOR #<SB-THREAD:THREAD "clack-handler-hunchentoot" RUNNING
                {10048B4133}>)
CL-USER> (setf (ningle:context :foo) 'bar)

The value NIL is not of type HASH-TABLE.
   [Condition of type TYPE-ERROR]

Restarts:
 0: [RETRY] Retry SLIME REPL evaluation request.
 1: [*ABORT] Return to SLIME's top level.
 2: [ABORT] abort thread (#<THREAD "new-repl-thread" RUNNING {100521F363}>)

Backtrace:
  0: (SB-KERNEL:%PUTHASH :FOO NIL BAR) [tl,external]
  1: (SB-INT:SIMPLE-EVAL-IN-LEXENV (SETF (NINGLE.CONTEXT:CONTEXT :FOO) (QUOTE BAR)) #<NULL-LEXENV>)
  2: (EVAL (SETF (NINGLE.CONTEXT:CONTEXT :FOO) (QUOTE BAR)))

Am I doing something wrong or there is some problem with Ningle/Clack?

params accumulating data from requirements

It seems like when i make multiple requests to a route with a requirement, weird things happen to the params input

(defvar *app* (make-instance 'ningle:app))

(setf (ningle:requirement *app* :test)
      #'(lambda (_)
          (declare (ignore _))
          (format t "Hit Requirement :test~%")
          t))

(setf (ningle:route *app* "/test" :test t)
      #'(lambda (params)
          (format t "~A~%" params)
          ""))

(defvar *server* (clack:clackup
                 (lack:builder *app*)
                 :server :hunchentoot :port 5001))

With this code, the first time i request /test the following prints to stdout

((TEST))

The second time the request is made the output is

((TEST) (TEST))

And so on as I keep making requests. Am I supposed to do something to reset the value?

Thanks for any help.

Documentation issue

Hi,

I was a little stumped whilst trying to figure out params in the examples given and found this - it might be worth amending.

The documentation says this:

(setf (ningle:route app "/hello/:name")
#'(lambda (params)
(format nil "Hello, ~A" (assoc "name" params :test #'string=))))

but I had to change it to this to get it working:

(setf (ningle:route app "/hello/:name")
#'(lambda (params)
(format nil "Hello, ~@A" (cdr (assoc :name params)))))

Note the addition of the cdr function and use of a keyword parameter rather than a string. When I used the original it didn't find anything unless I capitalized the string which felt bad to me.

Feel free to ignore or set me right if I've missed something.

static-vectors dependency not supporting 64-bit windows

I realize there's only a small minority using a system like this on a Windows machine, but loading the package :ningle from quicklisp results in some CFFI grovel errors related to the static-vectors package not supporting Windows' 64-bit architecture. I told the debugger to "Accept and continue", at which point I was successful at following the basic tutorial for the ningle package and serving basic text on a couple of ports. I'm not sure what is or may be affected by having continued past the static-vectors package errors, or if a workaround is worth pursuing, but I thought I would bring it to your attention. Great work, and thanks!

Serving arbitrary binary data

Hi there, great work on this project!

What would be the best way to serve an arbitrary (binary) file that is outside the static files root directory? E.g. say the request is "localhost/download/path/to/file.pdf".

Is ningle a fork of caveman?

My understanding is that ningle is used by caveman and that caveman offers some higher-level features. Perhaps the ningle README should be updated to indicate that ningle is one of the components of the used by caveman, but can also be used on it's own.

sbcl-ningle: permission denied

Hi,

I installed with guix. Could this be a guix specific issue?

When I try to build ningle I get the error below:

~ λ rlwrap sbcl
This is SBCL 2.1.9, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.

SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses.  See the CREDITS and COPYING files in the
distribution for more information.
CL-USER(1): (asdf:make :ningle)
WARNING: System definition file #P"/gnu/store/zwkhqlyijf8bwpm3a70f7ly978w2knqw-sbcl-flexi-streams-1.0.19/share/common-lisp/sbcl/flexi-streams/flexi-streams.asd" contains definition for system "flexi-streams-test". Please only define "flexi-streams" and secondary systems with a name starting with "flexi-streams/" (e.g. "flexi-streams/test") in that file.

debugger invoked on a SB-INT:SIMPLE-FILE-ERROR in thread
#<THREAD "main thread" RUNNING {100B790173}>:
  Error opening #P"/gnu/store/8jn98z4pr2r9dglsw37a70y8nl6ksb9i-sbcl-ningle-0.3.0-1.50bd4f0/lib/common-lisp/sbcl/ningle/context-tmpGHU3ALSV.fasl":

    Permission denied

Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.

restarts (invokable by number or by possibly-abbreviated name):
  0: [CONTINUE                     ] Retry opening.
  1: [USE-VALUE                    ] Try opening a different file.
  2: [RETRY                        ] Retry
                                     compiling #<CL-SOURCE-FILE "ningle/context" "file-type">.
  3: [ACCEPT                       ] Continue, treating
                                     compiling #<CL-SOURCE-FILE "ningle/context" "file-type">
                                     as having been successful.
  4:                                 Retry ASDF operation.
  5: [CLEAR-CONFIGURATION-AND-RETRY] Retry ASDF operation after resetting the
                                     configuration.
  6:                                 Retry ASDF operation.
  7:                                 Retry ASDF operation after resetting the
                                     configuration.
  8: [ABORT                        ] Exit debugger, returning to top level.

(SB-IMPL::FILE-PERROR #P"/gnu/store/8jn98z4pr2r9dglsw37a70y8nl6ksb9i-sbcl-ningle-0.3.0-1.50bd4f0/lib/common-lisp/sbcl/ningle/context-tmpGHU3ALSV.fasl" 13 "Error opening ~S" #P"/gnu/store/8jn98z4pr2r9dglsw37a70y8nl6ksb9i-sbcl-ningle-0.3.0-1.50bd4f0/lib/common-lisp/sbcl/ningle/context-tmpGHU3ALSV.fasl")

param alists

The new param data structure is very cumbersome to access. Even the readme misses some cdrs.

I would suggest to introduce something like (ningle:get-param params "foo") as a shortcut for (cdr (assoc "foo" params :test #'string=)) since it preserves the argument order of getf.

For :splat and :captures one could either add (ningle:get-splat params) and (ningle:get-captures params), or use equal instead of string= for ningle:get-param, making it a drop-in replacement for getf.

Also, the documentation at http://8arrow.org/ningle/ has to be updated accordingly, it sill says getf.

ningle middlewares and exposing lack's environment

Hey there,

In a way this one is related to #6 , where additional metadata can be passed as part of the context.

Currently in ningle it is possible to wrap an application using lack:builder and in turn embed additional middleware-specific data to the environment, but this data is not exposed to controllers.

For example.

(ql:quickload :ningle)
(ql:quickload :lack)
(ql:quickload :clack)

(defun my-custom-middleware ()
  (lambda (app)
    (lambda (env)
      (setf (getf env :my-custom-middleware/some-data) "Here goes my data")
      (funcall app env))))

(defparameter *app* (make-instance 'ningle:app))

(setf (ningle:route *app* "/")
      (lambda (params)
        (declare (ignore params))
        "Welcome to ningle!"))

(defparameter *wrapped-app* (lack:builder (my-custom-middleware) *app*))
(defparameter *server* (clack:clackup *wrapped-app*))

While this is valid and :my-custom-middleware/some-data ends up in the environment, this environment is never passed or exposed to controllers while ningle dispatches them, as a controller receives a single argument only, which is params.

On the other hand what is available to controllers as additional data is based on the context, but that context contains pre-defined list of keys to be passed down.

@export
(defun make-context (app env)
  "Create a new Context."
  (let ((*context* (make-hash-table)))
    (setf (context :request) (make-request app env)
          (context :response) (make-response app 200 ())
          (context :session) (getf env :lack.session))
    *context*))

Is there any way the env can be passed to controllers as well, similar to what Clack does, which would enable custom middlewares to be implemented as well?

Thanks!

Redirect function

Hi @fukamachi, should ningle have a redirect function built in like sinatra does?

This code or similar seems like it should be part of ningle:

(defun redirect (url &optional (status 302))
           (setf (getf (lack.response:response-headers ningle:*response*) :location) url)
           (setf (lack.response:response-status ningle:*response*) status))

Alex had shared the above with me.

Ed shared the following approach:

(setf (ningle:route app "/path") (lambda (params) '(list 302 (:location "https://google.com") ())))

or the following in a REPL:

https://fwoar.co/pastebin/bb324cec458c9b966c6fb15ed08018a5c63ef04a.lisp.html

I realize that caveman has a redirect function but I think ningle should maybe have one also.

WDYT?

JSON bodies not properly encapsulated

Apologies in advance if this isn't the right project for this issue; this could be considered an issue with http-body, an issue with myway, or just an issue with the way they're used together here.

The ultimate problem is that http-body is returning a single object from JSON-typed requests, instead of a list of parameters as it does for other request types. This causes various errors in routing (somewhere in myway, I think), some obvious and some subtle. A few examples:

;; Test app
(defvar *app* (make-instance 'ningle:<app>))

(setf (ningle:route *app* "/" :method :post :accept '("application/json"))
      (lambda (params)
        (format *debug-io* "Params:~%~S~%" params)
        '(200 () ("\"Hello, world\""))))

(clack:clackup *app*)

Sending non-compound JSON object as body data:

curl --verbose -X POST -d '1' -H "Content-Type: application/json" -H "Accept: application/json" http://localhost:5000

Result:

The value
  1
is not of type
  LIST
   [Condition of type TYPE-ERROR]

Sending compound data reveals the underlying issue:

curl --verbose -X POST -d '{"beep": "boop"}' -H "Accept: application/json" -H "Content-Type: application/json" http://localhost:5000

Result:

Params:
(("beep" . "boop") (:ACCEPT))

You can see that ningle (or myway) is now altering the input data because its appending onto the parsed object alist itself, rather than onto a list encapsulating that object. Weirder, if you leave the server running and perfom the call several times the input data is apparently re-used in a rather odd way:

Params:
(("beep" . "boop") (:ACCEPT))
Params:
(("beep" . "boop") (:ACCEPT) (:ACCEPT))
Params:
(("beep" . "boop") (:ACCEPT) (:ACCEPT) (:ACCEPT))

Ultimately, I think the proper fix here is for http-body to return the result of parsing a JSON request body in a list, rather than returning the decoded object itself -- you could argue that it should try to parse as many objects out of the request as it can, and return a list of all of them, but that's a whole other thing.

However, that's a breaking API change that affects every dependent of http-body, and would probably break any ningle apps that are currently taking JSON input. That would probably be a lot of trouble for a lot of people; I'm not sure if there's a good fix for this that isn't disruptive, short of creating a separate http-body2 package in http-body with the fix, and using that in a similarly forked package or class in ningle. It wouldn't be pretty, but it might at least let you have a deprecation period.

Response is broken

Directly from the documentation in a controller:

(setf (lack.response:response-status *response*) 201)
201 fell through ETYPECASE expression.
Wanted one of (NULL PATHNAME LIST (VECTOR (UNSIGNED-BYTE 8))).
   [Condition of type SB-KERNEL:CASE-FAILURE]
(setf (lack.response:response-headers *response*)
      (append (lack.response:response-headers *response*)
              (list :content-type "application/json")))
The value
  "application/json"
is not of type
  LIST
   [Condition of type TYPE-ERROR]

Clackup with ningle throws error

app.lisp

(ql:quickload '(:ningle))
(defvar *app* (make-instance 'ningle:app))

(setf (ningle:route *app* "/") "<h1>Welcome to ningle!</h1>")

Starting the server with:

clackup app.lisp

throws this error:

Backtrace for: #<SB-THREAD:THREAD "hunchentoot-worker-127.0.0.1:54316" RUNNING {1003ACF383}>
0: (SB-VM::CALL-SYMBOL)
1: ((LAMBDA (LACK.MIDDLEWARE.BACKTRACE::ENV) :IN "/Users/rajasegarchandran/.roswell/lisp/quicklisp/dists/quicklisp/software/lack-20210531-git/src/middleware/backtrace.lisp") (:REQUEST-METHOD :GET :SCRIPT-NAME "" :PATH-INFO "/" :SERVER-NAME "localhost" :SERVER-PORT 5000 :SERVER-PROTOCOL :HTTP/1.1 ...))
2: ((:METHOD HUNCHENTOOT:ACCEPTOR-DISPATCH-REQUEST (CLACK.HANDLER.HUNCHENTOOT::CLACK-ACCEPTOR T)) #<CLACK.HANDLER.HUNCHENTOOT::CLACK-ACCEPTOR (host 127.0.0.1, port 5000)> #<HUNCHENTOOT:REQUEST {10047CD463}>) [fast-method]
3: ((:METHOD HUNCHENTOOT:HANDLE-REQUEST (HUNCHENTOOT:ACCEPTOR HUNCHENTOOT:REQUEST)) #<CLACK.HANDLER.HUNCHENTOOT::CLACK-ACCEPTOR (host 127.0.0.1, port 5000)> #<HUNCHENTOOT:REQUEST {10047CD463}>) [fast-method]
4: ((:METHOD HUNCHENTOOT:PROCESS-REQUEST (T)) #<HUNCHENTOOT:REQUEST {10047CD463}>) [fast-method]
5: (HUNCHENTOOT::DO-WITH-ACCEPTOR-REQUEST-COUNT-INCREMENTED #<CLACK.HANDLER.HUNCHENTOOT::CLACK-ACCEPTOR (host 127.0.0.1, port 5000)> #<FUNCTION (LAMBDA NIL :IN HUNCHENTOOT:PROCESS-CONNECTION) {10045A5F3B}>)
6: ((:METHOD HUNCHENTOOT:PROCESS-CONNECTION (HUNCHENTOOT:ACCEPTOR T)) #<CLACK.HANDLER.HUNCHENTOOT::CLACK-ACCEPTOR (host 127.0.0.1, port 5000)> #<USOCKET:STREAM-USOCKET {1003ACA9A3}>) [fast-method]
7: ((:METHOD HUNCHENTOOT:PROCESS-CONNECTION :AROUND (HUNCHENTOOT:ACCEPTOR T)) #<CLACK.HANDLER.HUNCHENTOOT::CLACK-ACCEPTOR (host 127.0.0.1, port 5000)> #<USOCKET:STREAM-USOCKET {1003ACA9A3}>) [fast-method]
8: ((:METHOD HUNCHENTOOT:PROCESS-CONNECTION :AROUND (CLACK.HANDLER.HUNCHENTOOT::CLACK-ACCEPTOR T)) #<CLACK.HANDLER.HUNCHENTOOT::CLACK-ACCEPTOR (host 127.0.0.1, port 5000)> #<USOCKET:STREAM-USOCKET {1003ACA9A3}>) [fast-method]
9: ((FLET HUNCHENTOOT::PROCESS-CONNECTION% :IN HUNCHENTOOT::HANDLE-INCOMING-CONNECTION%) #<CLACK.HANDLER.HUNCHENTOOT::CLACK-ACCEPTOR (host 127.0.0.1, port 5000)> #<USOCKET:STREAM-USOCKET {1003ACA9A3}>)
10: ((LAMBDA NIL :IN BORDEAUX-THREADS::BINDING-DEFAULT-SPECIALS))
11: ((FLET SB-UNIX::BODY :IN SB-THREAD::RUN))
12: ((FLET "WITHOUT-INTERRUPTS-BODY-11" :IN SB-THREAD::RUN))
13: ((FLET SB-UNIX::BODY :IN SB-THREAD::RUN))
14: ((FLET "WITHOUT-INTERRUPTS-BODY-4" :IN SB-THREAD::RUN))
15: (SB-THREAD::RUN)
16: ("foreign function: call_into_lisp")
17: ("foreign function: funcall1")
Above backtrace due to this condition:
The value "<h1>Welcome to ningle!</h1>" is not of type (OR FUNCTION SYMBOL)

Request:
    REQUEST-METHOD: :GET
    SCRIPT-NAME: ""
    PATH-INFO: "/"
    SERVER-NAME: "localhost"
    SERVER-PORT: 5000
    SERVER-PROTOCOL: :HTTP/1.1
    REQUEST-URI: "/"
    URL-SCHEME: "http"
    REMOTE-ADDR: "127.0.0.1"
    REMOTE-PORT: 54316
    QUERY-STRING: NIL
    RAW-BODY: #<FLEXI-STREAMS:FLEXI-IO-STREAM {1004AA7633}>
    CONTENT-LENGTH: NIL
    CONTENT-TYPE: NIL
    CLACK.STREAMING: T
    CLACK.IO: #<CLACK.HANDLER.HUNCHENTOOT::CLIENT {1004F1EF33}>
    HEADERS:
        host: "localhost:5000"
        connection: "keep-alive"
        sec-ch-ua: "\"Chromium\";v=\"92\", \" Not A;Brand\";v=\"99\", \"Google Chrome\";v=\"92\""  
        sec-ch-ua-mobile: "?0"
        upgrade-insecure-requests: "1"
        user-agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36"
        accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
        sec-fetch-site: "none"
        sec-fetch-mode: "navigate"
        sec-fetch-user: "?1"
        sec-fetch-dest: "document"
        accept-encoding: "gzip, deflate, br"
        accept-language: "en-GB,en-US;q=0.9,en;q=0.8,es-ES;q=0.7,es;q=0.6,fr;q=0.5"
        x-hl-auth-token: "rhbq078s7umafk8bvlqlmmb6kpqdv9egd2inb7jq"
The value
  "<h1>Welcome to ningle!</h1>"
is not of type

But the same works inside SLIME perfectly.

Ningle + Lack builder fails as per example

(import 'lack.builder:builder)

(clack:clackup
(builder
:session
app))

The above causes the web server not to respond. I've tested clack/lack only and there is no issue. Issue present when app is an instance of ningle:app

How do I properly set cookies?

I can set cookies using the :headers property in ningle:*response*, but if I try to use :set-cookies` which is also part of the response struct, it just silently fails. I can see in the browser console no cookies are set:

(setf (lack.response:response-set-cookies ningle:*response*)
                "foobar=barbaz")
Screenshot 2023-08-29 at 11 24 54 PM

What is the correct way to set cookies?

clack:clackup fails after quicklisp update with ETYPECASE on <app>

Hey,

with

clack-20150505-git
ningle-20150505-git

in ~/quicklisp/dists/quicklisp/software

I am trying to follow your guide:

CL-USER> (ql:quickload :ningle)
To load "ningle":
  Load 1 ASDF system:
    ningle
; Loading "ningle"
.........................
(:NINGLE)
CL-USER> (defvar *app* (make-instance 'ningle:<app>))
*APP*
CL-USER> (setf (ningle:route *app* "/")
      "Welcome to ningle!")
"Welcome to ningle!"
CL-USER> (clack:clackup *app*)

And I fall into:

#<NINGLE.APP:<APP>
  {10066DCCF3}> fell through ETYPECASE expression.
Wanted one of ((OR PATHNAME STRING) FUNCTION).
   [Condition of type SB-KERNEL:CASE-FAILURE]

Restarts:
 0: [RETRY] Retry SLIME REPL evaluation request.
 1: [*ABORT] Return to SLIME's top level.
 2: [ABORT] abort thread (#<THREAD "repl-thread" RUNNING {1004D28033}>)

Backtrace:
  0: (SB-KERNEL:CASE-FAILURE ETYPECASE #<NINGLE.APP:<APP> {10066DCCF3}> ((OR PATHNAME STRING) FUNCTION))
  1: (CLACK:CLACKUP #<NINGLE.APP:<APP> {10066DCCF3}>)
  2: (SB-INT:SIMPLE-EVAL-IN-LEXENV (CLACK:CLACKUP *APP*) #<NULL-LEXENV>)
  3: (EVAL (CLACK:CLACKUP *APP*))

Do you have any ideas what's wrong here?

Cheers,
Slawek

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.