pax_global_header 0000666 0000000 0000000 00000000064 13455404637 0014525 g ustar 00root root 0000000 0000000 52 comment=06385ef65a54bb8e234da79e67d629403c224797
bidi-2.1.6/ 0000775 0000000 0000000 00000000000 13455404637 0012442 5 ustar 00root root 0000000 0000000 bidi-2.1.6/.circleci/ 0000775 0000000 0000000 00000000000 13455404637 0014275 5 ustar 00root root 0000000 0000000 bidi-2.1.6/.circleci/config.yml 0000664 0000000 0000000 00000000705 13455404637 0016267 0 ustar 00root root 0000000 0000000 version: 2
jobs:
test:
docker:
- image: circleci/clojure:lein-2.8.1
steps:
- checkout
- restore_cache:
keys:
- clj-deps-{{ checksum "project.clj" }}
- run: lein test
- run: lein test-cljs
- save_cache:
paths:
- ~/.m2/repository
- ~/.lein
key: clj-deps-{{ checksum "project.clj" }}
workflows:
version: 2
test:
jobs:
- test
bidi-2.1.6/.gitignore 0000664 0000000 0000000 00000000167 13455404637 0014436 0 ustar 00root root 0000000 0000000 /target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
/todo.org
.repl/
/.idea/
*.iml
out/ bidi-2.1.6/CHANGELOG.md 0000664 0000000 0000000 00000000464 13455404637 0014257 0 ustar 00root root 0000000 0000000 # Change Log
2.0.17 - Fixed some type-hints. Fixed bug with href-for which
ignored the :query-params in the options map. This may break
usages which workaround this bug resulting in duplicate
query-strings in the same URI.
2.0.12 - Replaced :uri-for with :uri-info in vhosts handler - breaking change
bidi-2.1.6/LICENSE 0000664 0000000 0000000 00000002063 13455404637 0013450 0 ustar 00root root 0000000 0000000 The MIT License (MIT)
Copyright © 2014 JUXT LTD.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
bidi-2.1.6/README.md 0000664 0000000 0000000 00000063737 13455404637 0013741 0 ustar 00root root 0000000 0000000 # bidi
[](https://gitter.im/juxt/bidi?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
> "bidi bidi bidi" -- Twiki, in probably every episode of
[Buck Rogers in the 25th Century](http://en.wikipedia.org/wiki/Buck_Rogers_in_the_25th_Century_%28TV_series%29)
In the grand tradition of Clojure libraries we begin with an irrelevant
quote.
Bi-directional URI dispatch. Like
[Compojure](https://github.com/weavejester/compojure), but when you want
to go both ways. If you are serving REST resources, you should be
[providing links](http://en.wikipedia.org/wiki/HATEOAS) to other
resources, and without full support for forming URIs from handlers
your code will become coupled with your routing. In short, hard-coded
URIs will eventually break.
In bidi, routes are *data structures*, there are no macros here. Generally
speaking, data structures are to be preferred over code structures. When
routes are defined in a data structure there are numerous
advantages - they can be read in from a configuration file, generated,
computed, transformed by functions and introspected - all things which
macro-based DSLs make harder.
For example, suppose you wanted to use the same set of routes in your
application and in your production [Nginx](http://wiki.nginx.org/Main)
or [HAProxy](http://haproxy.1wt.eu/) configuration. Having your routes
defined in a single data structure means you can programmatically
generate your configuration, making your environments easier to manage
and reducing the chance of discrepancies.
bidi also avoids 'terse' forms for the route definitions- reducing the
number of parsing rules for the data structure is valued over
convenience for the programmer. Convenience can always be added later
with macros.
Finally, the logic for matching routes is separated from the
responsibility for handling requests. This is an important
[architectural principle](http://www.infoq.com/presentations/Simple-Made-Easy). So
you can match on things that aren't necessarily handlers, like keywords
which you can use to lookup your handlers, or whatever you want to
do. Separation of concerns and all that.
## Comparison with other routing libraries
There are numerous Clojure(Script) routing libraries. Here's a table to help you compare.
bidi is written to do
['one thing well'](http://en.wikipedia.org/wiki/Unix_philosophy) (URI
dispatch and formation) and is intended for use with Ring middleware,
HTTP servers (including Jetty, [http-kit](http://http-kit.org/) and
[aleph](https://github.com/ztellman/aleph)) and is fully compatible with
[Liberator](http://clojure-liberator.github.io/liberator/).
If you're using with Liberator, see
https://github.com/juxt/bidi/issues/95 for some more details on how to
use them together.
## Installation
Add the following dependency to your `project.clj` file
[](http://clojars.org/bidi)
[](https://circleci.com/gh/juxt/bidi)
As bidi uses Clojure's reader conditionals, bidi is dependent on both Clojure 1.7 and Leiningen 2.5.3 or later.
## Version 2.x
Version 2.x builds on 1.x by providing a mechanism to envelope multiple virtual hosts with a single route map. The idea is to eventually create a route map which defines routes across multiple services and helps with the construction of URIs to other services, a process which is traditionally error-prone.
Version 2.x is backward compatible and forward compatible with version 1.x. If you are upgrading from 1.x to 2.x you will not need to change your existing route definitions.
## Take 5 minutes to learn bidi (using the REPL)
Let's create a route that matches `/index.html`. A route is simply a
pair, containing a pattern and a result.
```clojure
user> (def route ["/index.html" :index])
#'user/route
```
Let's try to match that route to a path.
```clojure
user> (use 'bidi.bidi)
nil
user> (match-route route "/index.html")
{:handler :index}
```
We have a match! A map is returned with a single entry with a `:handler`
key and `:index` as the value. We could use this result, for example, to
look up a Ring handler in a map mapping keywords to Ring handlers.
What happens if we try a different path?
```clojure
user> (match-route route "/another.html")
nil
```
We get a `nil`. Nil means 'no route matched'.
Now, let's go in the other direction.
```clojure
user> (path-for route :index)
"/index.html"
```
We ask bidi to use the same route definition to tell us the path that
would match the `:index` handler. In this case, it tells us
`/index.html`. So if you were forming a link to this handler from
another page, you could use this function in your view logic to create
the link instead of hardcoding in the view template (This gives your
code more resilience to changes in the organisation of routes during
development).
### Multiple routes
Now let's suppose we have 2 routes. We match partially on their common
prefix, which in this case is `"/"` but we could use `""` if there were
no common prefix. The patterns for the remaining path can be specified
in a map (or vector of pairs, if order is important).
```clojure
user> (def my-routes ["/" {"index.html" :index
"article.html" :article}])
#'user/my-routes
```
Since each entry in the map is itself a route, you can nest these
recursively.
```clojure
user> (def my-routes ["/" {"index.html" :index
"articles/" {"index.html" :article-index
"article.html" :article}}])
#'user/my-routes
```
We can match these routes as before :-
```clojure
user> (match-route my-routes "/index.html")
{:handler :index}
user> (match-route my-routes "/articles/article.html")
{:handler :article}
```
and in reverse too :-
```clojure
user> (path-for my-routes :article-index)
"/articles/index.html"
```
### Route patterns
It's common to want to match on a pattern or template, extracting some
variable from the URI. Rather than including special characters in
strings, we construct the pattern in segments using a Clojure vector
`[:id "/article.html"]`. This vector replaces the string we had in the
left hand side of the route pair.
```clojure
user> (def my-routes ["/" {"index.html" :index
"articles/" {"index.html" :article-index
[:id "/article.html"] :article}}])
#'user/my-routes
```
Now, when we match on an article path, the keyword values are extracted into a map.
```clojure
user> (match-route my-routes "/articles/123/article.html")
{:handler :article, :route-params {:id "123"}}
user> (match-route my-routes "/articles/999/article.html")
{:handler :article, :route-params {:id "999"}}
```
To form the path we need to supply the value of `:id` as extra
arguments to the `path-for` function.
```clojure
user> (path-for my-routes :article :id 123)
"/articles/123/article.html"
user> (path-for my-routes :article :id 999)
"/articles/999/article.html"
```
If you don't specify a required parameter an exception is thrown.
Apart from a few extra bells and whistles documented in the rest of this
README, that's basically it. Your five minutes are up!
### Verbose syntax
bidi also supports a verbose syntax which "compiles" to the more terse
default syntax. For example:
```clojure
(require '[bidi.verbose :refer [branch param leaf]])
(branch
"http://localhost:8080"
(branch "/users/" (param :user-id)
(branch "/topics"
(leaf "" :topics)
(leaf "/bulk" :topic-bulk)))
(branch "/topics/" (param :topic)
(leaf "" :private-topic))
(leaf "/schemas" :schemas)
(branch "/orgs/" (param :org-id)
(leaf "/topics" :org-topics)))
```
Will produce the following routes:
```clojure
["http://localhost:8080"
[[["/users/" :user-id]
[["/topics" [["" :topics] ["/bulk" :topic-bulk]]]]]
[["/topics/" :topic] [["" :private-topic]]]
["/schemas" :schemas]
[["/orgs/" :org-id] [["/topics" :org-topics]]]]]
```
## Going further
Here are some extra topics you'll need to know to use bidi in a project.
### Wrapping as a Ring handler
Match results can be any value, but are typically functions (either
in-line or via a symbol reference). You can easily wrap your routes to
form a Ring handler (similar to what Compojure's `routes` and
`defroutes` does) with the `make-handler` function.
```clojure
(ns my.handler
(:require [bidi.ring :refer (make-handler)]
[ring.util.response :as res]))
(defn index-handler
[request]
(res/response "Homepage"))
(defn article-handler
[{:keys [route-params]}]
(res/response (str "You are viewing article: " (:id route-params))))
(def handler
(make-handler ["/" {"index.html" index-handler
["articles/" :id "/article.html"] article-handler}]))
```
To chain this with middleware is simple.
```clojure
(ns my.app
(:require [my.handler :refer [handler]]
[ring.middleware.session :refer [wrap-session]
[ring.middleware.flash :refer [wrap-flash]))
(def app
(-> handler
wrap-session
wrap-flash))
```
### Regular Expressions
We've already seen how keywords can be used to extract segments from a path. By default, keywords only capture numbers and simple identifiers. This is on purpose, in a defence against injection attacks. Often you'll want to specify exactly what you're trying to capture using a regular expression.
If we want `:id` to match a number only, we can substitute the keyword with a pair, containing a regular expression followed by the keyword. For example, instead of this :-
```clojure
[ [ "foo/" :id "/bar" ] :handler ]
```
we write this :-
```clojure
[ [ "foo/" [ #"\d+" :id ] "/bar" ] :handler ]
```
which would match the string `foo/123/bar` but not `foo/abc/bar`.
## Advanced topics
These features are optional, you don't need to know about them to use
bidi, but they may come in useful.
### Guards
By default, routes ignore the request method, behaving like Compojure's
`ANY` routes. That's fine if your handlers deal with the request methods
themselves, as
[Liberator](http://clojure-liberator.github.io/liberator/)'s
do. However, if you want to limit a route to a request method, you can
wrap the route in a pair (or map entry), using a keyword for the
pattern. The keyword denotes the request method (`:get`, `:put`, etc.)
```clojure
["/" {"blog" {:get {"/index" (fn [req] {:status 200 :body "Index"})}}}]
```
You can also restrict routes by any other request criteria. Guards are
specified by maps. Map entries can specify a single value, a set of
possible values or even a predicate to test a value.
In this example, the `/zip` route is only matched if the server name in
the request is `juxt.pro`. You can use this feature to restrict routes
to virtual hosts or HTTP schemes.
```clojure
["/" {"blog" {:get
{"/index" (fn [req] {:status 200 :body "Index"})}}
{:request-method :post :server-name "juxt.pro"}
{"/zip" (fn [req] {:status 201 :body "Created"})}}]
```
Values in the guard map can be values, sets of acceptable values, or
even predicate functions to give fine-grained control over the dispatch
criteria.
### Keywords
Sometimes you want segments of the URI to be extracted as keywords rather than strings, and in the reverse direction, to use keywords as values to be encoded into URIs.
You can construct a pattern similarly to how you specify regular expressions but instead of the regex you use specify `keyword` core function.
```clojure
[ "foo/" [ keyword :db/ident ] "/bar" ]
```
When matching the path `foo/bidi/bar`, the `:route-params` of the result would be `{:db/ident :bidi}`. To construct the path, you would use `(path-for my-routes handler :db/ident :bidi)`, which results in `foo/bidi/bar` (the colon of the stringified keyword is omitted).
Namespaced keywords are also supported. Note that in the URI the `/` that separates the keyword's namespace from its name is URL encoded to %2F, rather than `/`.
### Catch-All Routes
Note that you can use the pattern `true` to match anything. This is
useful for writing catch-all routes.
For example, if we'd like to match a certain set of routes and return
`404 Not Found` for everything else, we can do the following:
```clojure
(def my-routes ["/" [["index.html" :index]
[true :not-found]]])
```
We used vectors rather than maps to define the routes because the
order of the definitions is significant (i.e. `true` will completely
subsume the other routes if we let it).
Now let's try to match on that:
```clojure
user> (match-route my-routes "/index.html")
{:handler :index}
user> (match-route my-routes "/other.html")
{:handler :not-found}
```
Note that `:not-found` doesn't have any special significance here--we
still need to provide a hander function that implements the desired
`404` behavior.
## Route definitions
A route is formed as a pair: [ *<pattern>* *<matched>* ]
The left-hand-side of a pair is the pattern. It can match a path, either
fully or partially. The simplest pattern is a string, but other types of
patterns are also possible, including segmented paths, regular
expressions, records, in various combinations.
The right-hand-side indicates the result of the match (in the case that
the pattern is matched fully) or a route sub-structure that attempts to
match on the remainder of the path (in the case that the pattern is
matched partially). The route structure is a recursive structure.
This [BNF](http://en.wikipedia.org/wiki/Backus%E2%80%93Naur_Form)
grammar formally defines the basic route structure, although it is
possible extend these definitions by adding types that satisfy the
protocols used in bidi (more on this later).
```
RouteStructure := RoutePair
RoutePair ::= [ Pattern Matched ]
Pattern ::= Path | [ PatternSegment+ ] | MethodGuard | GeneralGuard | true | false
MethodGuard ::= :get :post :put :delete :head :options
GeneralGuard ::= [ GuardKey GuardValue ]* (a map)
GuardKey ::= Keyword
GuardValue ::= Value | Set | Function
Path ::= String
PatternSegment ::= String | Regex | Keyword | [ (String | Regex) Keyword ]
Matched ::= Function | Symbol | Keyword | [ RoutePair+ ] { RoutePair+ }
```
In case of confusion, refer to bidi examples found in this README and in
the test suite.
A [schema](https://github.com/Prismatic/schema) is available as `bidi.schema/RoutePair`. You can use this to check or validate a bidi route structure in your code.
```clojure
(require '[schema.core :as s] bidi.schema)
(def route ["/index.html" :index])
;; Check that the route is properly structured - returns nil if valid;
;; otherwise, returns a value with 'bad' parts of the route.
(s/check bidi.schema/RoutePair route)
;; Throw an exception if the route is badly structured
(s/validate bidi.schema/RoutePair route)
```
## Virtual Hosts
If you are serving multiple virtual hosts with the same server, you may want to create a super-structure that allows routing across virtual host boundaries.
Here's a virtual-host structure:
```clojure
["https://example.org:8443"
["/index.html" :index]
["/login" :login]
["/posts" […]]
```
It's just like the vector-of-vectors syntax we've seen before in bidi, but this time the first element is a virtual-host declaration. This is usually a string but can also be a `java.net.URI` or `java.net.URL`, or a map like `{:scheme :https :host "example.org:8443"}`.
A virtual-hosts super-structure is created with the `bidi.vhosts/vhosts.model` variadic function, each argument is a virtual-host structure.
```clojure
(require '[bidi.vhosts :refer [vhosts-model]])
(def my-vhosts-model
(vhosts-model ["https://example.org:8443"
["/index.html" :index]
["/login" :login]]
["https://blog.example.org"
["/posts.html" […]]]))
```
### uri-info
When using virtual hosts, use the `bidi.vhosts/uri-info` to generate a map of URIs.
For example:
```
(uri-info my-vhosts-model :index {:query-params {"q" "juxt"}})
```
would return
```
{:uri "https://example.org:8443/index.html?q=juxt"
:path "/index.html"
:host "example.org:8443"
:scheme :https
:href "https://example.org:8443/index.html?q=juxt"}
```
A partially applied uri-info function is available in bidi's matching context and returns a map of the following elements. This partial applies the vhosts-model which can help with dependency cycles in your code (where your bidi router requires knowledge of resources, which have views that require knowledge of the bidi router's routes).
When called via bidi's match-context, the `:href` entry in the result may not contain the scheme, host and port, if these are redundant, whereas the `:uri` entry always contains an absolute URI. If you are creating HTML content for a browser, `:href` is safe to use. If, for example, you are creating an API returning a JSON-formatted response body, prefer `:uri`.
### Synonymous virtual-hosts
The virtual-host declaration can itself be a vector, if you need to match multiple possibilities. Here's another example, which matches two hosts:
```clojure
[["https://example.org:8443" "http://example.org:8000"]
["/index.html" :index]
["/login" :login]]
```
The rules for `uri-info` are that the first virtual-host in the vector is used. When the request is known to bidi (i.e. in the partially applied uri-info function in the match-context) the algorithm chooses the first virtual host that matches the request URI's scheme.
### Wildcards
An virtual host can be specified as a wildcard `:*`, which means it matches any scheme/host. Calls to `uri-info` will assume the scheme/host are that of the incoming request.
```clojure
[:*
["/index.html" :index]
["/login" :login]]
```
Wildcards can be mixed with other vhost forms.
## Composability
As they are simply nested data structures (strings, vectors, maps),
route structures are highly composable. They are consistent and easy to
generate. A future version of bidi may contain macros to reduce the
number of brackets needed to create route structures by hand.
## Extensibility
The implementation is based on Clojure protocols which allows the route
syntax to be extended outside of this library.
Built-in records are available but you can also create your own. Below
is a description of the built-in ones and should give you an idea what
is possible. If you add your own types, please consider contributing
them to the project. Make sure you test that your types in both
directions (for URI matching and formation).
### Redirect
The `Redirect` record is included which satisfies the `Matched` protocol.
Consider the following route definition.
```clojure
(defn my-handler [req] {:status 200 :body "Hello World!"})
["/articles" {"/new" my-handler
"/old" (->Redirect 307 my-handler)}]
```
Any requests to `/articles/old` yield
[*307 Temporary Redirect*](http://en.wikipedia.org/wiki/HTTP_307#3xx_Redirection)
responses with a *Location* header of `/articles/new`. This is a robust
way of forming redirects in your code, since it guarantees that the
*Location URI* matches an existing handler, both reducing the chance of
broken links and encouraging the practise of retaining old URIs (linking
to new ones) after refactoring. You can also use it for the common
practice of adding a *welcome page* suffix, for example, adding
`index.html` to a URI ending in `/`.
### Resources and ResourcesMaybe
The `Resources` and `ResourcesMaybe` record can be used on the
right-hand side of a route. It serves resources from the
classpath. After the pattern is matched, the remaining part of the path
is added to the given prefix.
```clojure
["/resources" (->ResourcesMaybe {:prefix "public/"})
```
There is an important difference between `Resources` and `ResourcesMaybe`. `Resources` will return a 404 response if the resource cannot be found, while `ResourcesMaybe` will return nil, allowing subsequent routes to be tried.
### Files
Similar to `Resources`, `Files` will serve files from a file-system.
```clojure
["pics/" (->Files {:dir "/tmp/pics"})]
```
### WrapMiddleware
You can wrap the target handler in Ring middleware as usual. But
sometimes you need to specify that the handlers from certain patterns
are wrapped in particular middleware.
For example :-
```clojure
(match-route ["/index.html" (->WrapMiddleware handler wrap-params)]
"/index.html")
```
Use this with caution. If you are using this _you are probably doing it wrong_.
Bidi separates URI routing from request handling. Ring middleware is
something that should apply to handlers, not routes. If you have a set
of middleware common to a group of handlers, you should apply the
middleware to each handler in turn, rather than use
`->WrapMiddleware`. Better to map a middleware applying function over
your handlers rather than use this feature.
### Alternates
Sometimes you want to specify a list of potential candidate patterns,
which each match the handler. The first in the list is considered the
canonical pattern for the purposes of URI formation.
```clojure
[#{"/index.html" "/index"} :index]
```
Any pattern can be used in the list. This allows quite sophisticated
matching. For example, if you want to match on requests that are either
HEAD or GET but not anything else.
```clojure
[#{:head :get} :index]
```
Or match if the server name is `juxt.pro` or `localhost`.
```clojure
[#{{:server-name "juxt.pro"}{:server-name "localhost"}}
{"/index.html" :index}]
```
### Tagged Match
Sometimes you need to apply a tag to a route, so you can use the tag
(rather than the handler) in a `path-for` function. This is very
convenient when forming routes, because you don't need to have a
reference to the handler itself.
You can use the `tag` function to construct these records.
```clojure
(tag my-handler :my-tag)
```
It's common to use the single threaded macro, so wrapping handlers in
tags is just like wrapping them in Ring middleware. For example :-
```clojure
["/" [["foo" (-> foo-handler (tag :foo)]
[["bar/" :id] (-> bar-handler (tag :bar)]]]
```
Paths can now be created like this :-
```clojure
(path-for my-routes :foo)
(path-for my-routes :bar :id "123")
```
### Route sequences
It's possible to extract all possible routes from a route structure with `route-seq`.
Call `route-seq` on a route structure returns a sequence of all the possible routes contained in the route structure. This is useful to generating a site map. Each route is a map containing a path and a handler entry.
If you use keywords to extract route parameters, they will be contained in the path. If you wish to control the expansion, use a custom record that satisfies both `bidi/Pattern` and `bidi/Matches`.
## Contributing
We welcome pull requests. If possible, please run the tests and make
sure they pass before you submit one.
```
$ lein test
lein test bidi.bidi-test
lein test bidi.perf-test
Time for 1000 matches using Compojure routes
"Elapsed time: 17.645077 msecs"
Time for 1000 matches using uncompiled bidi routes
"Elapsed time: 66.449164 msecs"
Time for 1000 matches using compiled bidi routes
"Elapsed time: 21.269446 msecs"
Ran 9 tests containing 47 assertions.
0 failures, 0 errors.
```
A big thank you to everyone involved in bidi so far, including
* Alexander Kiel
* Bobby Calderwood
* Cameron Desautels
* Chris Price
* David Thomas Hume
* Dene Simpson
* Dominic Monroe
* Elben Shira
* James Henderson
* Jeff Rose
* John Cowie
* Julian Birch
* Malcolm Sparks
* Martin Trojer
* Matt Mitchell
* Michael Sappler
* Nate Smith
* Neale Swinnerton
* Nicolas Ha
* Oliver Hine
* Philipp Meier
* Rob Mather
* Sebastian Bensusan
* Thomas Crowley
* Thomas Mulvaney
* Tom Crayford
* Andrew Phillips
## Copyright & License
The MIT License (MIT)
Copyright © 2014-2015 JUXT LTD.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
bidi-2.1.6/doc/ 0000775 0000000 0000000 00000000000 13455404637 0013207 5 ustar 00root root 0000000 0000000 bidi-2.1.6/doc/patterns.md 0000664 0000000 0000000 00000005511 13455404637 0015373 0 ustar 00root root 0000000 0000000
DEPRECATED - while there are some useful tidbits here, it is recommended that you solve circular dependencies with co-dependencies, as shown by bootsrap-cover :-
$ lein new modular foo bootstrap-cover
-------------
# bidi Patterns
Here are some patterns that you may find useful in your development.
## Handler map promise
### Situation
You have a bunch of REST resources, each of which might need to form
hyperlinks to one or more of the others. There may even be a mutual
dependency. Which handler do you create first?
### Solution
Use a promise to access a handler map, containing entries for each
handler keyed with keywords. The promise must be delivered prior to
calling dereferencing it in a call to `path-for`. This is ensured by the
`make-handlers` function, which delivers the promise prior to returning
the result, so the promise cannot escape the handler construction phase.
### Discussion
Here is an example which demonstrates the technique. It uses a pair of
[Liberator](http://clojure-liberator.github.io/liberator/) resources to
create a REST API, which need to create
[hyperlinks](http://en.wikipedia.org/wiki/HATEOAS) to each other.
In the code below we assume that the bidi routes are available in the
request, under the `:request` key. Every application is different, and its
up to the bidi user to ensure that request handlers have access to the
overall route structure.
Notice how both resources use the `path-for` function to form paths to
the other resource.
The `make-handlers` function creates a promise to a map containing each
handler, referenced by a known keyword, which can be used to look up the
handler, and thereby form the path to it.
```clojure
(defresource contacts [database handlers]
:allowed-methods #{:post}
:post! (fn [{{body :body} :request}]
{:id (create-contact! database body)})
:handle-created (fn [{{routes :routes} :request id :id}]
(assert (realized? handlers))
(ring-response
{:headers {"Location" (path-for routes (:contact @handlers) :id id)}})))
(defresource contact [handlers]
:allowed-methods #{:delete :put}
:available-media-types #{"application/json"}
:handle-ok (fn [{{{id :id} :route-params routes :routes} :request}]
(assert (realized? handlers))
(html [:div [:h2 "Contact: " id]
[:a {:href (path-for routes (:contacts @handlers))} "Index"]])))
(defn make-handlers [database]
(let [p (promise)]
;; Deliver the promise so it doesn't escape this function.
@(deliver p {:contacts (contacts database p)
:contact (contact p)})))
(defn make-routes [handlers]
["/" [["contacts" (:contacts handlers)]
[["contact/" :id] (:contact handlers)]
]])
;; Create the route structure like this :-
(-> database make-handlers make-routes)
```
bidi-2.1.6/project.clj 0000664 0000000 0000000 00000003255 13455404637 0014607 0 ustar 00root root 0000000 0000000 ;; Copyright © 2014, JUXT LTD.
(defproject bidi "2.1.6"
:description "Bidirectional URI routing"
:url "https://github.com/juxt/bidi"
:license {:name "The MIT License"
:url "http://opensource.org/licenses/MIT"}
;; :pedantic? :abort
:dependencies [[prismatic/schema "1.1.7"]]
:plugins [[lein-cljsbuild "1.1.1"]
[lein-doo "0.1.6"]]
:prep-tasks ["javac" "compile"]
:profiles {:provided {:dependencies [[ring/ring-core "1.5.0" :exclusions [org.clojure/clojure]]]}
:dev {:exclusions [[org.clojure/tools.reader]]
:resource-paths ["test-resources"]
;;:global-vars {*warn-on-reflection* true}
:dependencies [[org.clojure/clojure "1.8.0"]
[org.clojure/clojurescript "1.9.293"]
[org.clojure/tools.reader "1.0.0-beta4"]
[ring/ring-mock "0.3.0"]
[compojure "1.6.0-beta2"]
[criterium "0.4.3"]
[org.mozilla/rhino "1.7.7.1"]]}}
:aliases {"deploy" ["do" "clean," "deploy" "clojars"]
"test-cljs" ["doo" "rhino" "test" "once"]}
:jar-exclusions [#"\.swp|\.swo|\.DS_Store"]
:lein-release {:deploy-via :shell
:shell ["lein" "deploy"]}
:doo {:paths {:rhino "lein run -m org.mozilla.javascript.tools.shell.Main"}}
:cljsbuild {:builds
{:test {:source-paths ["src" "test"]
:compiler {:output-to "target/unit-test.js"
:main 'bidi.runner
:optimizations :whitespace}}}})
bidi-2.1.6/src/ 0000775 0000000 0000000 00000000000 13455404637 0013231 5 ustar 00root root 0000000 0000000 bidi-2.1.6/src/bidi/ 0000775 0000000 0000000 00000000000 13455404637 0014140 5 ustar 00root root 0000000 0000000 bidi-2.1.6/src/bidi/bidi.cljc 0000664 0000000 0000000 00000050213 13455404637 0015705 0 ustar 00root root 0000000 0000000 ;; Copyright © 2014, JUXT LTD.
(ns bidi.bidi
(:refer-clojure :exclude [uuid])
(:require clojure.string)
#?(:cljs (:import goog.Uri)))
(defn url-encode
[string]
(some-> string
str
#?(:clj (java.net.URLEncoder/encode "UTF-8")
:cljs (js/encodeURIComponent))
(.replace "+" "%20")))
(defn url-decode
([string] #?(:clj (url-decode string "UTF-8")
:cljs (some-> string str (js/decodeURIComponent))))
#?(:clj ([string encoding]
(some-> string str (java.net.URLDecoder/decode encoding)))))
(defn uuid
"Function for creating a UUID of the appropriate type for the platform.
Note that this function should _only_ be used in route patterns as, at least
in the case of ClojureScript, it does not validate that the input string is
actually a valid UUID (this is handled by the route matching logic)."
[s]
#?(:clj (java.util.UUID/fromString s)
:cljs (cljs.core/uuid s)))
;; When forming paths, parameters are encoded into the URI according to
;; the parameter value type.
(defprotocol ParameterEncoding
(encode-parameter [_]))
(extend-protocol ParameterEncoding
;; We don't URL encode strings, we leave the choice of whether to do so
;; to the caller.
#?(:clj String
:cljs string)
(encode-parameter [s] s)
#?(:clj CharSequence)
#?(:clj (encode-parameter [s] s))
#?(:clj Number
:cljs number)
(encode-parameter [s] s)
#?(:clj java.util.UUID
:cljs cljs.core.UUID)
(encode-parameter [s] (str s))
;; We do URL encode keywords, however. Namespaced
;; keywords use a separated of %2F (a URL encoded forward slash).
#?(:clj clojure.lang.Keyword
:cljs cljs.core.Keyword)
(encode-parameter [k]
(url-encode
(str (namespace k)
(when (namespace k) "/")
(name k)))))
;; A PatternSegment is part of a segmented pattern, where the pattern is
;; given as a vector. Each segment can be of a different type, and each
;; segment can optionally be associated with a key, thereby contributing
;; a route parameter.
(defprotocol PatternSegment
;; segment-regex-group must return the regex pattern that will consume the
;; segment, when matching routes.
(segment-regex-group [_])
;; param-key, if non nil, specifies the key in the parameter map which
;; holds the segment's value, returned from matching a route
(param-key [_])
;; transform specifies a function that will be applied the value
;; extracted from the URI when matching routes.
(transform-param [_])
;; unmatch-segment generates the part of the URI (a string) represented by
;; the segment, when forming URIs.
(unmatch-segment [_ params])
;; matches? is used to check if the type or value of the parameter
;; satisfies the segment qualifier when forming a URI.
(matches? [_ s]))
(extend-protocol PatternSegment
#?(:clj String
:cljs string)
(segment-regex-group [this]
#?(:clj (str "\\Q" this "\\E")
:cljs this))
(param-key [_] nil)
(transform-param [_] identity)
(unmatch-segment [this _] this)
#?(:clj java.util.regex.Pattern
:cljs js/RegExp)
(segment-regex-group [this]
#?(:clj (.pattern this)
:cljs (aget this "source")))
(param-key [_] nil)
(transform-param [_] identity)
(matches? [this s] (re-matches this (str s)))
#?(:clj clojure.lang.APersistentVector
:cljs cljs.core.PersistentVector)
;; A vector allows a keyword to be associated with a segment. The
;; qualifier for the segment comes first, then the keyword.
;; The qualifier is usually a regex
(segment-regex-group [this] (segment-regex-group (first this)))
(param-key [this]
(let [k (second this)]
(if (keyword? k)
k
(throw (ex-info (str "If a PatternSegment is represented by a vector, the second
element must be the keyword associated with the pattern: "
this)
{})))))
(transform-param [this] (transform-param (first this)))
(unmatch-segment [this params]
(let [k (second this)]
(if-not (keyword? k)
(throw (ex-info (str "If a PatternSegment is represented by a vector, the second element
must be the key associated with the pattern: "
this)
{})))
(if-let [v (get params k)]
(if (matches? (first this) v)
(encode-parameter v)
(throw (ex-info (str "Parameter value of " v " (key " k ") "
"is not compatible with the route pattern " this)
{})))
(throw (ex-info (str "No parameter found in params for key " k)
{})))))
#?(:clj clojure.lang.Keyword
:cljs cljs.core.Keyword)
;; This is a very common form, so we're conservative as a defence against injection attacks.
(segment-regex-group [_] "[A-Za-z0-9\\-\\_\\.]+")
(param-key [this] this)
(transform-param [_] identity)
(unmatch-segment [this params]
(if-let [v (this params)]
(encode-parameter v)
(throw (ex-info (str "Cannot form URI without a value given for "
this " parameter")
{}))))
#?(:clj clojure.lang.Fn
:cljs function)
(segment-regex-group [this]
(condp = this
keyword "[A-Za-z]+[A-Za-z0-9\\*\\+\\!\\-\\_\\?\\.]*(?:%2F[A-Za-z]+[A-Za-z0-9\\*\\+\\!\\-\\_\\?\\.]*)?"
long "-?\\d{1,19}"
uuid "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
:otherwise (throw (ex-info (str "Unidentified function qualifier to pattern segment: " this) {}))))
(transform-param [this]
(condp = this
;; keyword is close, but must be applied to a decoded string, to work with namespaced keywords
keyword (comp keyword url-decode)
long #?(:clj #(Long/parseLong %) :cljs #(js/Number %))
uuid uuid
(throw (ex-info (str "Unrecognized function " this) {}))))
(matches? [this s]
(condp = this
keyword (keyword? s)
long #?(:clj (some #(instance? % s) [Byte Short Integer Long])
:cljs (not (js/isNaN s)))
uuid (instance? #?(:clj java.util.UUID :cljs cljs.core.UUID) s))))
;; A Route is a pair. The pair has two halves: a pattern on the left,
;; while the right contains the result if the pattern matches.
(defprotocol Pattern
(match-pattern [_ env]
"Return a new state if this pattern matches the given state, or
falsy otherwise. If a new state is returned it will usually have the
rest of the path to match in the :remainder entry.")
(unmatch-pattern [_ m]))
(defprotocol Matched
(resolve-handler [_ m])
(unresolve-handler [_ m]))
(defn just-path
[path]
(let [uri-string (str "http://bidi.bidi/" path)]
(subs #?(:clj (.getPath (java.net.URL. uri-string))
:cljs (.getPath (goog.Uri. uri-string)))
1)))
(defn match-pair
"A pair contains a pattern to match (either fully or partially) and an
expression yielding a handler. The second parameter is a map
containing state, including the remaining path."
[[pattern matched] orig-env]
(let [env (update orig-env :remainder just-path)]
(when-let [match-result (match-pattern pattern env)]
(resolve-handler matched match-result))))
(defn match-beginning
"Match the beginning of the :remainder value in m. If matched, update
the :remainder value in m with the path that remains after matching."
[regex-pattern env]
(when-let [path (last (re-matches (re-pattern (str regex-pattern "(.*)"))
(:remainder env)))]
(assoc env :remainder path)))
(defn succeed [handler m]
(when (= (:remainder m) "")
(merge (dissoc m :remainder) {:handler handler})))
(extend-protocol Pattern
#?(:clj String
:cljs string)
#?(:clj (match-pattern [this env]
(if (= (.length this) 0)
env
(when (.startsWith ^String (:remainder env) this)
(assoc env :remainder (.substring ^String (:remainder env) (.length this))))))
;; TODO: Optimize cljs version as above
:cljs (match-pattern [this env]
(match-beginning (str "(" (segment-regex-group this) ")") env)))
(unmatch-pattern [this _] this)
#?(:clj java.util.regex.Pattern
:cljs js/RegExp)
(match-pattern [this env]
(match-beginning (str "(" (segment-regex-group this) ")") env))
;; We can't unmatch-pattern as you can't go from a regex to a
;; string (it's a many-to-one mapping)
#?(:cljs
(unmatch-pattern [this m]
(let [p (.pattern this)]
(unmatch-pattern (clojure.string/replace p #"\\\\" "") m))))
#?(:clj Boolean
:cljs boolean)
(match-pattern [this env]
(when this (assoc env :remainder "")))
(unmatch-pattern [this _] (when this ""))
#?(:clj clojure.lang.APersistentVector
:cljs cljs.core.PersistentVector)
(match-pattern [this env]
(when-let [groups (as-> this %
;; Make regexes of each segment in the vector
(map segment-regex-group %)
;; Form a regexes group from each
(map (fn [x] (str "(" x ")")) %)
(reduce str %)
;; Add the 'remainder' group
(str % "(.*)")
(re-pattern %)
(re-matches % (:remainder env))
(next %))]
(let [params (->> groups
butlast ; except the 'remainder' group
;; Transform parameter values if necessary
(map list) (map apply (map transform-param this))
;; Pair up with the parameter keys
(map vector (map param-key this))
;; Only where such keys are specified
(filter first)
;; Merge all key/values into a map
(into {}))]
(-> env
(assoc-in [:remainder] (last groups))
(update-in [:route-params] merge params)))))
(unmatch-pattern [this m]
(apply str (map #(unmatch-segment % (:params m)) this)))
#?(:clj clojure.lang.Keyword
:cljs cljs.core.Keyword)
(match-pattern [this env] (when (= this (:request-method env)) env))
(unmatch-pattern [_ _] "")
#?(:clj clojure.lang.APersistentMap
:cljs cljs.core.PersistentArrayMap)
(match-pattern [this env]
(when (every? (fn [[k v]]
(cond
(or (fn? v) (set? v)) (v (get env k))
:otherwise (= v (get env k))))
(seq this))
env))
(unmatch-pattern [_ _] "")
#?(:cljs cljs.core.PersistentHashMap)
#?(:cljs
(match-pattern [this env]
(when (every? (fn [[k v]]
(cond
(or (fn? v) (set? v)) (v (get env k))
:otherwise (= v (get env k))))
(seq this))
env)))
#?(:cljs
(unmatch-pattern [_ _] ""))
#?(:clj clojure.lang.APersistentSet
:cljs cljs.core.PersistentHashSet)
(match-pattern [this s]
(some #(match-pattern % s)
;; We try to match on the longest string first, so that the
;; empty string will be matched last, after all other cases
(sort-by count > this)))
(unmatch-pattern [this s] (unmatch-pattern (first this) s))
#?(:cljs cljs.core.PersistentTreeSet)
#?(:cljs
(match-pattern [this s]
(some #(match-pattern % s)
;; We try to match on the longest string first, so that the
;; empty string will be matched last, after all other cases
(sort-by count > this))))
#?(:cljs
(unmatch-pattern [this s] (unmatch-pattern (first this) s))))
(defn unmatch-pair [v m]
(when-let [r (unresolve-handler (second v) m)]
(str (unmatch-pattern (first v) m) r)))
(extend-protocol Matched
#?(:clj String
:cljs string)
(unresolve-handler [_ _] nil)
#?(:clj clojure.lang.APersistentVector
:cljs cljs.core.PersistentVector)
(resolve-handler [this m] (some #(match-pair % m) this))
(unresolve-handler [this m] (some #(unmatch-pair % m) this))
#?(:clj clojure.lang.PersistentList
:cljs cljs.core.List)
(resolve-handler [this m] (some #(match-pair % m) this))
(unresolve-handler [this m] (some #(unmatch-pair % m) this))
#?(:clj clojure.lang.APersistentMap
:cljs cljs.core.PersistentArrayMap)
(resolve-handler [this m] (some #(match-pair % m) this))
(unresolve-handler [this m] (some #(unmatch-pair % m) this))
#?(:cljs cljs.core.PersistentHashMap)
#?(:cljs (resolve-handler [this m] (some #(match-pair % m) this)))
#?(:cljs (unresolve-handler [this m] (some #(unmatch-pair % m) this)))
#?(:clj clojure.lang.LazySeq
:cljs cljs.core.LazySeq)
(resolve-handler [this m] (some #(match-pair % m) this))
(unresolve-handler [this m] (some #(unmatch-pair % m) this))
#?(:clj clojure.lang.Symbol
:cljs cljs.core.Symbol)
(resolve-handler [this m] (succeed this m))
(unresolve-handler [this m] (when (= this (:handler m)) ""))
#?(:clj clojure.lang.Var
:cljs cljs.core.Var)
(resolve-handler [this m] (succeed this m))
(unresolve-handler [this m] (unresolve-handler @this m))
#?(:clj clojure.lang.Keyword
:cljs cljs.core.Keyword)
(resolve-handler [this m] (succeed this m))
(unresolve-handler [this m] (when (= this (:handler m)) ""))
#?(:clj clojure.lang.Fn
:cljs function)
(resolve-handler [this m] (succeed this m))
(unresolve-handler [this m] (when (= this (:handler m)) ""))
nil
(resolve-handler [this m] nil)
(unresolve-handler [this m] nil))
(defn match-route*
[route path options]
(-> (match-pair route (assoc options :remainder path :route route))
(dissoc :route)))
(defn match-route
"Given a route definition data structure and a path, return the
handler, if any, that matches the path."
[route path & {:as options}]
(match-route* route path options))
(defn path-for*
"Given a route definition data structure, a handler and an option map, return a
path that would route to the handler. The map must contain the values to any
parameters required to create the path, and extra values are silently ignored."
[route handler params]
(when (nil? handler)
(throw (ex-info "Cannot form URI from a nil handler" {})))
(unmatch-pair route {:handler handler :params params}))
(defn path-for
"Given a route definition data structure, a handler and an unrolled option map,
return a path that would route to the handler. The map must contain the values
to any parameters required to create the path, and extra values are silently
ignored."
[route handler & {:as params}]
(path-for* route handler params))
;; --------------------------------------------------------------------------------
;; Route seqs
;; --------------------------------------------------------------------------------
(defprotocol Matches
(matches [_] "A protocol used in the expansion of possible matches that the pattern can match. This is used to gather all possible routes using route-seq below."))
(extend-protocol Matches
#?(:clj Object
:cljs default)
(matches [this] [this])
#?(:clj clojure.lang.APersistentSet
:cljs cljs.core.PersistentHashSet)
(matches [this] this)
#?(:cljs cljs.core.PersistentTreeSet)
#?(:cljs (matches [this] this)))
(defrecord Route [handler path])
(defprotocol RouteSeq
(gather [_ context] "Return a sequence of leaves"))
(defn route-seq
([[pattern matched] ctx]
(mapcat
identity
(for [p (matches pattern)]
(gather matched (update-in ctx [:path] (fnil conj []) p)))))
([route]
(route-seq route {})))
(extend-protocol RouteSeq
#?(:clj clojure.lang.APersistentVector
:cljs cljs.core.PersistentVector)
(gather [this context] (mapcat #(route-seq % context) this))
#?(:clj clojure.lang.PersistentList
:cljs cljs.core.List)
(gather [this context] (mapcat #(route-seq % context) this))
#?(:clj clojure.lang.APersistentMap
:cljs cljs.core.PersistentArrayMap)
(gather [this context] (mapcat #(route-seq % context) this))
#?(:cljs cljs.core.PersistentHashMap)
#?(:cljs (gather [this context] (mapcat #(route-seq % context) this)))
#?(:clj clojure.lang.LazySeq
:cljs cljs.core.LazySeq)
(gather [this context] (mapcat #(route-seq % context) this))
#?(:clj Object
:cljs default)
(gather [this context] [(map->Route (assoc context :handler this))]))
;; --------------------------------------------------------------------------------
;; Protocols
;; --------------------------------------------------------------------------------
;; RouteProvider - this protocol can be satisfied by records that provide
;; or generate bidi routes. The reason for providing this protocol in
;; bidi is to encourage compatibility between record implementations.
(defprotocol RouteProvider
(routes [_] "Provide a bidi route structure. Returns a vector pair,
the first element is the pattern, the second element is the matched
route or routes."))
;; --------------------------------------------------------------------------------
;; Utility records
;; --------------------------------------------------------------------------------
;; Alternates can be used as a pattern. It is constructed with a vector
;; of possible matching candidates. If one of the candidates matches,
;; the route is matched. The first pattern in the vector is considered
;; the canonical pattern for the purposes of URI formation with
;; (path-for).
;; This is deprecated. You should really use the literal set syntax.
(defrecord Alternates [alts]
Pattern
(match-pattern [this m]
(some #(match-pattern % m)
;; We try to match on the longest string first, so that the
;; empty string will be matched last, after all other cases
(sort-by count > alts)))
(unmatch-pattern [this m] (unmatch-pattern (first alts) m))
Matches
(matches [_] alts))
(defn alts [& alts]
(->Alternates alts))
;; If you have multiple routes which match the same handler, but need to
;; label them so that you can form the correct URI, wrap the handler in
;; a TaggedMatch.
(defrecord TaggedMatch [matched tag]
Matched
(resolve-handler [this m]
(resolve-handler matched (assoc m :tag tag)))
(unresolve-handler [this m]
(if (and (keyword? (:handler m)) (= tag (:handler m)))
""
(unresolve-handler matched m)))
RouteSeq
(gather [this context] [(map->Route (assoc context :handler matched :tag tag))]))
(defn tag [matched tag]
(->TaggedMatch matched tag))
(defrecord IdentifiableHandler [id handler]
Matched
(resolve-handler [this m]
(resolve-handler handler (assoc m :id id)))
(unresolve-handler [this m]
(when id
(if (= id (:handler m)) ""
(unresolve-handler handler m)))))
(defn ^:deprecated handler
([k handler]
(->IdentifiableHandler k handler))
([handler]
(->IdentifiableHandler nil handler)))
;; --------------------------------------------------------------------------------
;; Context
;; --------------------------------------------------------------------------------
;; bidi's match-context can be leveraged by Matched wrappers
(defrecord RoutesContext [routes context]
Matched
(resolve-handler [_ m]
(when-let [m (resolve-handler routes m)]
(merge context m)))
(unresolve-handler [_ m]
(unresolve-handler routes m))
RouteSeq
(gather [_ context]
(gather routes context)))
(defn routes-context
"Wrap a Matched such that a successful match will merge the given
context with the match-context. The merge is such that where there
is a conflict, the inner sub-tree overrides the outer container."
[routes context]
(->RoutesContext routes context))
;; --------------------------------------------------------------------------------
;; Deprecated functions
;; --------------------------------------------------------------------------------
;; Route compilation was only marginally effective and hard to
;; debug. When bidi matching takes in the order of 30 micro-seconds,
;; this is good enough in relation to the time taken to process the
;; overall request.
(defn ^:deprecated compile-route [route]
route)
bidi-2.1.6/src/bidi/ring.cljc 0000664 0000000 0000000 00000015244 13455404637 0015742 0 ustar 00root root 0000000 0000000 ;; Copyright © 2014, JUXT LTD.
(ns bidi.ring
#?(:clj
(:require
[bidi.bidi :as bidi :refer :all]
[clojure.java.io :as io]
[ring.util.response :refer (file-response url-response resource-response)]
[ring.middleware.content-type :refer (wrap-content-type)]
[ring.middleware.not-modified :refer (wrap-not-modified)]
[ring.middleware.resource :refer (wrap-resource)])
:cljs
(:require
[bidi.bidi :as bidi :refer (match-route*)])))
(defprotocol Ring
(request [_ req match-context]
"Handle a Ring request, but optionally utilize any context that was
collected in the process of matching the handler."))
(extend-protocol Ring
#?(:clj clojure.lang.Fn
:cljs function)
(request [f req _]
(f req))
#?(:clj clojure.lang.Var
:cljs cljs.core.Var)
(request [v req mc]
(request (deref v) req mc)))
(defn make-handler
"Create a Ring handler from the route definition data
structure. Matches a handler from the uri in the request, and invokes
it with the request as a parameter."
([route handler-fn]
(assert route "Cannot create a Ring handler with a nil Route(s) parameter")
(fn [{:keys [uri path-info] :as req}]
(let [path (or path-info uri)
{:keys [handler route-params] :as match-context}
(match-route* route path req)]
(when handler
(request
(handler-fn handler)
(-> req
(update-in [:params] merge route-params)
(update-in [:route-params] merge route-params))
(apply dissoc match-context :handler (keys req)))))))
([route] (make-handler route identity)))
;; Any types can be used which satisfy bidi protocols.
;; Here are some built-in ones.
;; Redirect can be matched (appear on the right-hand-side of a route)
;; and returns a handler that can redirect to the given target.
#?(:clj
(defrecord Redirect [status target]
bidi/Matched
(resolve-handler [this m]
(when (= "" (:remainder m))
(cond-> m
true (assoc :handler this)
(not (string? target))
(assoc :location
(apply path-for (:route m) target
(apply concat (seq (:route-params m)))))
true (dissoc :remainder))))
(unresolve-handler [this m]
(when (= this (:handler m)) ""))
Ring
(request [f req m]
(if-let [location (if-not (string? target) (:location m) target)]
{:status status
:headers {"Location" location}
:body (str "Redirect to " location)}
{:status 500
:body "Failed to determine redirect location"}))))
#?(:clj
(defn redirect [target]
(->Redirect 302 target)))
#?(:clj
(defn redirect-after-post [target]
(->Redirect 303 target)))
;; Use this to map to paths (e.g. /static) that are expected to resolve
;; to a Java resource, and should fail-fast otherwise (returning a 404).
#?(:clj
(defrecord Resources [options]
bidi/Matched
(resolve-handler [this m]
(let [path (url-decode (:remainder m))]
(when (not-empty path)
(assoc (dissoc m :remainder)
:handler
(-> (fn [req]
(if-let [res (resource-response (str (:prefix options) path))]
res
{:status 404}))
(wrap-content-type options))))))
(unresolve-handler [this m]
(when (= this (:handler m)) ""))))
#?(:clj
(defn resources [options]
(->Resources options)))
;; Use this to map to resources, will return nil if resource doesn't
;; exist, allowing other routes to be tried. Use this to try the path as
;; a resource, but to continue if not found. Warning: Java considers
;; directories as resources, so this will yield a positive match on
;; directories, including "/", which will prevent subsequent patterns
;; being tried. The workaround is to be more specific in your
;; patterns. For example, use /js and /css rather than just /. This
;; problem does not affect Files (below).
#?(:clj
(defrecord ResourcesMaybe [options]
bidi/Matched
(resolve-handler [this m]
(let [path (url-decode (:remainder m))]
(when (not-empty path)
(when-let [res (io/resource (str (:prefix options) path))]
(assoc (dissoc m :remainder)
:handler (->
(fn [req] (resource-response (str (:prefix options) path)))
(wrap-content-type options)))))))
(unresolve-handler [this m]
(when (= this (:handler m)) ""))))
#?(:clj
(defn resources-maybe [options]
(->ResourcesMaybe options)))
;; Use this to map to files, using file-response. Options sbould include
;; :dir, the root directory containing the files.
#?(:clj
(defrecord Files [options]
bidi/Matched
(resolve-handler [this m]
(assoc (dissoc m :remainder)
:handler (->
(fn [req] (file-response (url-decode (:remainder m))
{:root (:dir options)}))
(wrap-content-type options)
(wrap-not-modified))))
(unresolve-handler [this m]
(when (= this (:handler m)) ""))))
#?(:clj
(defn files [options]
(->Files options)))
;; Use this to route to an existing archive.
;; :archive should be a resource
;; :resource-prefix (defaults to /) says where in the archive the content is
#?(:clj
(defrecord Archive [options]
bidi/Matched
(resolve-handler [this m]
(let [path (url-decode (:remainder m))]
(when (not-empty path)
(-> m
(assoc
:handler
(->
(fn [req]
(url-response (java.net.URL.
(str "jar:" (:archive options) "!"
(or (:resource-prefix options) "/") path))))
(wrap-content-type)
(wrap-not-modified)))
(dissoc :remainder)))))
(unresolve-handler [this m]
(when (= this (:handler m)) ""))))
#?(:clj
(defn archive [options]
(->Archive options)))
;; WrapMiddleware can be matched (appear on the right-hand-side of a route)
;; and returns a handler wrapped in the given middleware.
#?(:clj
(defrecord WrapMiddleware [matched middleware]
bidi/Matched
(resolve-handler [this m]
(let [r (resolve-handler matched m)]
(if (:handler r) (update-in r [:handler] middleware) r)))
(unresolve-handler [this m] (unresolve-handler matched m)))) ; pure delegation
#?(:clj
(defn wrap-middleware [matched middleware]
(->WrapMiddleware matched middleware)))
bidi-2.1.6/src/bidi/router.cljs 0000664 0000000 0000000 00000004454 13455404637 0016344 0 ustar 00root root 0000000 0000000 (ns bidi.router
(:require [bidi.bidi :as bidi]
[goog.History :as h]
[goog.events :as e]
[clojure.string :as s])
(:import [goog History]))
(defprotocol Router
(set-location! [_ location])
(replace-location! [_ location]))
(defn start-router!
"Starts up a Bidi router based on Google Closure's 'History'
Types:
Location :- {:handler ...
:route-params {...}}
Parameters:
routes :- a Bidi route structure
on-navigate :- 0-arg function, accepting a Location
default-location :- Location to default to if the current token doesn't match a route
Returns :- Router
Example usage:
(require '[bidi.router :as br])
(let [!location (atom nil)
router (br/start-router! [\"\" {\"/\" ::home-page
\"/page2\" ::page2}]
{:on-navigate (fn [location]
(reset! !location location))
:default-location {:handler ::home-page}})]
...
(br/set-location! router {:handler ::page2}))"
[routes {:keys [on-navigate default-location]
:or {on-navigate (constantly nil)}}]
(let [history (History.)]
(.setEnabled history true)
(letfn [(token->location [token]
(or (bidi/match-route routes token)
default-location))
(location->token [{:keys [handler route-params]}]
(bidi/unmatch-pair routes {:handler handler
:params route-params}))]
(e/listen history h/EventType.NAVIGATE
(fn [e]
(on-navigate (token->location (.-token e)))))
(let [initial-token (let [token (.getToken history)]
(if-not (s/blank? token)
token
(or (location->token default-location) "/")))
initial-location (token->location initial-token)]
(.replaceToken history initial-token)
(on-navigate initial-location))
(reify Router
(set-location! [_ location]
(.setToken history (location->token location)))
(replace-location! [_ location]
(.replaceToken history (location->token location)))))))
bidi-2.1.6/src/bidi/schema.cljc 0000664 0000000 0000000 00000002674 13455404637 0016246 0 ustar 00root root 0000000 0000000 ;; Copyright © 2014-2015, JUXT LTD.
(ns bidi.schema
(:require
[bidi.bidi :as bidi]
#?(:clj [schema.core :as s]
:cljs [schema.core :as s :include-macros true])))
(def Path s/Str)
(defn valid-qualifier-function? [qual]
(contains? #{keyword long bidi/uuid} qual))
(def PatternSegment
(s/cond-pre s/Str
s/Regex
s/Keyword
(s/pair (s/conditional #(fn? %) (s/pred valid-qualifier-function?) :else s/Regex) "qual" s/Keyword "id")))
(def MethodGuard
(s/enum :get :post :put :patch :delete :head :options))
(def GeneralGuard
{s/Keyword (s/cond-pre s/Str s/Keyword (s/=> s/Any s/Any))})
(s/defschema SegmentedPattern
(s/constrained [PatternSegment] not-empty 'not-empty))
(declare Pattern)
(s/defschema AlternatesSet
(s/constrained #{(s/recursive #'Pattern)} not-empty 'not-empty))
(s/defschema DeprecatedAlternates
(s/record bidi.bidi.Alternates
{:alts [(s/recursive #'Pattern)]}))
(def Pattern
(s/cond-pre AlternatesSet #_(s/protocol bidi/Pattern)
DeprecatedAlternates
Path
SegmentedPattern
MethodGuard
GeneralGuard
s/Bool))
(declare ^:export RoutePair)
(def Matched
(s/cond-pre
(s/pred record?)
s/Symbol
s/Keyword
[(s/recursive #'RoutePair)]
{Pattern (s/recursive #'Matched)}
(s/=> s/Any s/Any)
))
(def ^:export RoutePair (s/pair Pattern "Pattern" Matched "Matched"))
bidi-2.1.6/src/bidi/verbose.cljc 0000664 0000000 0000000 00000000476 13455404637 0016451 0 ustar 00root root 0000000 0000000 ;; Copyright © 2014, JUXT LTD.
(ns bidi.verbose)
(defn leaf [fragment name]
[fragment name])
(defn branch [fragment & children]
(let [[[tag param]] children]
(if (= tag :bidi/param)
[[fragment param] (vec (rest children))]
[fragment (vec children)])))
(defn param [name]
[:bidi/param name])
bidi-2.1.6/src/bidi/vhosts.clj 0000664 0000000 0000000 00000021042 13455404637 0016157 0 ustar 00root root 0000000 0000000 ;; Copyright © 2014, JUXT LTD.
(ns bidi.vhosts
(:require
[bidi.bidi :as bidi :refer :all :exclude [path-for]]
[bidi.ring :as br]
[bidi.schema :as bsc]
[schema.core :as s]
[schema.coerce :as sc]
[schema.utils :refer [error?]])
(:import [java.net URL URI]))
(s/defschema VHost (s/conditional
map? {:scheme (s/enum :http :https)
:host s/Str}
keyword? s/Keyword))
(s/defschema VHostWithRoutes
(s/constrained [(s/one [VHost] "Virtual host")
bsc/RoutePair] (comp not-empty first) "Must have at least one vhost"))
(defn uri->host [^URI uri]
(cond-> (.getHost uri)
(pos? (.getPort uri))
(str ":" (.getPort uri))))
(def coerce-to-vhost
(sc/coercer
VHost
{VHost (fn [x]
(cond (instance? URI x)
{:scheme (keyword (.getScheme ^URI x))
:host (uri->host x)}
(instance? URL x) (recur (.toURI ^URL x))
(string? x) (recur (URI. x))
:otherwise x))}))
(def coerce-to-vhosts
(sc/coercer
[VHostWithRoutes]
{VHost coerce-to-vhost
[VHost] (fn [x]
(if
(or (string? x)
(= x :*)
(instance? URI x)
(instance? URL x))
[(coerce-to-vhost x)]
(if-not (s/check VHost x) (vector x) x)))}))
(defrecord VHostsModel [vhosts])
(defn vhosts-model [& vhosts-with-routes]
(let [vhosts (coerce-to-vhosts (vec vhosts-with-routes))]
(when (error? vhosts)
(throw (ex-info (format "Error in server model: %s"
(pr-str (:error vhosts)))
{:error (:error vhosts)})))
(map->VHostsModel {:vhosts vhosts})))
(defn- query-string [query-params]
(let [enc (fn [a b] (str (if (keyword? a) (name a) a) "=" (java.net.URLEncoder/encode (str b))))
join (fn [v] (apply str (interpose "&" v)))]
(join
(map (fn [[k v]]
(if (sequential? v)
(join (map enc (repeat k) v))
(enc k v)))
query-params))))
(defn- segments [^String s]
(let [l (re-seq #"[^/]*/?" s)]
(if (.endsWith s "/") l (butlast l))))
(defn relativize [from to]
(if (and from to)
(loop [from (segments from)
to (segments to)]
(if-not (= (first from) (first to))
(str (apply str (repeat (+ (dec (count from))) "../"))
(apply str to))
(if (next from)
(recur (next from) (next to))
(first to))))
to))
(defn scheme
"Get the real scheme (as a keyword) from a request, taking into
account reverse-proxy headers"
[req]
(or (some-> (get-in req [:headers "x-forwarded-proto"]) keyword)
(:scheme req)))
(defn host
"Get the real host from a request, taking into account reverse-proxy headers"
[req]
(or (get-in req [:headers "x-forwarded-host"])
(get-in req [:headers "host"])))
(defn prioritize-vhosts [vhosts-model vhost]
(cond->> (:vhosts vhosts-model)
vhost (sort-by (fn [[vhosts & _]] (if (first (filter (fn [x] (= x vhost)) vhosts)) -1 1)))))
(defn uri-info
"Return URI info as a map."
[prioritized-vhosts handler & [{:keys [request vhost route-params query-params prefer fragment path-info] :or {prefer :local} :as options}]]
(some
(fn [[vhosts & routes]]
(when-let [path (cond->
(apply bidi/path-for ["" (vec routes)] handler (mapcat identity route-params))
path-info (str path-info))]
(let [qs (when query-params
(query-string query-params))]
(let [to-vhost (case prefer
:local (or (first (filter (partial = vhost) vhosts))
(first vhosts))
:first (first vhosts)
:same-scheme (first (filter #(= (:scheme vhost) (:scheme %)) vhosts))
:http (first (filter #(= :http (:scheme %)) vhosts))
:https (first (filter #(= :https (:scheme %)) vhosts))
:local-then-same-scheme (or (first (filter (partial = vhost) vhosts))
(first (filter #(= (:scheme vhost) (:scheme %)) vhosts))
(first vhosts)))
uri (format "%s://%s%s%s%s"
(cond (= to-vhost :*) (name (scheme request))
:otherwise (name (:scheme to-vhost)))
(cond (= to-vhost :*) (host request)
:otherwise (:host to-vhost))
path
(if qs (str "?" qs) "")
(if fragment (str "#" fragment) ""))]
(merge {:uri uri
:path path
:host (:host to-vhost)
:scheme (:scheme to-vhost)
:href (if (and (= vhost to-vhost) request)
(format "%s%s%s"
(relativize (:uri request) path)
(if qs (str "?" qs) "")
(if fragment (str "#" fragment) ""))
uri)}
(when qs {:query-string qs})
(when fragment {:fragment fragment}))))))
prioritized-vhosts))
(def path-for (comp :path uri-info))
(def host-for (comp :host uri-info))
(def scheme-for (comp :scheme uri-info))
(def href-for (comp :href uri-info))
;; At some point in the future, uri-for will be removed, opening up
;; the possibility that it can be re-implemented.
(def ^{:deprecated true} uri-for uri-info)
(defn find-handler [vhosts-model req]
(let [vhost {:scheme (scheme req)
:host (host req)}]
(some
(fn [[vhosts & routes]]
(let [routes (vec routes)]
(when (some (fn [vh] (or (= vh :*) (= (:host vh) (:host vhost)))) vhosts)
(->
(resolve-handler
routes
(assoc req
:remainder (:uri req)
:route ["" routes]
:uri-info (fn [handler & [options]]
(uri-info (prioritize-vhosts vhosts-model vhost) handler (merge {:vhost vhost :request req} options)))))
(dissoc :route)))))
(:vhosts vhosts-model))))
(defn vhosts->roots [vhosts request]
(->> vhosts
(map first)
(apply concat)
(map (fn [x]
(cond
(= x :*) (format "%s://%s" (name (scheme request)) (host request))
:otherwise (let [{:keys [scheme host]} x]
(str (name scheme) "://" host)))))))
(defn make-default-not-found-handler [vhosts-model]
(fn [req]
{:status 404
:body (apply str "Not found\n\n"
;; It is useful to provide the hosts that are served from this server
(->> (vhosts->roots (:vhosts vhosts-model) req)
(interpose "\n")))}))
(defn make-handler
([vhosts-model] (make-handler vhosts-model identity))
([vhosts-model handler-fn]
(make-handler vhosts-model handler-fn (make-default-not-found-handler vhosts-model)))
([vhosts-model handler-fn not-found]
(fn [req]
(let [{:keys [handler route-params] :as match-context}
(find-handler vhosts-model req)]
(if-let [handler (handler-fn handler)]
(br/request
handler
(-> req
(update-in [:params] merge route-params)
(update-in [:route-params] merge route-params))
(apply dissoc match-context :handler (keys req)))
(not-found req))))))
(defrecord Redirect [status target query-params]
bidi/Matched
(resolve-handler [this m]
(when (= "" (:remainder m))
(cond-> m
true (assoc :handler this)
(not (string? target))
(assoc :location
(:uri ((:uri-info m) target
(merge
{:route-params (:route-params m)}
(when query-params {:query-params query-params})))))
true (dissoc :remainder))))
(unresolve-handler [this m]
(when (= this (:handler m)) ""))
br/Ring
(request [f req m]
(if-let [location (if-not (string? target) (:location m) target)]
{:status status
:headers {"location" location}
:body (str "Redirect to " location)}
{:status 500
:body "Failed to determine redirect location"})))
(defn redirect [target & [opts]]
(map->Redirect (merge {:status 302 :target target} opts)))
bidi-2.1.6/test-resources/ 0000775 0000000 0000000 00000000000 13455404637 0015431 5 ustar 00root root 0000000 0000000 bidi-2.1.6/test-resources/foo.css 0000664 0000000 0000000 00000000014 13455404637 0016721 0 ustar 00root root 0000000 0000000 // Some CSS
bidi-2.1.6/test/ 0000775 0000000 0000000 00000000000 13455404637 0013421 5 ustar 00root root 0000000 0000000 bidi-2.1.6/test/bidi/ 0000775 0000000 0000000 00000000000 13455404637 0014330 5 ustar 00root root 0000000 0000000 bidi-2.1.6/test/bidi/bidi_test.cljc 0000664 0000000 0000000 00000034442 13455404637 0017142 0 ustar 00root root 0000000 0000000 ;; Copyright © 2014, JUXT LTD.
(ns bidi.bidi-test
(:require
#?(:clj [clojure.test :refer :all]
:cljs [cljs.test :refer-macros [deftest is testing]])
[bidi.bidi :as bidi :refer [match-route path-for ->Alternates route-seq alts tag]]))
(def foo-var-handler identity)
(def bar-var-handler identity)
(deftest matching-routes-test
(testing "misc-routes"
(is (= (match-route ["/blog/foo" 'foo] "/blog/foo")
{:handler 'foo}))
;; In the case of a partial match, the right hand side of a pair can
;; contain further candidates to try. Multiple routes are contained
;; in a vector and tried in order.
(is (= (match-route ["/blog" [["/foo" 'foo]
["/bar" [["/abc" :bar]]]]]
"/blog/bar/abc")
{:handler :bar}))
;; If no determinstic order is required, a map can also be used.
(is (= (match-route ["/blog" {"/foo" 'foo
"/bar" [["/abc" :bar]]}]
"/blog/bar/abc")
{:handler :bar}))
(is (= (match-route ["/blog" [["/foo" 'foo]
[["/bar" [#".*" :path]] :bar]]]
"/blog/bar/articles/123/index.html")
{:handler :bar :route-params {:path "/articles/123/index.html"}}))
;; The example in the README, so make sure it passes!
(is (= (match-route ["/blog" [["/index.html" 'index]
[["/bar/articles/" :artid "/index.html"] 'article]]]
"/blog/bar/articles/123/index.html")
{:handler 'article :route-params {:artid "123"}}))
(is (= (match-route ["/blog" [["/foo" 'foo]
[["/bar/articles/" :artid "/index.html"] 'bar]]]
"/blog/bar/articles/123/index.html")
{:handler 'bar :route-params {:artid "123"}}))
(is (= (match-route ["/blog" [[["/articles/" :id "/index.html"] 'foo]
["/text" 'bar]]]
"/blog/articles/123/index.html")
{:handler 'foo :route-params {:id "123"}}))
(is (= (match-route ["/blog" [["/foo" 'foo]
["/bar" [["/abc" :bar]]]]]
"/blog/bar/abc?q=2&b=str")
{:handler :bar}))
(testing "var support"
(is (= (match-route ["/blog" [["/foo" #'foo-var-handler]
["/bar" [["/abc" #'bar-var-handler]]]]]
"/blog/bar/abc")
{:handler #'bar-var-handler})))
(testing "regex"
(is (= (match-route ["/blog" [[["/articles/" [#"[0-9]+" :id] "/index.html"] 'foo]
["/text" 'bar]]]
"/blog/articles/123/index.html")
{:handler 'foo :route-params {:id "123"}}))
(is (= (match-route ["/blog" [[["/articles/" [#"[0-9]+" :id] "/index.html"] 'foo]
["/text" 'bar]]]
"/blog/articles/123a/index.html")
nil))
(is (= (match-route ["/blog" [[["/articles/" [#"[0-9]+" :id] [#"[a-z]+" :a] "/index.html"] 'foo]
["/text" 'bar]]]
"/blog/articles/123abc/index.html")
{:handler 'foo :route-params {:id "123" :a "abc"}}))
#?(:clj
(is (= (match-route [#"/bl[a-z]{2}+" [[["/articles/" [#"[0-9]+" :id] [#"[a-z]+" :a] "/index.html"] 'foo]
["/text" 'bar]]]
"/blog/articles/123abc/index.html")
{:handler 'foo :route-params {:id "123" :a "abc"}})))
(is (= (match-route [["/blog/articles/123/" :path] 'foo]
"/blog/articles/123/index.html")
{:handler 'foo :route-params {:path "index.html"}})))
(testing "boolean patterns"
(is (= (match-route [true :index] "/any") {:handler :index}))
(is (= (match-route [false :index] "/any") nil)))))
(deftest unmatching-routes-test
(let [routes ["/"
[["blog"
[["/index.html" 'blog-index]
[["/article/" :id ".html"] 'blog-article-handler]
[["/archive/" :id "/" :page ".html"] 'archive-handler]]]
[["images/" :path] 'image-handler]]]]
(testing "unmatching"
(is
(= (path-for routes 'blog-index)
"/blog/index.html"))
(is
(= (path-for routes 'blog-article-handler :id 1239)
"/blog/article/1239.html"))
(is
;; If not all the parameters are specified we expect an error to be thrown
(thrown? #?(:clj clojure.lang.ExceptionInfo :cljs cljs.core.ExceptionInfo)
(path-for routes 'archive-handler :id 1239)
"/blog/archive/1239/section.html"))
(is
(= (path-for routes 'archive-handler :id 1239 :page "section")
"/blog/archive/1239/section.html"))
(is
(= (path-for routes 'image-handler :path "")
"/images/"))
(is
(= (path-for routes 'image-handler :path "123.png")
"/images/123.png")))
(testing "unmatching with constraints"
(let [routes ["/" [["blog"
[[:get [[["/index"] :index]]]
[{:request-method :post :server-name "juxt.pro"}
[[["/articles/" :artid] :new-article-handler]]]]]]]]
(is (= (path-for routes :index)
"/blog/index"))
(is (= (path-for routes :new-article-handler :artid 10)
"/blog/articles/10"))))
(testing "unmatching with regexes"
(let [routes
["/blog" [[["/articles/" [#"[0-9]+" :id] [#"[a-z]+" :a] "/index.html"] 'foo]
["/text" 'bar]]]]
(is (= (path-for routes 'foo :id "123" :a "abc")
"/blog/articles/123abc/index.html"))))
(testing "unmatching with nil handlers" ; issue #28
(let [routes ["/" {"foo" nil "bar" :bar}]]
(is (= (path-for routes :bar) "/bar"))))
(testing "unmatching tags"
(let [routes ["/" {"foo" (tag :handler :tag) "bar" :bar}]]
(is (= (path-for routes :tag) "/foo"))))))
(deftest unmatching-routes-with-anonymous-fns-test
(testing "unmatching when routes contains a ref to anonymous function(s) should not throw exception"
(let [routes
["/blog" [["/index.html" (fn [req] {:status 200 :body "Index"})]
["/list" 'list-blogs]
["/temp.html" :temp-html]]]]
(is (= (path-for routes 'list-blogs)
"/blog/list"))
(is (= (path-for routes :temp-html)
"/blog/temp.html")))))
(deftest keywords
(let [routes ["/" [["foo/" :x]
[["foo/" [keyword :id]] :y]
[["foo/" [keyword :id] "/bar"] :z]]]]
(is (= (:handler (match-route routes "/foo/")) :x))
(is (= (:handler (match-route routes "/foo/abc")) :y))
(is (= (:route-params (match-route routes "/foo/abc")) {:id :abc}))
(is (= (:route-params (match-route routes "/foo/abc%2Fdef")) {:id :abc/def}))
(is (= (path-for routes :y :id :abc) "/foo/abc"))
(is (= (path-for routes :y :id :abc/def) "/foo/abc%2Fdef"))
(is (= (:handler (match-route routes "/foo/abc/bar")) :z))
(is (= (path-for routes :z :id :abc) "/foo/abc/bar"))))
(deftest number-test
(let [routes ["/" [["foo/" :x]
[["foo/" [long :id]] :y]
[["foo/" [long :id] "/bar"] :z]]]]
(is (= (:handler (match-route routes "/foo/")) :x))
(is (= (:handler (match-route routes "/foo/345")) :y))
(is (= (:route-params (match-route routes "/foo/345")) {:id 345}))
(is (= (path-for routes :y :id -1000) "/foo/-1000"))
(is (= (path-for routes :y :id 1234567) "/foo/1234567"))
(is (= (:handler (match-route routes "/foo/0/bar")) :z))
(is (= (path-for routes :z :id (long 12)) "/foo/12/bar"))
(is (= (path-for routes :z :id (int 12)) "/foo/12/bar"))
(is (= (path-for routes :z :id (short 12)) "/foo/12/bar"))
(is (= (path-for routes :z :id (byte 12)) "/foo/12/bar"))
(testing "bigger than longs"
(is (nil? (match-route routes "/foo/1012301231111111111111111111"))))))
(deftest uuid-test
(let [routes ["/" [["foo/" :x]
[["foo/" [bidi/uuid :id]] :y]
[["foo/" [bidi/uuid :id] "/bar"] :z]]]]
(is (= (:handler (match-route routes "/foo/")) :x))
(is (= (:handler (match-route routes "/foo/649a50e8-0342-47af-894e-27eefea83ca9"))
:y))
(is (= (:route-params (match-route routes "/foo/649a50e8-0342-47af-894e-27eefea83ca9"))
{:id #uuid "649a50e8-0342-47af-894e-27eefea83ca9"}))
(is (= (path-for routes :y :id #uuid "649a50e8-0342-47af-894e-27eefea83ca9")
"/foo/649a50e8-0342-47af-894e-27eefea83ca9"))
(is (= (:handler (match-route routes "/foo/649a50e8-0342-47af-894e-27eefea83ca9/bar")) :z))
(is (= (path-for routes :z :id #uuid "649a50e8-0342-47af-894e-27eefea83ca9")
"/foo/649a50e8-0342-47af-894e-27eefea83ca9/bar"))
(is (= (:route-params (match-route routes "/foo/00000000-0000-0000-0000-000000000000"))
{:id #uuid "00000000-0000-0000-0000-000000000000"}))
(is (= (:route-params (match-route routes "/foo/649a50e8-0342-47af-c94e-27eefea83ca9"))
{:id #uuid "649a50e8-0342-47af-c94e-27eefea83ca9"}))
(is (= (:route-params (match-route routes "/foo/649a50e8-0342-67af-894e-27eefea83ca9"))
{:id #uuid "649a50e8-0342-67af-894e-27eefea83ca9"}))
(testing "invalid uuids"
(is (nil? (match-route routes "/foo/649a50e8034247afc94e27eefea83ca9")))
(is (nil? (match-route routes "/foo/1012301231111111111111111111"))))))
(deftest wrap-alternates-test
(let [routes [(->Alternates ["/index.html" "/index"]) :index]]
(is (= (match-route routes "/index.html") {:handler :index}))
(is (= (match-route routes "/index") {:handler :index}))
(is (= (path-for routes :index) "/index.html"))) ; first is the canonical one
(let [routes [(alts "/index.html" "/index") :index]]
(is (= (match-route routes "/index.html") {:handler :index}))
(is (= (match-route routes "/index") {:handler :index}))))
(deftest similar-alternates-test
(let [routes-test ["/" {(alts ["index" "index-x" "index.html" "x-index.html" "index-x.html" "x.html"]) :index}]]
(= (match-route routes-test "/index") :index)
(= (match-route routes-test "/index-x") :index)
(= (match-route routes-test "/index.html") :index)
(= (match-route routes-test "/x.html") :index)
(= (match-route routes-test "/x-index.html") :index)
(= (match-route routes-test "/index-x.html") :index)))
(deftest wrap-set-test
(let [routes [#{"/index.html" "/index"} :index]]
(is (= (match-route routes "/index.html") {:handler :index}))
(is (= (match-route routes "/index") {:handler :index}))
(is (= (path-for routes :index) "/index.html"))) ; first is the canonical one
(let [routes [#{"/index.html" "/index"} :index]]
(is (= (match-route routes "/index.html") {:handler :index}))
(is (= (match-route routes "/index") {:handler :index}))))
(deftest similar-set-test
(let [routes-test ["/" {#{"index" "index-x" "index.html" "x-index.html" "index-x.html" "x.html"} :index}]]
(= (match-route routes-test "/index") :index)
(= (match-route routes-test "/index-x") :index)
(= (match-route routes-test "/index.html") :index)
(= (match-route routes-test "/x.html") :index)
(= (match-route routes-test "/x-index.html") :index)
(= (match-route routes-test "/index-x.html") :index)))
(deftest route-seq-test
(testing ""
(let [myroutes
["/" [
["index" :index]
["docs/" [
[[:doc-id "/"]
[["view" :docview]
[["chapter/" :chapter "/"] {"view" :chapter-view}]]]]]]]
result (route-seq ["" [myroutes]])]
(is (= [["" "/" "index"]
["" "/" "docs/" [:doc-id "/"] "view"]
["" "/" "docs/" [:doc-id "/"] ["chapter/" :chapter "/"] "view"]]
(map :path result)))
(is (= 3 (count result)))))
(testing "qualifiers"
(is (= [["/" ["test/" [keyword :id] "/end"]]
["/" "test/"]]
(map :path (route-seq ["/" [[["test/" [keyword :id] "/end"] :view]
["test/" :test]]]))))
(is (= [[[[bidi/uuid :id]]]]
(map :path (route-seq [[[bidi/uuid :id]] :view]))))
(is (= [[[[long :id]]]]
(map :path (route-seq [[[long :id]] :view]))))
(let [pattern #"^m\d+b$"]
(is (= [[[[pattern :id]]]]
(map :path (route-seq [[[pattern :id]] :view])))))
(is (= [[[[keyword :id]]]]
(map :path (route-seq [[[keyword :id]] :view]))))
(is (= [[["test/" [keyword :id]]]]
(map :path (route-seq [["test/" [keyword :id]] :view]))))
(is (= [[[[keyword :id] "/end"]]]
(map :path (route-seq [[[keyword :id] "/end"] :view]))))
(is (= [[["test/" [keyword :id] "/end"]]]
(map :path (route-seq [["test/" [keyword :id] "/end"] :view])))))
(testing "set patterns"
(let [result (route-seq [#{"" "/"} [[:a "A"]
[:b "B"]]])]
(is (= [["" :a] ["" :b] ["/" :a] ["/" :b]]))
(is (= 4 (count result)))))
(testing "alt patterns"
(let [result (route-seq [(bidi/alts "" "/") [[:a "A"]
[:b "B"]]])]
(is (= [["" :a] ["" :b] ["/" :a] ["/" :b]]))
(is (= 4 (count result)))))
(testing "only leaves"
(let [result (route-seq [#{"" "/"} [[:a "A"]
[:b "B"]]])]
(is (= [["" :a] ["" :b] ["/" :a] ["/" :b]] (map :path result)))
(is (= 4 (count result)))))
(testing "tags"
(let [result (route-seq ["/" [[:a (bidi/tag "A" :abc)]
[:b "B"]]])]
(is (= [:abc nil] (map :tag result))))))
(deftest boolean-test
(let [myroutes [true :foo]]
(is (= {:handler :foo} (match-route myroutes "/")))
(is (= "" (path-for myroutes :foo)))))
(deftest colon-test
(let [myroutes ["/" {":a" :a
"b" :b}]]
(is (= {:handler :a} (match-route myroutes "/:a")))
(is (= {:handler :b} (match-route myroutes "/b")))
(is (nil? (match-route myroutes "/a")))
(is (nil? (match-route myroutes "/:b")))))
(deftest invalid-uri-test
(is (= (match-route ["/blog/foo" 'foo] "<><>") nil)))
bidi-2.1.6/test/bidi/ring_test.clj 0000664 0000000 0000000 00000014652 13455404637 0017030 0 ustar 00root root 0000000 0000000 ;; Copyright © 2014, JUXT LTD.
(ns bidi.ring-test
(:require
[bidi.ring :refer :all]
[bidi.bidi :refer :all]
[clojure.test :refer :all]
[ring.mock.request :refer (request) :rename {request mock-request}]))
(deftest make-handler-test
(testing "routes"
(let [handler
(make-handler ["/"
[["blog"
[["/index.html" (fn [req] {:status 200 :body "Index"})]
[["/article/" :id ".html"] 'blog-article-handler]
[["/archive/" :id "/" :page ".html"] 'archive-handler]]]
["images/" 'image-handler]]])]
(is (= (handler (mock-request :get "/blog/index.html"))
{:status 200 :body "Index"}))))
(testing "method constraints"
(let [handler
(make-handler ["/"
[["blog"
[[:get [["/index.html" (fn [req] {:status 200 :body "Index"})]]]
[:post [["/zip" (fn [req] {:status 201 :body "Created"})]]]]
]]])]
(is handler)
(is (= (handler (mock-request :get "/blog/index.html")) {:status 200 :body "Index"}))
(is (nil? (handler (mock-request :post "/blog/index.html"))))
(is (= (handler (mock-request :post "/blog/zip")) {:status 201 :body "Created"}))
(is (nil? (handler (mock-request :get "/blog/zip"))))))
(testing "other request constraints"
(let [handler
(make-handler ["/"
[["blog"
[[:get
[["/index"
(fn [req] {:status 200 :body "Index"})]
[["/article/" :artid "/article.html"]
(fn [req] {:status 200 :body (get-in req [:route-params :artid])})]
]]
[{:request-method :post :server-name "juxt.pro"}
[["/zip"
(fn [req] {:status 201 :body "Created"})]]]]]]])]
(is handler)
(is (nil? (handler (mock-request :post "/blog/zip"))))
(is (= (handler (mock-request :post "http://juxt.pro/blog/zip"))
{:status 201 :body "Created"}))
(is (nil? (handler (mock-request :post "/blog/zip"))))
(testing "artid makes it into :route-params"
(is (= (handler (mock-request :get "/blog/article/123/article.html"))
{:status 200 :body "123"})))))
(testing "applying optional function to handler"
(let [handler-lookup {:my-handler (fn [req] {:status 200 :body "Index"})}
handler (make-handler ["/" :my-handler] (fn [handler-id] (handler-id handler-lookup)))]
(is handler)
(is (= (handler (mock-request :get "/")) {:status 200 :body "Index"}))))
(testing "using handler vars"
(defn test-handler [req] {:status 200 :body "Index"})
(let [handler
(make-handler ["/"
[["" (-> #'test-handler (tag :index))]]])]
(is handler)
(is (= (handler (mock-request :get "/"))
{:status 200 :body "Index"})))))
(deftest route-params-hygiene-test
(let [handler
(make-handler [["/blog/user/" :userid "/article"]
(fn [req] {:status 201 :body (:route-params req)})])]
(is handler)
(testing "specified params like userid make it into :route-params
but other params do not"
(is (= (handler (-> (mock-request :put "/blog/user/8888/article")
(assoc :params {"foo" "bar"})))
{:status 201 :body {:userid "8888"}})))))
(deftest redirect-test
(let [content-handler (fn [req] {:status 200 :body "Some content"})
routes ["/articles/"
[[[:artid "/new"] content-handler]
[[:artid "/old"] (->Redirect 307 content-handler)]]]
handler (make-handler routes)]
(is (= (handler (mock-request :get "/articles/123/old"))
{:status 307, :headers {"Location" "/articles/123/new"}, :body "Redirect to /articles/123/new"} ))))
(deftest wrap-middleware-test
(let [wrapper (fn [h] (fn [req] (assoc (h req) :wrapper :evidence)))
handler (fn [req] {:status 200 :body "Test"})]
(is (= ((:handler (match-route ["/index.html" (->WrapMiddleware handler wrapper)] "/index.html"))
{:uri "/index.html"})
{:wrapper :evidence :status 200 :body "Test"}))
(is (= ((:handler (match-route ["/index.html" (->WrapMiddleware handler wrapper)] "/index.html"))
{:path-info "/index.html"})
{:wrapper :evidence :status 200 :body "Test"}))
(is (= (path-for ["/index.html" (->WrapMiddleware handler wrapper)] handler) "/index.html"))
(is (= (path-for ["/index.html" handler] handler) "/index.html"))))
(deftest tagger-handlers
(let [routes ["/" [["foo" (tag (fn [req] "foo!") :foo)]
[["bar/" :id] (tag (fn [req] "bar!") :bar)]]]]
(is (= ((make-handler routes) (mock-request :get "/foo")) "foo!"))
(is (= ((make-handler routes) (mock-request :get "/bar/123")) "bar!"))
(is (= (path-for routes :foo) "/foo"))
(is (= (path-for routes :bar :id "123") "/bar/123"))))
(deftest unresolve-handlers
(testing "Redirect"
(let [foo (->Redirect 303 "/home/foo")
bar (->Redirect 303 "/home/bar")
routes ["/" [["foo" foo]
["bar" bar]]]]
(is (= "/foo" (path-for routes foo)))
(is (= "/bar" (path-for routes bar)))))
(testing "Resources"
(let [foo (->Resources {:id :foo})
bar (->Resources {:id :bar})
routes ["/" [["foo" foo]
["bar" bar]]]]
(is (= "/foo" (path-for routes foo)))
(is (= "/bar" (path-for routes bar)))))
(testing "ResourcesMaybe"
(let [foo (->ResourcesMaybe {:id :foo})
bar (->ResourcesMaybe {:id :bar})
routes ["/" [["foo" foo]
["bar" bar]]]]
(is (= "/foo" (path-for routes foo)))
(is (= "/bar" (path-for routes bar)))))
(testing "Files"
(let [foo (->Files {:id :foo})
bar (->Files {:id :bar})
routes ["/" [["foo" foo]
["bar" bar]]]]
(is (= "/foo" (path-for routes foo)))
(is (= "/bar" (path-for routes bar))))))
(deftest resource-mimetype-detection []
(let [h (make-handler ["/resources/" (->ResourcesMaybe {})])
resp (h (mock-request :get "/resources/foo.css"))]
(is (= "text/css" (get-in resp [:headers "Content-Type"])))))
bidi-2.1.6/test/bidi/runner.cljs 0000664 0000000 0000000 00000000334 13455404637 0016516 0 ustar 00root root 0000000 0000000 ;; Copyright © 2014, JUXT LTD.
(ns bidi.runner
(:require [doo.runner :refer-macros [doo-tests]]
[bidi.bidi-test]
[bidi.schema-test]))
(doo-tests 'bidi.bidi-test
'bidi.schema-test)
bidi-2.1.6/test/bidi/schema_test.cljc 0000664 0000000 0000000 00000011471 13455404637 0017470 0 ustar 00root root 0000000 0000000 ;; Copyright © 2014-2015, JUXT LTD.
(ns bidi.schema-test
(:require
#?(:clj [clojure.test :refer :all]
:cljs [cljs.test :refer-macros [deftest is testing]])
[schema.core :as s]
#?(:clj [schema.macros :refer [if-cljs]])
[bidi.bidi :as bidi]
[bidi.schema :as bs])
#?(:cljs (:require-macros [bidi.schema-test :refer [is-valid is-invalid testing-schema]])))
;; TODO
(def ^{:dynamic true :tag s/Schema} *schema* nil)
(defmacro is-valid
([actual]
`(is-valid *schema* ~actual))
([schema actual]
`(schema.macros/if-cljs
(cljs.test/is (= nil (s/check ~schema ~actual)))
(clojure.test/is (= nil (s/check ~schema ~actual))))))
(defmacro is-invalid
([expected actual]
`(is-invalid *schema* ~expected ~actual))
([schema expected actual]
`(schema.macros/if-cljs
(cljs.test/is (= (str (quote ~expected)) (str (s/check ~schema ~actual))))
(clojure.test/is (= (str (quote ~expected)) (str (s/check ~schema ~actual)))))))
(defmacro testing-schema [name schema & args]
`(schema.macros/if-cljs
(cljs.test/testing ~name
(binding [*schema* ~schema]
~@args))
(clojure.test/testing ~name
(binding [*schema* ~schema]
~@args))))
(deftest schema-test
(testing-schema "route pairs" bs/RoutePair
(testing "simple"
(is-valid ["/index/" :index])
(is-valid ["/index/" [["a" :alpha]
["b" :beta]
["c" [["z" :zeta]]]]])
(is-valid ["/index/" {"a" :alpha
"b" :beta
"c" {"z" :zeta}}]))
(testing "path segments"
(is-valid [["/" :i] :target])
(is-invalid [(named (not (not-empty [])) "Pattern") nil]
[[] :test]))
(testing "qualified path segments"
(is-valid [["/" [#".*" :i]] :target])
(is-valid [["/" [keyword :i]] :target])
(is-valid [["/" [long :i]] :target])
(is-valid [["/" [bidi/uuid :i]] :target])
(is-invalid
[(named [nil [(named (not (instance? #?(:clj java.util.regex.Pattern
:cljs js/RegExp)
"muh"))
"qual")
nil]]
"Pattern")
nil]
[["/" ["muh" :i]] :target])
(is-invalid
[(named [nil [(named (not #?(:clj (bidi.schema/valid-qualifier-function? a-clojure.core$symbol)
:cljs (bidi$schema$valid-qualifier-function? a-function)))
"qual")
nil]]
"Pattern")
nil]
[["/" [symbol :i]] :target]))
(testing "method guards"
(is-valid ["/" {:get :get-handler
:post :post-handler
:patch :patch-handler}])
(is-valid [:get :test-handler])
(is-invalid [nil (named {(not (matches-some-precondition? #?(:clj a-clojure.lang.Keyword
:cljs a-object)))
invalid-key}
"Matched")]
["/" {:not-a-recognised-method :handler}]))
(testing "general guards"
(is-valid
["/" {"blog" {:get
{"/index" (fn [req] {:status 200 :body "Index"})}}
{:request-method :post :server-name "juxt.pro"}
{"/zip" (fn [req] {:status 201 :body "Created"})}}]))
(testing "wrong patterns"
(is-invalid [(named (not (matches-some-precondition? :test)) "Pattern") nil]
[:test :test-handler])
(is-invalid [(named (not (matches-some-precondition? 12)) "Pattern") nil]
[12 :test])
(is-invalid
[(named {(not (#?(:clj keyword?
:cljs cljs$core$keyword?)
14))
invalid-key}
"Pattern")
nil]
[{14 12} :test])
(is-invalid
[(named {(not (#?(:clj keyword?
:cljs cljs$core$keyword?)
"test"))
invalid-key}
"Pattern")
nil]
[{"test" 12} :test]))
(testing "common mistake"
(is-invalid [nil (named [(not (sequential? "a"))
(not (sequential? :alpha))] "Matched")
(not (has-extra-elts? 2))]
["/index/"
["a" :alpha]
["b" :beta]
["c" [["z" :zeta]]]]))
(testing "sets"
(is-valid ["/index/" [[#{"foo" "bar"} :foo-or-bar]]])
(is-invalid [(named (not (not-empty #{})) "Pattern") nil]
[#{} :test]))
(testing "alts"
(is-valid [(bidi/alts) :empty])
(is-valid ["/index/" [[(bidi/alts "foo" "bar") :foo-or-bar]]]))))
bidi-2.1.6/test/bidi/verbose_test.cljc 0000664 0000000 0000000 00000001720 13455404637 0017671 0 ustar 00root root 0000000 0000000 ;; Copyright © 2014, JUXT LTD.
(ns bidi.verbose-test
(:require
#?(:clj [clojure.test :refer :all]
:cljs [cljs.test :refer-macros [deftest is testing]])
[bidi.verbose :refer [branch param leaf]]))
(deftest verbose-syntax-test
(is (= ["http://localhost:8080"
[[["/users/" :user-id]
[["/topics" [["" :topics] ["/bulk" :topic-bulk]]]]]
[["/topics/" :topic] [["" :private-topic]]]
["/schemas" :schemas]
[["/orgs/" :org-id] [["/topics" :org-topics]]]]]
(branch
"http://localhost:8080"
(branch "/users/" (param :user-id)
(branch "/topics"
(leaf "" :topics)
(leaf "/bulk" :topic-bulk)))
(branch "/topics/" (param :topic)
(leaf "" :private-topic))
(leaf "/schemas" :schemas)
(branch "/orgs/" (param :org-id)
(leaf "/topics" :org-topics))))))
bidi-2.1.6/test/bidi/vhosts_test.clj 0000664 0000000 0000000 00000017017 13455404637 0017415 0 ustar 00root root 0000000 0000000 ;; Copyright © 2014, JUXT LTD.
(ns bidi.vhosts-test
(:require
[clojure.test :refer :all]
[schema.core :as s]
[schema.utils :refer [error?]]
[bidi.vhosts :refer :all]
[ring.mock.request :refer (request) :rename {request mock-request}]))
(def example-vhosts-model
(vhosts-model
[[{:scheme :https :host "a.org"}
{:scheme :http :host "a.org"}
{:scheme :http :host "www.a.org"}
{:scheme :https :host "www.a.org"}]
["/index" :a]]
[{:scheme :https :host "b.org"}
[["/b/" :n "/b1.html"] :b1]
[["/b/" :n "/b2.html"] :b2]]
[[{:scheme :http :host "c.com:8000"}
{:scheme :https :host "c.com:8001"}]
["/index.html" :c]
["/x" :x]]
[{:scheme :http :host "d.com:8002"}
["/index/" [["d" :d]]]
;; :x is in both this and the one above
["/dir/x" :x]]
[:* ["/index.html" :wildcard-index]]))
(deftest find-handler-test
(is (= :c (:handler (find-handler
example-vhosts-model
;; Ring request
{:scheme :http
:headers {"host" "c.com:8000"}
;; Ring confusingly calls the URI's path
:uri "/index.html"})) ))
(is (= :d (:handler (find-handler
(vhosts-model [[:* {:scheme :http :host "example.org"}] ["/index.html" :d]])
;; Matches due to wildcard
{:scheme :http
:headers {"host" "c.com:8000"}
:uri "/index.html"})) )))
(deftest relativize-test
(are [source dest href] (= href (relativize source dest))
"" "" nil
"/abc/cd/d.html" "/abc/" "../"
"/abc/foo.html" "/abc/bar.html" "bar.html"
"/abc/foo/a.html" "/abc/bar/b.html" "../bar/b.html"
"/abc/foo/a/b" "/abc/bar/b" "../../bar/b"
"/abc.html" "/abc.html" "abc.html"
"/a/abc.html" "/a/abc.html" "abc.html"
"/a/" "/a/abc.html" "abc.html"
"/a" "/a/abc.html" "a/abc.html"
"/a/abc.html" "/a/" ""
))
(deftest uri-info-test
(let [raw-model example-vhosts-model
model (prioritize-vhosts raw-model nil)]
(testing "uris"
(is (= "https://a.org/index" (:uri (uri-info model :a {:vhost {:scheme :https :host "a.org"}}))))
(is (= "http://c.com:8000/index.html" (:uri (uri-info model :c {:vhost {:scheme :http :host "c.com:8000"}}))))
(is (= "http://d.com:8002/index/d" (:uri (uri-info model :d {:vhost {:scheme :http :host "d.com:8002"}})))))
(testing "route-params"
(is (= "https://b.org/b/1/b1.html" (:uri (uri-info model :b1 {:route-params {:n 1}
:vhost {:scheme :https :host "b.org"}}))))
(is (= "https://b.org/b/abc/b2.html" (:uri (uri-info model :b2 {:route-params {:n "abc"}
:vhost {:scheme :https :host "b.org"}})))))
(testing "relative"
(is (= "http://a.org/index" (:uri (uri-info model :a {:vhost {:scheme :http :host "a.org"}}))))
(is (= "http://c.com:8000/index.html" (:uri (uri-info model :c {:vhost {:scheme :http :host "c.com:8000"}}))))
(is (= "https://c.com:8001/index.html" (:uri (uri-info model :c {:vhost {:scheme :https :host "c.com:8001"}})))))
(testing "same scheme is preferred by default"
(is (= "http://www.a.org/index" (:uri (uri-info model :a {:vhost {:scheme :http :host "www.a.org"}}))))
(is (= "https://www.a.org/index" (:uri (uri-info model :a {:vhost {:scheme :https :host "www.a.org"}})))))
(testing "query params"
(is (= "https://b.org/b/1/b1.html?foo=bar"
(:uri (uri-info model :b1 {:route-params {:n 1}
:query-params {"foo" "bar"}
:vhost {:scheme :https :host "b.org"}}))))
(is (= "https://b.org/b/1/b1.html?foo=bar&foo=fry%26laurie"
(:uri (uri-info model :b1 {:route-params {:n 1}
:query-params {"foo" ["bar" "fry&laurie"]}
:vhost {:scheme :https :host "b.org"}}))))
;; :href should include query strings and fragments, like :uri
(is (= "/b/1/b1.html?foo=bar&foo=zip"
(:href (uri-info model :b1 {:route-params {:n 1}
:query-params {"foo" ["bar" "zip"]}
:vhost {:scheme :https :host "b.org"}
:request {:scheme :https :headers {"host" "example.org"}}})))))
(testing "wildcards"
(is (= "https://example.org/index.html" (:uri (uri-info model :wildcard-index {:request {:scheme :https :headers {"host" "example.org"}}})))))
(testing "path-info"
(is (= "https://a.org/index.html" (:uri (uri-info model :c {:path-info "/index.html"})))))))
(deftest duplicate-routes-test
(testing "same vhost takes priority"
(is (= "https://c.com:8001/x" (:uri (uri-info (prioritize-vhosts example-vhosts-model {:scheme :https :host "c.com:8001"}) :x {:prefer :https}))))
(is (= "http://d.com:8002/dir/x" (:uri (uri-info (prioritize-vhosts example-vhosts-model {:scheme :http :host "d.com:8002"}) :x))))))
(deftest make-handler-test
(let [h (make-handler example-vhosts-model
{:c (fn [req] {:status 200})})]
(is (= {:status 200}
(h
{:scheme :http
:headers {"host" "c.com:8000"}
:uri "/index.html"})))))
(deftest redirect-test
(let [model (vhosts-model
[[{:scheme :https :host "a.org"}
{:scheme :http :host "www.a.org"}]
["" [["/index" :a] ["/" (redirect :a)]]]])
h (make-handler model)]
(let [resp (h {:scheme :http
:headers {"host" "www.a.org"}
;; Ring confusingly calls the URI's path
:uri "/"})]
(is (= 302 (:status resp)))
(is (= "http://www.a.org/index" (get-in resp [:headers "location"]))))))
(deftest coercion-test
(testing "coercions"
(let [m
(coerce-to-vhosts
[
["https://abc.com"
["/" :a/index]
["/foo" :a/foo]
]
[{:scheme :http :host "abc"}
["/" :b/index]
["/bar" :b/bar]
]
[[{:scheme :http :host "localhost"} "http://def.org"]
["/" :c/index]
["/zip" :c/zip]
]
;; Coerce wildcard
[:* ["/" :d/index]]])]
(is (not (error? m)))
(is (= [[[{:scheme :https, :host "abc.com"}] ["/" :a/index] ["/foo" :a/foo]]
[[{:scheme :http, :host "abc"}] ["/" :b/index] ["/bar" :b/bar]]
[[{:scheme :http, :host "localhost"}
{:scheme :http, :host "def.org"}]
["/" :c/index]
["/zip" :c/zip]]
[[:*] ["/" :d/index]]] m))))
(testing "synonymous vhosts"
(is (nil? (:error (coerce-to-vhosts
[[["http://localhost:8000"
"http://localhost:8001"]
["/" :index]
]])))))
(testing "cannot have empty vhosts"
(is (:error (coerce-to-vhosts [[[] ["/" :index]]])))))
(deftest vhosts->roots-test []
(is (= ["https://a.org"
"http://a.org"
"http://www.a.org"
"https://www.a.org"
"https://b.org"
"http://c.com:8000"
"https://c.com:8001"
"http://d.com:8002"
"https://example.org"]
(vhosts->roots (:vhosts example-vhosts-model) {:scheme :https :headers {"host" "example.org"}}))))