fukamachi / caveman

Lightweight web application framework for Common Lisp.
http://8arrow.org/caveman/
775 stars 63 forks source link

Issue with model and json-encode #83

Open lifeisfoo opened 8 years ago

lifeisfoo commented 8 years ago

I'm trying Caveman2 with a postgres db and a simple books collection:

(with-connection (db)
  (execute
   (create-table (:books :if-not-exists t)
       ((id :type :bigserial
            :primary-key t)
        (title :type :text)
        (author :type :text)
        (created_at :type :timestamp)))))
@model
(defstruct book
  id
  title
  author
  created_at)

I'm expecting to get a book by id with this function:

@route GET "/books/:id"
(lambda (&key id)
  (handler-case
      (with-connection (db)
        (let ((l
                (retrieve-one
                 (select :*
                         (from :books)
                         (where (:= :id id)))
                 :as 'book)))
          (cond (l (render-json l))
                (t (throw-code 404)))))
    (dbi.error:<dbi-error> (e)
      (throw-code 500))))

But it doesn't work as expected:

Backtrace for: #<SB-THREAD:THREAD "hunchentoot-worker-127.0.0.1:63696" RUNNING {1002A0D863}>
0: ((:METHOD JONATHAN.ENCODE:%TO-JSON (STRING)) #<unavailable argument>) [fast-method]
1: (DATAFLY.JSON::CONVERT-FOR-JSON #<unavailable argument>)
2: ((:METHOD DATAFLY.JSON:ENCODE-JSON (T)) #S(LISPIBLO.WEB::BOOK :ID 1 :TITLE "MyBookTitle" :AUTHOR "MyBookAuthor" :CREATED_AT NIL) NIL) [fast-method]
...
...
debugger invoked on a TYPE-ERROR in thread
#<THREAD "hunchentoot-worker-127.0.0.1:63696" RUNNING {1002A0D863}>:
  The value "MyBookTitle" is not of type SIMPLE-STRING.

Seem that the slot value title is a (VECTOR CHARACTER 64).

I've also tried using a varchar column instead of text but the same error is thrown.

Thank you for your work.

knobo commented 8 years ago

I don't know why json-encode expects simple-string, but here is what I do:

(defun ensure-simple-string (db-result)
  (typecase db-result
    (cons (mapcar 'ensure-string db-result))
    ((vector character *) (coerce db-result 'simple-string))
    (t db-result)))

(encode-json  (ensure-simple-string db-result))
lifeisfoo commented 8 years ago

Thank you @knobo for the function, but seems that nothing is changed, the same error is thrown:

debugger invoked on a TYPE-ERROR in thread
#<THREAD "hunchentoot-worker-127.0.0.1:52138" RUNNING {1002C590D3}>:
  The value "MyBookTitle" is not of type SIMPLE-STRING.

Yesterday I found a workaround using a custom render function that use coerce (just like your function) to convert the value to a simple-string

(defun render-book-as-json (book)
  (setf (getf (response-headers *response*) :content-type) "application/json")
  (let ((json
          (with-output-to-string*
              (with-object
                (write-key-value "id" (book-id book))
                (write-key-value "title" (coerce (book-title book) 'simple-string))
                (write-key-value "author" (coerce (book-author book) 'simple-string))
                (write-key-value "created" (book-created book))))))
    (setf (response-body *response*) json)))

(render-book-as-json my-model-from-db)

Then I've found a better, at least from my point of view, solution using a custom datafly inflation function:

(defun chars-vector-to-string (val)
  (etypecase val
    (null nil)
    ((vector character) (coerce val 'simple-string))))

@model
(defstruct (book (:inflate created #'datetime-to-timestamp)
                  (:inflate (title author) #'chars-vector-to-string)) 
  id
  title
  author
  created)

Using this I can just evaluate (render-json my-book-model-from-db) to get the right json.

I don't now if this is an issue or if there is something wrong or missing in my code

I'm using hunchentoot and postgres 9.5.2.

knobo commented 8 years ago

Ref this: DO NOT expect simple-strings! This issue belongs in https://github.com/Rudolph-Miller/jonathan/issues So maybe you close it here?

lifeisfoo commented 8 years ago

Nice catch @knobo.

Seems that declare type simple-string was added in this commit, then moved to its actual position with another commit.

So, since caveman isn't using jonhatan directly, but it's using datafly encode-json function, and datafly uses jonhatan internally, I think that this is an issue of datafly.