Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ lib
clout*.jar
pom.xml
.lein-failures
.lein-*
node_modules/
target/
resources/js/clout_*.js
22 changes: 22 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
language:
- node_js
- clojure
lein: lein2
jdk:
- oraclejdk7
node_js:
- "0.8"
before_script:
- "export DISPLAY=:99:0"
- "echo 'Compile to javascript'"
- "lein2 cljsbuild once"
- "echo 'Starting buster server'"
- "./node_modules/buster/bin/buster-server &"
- "sleep 3"
- "echo 'Done.'"
- "echo 'Starting phantomjs server'"
- "phantomjs ./node_modules/buster/script/phantom.js &"
- "sleep 3"
- "echo 'Done.'"
script:
- lein2 all test && npm test
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Clout
Clout [![Build Status](https://travis-ci.org/BirdseyeSoftware/clout.png?branch=cljs)](https://travis-ci.org/BirdseyeSoftware/clout)
=====

Clout is a library for matching [Ring][1] HTTP requests. It uses the same
Expand Down Expand Up @@ -56,15 +56,15 @@ For additional performance, you can choose to pre-compile a route:
user=> (def user-route (route-compile "/user/:id"))
#'user/user-route
user=> (route-matches user-route (request :get "/user/10"))
{:user "10"}
{:id "10"}

When compiling a route, you can specify a map of regular expressions to use
for different keywords. This allows more specific routing:

user=> (def user-route (route-compile "/user/:id" {:id #"\d+"}))
#'user/user-route
user=> (route-matches user-route (request :get "/user/10"))
{:user "10"}
{:id "10"}
user=> (route-matches user-route (request :get "/user/jsmith"))
nil

Expand Down
14 changes: 14 additions & 0 deletions buster.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
var config = exports;

config['Browser Tests'] = {
environment: 'browser',
sources: [],
tests: [ "resources/js/clout_browser_test.js"
, "resources/js/clout_browser_advanced_test.js"]
};

config['Server Tests'] = {
environment: 'node',
sources: [],
tests: ["resources/js/clout_node_test.js"]
};
7 changes: 7 additions & 0 deletions dalap_rules.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
["src/clj/clout/core.clj" "src/cljs/clout/core.cljs"]
[]

["test/clj/clout/test/core.clj" "test/cljs/clout/test/core.cljs"]
[]
}
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "clout",
"description": "",
"version": "1.1.0",
"author": "",
"dependencies" : { "buster" : "0.6.11" },
"scripts": { "test": "buster-test -e node && buster-test -e browser" }
}
51 changes: 48 additions & 3 deletions project.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,51 @@
(defproject clout "1.1.0"
(defproject clout "1.1.1-SNAPSHOT"
:description "A HTTP route matching library"
:url "http://github.com/weavejester/clout"
:dependencies [[org.clojure/clojure "1.2.1"]]
:dependencies [[org.clojure/clojure "1.5.1"]
[com.birdseye-sw/buster-cljs "0.1.0"]]
:plugins [[lein-cljsbuild "0.3.0"]
[com.birdseye-sw/lein-dalap "0.1.0"]]
:hooks [leiningen.dalap]
:profiles
{:dev {:dependencies [[ring-mock "0.1.1"]]}})
{:1.4 {:dependencies [[org.clojure/clojure "1.4.0"]]}
:dev {:dependencies [[org.clojure/tools.namespace "0.2.1"]
[ring-mock "0.1.1"]]}}

:aliases { "all" ["with-profile" "dev:dev,1.4"]}
:source-paths ["src/clj"]
:test-paths ["test/clj"]

:cljsbuild
{:builds
[{:id "dev"
:source-path "src/cljs"
:compiler
{:optimizations :simple
:pretty-print true
:output-to "resources/js/clout_dev.js"}}
;;
{:id "browser-test"
:source-path "test/cljs"
:compiler
{:optimizations :simple
:pretty-print true
:externs ["externs/buster.js"]
:libraries ["resources/js/clout_dev.js"]
:output-to "resources/js/clout_browser_test.js"}}
{:id "browser-advanced-test"
:source-path "test/cljs"
:compiler
{:optimizations :advanced
:externs ["externs/buster.js"]
:libraries ["resources/js/clout_dev.js"]
:output-to "resources/js/clout_browser_advanced_test.js"}}
;;
{:id "node-test"
:source-path "test/cljs"
:compiler
{:optimizations :simple
:pretty-print true
:target :node
:externs ["externs/buster.js"]
:libraries ["resources/js/clout_dev.js"]
:output-to "resources/js/clout_node_test.js"}}]})
97 changes: 82 additions & 15 deletions src/clout/core.clj → src/clj/clout/core.clj
Original file line number Diff line number Diff line change
@@ -1,10 +1,51 @@
^{:cljs
'(ns clout.core
"Library for parsing the Rails routes syntax."
(:require [goog.string :as gstring]
[clojure.string :as string]
[goog.Uri :as uri]))}
(ns clout.core
"Library for parsing the Rails routes syntax."
(:require [clojure.string :as string])
(:import java.util.Map
java.util.regex.Matcher
(:import java.util.regex.Matcher
[java.net URLDecoder URLEncoder]))

#_(:cljs
(defn re-matcher [re s]
(let [re (js* "new RegExp(re.source, 'g')")
match (.exec re s)
matcher (js-obj)]
;; (log "* " re " " s)
;; NOTE: we avoid setting this attributes
;; through js-obj given that it doesn't work
;; on advanced compilation mode
(set! (.-lookingAt matcher)
(fn []
(and match
(= (.-index match) 0))))
(set! (.-matches matcher)
(fn []
(and match
(= s (aget match 0)))))
(set! (.-start matcher)
(fn []
(and match
(.-index match))))
(set! (.-end matcher)
(fn []
(if match
(.-lastIndex re)
(throw (js/Error. "invalid state")))))
(set! (.-groupCount matcher)
(fn []
(or (and match (.-length match))
0)))
(set! (.-group matcher)
(fn [& [i]]
(and match
(aget match (or i 0)))))
matcher)))

;; Regular expression utilties

(def ^{:private true} re-chars
Expand All @@ -15,14 +56,16 @@
[s]
(string/escape
s
#(if (re-chars %) (str \\ %))))
(reduce (fn [m c] (assoc m c (str \\ c))) {} re-chars)))

(defn re-groups*
"More consistant re-groups that always returns a vector of groups, even if
there is only one group."
[^Matcher matcher]
(for [i (range (.groupCount matcher))]
(.group matcher (int (inc i)))))
(do
;; (log ".group - " (.group matcher (int (inc i))))
(.group matcher (int (inc i))))))

;; Route matching

Expand All @@ -31,7 +74,10 @@
([path]
(path-decode path "UTF-8"))
([path encoding]
(-> (string/replace path "+" (URLEncoder/encode "+" encoding))
(-> (string/replace path "+"
^{:cljs '(gstring/urlEncode "+")}
(URLEncoder/encode "+" encoding))
^{:cljs '(gstring/urlDecode)}
(URLDecoder/decode encoding))))

(defn- assoc-vec
Expand All @@ -43,6 +89,7 @@
(if (vector? cur)
(conj cur v)
[cur v])

v)))

(defn- assoc-keys-with-groups
Expand Down Expand Up @@ -80,8 +127,10 @@
(request-url request)
(path-info request))
matcher (re-matcher re path-info)]
;; (log ".matches - " (.matches matcher))
(if (.matches matcher)
(assoc-keys-with-groups
^{:cljs '(re-groups* matcher)}
(map path-decode (re-groups* matcher))
keys)))))

Expand All @@ -94,8 +143,11 @@
(fn [[re action]]
(let [matcher (re-matcher re src)]
(if (.lookingAt matcher)
[(if (fn? action) (action matcher) action)
(subs src (.end matcher))])))
(do
;; (log ".lookingAt - " (.lookingAt matcher))
;; (log ".end - " (.end matcher))
[(if (fn? action) (action matcher) action)
(subs src (.end matcher))]))))
(partition 2 clauses)))

(defn- lex
Expand All @@ -116,33 +168,48 @@
[path]
(boolean (re-matches #"(https?:)?//.*" path)))

^{:cljs '(def -word-regexp #":([a-zA-Z_][\w\-]*)")}
(def -word-regexp #":([\p{L}_][\p{L}_0-9-]*)")

^{:cljs '(def -literal-regexp #"(:[^a-zA-Z_*]|[^:*])+")}
(def -literal-regexp #"(:[^\p{L}_*]|[^:*])+")

(defn route-compile
"Compile a path string using the routes syntax into a uri-matcher struct."
([path]
(route-compile path {}))
([path regexs]
(let [splat #"\*"
word #":([\p{L}_][\p{L}_0-9-]*)"
literal #"(:[^\p{L}_*]|[^:*])+"
word-group #(keyword (.group ^Matcher % 1))
word -word-regexp
literal -literal-regexp
word-group #(do
;; (log ".group - " (.group ^Matcher % 1))
(keyword (.group ^Matcher % 1)))
word-regex #(regexs (word-group %) "[^/,;?]+")]
(CompiledRoute.
(re-pattern
(apply str
(lex path
splat "(.*?)"
splat "(.*)"
#"^//" "https?://"
word #(str "(" (word-regex %) ")")
literal #(re-escape (.group ^Matcher %)))))
word ^{:cljs
'(fn [w]
(let [w (word-regex w)]
(str "("
(if (regexp? w) (.-source w) w)
")")))}
(fn [w] (str "(" (word-regex w) ")"))
literal #(do
;; (log ".group - " (.group ^Matcher %))
(re-escape (.group ^Matcher %))))))
(remove nil?
(lex path
splat :*
word word-group
literal nil))
(absolute-url? path)))))

(extend-type String
(extend-type java.lang.String
Route
(route-matches [route request]
(route-matches (route-compile route) request)))

23 changes: 23 additions & 0 deletions src/cljs/clout/core.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
;; This file was generated with lein-dalap from
;;
;; src/clj/clout/core.clj @ Wed Mar 13 05:25:02 PDT 2013
;;
(ns clout.core "Library for parsing the Rails routes syntax." (:require [goog.string :as gstring] [clojure.string :as string] [goog.Uri :as uri]))
(do (defn re-matcher [re s] (let [re (js* "new RegExp(re.source, 'g')") match (.exec re s) matcher (js-obj)] (set! (.-lookingAt matcher) (fn [] (and match (= (.-index match) 0)))) (set! (.-matches matcher) (fn [] (and match (= s (aget match 0))))) (set! (.-start matcher) (fn [] (and match (.-index match)))) (set! (.-end matcher) (fn [] (if match (.-lastIndex re) (throw (js/Error. "invalid state"))))) (set! (.-groupCount matcher) (fn [] (or (and match (.-length match)) 0))) (set! (.-group matcher) (fn [& [i]] (and match (aget match (or i 0))))) matcher)))
(def re-chars (set "\\.*+|?()[]{}$^"))
(defn- re-escape "Escape all special regex chars in a string." [s] (string/escape s (reduce (fn [m c] (assoc m c (str \\ c))) {} re-chars)))
(defn re-groups* "More consistant re-groups that always returns a vector of groups, even if\n there is only one group." [matcher] (for [i (range (.groupCount matcher))] (do (.group matcher (int (inc i))))))
(defn path-decode "Decode a path segment in a URI. Defaults to using UTF-8 encoding." ([path] (path-decode path "UTF-8")) ([path encoding] (-> (string/replace path "+" (gstring/urlEncode "+")) (gstring/urlDecode))))
(defn- assoc-vec "Associate a key with a value. If the key already exists in the map, create a\n vector of values." [m k v] (assoc m k (if-let [cur (m k)] (if (vector? cur) (conj cur v) [cur v]) v)))
(defn- assoc-keys-with-groups "Create a hash-map from a series of regex match groups and a collection of\n keywords." [groups keys] (reduce (fn [m [k v]] (assoc-vec m k v)) {} (map vector keys groups)))
(defn- request-url "Return the complete URL for the request." [request] (str (name (:scheme request)) "://" (get-in request [:headers "host"]) (:uri request)))
(defn- path-info "Return the path info for the request." [request] (or (:path-info request) (:uri request)))
(defprotocol Route (route-matches [route request] "If the route matches the supplied request, the matched keywords are\n returned as a map. Otherwise, nil is returned."))
(defrecord CompiledRoute [re keys absolute?] Route (route-matches [route request] (let [path-info (if absolute? (request-url request) (path-info request)) matcher (re-matcher re path-info)] (if (.matches matcher) (assoc-keys-with-groups (re-groups* matcher) keys)))))
(defn- lex-1 "Lex one symbol from a string, and return the symbol and trailing source." [src clauses] (some (fn [[re action]] (let [matcher (re-matcher re src)] (if (.lookingAt matcher) (do [(if (fn? action) (action matcher) action) (subs src (.end matcher))])))) (partition 2 clauses)))
(defn- lex "Lex a string into tokens by matching against regexs and evaluating\n the matching associated function." [src & clauses] (loop [results [] src src clauses clauses] (if-let [[result src] (lex-1 src clauses)] (let [results (conj results result)] (if (= src "") results (recur results src clauses))))))
(defn- absolute-url? "True if the path contains an absolute or scheme-relative URL." [path] (boolean (re-matches #"(https?:)?//.*" path)))
(def -word-regexp #":([a-zA-Z_][\w\-]*)")
(def -literal-regexp #"(:[^a-zA-Z_*]|[^:*])+")
(defn route-compile "Compile a path string using the routes syntax into a uri-matcher struct." ([path] (route-compile path {})) ([path regexs] (let [splat #"\*" word -word-regexp literal -literal-regexp word-group (fn* [p1__1197#] (do (keyword (.group p1__1197# 1)))) word-regex (fn* [p1__1198#] (regexs (word-group p1__1198#) "[^/,;?]+"))] (CompiledRoute. (re-pattern (apply str (lex path splat "(.*)" #"^//" "https?://" word (fn [w] (let [w (word-regex w)] (str "(" (if (regexp? w) (.-source w) w) ")"))) literal (fn* [p1__1199#] (do (re-escape (.group p1__1199#))))))) (remove nil? (lex path splat :* word word-group literal nil)) (absolute-url? path)))))
(extend-type string Route (route-matches [route request] (route-matches (route-compile route) request)))
Loading