Skip to content

A bi-directional "routes as data" routing library for CLJS with a route syntax inspired by Express.

License

Notifications You must be signed in to change notification settings

runningskull/trout

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

52 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Trout

Trout is a bi-directional "routes are data" route matching/lookup library for ClojureScript that aims to be intuitive to use.

  • Route syntax inspired by Node's path-to-regexp library
  • Routes are just vectors - even ones created via strings.
    Extend & compose them to your heart's content.
  • No keeping track of routes for you; no macros necessary.

Clojars Project

TL;DR

Syntax ...
"/foo"            ["foo"]                  ; string path segment 
"/foo/:bar"       ["foo" :bar]             ; named parameter
"/foo/:bar?"      ["foo" :bar/?]           ; named parameter (optional)
"/foo/:bar*"      ["foo" :bar/*]           ; named parameter (zero or more)
"/foo/:bar+"      ["foo" :bar/+]           ; named parameter (one or more)
"/foo/:bar(\\d+)" ["foo" [:bar #"(\d+)"]]  ; named parameter w/ custom pattern
"/foo/(.*)"       ["foo" #"(.*)"]          ; unnamed parameter
"/foo/*"          ["foo" '*]               ; unnamed parameter (shorthand for (.*))
Usage ...
(require '[trout.core :as t])

(def my-route (t/route "/user/:user-id/invoices/:invoice-id(inv-\d+)"))

(t/matches? my-route "/user/abc123/invoices/inv-456") ;;=> true
(t/match my-route "/user/abc123/invoices/inv-456")    ;;=> {:user-id "abc123", :invoice-id "inv-456"}

(t/->str my-route {:user-id "xyz789"
                   :invoice-id "inv-123"})   ;;=> "/user/xyz789/invoices/inv-123

Usage

Require the library:

(require '[trout.core :as t])

Make a simple route:

(t/route "/user/:id")
(t/route ["user" :id]) ;; equivalent route in vector notation

Routes are vectors. Do vector things to them:

(let [route (t/route "/user/:id/settings")]

  (str (cons "my-site" route))  ;;=> "/my-site/user/:id/settings"
  (str (conj route "billing"))  ;;=> "/user/:id/settings/billing"
  (str (assoc route 1 :ident))  ;;=> "/user/:ident/settings"
  (str (pop route))             ;;=> "/user/:id"

  ;; seq functions return seqs. call (route) on them again:

  (t/route (reverse route))            ;;=> /settings/:id/user
  (t/route (rest route))               ;;=> :id/settings
  (t/route (concat route
                   (t/route "/foo")))  ;;=> /user/:id/settings/foo
  ))

Matching and Parsing

Test a route against some paths:

(let [route (t/route "/user/:id")]

  (t/matches? route "/user/123")  ;;=> true
  (t/matches? route "/user/123/") ;;=> true  (trailing slashes are configurable)

  (t/matches? route "/some/other/route") ;;=> false
  )

Extract the parameters:

(let [route (t/route "/user/:user-id/dirs/:dirname")]

  (t/match route "/user/123/dirs/foobar") ;;=> {:user-id "123", :dirname "foobar"}
  (t/match route "/user/xyz/dirs/789")    ;;=> {:user-id "xyz", :dirname "789"}

  (t/match route "/not/a/valid/match")    ;;=> nil
  )

Test multiple routes at once:

(let [routes [(t/route "/user/:id")                ;; route-string syntax
              (t/route ["user" :id "settings"])]]  ;; vector syntax

  (t/matches? routes "/user/123/settings") ;;=> true
  (t/match routes "/user/123")             ;;=> {:id "123"}

  (t/matches? routes "/user/settings")     ;;=> false
  (t/match routes "/log-me-in")            ;;=> nil
  )

Route Handlers

Trout can call route handlers for you, provided a map of routes and a matching map of handlers. Handlers will be passed the parsed parameters as a map:

(defn show-page [{:keys [page-id]}]
  (js/alert (str "Welcome to " page-id " page")))

(defn greet-user [{:keys [user-id]}]
  (js/alert (str "Welcome, User " user-id)))


(let [routes {:home (t/route "/home/:page-id")
              :user (t/route "/user/:user-id")}
      handlers {:home show-page
                :user greet-user}]

  (t/handle! routes handlers "/home/features") ;;=> Welcome to features page
  (t/handle! routes handlers "/user/123")      ;;=> Welcome, User 123
  )

If you prefer, you can use indexed collections instead of maps:

(let [routes [(t/route "/home/:page-id"),
              (t/route "/user/:user-id")]
      handlers [show-page
                greet-user]]

  (t/handle! routes handlers "/home/pricing")  ;;=> Welcome to page pricing
  (t/handle! routes handlers "/user/abc-123")  ;;=> Welcome, User abc-123
  )

You can also provide a callback if the route isn't found:

(defn not-found-handler []
  (str "NOT FOUND!"))

(let [routes [(t/route "/foo/:bar")]
      handlers [#(str "handled" (:bar %))]]

  (t/handle! routes handlers "/not/real" not-found-handler)  ;;=> NOT FOUND!
  )

Generating Strings

You can generate a string from a route + arguments:

(let [route (t/route "/user/:id")]

  (t/->str route {:id "123"}) ;;=> "/user/123"
  (t/->str route {:id 123})   ;;=> "/user/123"
  )

Invalid input will throw a js/TypeError:

(let [route (t/route "/user/:id(\d+)")]

  (t/->str route {})           ;;=> throws js/TypeError
  (t/->str route {:id "xyz"})  ;;=> throws js/TypeError
  )

Modifiers like optional & repeat are respected:

(let [route (t/route "/user/:uid(\d+)/~/:folder-name+/:filename?")]
  
  (t/->str route {:uid 123
                  :folder-name ["docs" "invoices"]  ;;=> "/user/123/~/docs/invoices/Jan2015.pdf"
                  :filename "Jan2015.pdf"})

  (t/->str route {:uid 123
                  :folder-name []                   ;;=> throws js/TypeError (folder-name is empty)
                  :filename "whoops.pdf"})
  )

Miscellany

Navigation

Trout can change the browser's location for you:

(let [route (t/route "/user/:id")]

  (t/navigate! route {:id 123})  ;;=> document location will be ".../user/123"
  )
Match Against...

To match against a whole URL instead of a relative path:

(t/matches? route (t/url "http://site.com/user/123"))

You can also match against Location objects:

(let [route (t/route "/user/:id")
      loc (.-location js/document)] ;; href = "http://site.com/user/123"

  (t/matches? route loc)  ;;=> true
  )

Note: Actually any javascript object with a .-href will do.

Common Patterns

To use a hash-bang prefix (more)

(set! trout.settings/*prefix* "#!")

(let [route (t/route "/user/:id")]
  
  (t/matches? route "/#!/user/123")  ;;=> true
  (t/->str route {:id "456"})        ;;=> "/#!/user/456"
)
Convenience Functions
(str (t/concat "/user"
               [:id]
               "/settings/:page-id"
               ["alerts"])
;;=> "/user/:id/settings/:page-id/alerts

Route Syntax

Trout implements1 Express-style route syntax. The vector syntax stays as close as possible to string syntax, so there's nothing new to learn.

This section assumes you're familiar with the syntax, and provides a mapping from string <-> vector notations.

Named Parameters

(= (route "/foo/:bar")
   (route ["foo" :bar])) ;;=> true

Suffixed Parameters

Optional

(= (route "/foo/:bar?")
   (route ["foo" :bar/?])) ;;=> true

Zero or more

(= (route "/foo/:bar*")
   (route ["foo" :bar/*])) ;;=> true

One or more

(= (route "/foo/:bar+")
   (route ["foo" :bar/+])) ;;=> true

Custom Match Parameters

(= (route "/foo/:bar(\\d+)")
   (route ["foo" [:bar #(\d+)]])) ;;=> true

Note: make sure to enclose your pattern in parens

Unnamed Parameters

(= (route "/foo/(.*)")
   (route ["foo" #"(.*)"])) ;;=> true

Note: make sure to enclose your pattern in parens

Asterisk

(= (route "/foo/*")
   (route ["/foo" '*])) ;;=> true

Configuration

You can configure various aspects of Trout by set!ing the variables in trout.settings:

(require '[trout.settings :as ts])

;;;; These are the default values for each setting. 
;;;; You can change them to suit your needs.

;; Change the separator used to split & join paths
(set! ts/*path-separator* "/")

;; Set to `false` for stricter matching
(set! ts/*allow-trailing-slashes* true)

;; A function that takes a path string, and presumably side-effects
(set! ts/*navigator* (fn [path-str]
                       (.replace js/location path-str)))

;; A (string) path segment to expect on inputs & include in outputs
(set! ts/*prefix* nil)

Development

From a lein repl (or cider-jack-in), choose from:

;; Node repls are the easiest to work with
(cljs-repl :node)

;; Phantom repl works great for eval, but cider-load-[buffer|file] don't work,
;; making the development workflow a bit clunky. But sometimes you need a browser:
(cljs-repl :phantom)

Run tests with lein test. You'll need phantom installed.

Helpful lein aliases: lein clean-build, lein clean-repl, lein clean-test

Links

(Un) License

This is free and unencumbered software released into the public domain. For more information see UNLICENSE or unlicense.org.

Homepage

File an issue or pull-request at http://github.com/Rafflecopter/trout


Notes:

1: Right now we're aiming for "mostly-compatible", but might try to provide rigorous support in the future. PR's welcome ;)

About

A bi-directional "routes as data" routing library for CLJS with a route syntax inspired by Express.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Clojure 100.0%