JanetDocsSourcePlaygroundTutorialsI'm Feeling luckyCommunityGitHub sign in

Inspired by sinatra, swlkr made Osprey, more opinionated and stripped down than his earlier Joy.

Currently, only (use osprey) or (import osprey :prefix "") work, not (import osprey) due to macro issues.

Minimal

(use osprey)

(GET "/" "osprey")

(server 9001)

If you import, things won't work properly.

Handling Defaults

While in Joy, you pass a dictionary to joy/app or call the relevant middleservice in the object passed to joy/server, in Osprey you call configuration functions like layout on your layout (used on any routes returning an array, presumed to be hiccup-html):

(layout
  (doctype :html5)
  [:html
   [:head
    [:title (request :path)]]
   [:body response]])


(GET "/"
  [:h1 "home"])

With multiple layouts, you may name and call them with use-layout:

(layout :a (doctype :html5) [:html])
(GET "/something"
	(use-layout :a)
	[:div "/a!"])

Similarly, not-found sets what returns if route or file not found:

(not-found
  (status :404)
  (content-type "text/html")
  (html/encode
    [:h1 "404 Page not found"]))

The enable func sets some 4 entries a la Joy's app:

(enable :logging (fn [s _ _]
                   (def dur (* 1000 (- (os/clock) s)))
                   (print "Hey it took " dur "ms." (if (> 0.02 dur) " Not bad!"))))
                   
(enable :static-files)

# see https://github.com/swlkr/osprey/blob/master/examples/csrf-tokens.janet
(enable :sessions {:secure false})
(enable :csrf-tokens) 

before will execute code before any route:

(before # "/todo/" # with this optional arg, only for such routes
 (header "X-Powered-By" "osprey")) # header adds this to the response

In general, there is a hook system to run code before or after the handler. Something like layout or not-found add a function to the hook list, while GET or POST push to *routes* which the handler checks.

enable aids us on more complex tasks on both sides of a handler, e.g. :sessions descrypts the cookie into (dyn :session) before the handler and after the handler encrypts (dyn :session)'s current state into a Set-Cookie header.

But ultimately, this is all just a different way of handling the handler pipeline/threading macro running through middlewares or hooks, like Joy.

Some Example Programs

Swlkr wrote these:

Sessions
(use osprey
)
# enable session cookies
(enable :sessions {:secure false})


# wrap all html responses in the html below
(layout
  (doctype :html5)
  [:html {:lang "en"}
   [:head
    [:title (request :uri)]]
   [:body response]])


(GET "/"
     [:main
      (if (session :session?)
        [:p "yes, there is a session!"]
        [:p "no, there is not a session"])

      # the form helper is only available in route macros
      # it also automatically assigns method to POST
      (form {:action "/create-session"}
            [:input {:type "submit" :value "Sign in"}])

      (form {:action "/delete-session"}
            [:input {:type "submit" :value "Sign out"}])])


(POST "/create-session"
      (session :session? true)

      (redirect "/"))


(POST "/delete-session"
      (session :session? nil)

      (redirect "/"))


(server 9001)
Halt
(use osprey)

(defn protected? [request]
  (or (= (request :path) "/")
      (= (request :path) "/protected")))

# halt with a 401
(before
  (unless (protected? request)
    (halt {:status 401 :body "Nope." :headers {"Content-Type" "text/plain"}})))


# wrap all html responses with layout
(layout
  (doctype :html5)

  [:html {:lang "en"}

   [:head
    [:title (request :path)]]

   [:body response]])


# returns 200
(GET "/"
     [:h1 "welcome to osprey!"])


# halt works in handlers as well
# try curl -v 'localhost:9001?bypass='
(GET "/protected"
     (unless (params :bypass)
       (halt {:status 401 :body "Nope." :headers {"Content-Type" "text/plain"}}))

     [:h1 "Yep!"])


(server 9001)
JSON API
(use osprey)
(import json)


# put the todos somewhere
# since there isn't a database
(def todos @[])


# before everything try to parse json body
(before
  (content-type "application/json")

  (when (and body (not (empty? body)))
    (update request :body json/decode))

  # before urls with :id
  (when (params :id)
    (update-in request [:params :id] scan-number)))


# just a nice status message on root

# try this
# $ curl -v localhost:9001
(GET "/"
     (json/encode {:status "up"}))


# here's the meat and potatoes
# return the todos array from earlier

# try this
# $ curl -v localhost:9001/todos
(GET "/todos"
     (json/encode todos))


# this appends todos onto the array

# try this
# $ curl -v -X POST -H "Content-Type: application/json" --data '{"todo": "buy whole wheat bread"}' localhost:9001/todos
(POST "/todos"
      (json/encode (array/push todos body)))


# this updates todos in the array
# :id is assumed to be an integer
# since todos is an array

# try this
# $ curl -v -X PATCH -H "Content-Type: application/json" --data '{"todo": "buy whole grain bread"}' localhost:9001/todos/0
(PATCH "/todos/:id"
       (json/encode (update todos (params :id) merge body)))


# this deletes todos from the array
# :id is assumed to be an integer
# since todos is an array

# try this
# $ curl -v -X DELETE localhost:9001/todos/0
(DELETE "/todos/:id"
        (json/encode (array/remove todos (params :id))))


# start the server on port 9001
(server 9001)