Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelklishin committed Mar 31, 2012
0 parents commit 2f7ea75
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 0 deletions.
1 change: 1 addition & 0 deletions .dir-locals.el
@@ -0,0 +1 @@
((clojure-mode . ((clojure-swank-command . "lein2 jack-in %s"))))
8 changes: 8 additions & 0 deletions .gitignore
@@ -0,0 +1,8 @@
pom.xml
*jar
/lib/
/classes/
.lein-failures
.lein-deps-sum
TAGS
checkouts/*
3 changes: 3 additions & 0 deletions .travis.yml
@@ -0,0 +1,3 @@
language: clojure
lein: lein2
script: lein2 all test
96 changes: 96 additions & 0 deletions README.md
@@ -0,0 +1,96 @@
# What is Route One

Route One is a tiny Clojure library that generates HTTP resource routes (as in Ruby on Rails, Jersey, Noir and similar
modern Web application frameworks). It is meant to be used in HTTP clients, programs that deliver emails with links,
testing environments and so on.

Route One is intentionally small and has very limited feature scope.


## Documentation & Examples

``` clojure
(ns my.app
(:use [clojurewerkz.route-one.core :only [route-map route path-for named-path]]))

;; define a route map
(route-map
(route "/about" :named "about page")
(route "/faq")
(route "/help" :named "help page")
(route "/docs/:title" :named "documents"))

;; generate relative paths
(path-for "/docs/title" { :title "ohai" }) ;; => "/docs/title"
(path-for "/docs/:title" { :title "ohai" }) ;; => "/docs/ohai"
(path-for "/docs/:category/:title" { :category "greetings" :title "ohai" }) ;; => "/docs/greetings/ohai"
(path-for "/docs/:category/:title" { :category "greetings" }) ;; => IllegalArgumentException, because :title value is missing

(named-path "about page") ;; => "/about"
(named-path "documents" {:title "a-title"}) ;; => "/docs/a-title"

(with-base-url "https://myservice.com"
(url-for "/docs/title" { :title "ohai" }) ;; => "https://myservice.com/docs/title"
(url-for "/docs/:title" { :title "ohai" }) ;; => "https://myservice.com/docs/ohai"
(url-for "/docs/:category/:title" { :category "greetings" :title "ohai" }) ;; => "https://myservice.com/docs/greetings/ohai"
(url-for "/docs/:category/:title" { :category "greetings" }) ;; => IllegalArgumentException, because :title value is missing

(named-url "about page") ;; => "https://myservice.com/about"
(named-url "documents" {:title "a-title"}) ;; => "https://myservice.com/docs/a-title"
)
```

Documentation site for Urly is coming in the future (sorry!). Please see our test suite for more code examples.


## Maven Artifacts

### The Most Recent Release

With Leiningen:

[clojurewerkz/route-one "1.0.0-beta1"]

With Maven:

<dependency>
<groupId>clojurewerkz</groupId>
<artifactId>route-one</artifactId>
<version>1.0.0-beta1</version>
</dependency>


## Supported Clojure versions

Route One is built from the ground up for Clojure 1.3+ and JDK 6+.


## Route One Is a ClojureWerkz Project

Route One is part of the group of libraries known as ClojureWerkz, together with
[Neocons](https://github.com/michaelklishin/neocons), [Monger](https://github.com/michaelklishin/monger), [Langohr](https://github.com/michaelklishin/langohr), [Elastisch](https://github.com/clojurewerkz/elastisch), [Quartzite](https://github.com/michaelklishin/quartzite), [Urly](https://github.com/michaelklishin/urly) and several others.


## Continuous Integration

[![Continuous Integration status](https://secure.travis-ci.org/clojurewerkz/route-one.png)](http://travis-ci.org/clojurewerkz/route-one)

CI is hosted by [travis-ci.org](http://travis-ci.org)


## Development

Route One uses [Leiningen 2](https://github.com/technomancy/leiningen/blob/master/doc/TUTORIAL.md). Make sure you have it installed and then run tests against Clojure 1.3.0 and 1.4.0[-beta6] using

lein2 all test

Then create a branch and make your changes on it. Once you are done with your changes and all tests pass, submit
a pull request on Github.



## License

Copyright © 2012 Michael S. Klishin, Alex Petrov

Distributed under the Eclipse Public License, the same as Clojure.
15 changes: 15 additions & 0 deletions project.clj
@@ -0,0 +1,15 @@
(defproject clojurewerkz/route-one "1.0.0-beta1"
:description "A tiny URL/route generation library"
:url "http://github.com/clojurewerkz/route-one"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.3.0"]
[clojurewerkz/urly "1.0.0-rc2"]]
:source-paths ["src/clojure"]
:profiles {:1.4 {:dependencies [[org.clojure/clojure "1.4.0-beta6"]]}}
:aliases { "all" ["with-profile" "dev:dev,1.4"] }
:repositories {"clojure-releases" "http://build.clojure.org/releases",
"sonatype" {:url "http://oss.sonatype.org/content/repositories/releases",
:snapshots false,
:releases {:checksum :fail, :update :always}}}
:warn-on-reflection true)
102 changes: 102 additions & 0 deletions src/clojure/clojurewerkz/route_one/core.clj
@@ -0,0 +1,102 @@
(ns clojurewerkz.route-one.core
(:import clojurewerkz.urly.UrlLike)
(:use [clojure.string :only [split join]]
[clojurewerkz.urly.core :only [url-like]]))

;;
;; Implementation
;;

(defrecord Route [path name opts])

(def route-maps (atom []))
(def empty-route-map [])


(def ^{:constant true :private true}
slash-re #"/")
(def ^{:constant true :private true}
slash "/")

(defn- replace-segment
[^String s data]
(if (.startsWith s ":")
(if-let [v (get data (keyword (.substring s 1)))]
v
(throw (IllegalArgumentException. (str "Segment " s " is not found in the map " data))))
s))

(defn replace-segments
"Replaces segments that start with a colon with respective values from the data map.
Example: (\"/docs/title\" { :title \"ohai\" }) ;; => \"/docs/title\""
[^String s data]
(let [parts (split s slash-re)]
(join slash (map #(replace-segment % data) parts))))


;;
;; API
;;

(def ^{:dynamic true} *base-url*)

(defn route
"Defines an individual route.
Example:
(route \"/about\" :named \"about page\")
(route \"/faq\")
(route \"/help\" :named \"help page\")
(route \"/docs/:title\" :named \"documents\")"
[v path &{ :as opts }]
(conj v (Route. path (:named opts) opts)))

(defmacro route-map
"Defines a route map.
Example:
(route-map
(route \"/about\" :named \"about page\")
(route \"/faq\")
(route \"/help\" :named \"help page\")
(route \"/docs/:title\" :named \"documents\"))"
[& routes]
`(let [coll# (-> empty-route-map ~@routes)]
(reset! route-maps coll#)))

(defn path-for
"Generates a regular path, replacing segments that start with a colon with respective values from the data map"
[^String s data]
(replace-segments s data))

(defn named-path
"Like path-for but generates named paths"
([^String s]
(if-let [p (some (fn [r] (when (= s (:name r))
r)) @route-maps)]
(:path p)
(throw (IllegalArgumentException. (str "Route with name " s " is not found")))))
([^String s data]
(let [path (named-path s)]
(replace-segments path data))))


(defmacro with-base-url
[s & body]
`(binding [*base-url* ~s]
~@body))

(defn url-for
"Like path-for but generates full URLs. Use together with with-base-url."
[^String s data]
(str (.mutatePath (url-like *base-url*) (path-for s data))))

(defn named-url
"Like url-for but generates named paths. Use together with with-base-url."
([^String s]
(str (.mutatePath ^UrlLike (url-like *base-url*) (named-path s))))
([^String s data]
(str (.mutatePath ^UrlLike (url-like *base-url*) (named-path s data)))))
47 changes: 47 additions & 0 deletions test/clojurewerkz/route_one/core_test.clj
@@ -0,0 +1,47 @@
(ns clojurewerkz.route-one.core-test
(:use clojure.test
clojurewerkz.route-one.core))


(route-map
(route "/about" :named "about page")
(route "/faq")
(route "/help" :named "help page")
(route "/docs/:title" :named "documents"))


(deftest test-replace-segments
(testing "cases with all segments present in the data map"
(are [path data output] (is (= output (replace-segments path data)))
"/docs/title" { :title "ohai" } "/docs/title"
"/docs/:title" { :title "ohai" } "/docs/ohai"
"/docs/:category/:title" { :category "greetings" :title "ohai" } "/docs/greetings/ohai"))
(testing "cases with some segments missing from the data map"
(are [path data] (is (thrown? IllegalArgumentException (replace-segments path data)))
"/docs/:title" { :greeting "ohai" }
"/docs/:category/:title" { :title "ohai" })))

(deftest test-path-generation
(testing "generation of routes w/o segments"
(is (= "/about" (path-for "/about" {})))
(is (= "/about/project" (path-for "/about/project" {}))))
(testing "generation of routes with segments"
(is (= "/clojurewerkz/route-one" (path-for "/:organization/:project" {:organization "clojurewerkz" :project "route-one"}))))
(testing "generation of named routes w/o segments"
(is (= "/about" (named-path "about page"))))
(testing "generation of named routes with segments"
(is (= "/docs/a-title" (named-path "documents" {:title "a-title"})))))

(deftest test-url-generation
(with-base-url "http://giove.local"
(testing "generation of routes w/o segments"
(is (= "http://giove.local/about" (url-for "/about" {})))
(is (= "http://giove.local/about/project" (url-for "/about/project" {})))))
;; really broken input
(with-base-url "HTTP://https://API.MYAPP.COM/"
(testing "generation of routes with segments"
(is (= "https://api.myapp.com/clojurewerkz/route-one" (url-for "/:organization/:project" {:organization "clojurewerkz" :project "route-one"}))))
(testing "generation of named routes w/o segments"
(is (= "https://api.myapp.com/about" (named-url "about page"))))
(testing "generation of named routes with segments"
(is (= "https://api.myapp.com/docs/a-title" (named-url "documents" {:title "a-title"}))))))

0 comments on commit 2f7ea75

Please sign in to comment.