Giter Site home page Giter Site logo

silk's Introduction

Silk Current Version

Isomorphic Clojure[Script] Routing

Design Goals and Solutions

Compatible with Clojure and ClojureScript

The core functionality of Silk in domkm.silk is fully compatible with both Clojure and ClojureScript and was designed to be so from the ground up. Server-specific code for use with Ring is in domkm.silk.serve. There is currently no browser-specific code, though there probably will be in the future.

Extensible Routes

An isomorphic routing library must be extensible for environment-specific constraints. For example, on the server-side we may want a route to only respond to GET HTTP requests, while on the browser-side we want that same route to work even though there is no concept of incoming HTTP requests.

This is easy to do with Silk's core protocol, domkm.silk/Pattern, and is shown below.

Bidirectional Routes

Routes should be bidirectional (commonly referred to as named). If you are serving resources, the likelihood is that you are also providing links to other resources. Your code and routes quickly become coupled and brittle without support for URL formation. Unidirectional routes (like Compojure) will, eventually, break; bidirectional routes will not.

Silk routes are named and a specific route can be retrieved from a collection of routes by its name.

Decoupled Matching and Handling

This architectural principle is especially important in isomorphic applications. Matching will probably be similar on the server-side and browser-side (except for extensions mentioned above) but handling will likely be completely different.

Names are not restricted to strings or keywords, so a name could be a handler function. Or you could store handler functions elsewhere and look them up by route name. Silk does not impose restrictions.

Data, Not Functions/Macros

Data structures can be generated, transformed, and analyzed at runtime and, perhaps more importantly, can be inspected and printed in meaningful ways. Macro DSLs and function composition make these things very difficult.

Silk routes are data structures. They are not nested functions and are not created with complex macros. They are easy to create, manipulate, and inspect from Clojure and ClojureScript.

Reasonably Convenient

Silk has a few special rules to make route definition and use fairly terse. However, since routes are just data and are therefore easy to create and compose, users can easily define more convenient syntax for their specific use cases.

Reasonably Performant

This goal is not yet met. Well, it may be, but there are no benchmarks yet.

Use

Take a few minutes to learn Silk

Patterns can be matched and unmatched with domkm.silk/match and domkm.silk/unmatch respectively.

Strings only match themselves.

(silk/match "foo" "foo")
;=> {}
(silk/unmatch "foo" {})
;=> "foo"

Keywords are wildcards.

(silk/match :foo "bar")
;=> {:foo "bar"}
(silk/unmatch :foo {:foo "bar"})
;=> "bar"

There are also built in patterns for common use cases.

(silk/match (silk/int :answer) "42")
;=> {:answer 42}
(silk/unmatch (silk/int :answer) {:answer 42})
;=> "42"

(silk/match (silk/uuid :id) "c11902f0-21b6-4645-a218-9fa40ef69333")
;=> {:id #uuid "c11902f0-21b6-4645-a218-9fa40ef69333"}
(silk/unmatch (silk/uuid :id) {:id #uuid "c11902f0-21b6-4645-a218-9fa40ef69333"})
;=> "c11902f0-21b6-4645-a218-9fa40ef69333"

(silk/match (silk/cat "user-" (silk/int :id)) "user-42")
;=> {:id 42}
(silk/unmatch (silk/cat "user-" (silk/int :id)) {:id 42})
;=> "user-42"

(silk/match (silk/? :this {:this "that"}) "foo")
;=> {:this "foo"}
(silk/match (silk/? :this {:this "that"}) nil)
;=> {:this "that"}

Patterns can be data structures.

(silk/match ["users" (silk/int :id)] ["users" "42"])
;=> {:id 42}

A route can be created with a 2-tuple. The first element is a route name and the second element is something that can be turned into a URL pattern. If the second element is a vector, the first and second elements are associated into the third element under :path and :query keys respectively. If the second element is a map, it is left unchanged.

(silk/url-pattern [["users" "list"] {"filter" :filter "limit" :limit} {:scheme "https"}])
;=> {:path ["users" "list"], :query {"filter" :filter, "limit" :limit}, :scheme "https"}

(silk/url-pattern {:path ["users" "list"] :query {"filter" :filter "limit" :limit} :scheme "https"})
;=> {:path ["users" "list"], :query {"filter" :filter, "limit" :limit}, :scheme "https"}

(silk/route [:route-name [["users" "list"] {"filter" :filter "limit" :limit} {:scheme "https"}]])
;=> #<Route domkm.silk.Route@6ebe4324>

Routes are patterns.

(silk/match (silk/route [:route-name [["users" :username]]]) {:path ["users" "domkm"]})
;=> {:username "domkm", :domkm.silk/name :route-name, :domkm.silk/pattern {:path ["users" :username]}}
(silk/unmatch (silk/route [:route-name [["users" :username]]]) {:username "domkm"})
;=> #domkm.silk.URL{:scheme nil, :user nil, :host nil, :port nil, :path ["users" "domkm"], :query nil, :fragment nil}

None of that is particularly useful unless you can match and unmatch route collections. Fortunately, a collection of routes is also a pattern.

(def user-routes
  (silk/routes [[:users-index [["users"]]]
                [:users-show [["users" (silk/int :id)]]]]))

(silk/match user-routes {:path ["users" "42"]})
;=> {:id 42, :domkm.silk/name :users-show, :domkm.silk/routes #<Routes domkm.silk.Routes@c6f8bbc>, ...}
(silk/unmatch user-routes {:id 42 :domkm.silk/name :users-show})
;=> #domkm.silk.URL{:scheme nil, :user nil, :host nil, :port nil, :path ["users" "42"], :query nil, :fragment nil}

If you don't care about the match order, you can create routes with a map.

(def page-routes
  (silk/routes {:home-page [[]] ; match "/"
                :other-page [["pages" :title]]}))

Routes can be constrained by request methods.

(def api-routes
  (silk/routes {:api-data [["api"] {"limit" (silk/? (silk/int :limit) {:limit 100})
                                    "offset" (silk/? (silk/int :offset) {:offset 0})} (serve/POST)]}))


(silk/match api-routes {:path ["api"]})
;=> nil
(silk/match api-routes {:path ["api"] :request-method :post})
;=> {:limit 100, :offset 0, :domkm.silk/name :api-data, ...}

Routes can be combined.

(def all-routes
  (silk/routes [user-routes
                page-routes
                [:comments [["comments"] {"id" (silk/uuid :id)}]]
                api-routes]))

All matching and unmatching is pure and bidirectional.

Matching and unmatching patterns is powerful and pure but quite verbose. Silk provides a higher-level interface via domkm.silk/arrive and domkm.silk/depart

(silk/arrive all-routes "/pages/about")
;=> {:title "about", :domkm.silk/name :other-page, ...}

You can also provide a handler function.

(silk/arrive all-routes "/pages/about" :title)
;=> "about"

Unmatching is almost as easy.

(silk/depart all-routes :other-page {:title "about"})
;=> "/pages/about"

As with domkm.silk/arrive, you can provide a handler function.

(silk/depart all-routes :other-page {:title "about"} clojure.string/upper-case)
;=> "/PAGES/ABOUT"

Go forth and route!

Status

Silk is very much a work-in-progress. Everything is subject to change.

If you have suggestions, please do share them.

License

Copyright © 2014 Dom Kiva-Meyer

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.

silk's People

Contributors

adamfrey avatar deraen avatar domkm avatar jeluard avatar stuarth avatar tomconnors 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

silk's Issues

default query parameter not being merged within [:domkm.silk/url :query]

I'was kind of rewriting a buddy example https://github.com/niwibe/buddy/blob/master/examples/sessionexample/src/sessionexample/core.clj which uses another routing library and came to this:

The :next default key is present but not within [:domkm.silk/url :query]

(silk/arrive 
  (silk/routes [[:home-page [["login"] {"next" (silk/? :next {:next "/"}) }] ]]) 
  "/login?foo=bar")
dev=> (pp)
{:domkm.silk/url
 {:scheme nil,
  :user nil,
  :host nil,
  :port nil,
  :path ["login"],
  :query {"foo" "bar"},
  :fragment nil},
 :domkm.silk/routes #<Routes domkm.silk.Routes@5c383a2f>,
 :domkm.silk/pattern
 {:query
  {"next"
   #<silk$_QMARK_$reify__10692 domkm.silk$_QMARK_$reify__10692@6f7412f5>},
  :path ["login"]},
 :domkm.silk/name :home-page,
 :next "/"}

Two points:
When you have just one query parameter and it's defined as default.

  1. The default mechanism does not trigger unless you have a bogus query parameter as above
dev=> (silk/arrive 
        (silk/routes [[:home-page 
                       [["login"] {"next" (silk/? :next {:next "/"}) }] ]]) 
        "/login")
nil

That forces me to write two rules for this case.

  1. Even when it triggers the default value does not appear in :query

silk group & is this a proper way of using ?

Hi,

First, thnx for Silk. I'm trying to use it. I miss a group where I can post doubts.

I've coded the following in order to use silk at client: Any thoughts on this ? Is this a proper way of using silk ?

(def client-routes
  (silk/routes [[:home-page [[]]]
                [:empresa [["empresa"]]]]))

(def client-handlers
  {:home-page (fn [silk-url] (swap! app-state assoc :view :principal))
   :empresa (fn [silk-url] (swap! app-state assoc :view :empresa))})

(defn fn-handler-by-map-key
  "Generates a silk handler function that is capable of looking up the handler-map by :domkm.silk/name and
  call the function returned"
  [handler-map no-match-fn]
  (fn [silk-result]
    (if (nil? silk-result)
      (no-match-fn)
      (let [k (:domkm.silk/name silk-result)
            h (get handler-map k)]
        (if-not h
          (prn "Client handler for key " k " not found")
          (if-not (fn? h)
            (prn "Client handler for key " k " is not a function")
            (h (:domkm.silk/url silk-result))))))))

(def silk-handler (fn-handler-by-map-key client-handlers
                                         #(swap! app-state assoc :view :principal)))

(defn nav-callback
  [{:keys [token]}]
  (silk/arrive client-routes token silk-handler))

(gevents/listen history/history history-event/NAVIGATE
                  (fn [e]
                    (nav-callback {:token (.-token e)
                                   :type (.-type e)
                                   :navigation? (.-isNavigation e)})))

Generate URL with query params?

Is there any way to generate a URL for a route with some additional query parameters? I would want to do something like (silk/depart routes :login {} {:query {"returnUrl" "/somewhere"}}), but AFAICT there's no way to feed query params into depart. Am I holding it wrong?

wildcard match nested paths

I'm attempting to match the whole tree under a root path. I tried the following, but it only matches a wildcard on one level, but returns nil on anything deeper:

[:css [["css" (silk/regex :uri #"(.*)")]]]

That matches "/css/this.css" but not "/css/deeper/that.css" I'm not seeing any mention of this in the readme or the docs?

Also this always returns nil:

[:css [[(silk/regex :uri #"css(.*)")]]]

uuid conflicts with cljs.core/uuid

With latest ClojureScript release uuid conflicts with cljs.core/uuid. A simple way to fix this is to exclude uuid and it looks safe for older ClojureScript too.
I can send a PR if that sounds fine.

Feature requests: non-encoded and not-blank parameter based departs

Non-encoded departs:

UTF-8 in URLs seems to have been fine for a long time so it would be nice to have an option to not not encode departs. https://blog.mozilla.org/gen/2008/05/23/firefox-3-utf-8-support-in-location-bar/

Departs based on non-nil parameters

I would like to create routes like

[[:front-page [[""]]]
 [:front-page [["search" :search]]]

In a way that would enable me to depart based on whether a parameter is not-blank, like

(depart routes :front-page {:search nil}) ;=> "/"
(depart routes :front-page {:search "this"}) ;=> "/search/this

I see arrives are fine as they are matched against the sequence of vectors, but the depart map should be modified from

{:front-page <Route>}

to something around

{:front-page {#{} <Route> #{:search} <Route2>}}

So basically kind of like using query-params except not looking super ugly to the user.

Anyways do you think silk would be fit to support such features? I wrote infer ages ago but would be nice to use a community supported router.

Readme is really confusing

I just read your really detailed and excellent comparison of Silk, Bidi, and Secretary on some mailing list. And I'm super glad you didn't give in to the pressure to stop working on it, because theoretically and philosophically, I think it's the best one so far, and I agreed with all the pros/cons you outlined.

That said, in reading the readme page and trying to get started with it, the "Use" section is really really confusing. Nothing in there resembles the router I saw described in the mailing list. Nowhere do I see any kind of path-to-function mapping. So I'm left kind of confused how to use this. I'd love to use it, I'm sold on it, I just want to figure out how :)

It does not return ;=> {:username "domkm", :domkm.silk/name :route-name, :domkm.silk/pattern {:path ["users" :username]}}

(silk/match (silk/route [:route-name [["users" :username]]]) {:path ["users" "domkm"]})

does not return

;=> {:username "domkm", :domkm.silk/name :route-name, :domkm.silk/pattern {:path ["users" :username]}}

as docs says

as well as

(def user-routes
(silk/routes [[:users-index [["users"]]]
[:users-show [["users" (silk/int :id)]]]]))

(silk/match user-routes {:path ["users" "42"]})

AbstractMethodError from README example

I fired up a bare repl, required silk and silk.serve and pasted this example from the README:

(def api-routes
  (silk/routes {:api-data [["api"] {"limit" (silk/? (silk/int :limit) {:limit 100})
                                    "offset" (silk/? (silk/int :offset) {:offset 0})} (serve/POST)]}))

and the result is:

clojure.lang.Compiler$CompilerException: java.lang.AbstractMethodError, compiling:(boot.user4378993532146088720.clj:3:87)
          java.lang.AbstractMethodError:

any thoughts on why I'm seeing this?

Allow adding arbitrary values to routes

Right now, what I get out of silk is mostly a name of a route and then I have another map that I use to dispatch based on the name, which function to run. These two maps have to stay in sync or the system would be broken, which led me to believe they should be one map. I implemented some wrapping functions to take care of this but I think it would be better if Silk itself would provide a way to do this.

I came up with two potential APIs.

The first one keeps routes being vectors but optionally, if the first item is a map, it'll allow adding arbitrary data. If it's a map, it should define :name with the route name and then all other key/value pairs are merged with the result whet matched:

(silk/route [{:name :route-name :arbitrary :data} [["users" "list"] {"filter" :filter "limit" :limit} {:scheme "https"}]])

I think this option is the one that requires the least amount of changes but it's a bit obscure, which might be good or bad. Backwards compatibility is retained by dispatching on whether the first item is a vector or not.

The second option is making routes a hasmap:

(silk/route {:name :route-name 
                    :pattern [["users" "list"] {"filter" :filter "limit" :limit} {:scheme "https"}]]
                    :arbitrary :data})

In this case, the change to the API is a bit bigger but the result is cleaner. Again, arbitrary data is just merged into the result of matching. In both cases, keys generated by the match, like :routes or :url should override arbitrary data (this poses the problem that adding new keys in the future might break systems out there, so, maybe it shouldn't do that, an alternative would be to put arbitrary data in a key, like :extra).

In this case backwards compatibility can be kept by dispatching on whether the argument to route as a whole, is a vector or a map.

I'm happy to do the work of implementing this.

Arrive/depart error in cljs

the following from your examples works fine in clojure, not in cljs

(def user-routes
(silk/routes [[:users-index [["users"]]]
[:users-show [["users" (silk/int :id)]]]]))

(silk/arrive user-routes "users/40")
(silk/depart user-routes :users-show {:id 42})

not sure how far this extends to other functions, but makes the current distribution unuseable

Error: Assert failed: (pattern? ptrn)
at match (http://localhost:8080/out/dev/domkm/silk.js:412:9)
at domkm.silk.Route.domkm$silk$Pattern$_match$arity$2 (http://localhost:8080/out/dev/domkm/silk.js:922:64)
at _match (http://localhost:8080/out/dev/domkm/silk.js:284:15)
at match (http://localhost:8080/out/dev/domkm/silk.js:414:86)
at http://localhost:8080/out/dev/domkm/silk.js:962:68
at some (http://localhost:8080/out/dev/cljs/core.js:9035:30)
at domkm.silk.Routes.domkm$silk$Pattern$_match$arity$2 (http://localhost:8080/out/dev/domkm/silk.js:961:44)
at _match (http://localhost:8080/out/dev/domkm/silk.js:284:15)
at match (http://localhost:8080/out/dev/domkm/silk.js:414:86)
at domkm.silk.arrive.arrive__3 (http://localhost:8080/out/dev/domkm/silk.js:1009:86)

silk/composite throws an exception when unable to match

This matches:

(silk/match (silk/composite ["pre" (silk/integer :i) "post"]) "pre5post")
; => {:i 5}

But this throws a NumberFormatException (diff: extra letter 'c' after the 5)

(silk/match (silk/composite ["pre" (silk/integer :i) "post"]) "pre5cpost")
; => NumberFormatException For input string: "5c"  java.lang.NumberFormatException.forInputString (NumberFormatException.java:65)

But I expect it to return nil

Custom Patterns

Hey stoked on the tool, thank you!
I thought I'd share a little keyword pattern that I cooked up:

(defrecord IDKey [k]
  silk/Pattern
  (-match [_ s]
    (hash-map k (keyword s)))
  (-unmatch [_ params]
    (name (get params k)))
  (-match-validator [_]
    string?)
  (-unmatch-validators [_]
    {k keyword?}))

It simply coerces strings to keywords. It helps me in clojurescript land when moving data between clj and js. Cheers :)

ring-handler clobbers :params

It seems that if one uses the standard ring middle-ware 'wrap-params', that map gets lost when using ring-handler. I was thinking we could either merge the :params map provided by wrap-params with the one provided by silk, or put the silk params under a different key. Thoughts?

Can't put RequestMethodPatterns in vars

Hi there, strange problem here.
If I eval (domkm.silk.serve/POST) I see the expected {:request-method #object[etc...]} output, but if I then eval (def post-method (domkm.silk.serve/POST)) I get a clojure.lang.Compiler$CompilerException. Same thing happens with the other request methods. Have you seen this behavior? I'm on clojure 1.7.

corner case (silk/arrive (silk/routes [[:home-page [[]{}] ]]) "/")

dev=> (silk/arrive (silk/routes [[:home-page [[]] ]]) "/")
{:domkm.silk/url #domkm.silk.URL{:scheme nil, :user nil, :host nil, :port nil, :path [], :query nil, :fragment nil}, :domkm.silk/routes #<Routes domkm.silk.Routes@fc29e0>, :domkm.silk/pattern {:path []}, :domkm.silk/name :home-page}

Isn't the previous form semantically equivalent to ?
dev=> (silk/arrive (silk/routes [[:home-page [[]{}] ]]) "/")
nil
dev=>

Geraldo

`match-coll` not working as expected

I know the title is broad and unspecific.

I tried this snippet from the README and it doesn't work:

Routes can be constrained by request methods.

(def api-routes
  (silk/routes {:api-data [["api"] {"limit" (silk/? (silk/int :limit) {:limit 100})
                                    "offset" (silk/? (silk/int :offset) {:offset 0})} (serve/POST)]}))


(silk/match api-routes {:path ["api"]})
;=> nil
(silk/match api-routes {:path ["api"] :request-method :post})
;=> {:limit 100, :offset 0, :domkm.silk/name :api-data, ...}

Last silk/match call also returned nil.

I debugged the error for a while, and narrowed it down to match-coll, but I'm not sure exactly what could be happening there.

I'm posting this here so we can discuss way to fix it, and I'll be happy to submit a PR if some guidance is provided.

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.