pax_global_header00006660000000000000000000000064152366657600014532gustar00rootroot0000000000000052 comment=677c1538c5aaca0144808e9b4eead4cb247e6fa7 OpenVoxProject-comidi-cbcce91/000077500000000000000000000000001523666576000164745ustar00rootroot00000000000000OpenVoxProject-comidi-cbcce91/.github/000077500000000000000000000000001523666576000200345ustar00rootroot00000000000000OpenVoxProject-comidi-cbcce91/.github/dependabot.yml000066400000000000000000000005751523666576000226730ustar00rootroot00000000000000version: 2 updates: # raise PRs for gem updates - package-ecosystem: bundler directory: "/" schedule: interval: daily time: "13:00" open-pull-requests-limit: 10 # Maintain dependencies for GitHub Actions - package-ecosystem: github-actions directory: "/" schedule: interval: daily time: "13:00" open-pull-requests-limit: 10 OpenVoxProject-comidi-cbcce91/.github/release.yml000066400000000000000000000013231523666576000221760ustar00rootroot00000000000000--- # https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes changelog: exclude: labels: - duplicate - invalid - modulesync - question - skip-changelog - wont-fix - wontfix categories: - title: Breaking Changes 🛠 labels: - backwards-incompatible - title: New Features 🎉 labels: - enhancement - title: Bug Fixes 🐛 labels: - bug - title: Documentation Updates 📚 labels: - documentation - docs - title: Dependency Updates ⬆️ labels: - dependencies - title: Other Changes labels: - "*" OpenVoxProject-comidi-cbcce91/.github/workflows/000077500000000000000000000000001523666576000220715ustar00rootroot00000000000000OpenVoxProject-comidi-cbcce91/.github/workflows/pr-testing.yaml000066400000000000000000000031471523666576000250560ustar00rootroot00000000000000--- name: lein_test on: pull_request: {} push: branches: - main # minimal permissions permissions: contents: read jobs: run-lein-tests: name: lein test - Java ${{ matrix.java }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: java: - '25' - '21' - '17' steps: - name: Check out repository code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup java uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: temurin java-version: ${{ matrix.java }} - name: Install Clojure tools uses: DeLaGuardo/setup-clojure@4c7a6f613e5089821bb3bb2a33a3ee115578580d # 13.6.1 with: cli: latest # Clojure CLI based on tools.deps lein: latest # Leiningen boot: latest # Boot.clj bb: latest # Babashka clj-kondo: latest # Clj-kondo cljstyle: latest # cljstyle zprint: latest # zprint - name: Run lein tests with update profile run: | set -x set -e lein version echo "Running tests" lein -U test tests: if: always() needs: - run-lein-tests runs-on: ubuntu-24.04 name: Test suite steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} OpenVoxProject-comidi-cbcce91/.github/workflows/release.yaml000066400000000000000000000007411523666576000243770ustar00rootroot00000000000000--- name: Release and publish package to clojars.org permissions: contents: write packages: write on: workflow_dispatch: jobs: release: uses: 'openvoxproject/shared-actions/.github/workflows/clojure_release.yml@main' secrets: github_pat: ${{ secrets.OPENVOXBOT_COMMIT_AND_PRS }} ssh_private_key: ${{ secrets.OPENVOXBOT_SSH_PRIVATE_KEY }} clojars_username: ${{ secrets.CLOJARS_USERNAME }} clojars_password: ${{ secrets.CLOJARS_PASSWORD }} OpenVoxProject-comidi-cbcce91/.gitignore000066400000000000000000000002121523666576000204570ustar00rootroot00000000000000pom.xml pom.xml.asc *jar /lib/ /classes/ /target/ /checkouts/ .lein-deps-sum .lein-repl-history .lein-plugins/ .lein-failures .nrepl-port OpenVoxProject-comidi-cbcce91/CHANGELOG.md000066400000000000000000000025431523666576000203110ustar00rootroot00000000000000## 1.0.0 This is a feature release - Version as 1.0, has been in production for years - Add PATCH macro - Bump bidi dependency to 2.1.6 ## 0.3.3 This release does not include any new content. * Test against Java 8 and Java 11 * Update clj-parent to 4.8.1 (latest) ## 0.3.2 This is a minor bugfix release. * [TK-464](https://tickets.puppetlabs.com/browse/TK-464) : Fix bug with URL parsing by updating bidi to 2.1.3. Also update clj-parent. ## 0.3.1 This is a minor bugfix release. * [TK-297](https://tickets.puppetlabs.com/browse/TK-297) : Fix bug where `wrap-routes` didn't work properly with certain kinds of bidi route trees - Michal Růžička (159ef0f) * Update dependencies on clojure, bidi, compojure, schema ## 0.3.0 Added mechanism for wrapping intermediate comidi routes with middleware. Added support for "true/false" Bidi patterns. ## 0.2.2 Same content as 0.3.0 - please prefer that version ## 0.2.1 Added 'resources' route utility fn. Routes can now only map to Ring handlers. ## 0.2.0 Make use of schema introduced in Bidi 1.20.0 (SERVER-777). LICENSE and CONTRIBUTING updates. ## 0.1.3 Improved dependency specification. ## 0.1.2 Upgrade compojure to 1.3.3 for Clojure 1.7.0 compatibility. ## 0.1.1 Make repository public, deploy to clojars. ## 0.1.0 Initial release, with the goal of soliticing API feedback. OpenVoxProject-comidi-cbcce91/LICENSE000066400000000000000000000012131523666576000174760ustar00rootroot00000000000000 Copyright (C) 2005-2015 Puppet Labs Inc Puppet Labs can be contacted at: info@puppetlabs.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. OpenVoxProject-comidi-cbcce91/README.md000066400000000000000000000143571523666576000177650ustar00rootroot00000000000000# comidi A committee approach to defining Clojure HTTP routes. Comidi is a library containing utility functions and [compojure](https://github.com/weavejester/compojure)-like syntax-sugar wrappers around the [bidi](https://github.com/juxt/bidi) web routing library. It aims to provide a way to define your web routes that takes advantage of the strengths of both bidi and compojure: * Route definitions are, at the end of the day, simple data structures (like bidi), so you can compose / introspect them. * Helper functions / macros for defining routes still provide the nice syntax of compojure; destructuring the request in a simple binding form, 'rendering' the response whether you define it as a string/map literal, a function reference, an inline body form, etc. ## Quick Start [![Clojars Project](http://clojars.org/org.openvoxproject/comidi/latest-version.svg)](http://clojars.org/org.openvoxproject/comidi) ```clj (let [my-routes (context "/my-app/" (routes (GET "/foo/something" request "foo!") (POST ["/bar/" :bar] [bar] (str "bar:" bar)) (PUT ["/baz/" [#".*" :rest]] request (call-baz-fn request)) (ANY ["/bam/" [#"(bip|bap)" :rest]] request {:orig-req request :rest (-> request :route-params :rest))) app (-> (routes->handler my-routes) wrap-with-my-middleware)] (add-ring-handler app)) ``` Notable differences from compojure above: * use vectors to separate segments of a route, rather than using special syntax inside of a string (e.g. `["/bar/" :bar]` instead of compojure's `"/bar/:bar") * use a nested vector with a regex (e.g. `[".*" :rest]`) to match a regex * `context` macro does not provide a binding form for request vars like compojure's does. Other than those differences, the API should be very close to compojure's. You can apply a Ring middlware to all the handlers at the leaves of a route using ```wrap-routes```, which has behaviour analagous to its counterpart in compojure. Multiple middlewares can be applied to the same routes, with those applied later wrapped around those applied earlier. This allows you to create multiple routes wrapped with different middleware yet still combine them into one overarching route that can be introspected. ```clj (let [my-routes ... my-singly-wrapped-routes (wrap-routes my-routes inner-middleware) my-doubly-wrapped-routes (wrap-routes my-singly-wrapped-routes outer-middleware) my-other-routes ... my-wrapped-other-routes (wrap-routes my-other-routes other-middleware) my-combined-routes (routes my-doubly-wrapped-routes my-wrapped-other-routes)] ``` ## What does Comidi do? Comidi provides some macros and functions that are intended to feel very similar to the compojure routing macros / functions, but under the hood they construct, compose, and return bidi route trees rather than compojure handler functions. This way, you can define your routes with almost exactly the same syntax you've been using (or port over a compojure app with minimal effort), but end up with an introspectable route tree data structure that you can do all sorts of cool things with before you wrap it as a ring handler. Under the hood: comidi uses bidi to do all of the work for routing, and uses a few functions from compojure to maintain some of the nice syntax. Specifically, it uses compojure's route destructuring to bind local variables for parameters from the requests, and it uses compojure's "rendering" functions to allow you to define the implementation of your route flexibly (so, just like in compojure, your route definition can be a literal return value, a reference to a function, a call to a function, a String, etc.) Comidi also provides a function called `route-metadata`. This function walks over your route tree and generates a metadata structure that gives you information about the all of the routes; e.g.: ```clj (clojure.pprint/pprint (-> (route-metadata (routes (GET "/foo" request "foo!") (PUT ["/bar/" :bar] [bar] (str "bar: " bar)))) :routes)) ``` ``` [{:route-id "foo", :path ["" "/foo"], :request-method :get} {:route-id "bar-:bar", :path ["" "/bar/" :bar], :request-method :put}] ``` Comidi also provides its own middleware function, `wrap-with-route-metadata`. If you use this middleware, your ring request map will be supplemented with two extra keys: `:route-metadata`, which gives you access to the metadata for all of the routes in your route tree, and `:route-info`, which tells you which of those routes the request matches. e.g.: ```clj (clojure.pprint/pprint (let [my-routes (routes (ANY "/foo" request {:route-info (:route-info request)})) handler (-> my-routes routes->handler (wrap-with-route-metadata my-routes))] (:route-info (handler {:uri "/foo"})))) ``` ```clj {:route-id "foo", :path ["" "/foo"], :request-method :any} ``` ## Trapperkeeper / Metrics Integration The [`trapperkeeper-comidi-metrics`](https://github.com/openvoxproject/trapperkeeper-comidi-metrics) contains some middleware that will automatically generate and track [metrics](https://github.com/dropwizard/metrics) for all of the routes in your comidi/bidi route tree, as well as easy integration into [`trapperkeeper`](https://github.com/openvoxproject/trapperkeeper). ## What's next? * API docs: looking into swagger integration. I could swear I found some bidi-swagger bindings somewhere a while back, but am not finding them at the moment. It might be possible to re-use some of the code from `compojure-api` because of the similarity between the comidi API and the compojure API. * You tell me! This is pre-1.0 and the API should still be considered fungible. If there's something you need that this library isn't doing, we can probably do it. Ping us or submit a PR. ## Support GitHub issues and PRs are welcome! Additionally, drop us a line in [the Vox Pupuli Slack or IRC channel](https://voxpupuli.org/connect/). OpenVoxProject-comidi-cbcce91/project.clj000066400000000000000000000026551523666576000206440ustar00rootroot00000000000000(defproject org.openvoxproject/comidi "1.1.4" :description "OpenVox utility functions and compojure-like wrappers for use with the bidi web routing library" :url "https://github.com/openvoxproject/comidi" :license {:name "Apache License, Version 2.0" :url "http://www.apache.org/licenses/LICENSE-2.0.html"} :pedantic? :abort :min-lein-version "2.9.1" ;; These are to enforce consistent versions across dependencies of dependencies, ;; and to avoid having to define versions in multiple places. If a component ;; defined under :dependencies ends up causing an error due to :pedantic? :abort, ;; because it is a dep of a dep with a different version, move it here. :managed-dependencies [[org.clojure/clojure "1.12.5"] [bidi "2.1.6"] [commons-io "2.22.0"] [compojure "1.7.2"] [org.openvoxproject/kitchensink "3.5.8"] [prismatic/schema "1.4.2"]] :dependencies [[org.clojure/clojure] [bidi] [compojure] [org.openvoxproject/kitchensink] [prismatic/schema]] :deploy-repositories [["releases" {:url "https://clojars.org/repo" :username :env/CLOJARS_USERNAME :password :env/CLOJARS_PASSWORD :sign-releases false}]]) OpenVoxProject-comidi-cbcce91/renovate.json000066400000000000000000000004061523666576000212120ustar00rootroot00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "addLabels": ["renovate", "dependencies"], "assigneesFromCodeOwners": true, "automerge": true, "automergeType": "pr", "dependencyDashboard": true, "enabledManagers": ["leiningen"] } OpenVoxProject-comidi-cbcce91/src/000077500000000000000000000000001523666576000172635ustar00rootroot00000000000000OpenVoxProject-comidi-cbcce91/src/puppetlabs/000077500000000000000000000000001523666576000214425ustar00rootroot00000000000000OpenVoxProject-comidi-cbcce91/src/puppetlabs/comidi.clj000066400000000000000000000336401523666576000234060ustar00rootroot00000000000000(ns puppetlabs.comidi (:require [bidi.ring :as bidi-ring] [bidi.schema :as bidi-schema] [bidi.bidi :as bidi] [clojure.zip :as zip] [compojure.core :as compojure] [compojure.response :as compojure-response] [ring.util.mime-type :as mime] [ring.util.response :as ring-response] [schema.core :as schema] [puppetlabs.kitchensink.core :as ks] [clojure.string :as str]) (:import (java.util.regex Pattern))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Schemas (defn pattern? [x] (instance? Pattern x)) (def Zipper (schema/pred ks/zipper?)) (def RequestMethod (schema/enum :any :get :post :put :delete :head :options)) ; Derived from bidi-schema PatternSegment (def RegexPatternSegment (schema/pair schema/Regex "qual" schema/Keyword "id")) (def RouteInfo {:path [bidi-schema/PatternSegment] :request-method RequestMethod}) (def RouteInfoWithId (merge RouteInfo {:route-id schema/Str})) (def Handler (schema/conditional keyword? schema/Keyword fn? (schema/pred fn?) map? {RequestMethod (schema/recursive #'Handler)})) (def RouteMetadata {:routes [RouteInfoWithId] :handlers {Handler RouteInfoWithId}}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Private - route id computation (defn slashes->dashes "Convert all forward slashes to hyphens" [s] (str/replace s #"\/" "-")) (defn remove-leading-and-trailing-dashes [s] (-> s (str/replace #"^-" "") (str/replace #"-$" ""))) (defn special-chars->underscores "Convert all non-alpha chars except ! * and - to underscores" [s] (str/replace s #"[^\w\!\*\-]" "_")) (defn collapse-consecutive-underscores [s] (str/replace s #"_+" "_")) (defn remove-leading-and-trailing-underscores [s] (-> s (str/replace #"^_" "") (str/replace #"_$" ""))) (defn add-regex-symbols "Wrap a regex pattern with forward slashes to make it easier to recognize as a regex" [s] (str "/" s "/")) (schema/defn ^:always-validate path-element->route-id-element :- schema/Str "Given a String path element from comidi route metadata, convert it into a string suitable for use in building a route id string." [path-element :- schema/Str] (-> path-element slashes->dashes remove-leading-and-trailing-dashes special-chars->underscores collapse-consecutive-underscores remove-leading-and-trailing-underscores)) (schema/defn ^:always-validate regex-path-element->route-id-element :- schema/Str "Given a Regex path element from comidi route metadata, convert it into a string suitable for use in building a route id string." [path-element :- RegexPatternSegment] (-> path-element first str path-element->route-id-element add-regex-symbols)) (schema/defn ^:always-validate route-path-element->route-id-element :- schema/Str "Given a route path element from comidi route metadata, convert it into a string suitable for use in building a route id string. This function is mostly responsible for determining the type of the path element and dispatching to the appropriate function." [path-element] (cond (string? path-element) (path-element->route-id-element path-element) (keyword? path-element) (pr-str path-element) (nil? (schema/check RegexPatternSegment path-element)) (regex-path-element->route-id-element path-element) :else (throw (IllegalStateException. (str "Unrecognized path element: " path-element))))) (schema/defn ^:always-validate route-path->route-id :- schema/Str "Given a route path (from comidi route-metadata), build a route-id string for the route. This route-id can be used as a unique identifier for a route." [route-path :- [bidi-schema/PatternSegment]] (->> route-path (map route-path-element->route-id-element) (filter #(not (empty? %))) (str/join "-"))) (schema/defn ^:always-validate add-route-name :- RouteInfoWithId "Given a RouteInfo, compute a route-id and return a RouteInfoWithId." [route-info :- RouteInfo] (assoc route-info :route-id (route-path->route-id (:path route-info)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Private - route metadata computation (def http-methods #{:any :get :post :put :delete :head :options}) (schema/defn ^:always-validate update-route-info* :- RouteInfo "Helper function, used to maintain a RouteInfo data structure that represents the current path elements of a route as we traverse the Bidi route tree via zipper." [route-info :- RouteInfo pattern :- bidi-schema/Pattern] (cond (contains? http-methods pattern) (assoc-in route-info [:request-method] pattern) (nil? (schema/check RegexPatternSegment pattern)) (update-in route-info [:path] concat [pattern]) (= true pattern) (update-in route-info [:path] conj "*") (= false pattern) (update-in route-info [:path] conj "!") (sequential? pattern) (update-in route-info [:path] into pattern) :else (update-in route-info [:path] conj pattern))) (declare breadth-route-metadata*) (schema/defn ^:always-validate depth-route-metadata* :- RouteMetadata "Helper function used to traverse branches of the Bidi route tree, depth-first." [route-meta :- RouteMetadata route-info :- RouteInfo loc :- Zipper] (let [[pattern matched] (zip/node loc)] (cond (map? matched) (depth-route-metadata* route-meta route-info (-> loc zip/down zip/right (zip/edit #(into [] %)) zip/up)) (vector? matched) (breadth-route-metadata* route-meta (update-route-info* route-info pattern) (-> loc zip/down zip/right zip/down)) :else (let [route-info (-> (update-route-info* route-info pattern) add-route-name)] (-> route-meta (update-in [:routes] conj route-info) (assoc-in [:handlers matched] route-info)))))) (schema/defn ^:always-validate breadth-route-metadata* :- RouteMetadata "Helper function used to traverse branches of the Bidi route tree, breadth-first." [route-meta :- RouteMetadata route-info :- RouteInfo loc :- Zipper] (loop [route-meta route-meta loc loc] (let [routes (depth-route-metadata* route-meta route-info loc)] (if-let [next (zip/right loc)] (recur routes next) routes)))) (schema/defn ^:always-validate route-metadata* :- RouteMetadata "Traverses a Bidi route tree and returns route metadata, which includes a list of RouteInfo objects (one per route), plus a mechanism to look up the RouteInfo for a given handler." [routes :- bidi-schema/RoutePair] (let [route-info {:path [] :request-method :any} loc (-> [routes] zip/vector-zip zip/down)] (breadth-route-metadata* {:routes [] :handlers {}} route-info loc))) (def memoized-route-metadata* (memoize route-metadata*)) (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. (This code is largely copied from the bidi upstream, but we add support for inserting the match-context via middleware.)" [route] (fn [{:keys [uri path-info] :as req}] (let [path (or path-info uri) {:keys [handler route-params] :as match-context} (or (:match-context req) (apply bidi/match-route route path (apply concat (seq req))))] (when handler (bidi-ring/request handler (-> req (update-in [:params] merge route-params) (update-in [:route-params] merge route-params)) (apply dissoc match-context :handler (keys req))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Private - helpers for compojure-like syntax (defn- add-mime-type [response path options] (if-let [mime-type (mime/ext-mime-type path (:mime-types options {}))] (ring-response/content-type response mime-type) response)) (defmacro handler-fn* "Helper macro, used by the compojure-like macros (GET/POST/etc.) to generate a function that provides compojure's destructuring and rendering support." [bindings body] `(fn [request#] (compojure-response/render (compojure/let-request [~bindings request#] ~@body) request#))) (defn route-with-method* "Helper function, used by the compojure-like macros (GET/POST/etc.) to generate a bidi route that includes a wrapped handler function." [method pattern bindings body] `[~pattern {~method (handler-fn* ~bindings ~body)}]) (defn route-tree-zip "Returns a zipper for a bidi route tree i.e. for an arbitrarily nested structure of `bidi.schema/RoutePair`s" [root] (zip/zipper (fn [[_ matched]] (or (vector? matched) (map? matched))) (fn [[_ matched]] (seq matched)) (fn [[pattern matched :as node] children] (with-meta [pattern (into (if (vector? matched) [] {}) children)] (meta node))) root)) (defn wrap-routes* "Help function, used by compojure-like wrap-routes function to wrap leaf handlers in the bidi route with the middleware" [loc middleware] (if (zip/end? loc) loc (let [loc (if (zip/branch? loc) ; we only want modify the leaf nodes loc (let [[pattern matched] (zip/node loc)] (if (fn? matched) (zip/replace loc [pattern (middleware matched)]) loc)))] (recur (zip/next loc) middleware)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; Public - core functions (schema/defn ^:always-validate route-metadata :- RouteMetadata "Build up a map of metadata describing the routes. This metadata map can be used for introspecting the routes after building the handler, and can also be used with the `wrap-with-route-metadata` middleware." [routes :- bidi-schema/RoutePair] (memoized-route-metadata* routes)) (schema/defn ^:always-validate wrap-with-route-metadata :- (schema/pred fn?) "Ring middleware; adds the comidi route-metadata to the request map, as well as a :route-info key that can be used to determine which route a given request matches." [app :- (schema/pred fn?) routes :- bidi-schema/RoutePair] (let [compiled-routes (bidi/compile-route routes) route-meta (route-metadata routes)] (fn [{:keys [uri path-info] :as req}] (let [path (or path-info uri) {:keys [handler] :as match-context} (apply bidi/match-route compiled-routes path (apply concat (seq req))) route-info (get-in route-meta [:handlers handler])] (app (assoc req :route-metadata route-meta :route-info route-info :match-context match-context)))))) (schema/defn ^:always-validate wrap-routes :- bidi-schema/RoutePair "Wraps middleware around the handlers at every leaf in the route in a manner analagous to compojure's wrap-routes function" [routes :- bidi-schema/RoutePair middleware :- (schema/pred fn?)] (-> routes route-tree-zip (wrap-routes* middleware) zip/root)) (schema/defn ^:always-validate routes :- bidi-schema/RoutePair "Combines multiple bidi routes into a single data structure; this is largely just a convenience function for grouping several routes together as a single object that can be passed around." [& routes :- [bidi-schema/RoutePair]] ["" (vec routes)]) (schema/defn ^:always-validate context :- bidi-schema/RoutePair "Combines multiple bidi routes together into a single data structure, but nests them all under the given url-prefix. This is similar to compojure's `context` macro, but does not accept a binding form. You can still destructure variables by passing a bidi pattern for `url-prefix`, and the variables will be available to all nested routes." [url-prefix :- bidi-schema/Pattern & routes :- [bidi-schema/RoutePair]] [url-prefix (vec routes)]) (schema/defn ^:always-validate routes->handler :- (schema/pred fn?) "Given a bidi route tree, converts into a ring request handler function" [routes :- bidi-schema/RoutePair] (let [compiled-routes (bidi/compile-route routes)] (make-handler compiled-routes))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Public - compojure-like convenience macros (defmacro ANY [pattern bindings & body] `[~pattern (handler-fn* ~bindings ~body)]) (defmacro GET [pattern bindings & body] (route-with-method* :get pattern bindings body)) (defmacro HEAD [pattern bindings & body] (route-with-method* :head pattern bindings body)) (defmacro PUT [pattern bindings & body] (route-with-method* :put pattern bindings body)) (defmacro POST [pattern bindings & body] (route-with-method* :post pattern bindings body)) (defmacro DELETE [pattern bindings & body] (route-with-method* :delete pattern bindings body)) (defmacro PATCH [pattern bindings & body] (route-with-method* :patch pattern bindings body)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Public - pre-built routes (defn not-found [body] [[[#".*" :rest]] (fn [request] (-> (compojure-response/render body request) (ring-response/status 404)))]) (defn resources "A route for serving resources on the classpath. Accepts the following keys: :root - the root prefix path of the resources, defaults to 'public' :mime-types - an optional map of file extensions to mime types" [path & [options]] (GET [path [#".*" :resource-path]] [resource-path] (let [root (:root options "public")] (some-> (ring-response/resource-response (str root "/" resource-path)) (add-mime-type resource-path options))))) OpenVoxProject-comidi-cbcce91/test/000077500000000000000000000000001523666576000174535ustar00rootroot00000000000000OpenVoxProject-comidi-cbcce91/test/puppetlabs/000077500000000000000000000000001523666576000216325ustar00rootroot00000000000000OpenVoxProject-comidi-cbcce91/test/puppetlabs/comidi_test.clj000066400000000000000000000467251523666576000246450ustar00rootroot00000000000000(ns puppetlabs.comidi-test (:require [clojure.test :refer :all] [puppetlabs.comidi :as comidi :refer :all] [schema.test :as schema-test] [schema.core :as schema] [clojure.zip :as zip] [bidi.bidi :as bidi])) (use-fixtures :once schema-test/validate-schemas) (defn replace-regexes-for-equality-check [xs] (loop [loc (zip/vector-zip xs)] (if (zip/end? loc) (zip/root loc) (recur (let [node (zip/node loc)] (if (pattern? node) (zip/edit loc #(str "REGEX: " (.pattern %))) (zip/next loc))))))) (deftest update-route-info-test (let [orig-route-info {:path [] :request-method :any}] (testing "HTTP verb keyword causes request-method to be updated" (doseq [verb [:get :post :put :delete :head]] (is (= {:path [] :request-method verb} (update-route-info* orig-route-info verb))))) (testing "string path gets added to the path" (is (= {:path ["/foo"] :request-method :any} (update-route-info* orig-route-info "/foo")))) (testing "string path elements get added to the path" (is (= {:path ["/foo"] :request-method :any} (update-route-info* orig-route-info ["/foo"])))) (testing "keyword path elements get added to the path" (is (= {:path [:foo] :request-method :any} (update-route-info* orig-route-info [:foo])))) (testing "vector path elements get flattened and added to the path" (is (= {:path ["/foo/" :foo] :request-method :any} (update-route-info* orig-route-info ["/foo/" :foo])))) (testing "boolean true is handled specially" (is (= {:path ["*"] :request-method :any} (update-route-info* orig-route-info true)))) (testing "boolean false is handled specially" (is (= {:path ["!"] :request-method :any} (update-route-info* orig-route-info false)))) (testing "regex path element gets added to the path" (is (= {:path ["/foo/" ["REGEX: .*" :foo]] :request-method :any} (-> (update-route-info* orig-route-info ["/foo/" [#".*" :foo]]) (update-in [:path] replace-regexes-for-equality-check))))))) (deftest route-metadata-test (testing "route metadata includes ordered list of routes and lookup by handler" (let [routes ["" [[["/foo/" :foo] :foo-handler] [["/bar/" :bar] [["/baz" {:get :baz-handler}] ["/bam" {:put :bam-handler}] ["/boop" :boop-handler] ["/bap" {:options :bap-handler}]]] ["/buzz" {:post :buzz-handler}] [true {:get :true-handler}]]] expected-foo-meta {:path ["" "/foo/" :foo] :route-id "foo-:foo" :request-method :any} expected-baz-meta {:path ["" "/bar/" :bar "/baz"] :route-id "bar-:bar-baz" :request-method :get} expected-bam-meta {:path ["" "/bar/" :bar "/bam"] :route-id "bar-:bar-bam" :request-method :put} expected-boop-meta {:path ["" "/bar/" :bar "/boop"] :route-id "bar-:bar-boop" :request-method :any} expected-bap-meta {:path ["" "/bar/" :bar "/bap"] :route-id "bar-:bar-bap" :request-method :options} expected-buzz-meta {:path ["" "/buzz"] :route-id "buzz" :request-method :post} expected-true-meta {:path ["" "*"] :route-id "*" :request-method :get}] (is (= (comidi/route-metadata* routes) {:routes [expected-foo-meta expected-baz-meta expected-bam-meta expected-boop-meta expected-bap-meta expected-buzz-meta expected-true-meta] :handlers {:foo-handler expected-foo-meta :baz-handler expected-baz-meta :bam-handler expected-bam-meta :boop-handler expected-boop-meta :bap-handler expected-bap-meta :buzz-handler expected-buzz-meta :true-handler expected-true-meta}}))))) (deftest routes-test (is (= ["" [["/foo" :foo-handler] [["/bar/" :bar] :bar-handler]]] (routes ["/foo" :foo-handler] [["/bar/" :bar] :bar-handler])))) (deftest context-test (testing "simple context" (is (= ["/foo" [["/bar" :bar-handler] [["/baz" :baz] :baz-handler]]] (context "/foo" ["/bar" :bar-handler] [["/baz" :baz] :baz-handler])))) (testing "context with variable" (is (= [["/foo" :foo] [["/bar" :bar-handler] [["/baz" :baz] :baz-handler]]] (context ["/foo" :foo] ["/bar" :bar-handler] [["/baz" :baz] :baz-handler]))))) (deftest routes->handler-test (testing "routes are matched against a request properly, with route params" (let [handler (routes->handler ["/foo" [["" [["/bar" (fn [req] :bar)] [["/baz/" :baz] (fn [req] {:endpoint :baz :route-params (:route-params req)})] [true (fn [req] :true)]]]]])] (is (= :true (handler {:uri "/foo/something/else"}))) (is (= :bar (handler {:uri "/foo/bar"}))) (is (= {:endpoint :baz :route-params {:baz "howdy"}} (handler {:uri "/foo/baz/howdy"}))))) (testing "request-methods are honored" (let [handler (routes->handler ["/foo" {:get (fn [req] :foo)}])] (is (nil? (handler {:uri "/foo"}))) (is (= :foo (handler {:uri "/foo" :request-method :get}))))) (testing "contexts can bind route variables" (let [handler (routes->handler (context ["/foo/" :foo] [["/bar/" :bar] (fn [req] (:route-params req))]))] (is (= {:foo "hi" :bar "there"} (handler {:uri "/foo/hi/bar/there"})))))) (deftest compojure-macros-test (let [routes (context ["/foo/" :foo] (ANY ["/any/" :any] [foo any] (str "foo: " foo " any: " any)) (GET ["/get/" :get] [foo get] (fn [req] {:foo foo :get get})) (HEAD ["/head/" :head] [foo head] {:foo foo :head head}) (PUT "/put" [foo] {:status 500 :body foo}) (POST ["/post/" :post] [post] post) (DELETE ["/delete/" :delete] [foo delete] (atom {:foo foo :delete delete}))) handler (routes->handler routes)] (is (nil? (handler {:uri "/foo/hi/get/there" :request-method :post}))) (is (nil? (handler {:uri "/foo/hi/head/there" :request-method :get}))) (is (nil? (handler {:uri "/foo/hi/put" :request-method :get}))) (is (nil? (handler {:uri "/foo/hi/post/there" :request-method :get}))) (is (nil? (handler {:uri "/foo/hi/delete/there" :request-method :get}))) (is (= "foo: hi any: there" (:body (handler {:uri "/foo/hi/any/there"})))) (is (= {:foo "hi" :get "there"} (select-keys (handler {:uri "/foo/hi/get/there" :request-method :get}) [:foo :get]))) (is (= {:foo "hi" :head "there"} (select-keys (handler {:uri "/foo/hi/head/there" :request-method :head}) [:foo :head]))) (is (= {:status 500 :body "hi"} (select-keys (handler {:uri "/foo/hi/put" :request-method :put}) [:status :body]))) (is (= {:status 200 :body "there"} (select-keys (handler {:uri "/foo/hi/post/there" :request-method :post}) [:status :body]))) (is (= {:status 200 :foo "hi" :delete "there"} (select-keys (handler {:uri "/foo/hi/delete/there" :request-method :delete}) [:status :foo :delete]))))) (deftest not-found-test (testing "root not-found handler" (let [handler (routes->handler (not-found "nobody's home, yo"))] (is (= {:status 404 :body "nobody's home, yo"} (select-keys (handler {:uri "/hi/there"}) [:body :status]))))) (testing "nested not-found handler" (let [handler (routes->handler (routes ["/bar" [["" (fn [req] :bar)] (not-found "nothing else under bar!")]] (not-found "nothing else under root!")))] (is (= :bar (handler {:uri "/bar"}))) (is (= {:status 404 :body "nothing else under bar!"} (select-keys (handler {:uri "/bar/baz"}) [:status :body]))) (is (= {:status 404 :body "nothing else under root!"} (select-keys (handler {:uri "/yo/mang"}) [:status :body])))))) (deftest regex-test (let [handler (routes->handler ["/foo" [[["/boo/" [#".*" :rest]] (fn [req] (:rest (:route-params req)))]]])] (is (= "hi/there" (handler {:uri "/foo/boo/hi/there"}))))) (deftest route-names-test (let [test-routes (routes ; Bidi Pattern: Path (GET "/foo" request "foo!") (GET ["/foo/something/"] request "foo something!") ; Bidi Pattern: [ PatternSegment+ ] (POST ["/bar"] request "bar!") (POST ["/bar" "bie"] request "barbie!") (PUT ["/baz" [#".*" :rest]] request "baz!") (ANY ["/bam/" [#"(?:bip|bap)" :rest]] request "bam!") (HEAD ["/bang/" [#".*" :rest] "/pow/" :pow] request "bang!") ; Bidi Pattern: false (GET false request "catch none!") (GET "/false" request "omg why would you do this?") (GET ["/is" :false] request "it hurts so bad") ; Bidi Pattern: true (GET true request "catch all!") (GET "/true" request "omg why would you do this?") (GET ["/is" :true] request "it hurts so bad")) route-meta (route-metadata test-routes) route-ids (map :route-id (:routes route-meta))] (is (= #{"foo" "foo-something" "bar" "bar-bie" "baz-/*/" "bam-/bip_bap/" "bang-/*/-pow-:pow" "*" "true" "is-:true" "!" "false" "is-:false"} (set route-ids))))) (deftest wrap-with-route-metadata-test (let [test-routes (routes (ANY ["/foo/" :foo] request "foo!") (GET ["/bar/" :bar] request "bar!") (GET "/false" request "falsefalse!") (GET false request "truefalse!") (GET "/true" request "falsetrue!") (GET true request "truetrue!")) route-meta (route-metadata test-routes) test-atom (atom {}) test-middleware (fn [f] (fn [req] (reset! test-atom (select-keys req [:route-info :route-metadata])) (f req))) handler (-> (routes->handler test-routes) test-middleware (wrap-with-route-metadata test-routes))] (handler {:uri "/foo/something"}) (is (= (-> test-atom deref :route-info :route-id) "foo-:foo")) (is (= (-> test-atom deref :route-metadata) route-meta)) (handler {:uri "/bar/something" :request-method :get}) (is (= (-> test-atom deref :route-info :route-id) "bar-:bar")) (is (= (-> test-atom deref :route-metadata) route-meta)) (handler {:uri "/false" :request-method :get}) (is (= (-> test-atom deref :route-info :route-id) "false")) (is (= (-> test-atom deref :route-metadata) route-meta)) (handler {:uri "/true" :request-method :get}) (is (= (-> test-atom deref :route-info :route-id) "true")) (is (= (-> test-atom deref :route-metadata) route-meta)) (handler {:uri "/wat" :request-method :get}) (is (= (-> test-atom deref :route-info :route-id) "*")) (is (= (-> test-atom deref :route-metadata) route-meta)))) (deftest route-handler-uses-existing-match-context-test (testing "Middleware can provide match-context for comidi handler" (let [routes (routes (GET ["/foo-:foo"] request "foo!")) fake-match-context {:handler (fn [req] (-> req :route-params :bar)) :route-params {:bar "bar!"}} wrap-with-fake-match-context (fn [app] (fn [req] (let [req (assoc req :match-context fake-match-context)] (app req)))) handler (-> routes routes->handler wrap-with-fake-match-context)] (is (= "bar!" (handler {:uri "/bunk"})))))) (deftest wrap-leaves-with-middleware-test (let [inner-middleware (fn [handler] (fn [request] (update-in (handler request) [:body] #(str "inner-" %)))) bb-wrapper-middleware (fn [handler] (fn [request] (update-in (handler request) [:body] #(str "bb-wrapper-" %)))) outer-middleware (fn [handler] (fn [request] (update-in (handler request) [:body] #(str "outer-" %)))) aa-route (GET "/aa" request "aa!") bb-route (ANY "/bb" request "bb!") cc-route (ANY "/cc" request "cc!") dd-route (DELETE "/dd" request "dd!") ee-route (GET "/ee" request "ee!") ff-route (ANY "/ff" request "ff!") gh-route (ANY (bidi/alts "/gg" "/hh") request "gg-or-hh!") left-routes (context "/left" aa-route bb-route) middle-routes (context "/middle" cc-route dd-route) right-routes (context "/right" ee-route ff-route) alternate-routes ["/alts" [gh-route]] handler (-> (routes left-routes middle-routes right-routes alternate-routes) routes->handler)] (testing "Routes without middleware applied" (is (= (:body (handler {:uri "/left/aa" :request-method :get})) "aa!")) (is (= (:body (handler {:uri "/left/bb" :request-method :post})) "bb!")) (is (= (:body (handler {:uri "/middle/cc" :request-method :get})) "cc!")) (is (= (:body (handler {:uri "/middle/dd" :request-method :delete})) "dd!")) (is (= (:body (handler {:uri "/right/ee" :request-method :get})) "ee!")) (is (= (:body (handler {:uri "/right/ff" :request-method :delete})) "ff!")) (is (= (:body (handler {:uri "/alts/gg" :request-method :put})) "gg-or-hh!")) (is (= (:body (handler {:uri "/alts/hh" :request-method :post})) "gg-or-hh!")) (is (= (:body (handler {:uri "/alts/ii" :request-method :post})) nil))) (testing "Routes but now with middleware applied" (let [wrapped-bb-route (-> bb-route (wrap-routes bb-wrapper-middleware)) left-routes (-> (context "/left" aa-route wrapped-bb-route) (wrap-routes inner-middleware) (wrap-routes outer-middleware)) right-routes (-> right-routes (wrap-routes outer-middleware)) alternate-routes (-> alternate-routes (wrap-routes inner-middleware) (wrap-routes outer-middleware)) handler (-> (routes left-routes middle-routes right-routes alternate-routes) routes->handler)] (is (= (:body (handler {:uri "/left/aa" :request-method :get})) "outer-inner-aa!")) (is (= (:body (handler {:uri "/left/bb" :request-method :post})) "outer-inner-bb-wrapper-bb!")) (is (= (:body (handler {:uri "/middle/cc" :request-method :get})) "cc!")) (is (= (:body (handler {:uri "/middle/dd" :request-method :delete})) "dd!")) (is (= (:body (handler {:uri "/right/ee" :request-method :get})) "outer-ee!")) (is (= (:body (handler {:uri "/right/ff" :request-method :delete})) "outer-ff!")) (is (= (:body (handler {:uri "/alts/gg" :request-method :delete})) "outer-inner-gg-or-hh!")) (is (= (:body (handler {:uri "/alts/hh" :request-method :delete})) "outer-inner-gg-or-hh!")))))) (deftest destructuring-test (testing "Compojure-style destructuring works as expected" (let [test-request {:uri "/foo" :params {:aa "aa" :bb "bb"}}] (testing "A single binding outside a vector is the whole request" ((routes->handler (ANY "/foo" request (is (= test-request (select-keys request [:uri :params]))) "foo!")) test-request)) (testing "A vector binding called 'request' tries to bind to non-existent query param of the same name" ((routes->handler (ANY "/foo" [request] (is (nil? request)) "foo!")) test-request)) (testing "A vector binding with two params binds as expected" ((routes->handler (ANY "/foo" [aa bb] (is (= aa "aa")) (is (= bb "bb")) "foo!")) test-request)) (testing "A vector binding with two valid params, one invalid param and an ':as request' segment binds as expected" ((routes->handler (ANY "/foo" [aa bb cc :as request] (is (= aa "aa")) (is (= bb "bb")) (is (nil? cc)) (is (= test-request (select-keys request [:uri :params]))) "foo!")) test-request)))))