bidi

repository·master·Indexed 21 days ago

https://github.com/juxt/bidi

A bi-directional URI dispatch library for Clojure and ClojureScript. bidi uses data structures to define routes, enabling both URI matching via `match-route` and URI generation via `path-for` from the same definitions. It supports nested routes, regular expression parameters, Ring handler integration via `make-handler`, virtual host management, and guards for HTTP methods and request criteria.

Tokens
4.9K
Snippets
21
Records
24
Agent score
27%

What's inside bidi

  1. Define multiple candidate patterns with Alternates

    master

    The Alternates pattern allows you to specify a list of potential candidate patterns that all match the same handler. The first pattern in the list is treated as the canonical pattern for URI formation (via path-for).

    This enables sophisticated matching, such as matching multiple HTTP methods or multiple server names.

    ;; Match multiple paths (canonical is /index.html)
    [#{"/index.html" "/index"} :index]
    
    ;; Match multiple HTTP methods
    [#{:head :get} :index]
    
    ;; Match based on server name
    [#{{:server-name "juxt.pro"}{:server-name "localhost"}}
     {"/index.html" :index}]
  2. Define Route structures

    master

    A route is a pair: [pattern matched].

    • Pattern: The left-hand side. Can be a String, a list of PatternSegments (String, Regex, Keyword, or a combination), a MethodGuard, a GeneralGuard (map), or true/false.
    • Matched: The right-hand side. Can be a Function, Symbol, Keyword, or a nested RoutePair (recursive structure).
  3. Resolve circular handler dependencies using a handler map promise

    master

    When building REST resources that need to generate hyperlinks to one another (HATEOAS), you may encounter circular dependencies where Resource A needs to know the path for Resource B, and vice versa.

    To solve this, use a promise to manage a handler map. This map contains entries for each handler keyed by keywords. By passing a promise into the handler constructors, you can defer the dereferencing of the map until the handlers are actually invoked. This ensures that the make-handlers function can deliver the completed map before the handlers are ever called, preventing the promise from escaping the construction phase prematurely.

    Key requirements:

    1. The promise must be delivered (realized) before calling path-for.
    2. Use assert (realized? handlers) within your handlers to ensure the dependency is ready.
    3. Use path-for with the routes object and the keyword from the handler map to generate paths.
    ;; 1. Define resources that accept a promise for handlers
    (defresource contacts [database handlers]
      :handle-created (fn [{{:routes routes :request} :request :id id}] 
                        (assert (realized? handlers))
                        (ring-response 
                          {:headers {"Location" (path-for routes (:contacts @handlers)) :id id}}))) 
    
    (defresource contact [handlers]
      :handle-ok (fn [{{{id :id} :route-params :routes routes :routes} :request}] 
                   (assert (realized? handlers))
                   (html [:a {:href (path-for routes (:contacts @handlers))} "Index"]))) 
    
    ;; 2. Use make-handlers to encapsulate the promise delivery
    (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)}))) 
    
    ;; 3. Compose the application
    (-> database make-handlers make-routes)
  4. How bidi routing works (Concept)

    master

    Unlike macro-based routing libraries, bidi uses data structures to define routes. A route is a pair consisting of a pattern and a result.

    Because routes are data, they can be:

    • Read from configuration files
    • Generated or computed programmatically
    • Transformed by functions
    • Introspected

    This approach enables bi-directional routing: you can use the same data structure to match a URI to a handler and to generate a URI from a handler (path formation). This prevents hard-coded URIs from breaking your application when route structures change.

  5. Restrict routes using Guards

    master

    By default, bidi routes ignore the request method (behaving like ANY). You can restrict routes using two types of guards:

    1. Method Guards: Wrap a route in a pair where the pattern is a keyword representing the HTTP method (:get, :post, :put, :delete, :head, :options).
    2. General Guards: Use a map to specify other request criteria (e.g., :server-name, :scheme). Map entries can be a single value, a set of acceptable values, or a predicate function.

    Note: When using method guards, the route is defined as [method {pattern matched}] or within a larger structure.

    ;; Method guard example
    ["/" {"blog" {:get {"/index" (fn [req] {:status 200 :body "Index"})}}}]
    
    ;; General guard example (restricting by server-name)
    ["/" {"blog" {:get {"/index" (fn [req] {:status 200 :body "Index"})}}
         {:request-method :post :server-name "juxt.pro"}
         {"/zip" (fn [req] {:status 201 :body "Created"})}]
  6. Implement Catch-All routes

    master

    To create a catch-all route (e.g., for a 404 handler), use the pattern true. Because bidi matches routes in order, ensure the catch-all pattern is defined last so it doesn't subsume other routes.

    Note: The handler returned by a catch-all route is just a value (like a symbol); you must still provide a function that implements the actual 404 logic.

    (def my-routes ["/" [["index.html" :index]
                      [true         :not-found]]])
    
    (match-route my-routes "/index.html") ;; => {:handler :index}
    (match-route my-routes "/other.html") ;; => {:handler :not-found}
  7. Manage Virtual Hosts with bidi.vhosts

    master

    To route across multiple virtual hosts, define a super-structure where the first element of a vector is the virtual-host declaration (a String, java.net.URI, java.net.URL, or a map like {:scheme :https :host "example.org:8443"}).

    Use bidi.vhosts/vhosts-model to combine multiple virtual-host structures into a single model.

    Special Virtual Host Forms:

    • Synonymous Hosts: A vector of possible host declarations. uri-info uses the first one in the vector, but when matching, it selects the one matching the request's scheme.
    • Wildcards: Use :* to match any scheme/host. uri-info will then assume the scheme/host of the incoming request.
    (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" [...]]]))
  8. Install bidi via Leiningen

    master

    Add bidi as a dependency to your project.clj file.

    Requirements:

    • Clojure 1.7 or later
    • Leiningen 2.5.3 or later

    Note: bidi uses Clojure's reader conditionals.

    ;; Add to project.clj
    [bidi "2.1.6"]
  9. Define multiple and nested routes

    master

    Routes can be grouped using a map or a vector of pairs. You can nest routes recursively by providing a map where the keys are path segments and the values are further route definitions.

    ;; Nested routes example
    (def my-routes ["/" {"index.html" :index
                            "articles/" {"index.html" :article-index
                                         "article.html" :article}}])
    
    ;; Matching
    (match-route my-routes "/articles/article.html") ; => {:handler :article}
    
    ;; Reverse matching
    (path-for my-routes :article-index) ; => "/articles/index.html"
  10. Generate URIs using uri-info

    master

    Use bidi.vhosts/uri-info to generate a map containing URI information from a virtual-hosts model. This is useful for generating links in your application.

    Returned Map Keys:

    • :uri: The absolute URI (always contains scheme, host, and port). Use this for API responses.
    • :href: A URI suitable for HTML content. It may omit redundant scheme/host/port.
    • :path: The path component.
    • :host: The host component.
    • :scheme: The scheme component.

    When using the partially applied uri-info provided in bidi's matching context, it can help avoid dependency cycles.

    ;; Example output for (uri-info my-vhosts-model :index {:query-params {"q" "juxt"}})
    {: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"}
  11. Extract all routes using route-seq

    master
    The route-seq function can be used to extract a sequence of all possible routes contained within a route structure. Each element in the resulting sequence is a map containing a path and a handler. This is particularly useful for generating site maps.
  12. Serve files from the file-system with Files

    master

    The Files record allows you to serve files directly from a specific directory on the file-system. Use the :dir key to specify the base directory.

    ["pics/" (->Files {:dir "/tmp/pics"})]