").css({width:"100%",height:"100%"}),N=new google.visualization.ChartWrapper({dataTable:g,chartType:e,options:b}),N.draw(x[0]),x.bind("dblclick",function(){var t;return t=new google.visualization.ChartEditor,google.visualization.events.addListener(t,"ok",function(){return t.getChartWrapper().draw(x[0])}),t.openDialog(N)}),x}},t.pivotUtilities.gchart_renderers={"Line Chart":e("LineChart"),"Bar Chart":e("ColumnChart"),"Stacked Bar Chart":e("ColumnChart",{isStacked:!0}),"Area Chart":e("AreaChart",{isStacked:!0}),"Scatter Chart":e("ScatterChart")}})}).call(this);
2 | //# sourceMappingURL=gchart_renderers.min.js.map
--------------------------------------------------------------------------------
/samples/vasebi/resources/public/dist/pivot.css:
--------------------------------------------------------------------------------
1 | .pvtUi { color: #333; }
2 |
3 |
4 | table.pvtTable {
5 | font-size: 8pt;
6 | text-align: left;
7 | border-collapse: collapse;
8 | }
9 | table.pvtTable thead tr th, table.pvtTable tbody tr th {
10 | background-color: #e6EEEE;
11 | border: 1px solid #CDCDCD;
12 | font-size: 8pt;
13 | padding: 5px;
14 | }
15 |
16 | table.pvtTable .pvtColLabel {text-align: center;}
17 | table.pvtTable .pvtTotalLabel {text-align: right;}
18 |
19 | table.pvtTable tbody tr td {
20 | color: #3D3D3D;
21 | padding: 5px;
22 | background-color: #FFF;
23 | border: 1px solid #CDCDCD;
24 | vertical-align: top;
25 | text-align: right;
26 | }
27 |
28 | .pvtTotal, .pvtGrandTotal { font-weight: bold; }
29 |
30 | .pvtVals { text-align: center;}
31 | .pvtAggregator { margin-bottom: 5px ;}
32 |
33 | .pvtAxisContainer, .pvtVals {
34 | border: 1px solid gray;
35 | background: #EEE;
36 | padding: 5px;
37 | min-width: 20px;
38 | min-height: 20px;
39 | }
40 | .pvtAxisContainer li {
41 | padding: 8px 6px;
42 | list-style-type: none;
43 | cursor:move;
44 | }
45 | .pvtAxisContainer li.pvtPlaceholder {
46 | -webkit-border-radius: 5px;
47 | padding: 3px 15px;
48 | -moz-border-radius: 5px;
49 | border-radius: 5px;
50 | border: 1px dashed #aaa;
51 | }
52 |
53 | .pvtAxisContainer li span.pvtAttr {
54 | -webkit-text-size-adjust: 100%;
55 | background: #F3F3F3;
56 | border: 1px solid #DEDEDE;
57 | padding: 2px 5px;
58 | white-space:nowrap;
59 | -webkit-border-radius: 5px;
60 | -moz-border-radius: 5px;
61 | border-radius: 5px;
62 | }
63 |
64 | .pvtTriangle {
65 | cursor:pointer;
66 | color: grey;
67 | }
68 |
69 | .pvtHorizList li { display: inline; }
70 | .pvtVertList { vertical-align: top; }
71 |
72 | .pvtFilteredAttribute { font-style: italic }
73 |
74 | .pvtFilterBox{
75 | z-index: 100;
76 | width: 300px;
77 | border: 1px solid gray;
78 | background-color: #fff;
79 | position: absolute;
80 | text-align: center;
81 | }
82 |
83 | .pvtFilterBox h4{ margin: 15px; }
84 | .pvtFilterBox p { margin: 10px auto; }
85 | .pvtFilterBox label { font-weight: normal; }
86 | .pvtFilterBox input[type='checkbox'] { margin-right: 10px; margin-left: 10px; }
87 | .pvtFilterBox input[type='text'] { width: 230px; }
88 | .pvtFilterBox .count { color: gray; font-weight: normal; margin-left: 3px;}
89 |
90 | .pvtCheckContainer{
91 | text-align: left;
92 | font-size: 14px;
93 | white-space: nowrap;
94 | overflow-y: scroll;
95 | width: 100%;
96 | max-height: 250px;
97 | border-top: 1px solid lightgrey;
98 | border-bottom: 1px solid lightgrey;
99 | }
100 |
101 | .pvtCheckContainer p{ margin: 5px; }
102 |
103 | .pvtRendererArea { padding: 5px;}
104 |
--------------------------------------------------------------------------------
/samples/vasebi/resources/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Food Mart Data Dash
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
Food Mart Pivot Explorer
24 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/samples/vasebi/src/vasebi/server.clj:
--------------------------------------------------------------------------------
1 | (ns vasebi.server
2 | (:gen-class) ; for -main method in uberjar
3 | (:require [clojure.spec.alpha :as spec]
4 | [io.pedestal.http :as server]
5 | [io.pedestal.http.route :as route]
6 | [com.cognitect.vase :as vase]
7 | [com.cognitect.vase.spec :as vase.spec]
8 | [vasebi.service :as service]))
9 |
10 | (defn activate-vase
11 | ([base-routes api-root spec-paths]
12 | (activate-vase base-routes api-root spec-paths vase/load-edn-resource))
13 | ([base-routes api-root spec-paths vase-load-fn]
14 | (let [vase-specs (mapv vase-load-fn spec-paths)]
15 | (when (seq vase-specs)
16 | (spec/check-asserts true)
17 | (doseq [vase-spec vase-specs]
18 | (spec/assert ::vase.spec/spec vase-spec))
19 | (vase/ensure-schema vase-specs)
20 | (vase/specs vase-specs))
21 | {::routes (if (empty? vase-specs)
22 | base-routes
23 | (into base-routes (vase/routes api-root vase-specs)))
24 | ::specs vase-specs})))
25 |
26 | (defn vase-service
27 | "Optionally given a default service map and any number of string paths
28 | to Vase API Specifications,
29 | Return a Pedestal Service Map with all Vase APIs parsed, ensured, and activated."
30 | ([]
31 | (vase-service service/service))
32 | ([service-map]
33 | (vase-service service-map vase/load-edn-resource))
34 | ([service-map vase-load-fn]
35 | (merge {:env :prod
36 | ::server/routes (::routes (activate-vase
37 | (::service/route-set service-map)
38 | (::vase/api-root service-map)
39 | (::vase/spec-resources service-map)
40 | vase-load-fn))}
41 | service-map)))
42 |
43 | ;; This is an adapted service map, that can be started and stopped
44 | ;; From the REPL you can call server/start and server/stop on this service
45 | (defonce runnable-service (server/create-server (vase-service)))
46 |
47 | (defn run-dev
48 | "The entry-point for 'lein run-dev'"
49 | [& args]
50 | (println "\nCreating your [DEV] server...")
51 | (-> service/service ;; start with production configuration
52 | (merge {:env :dev
53 | ;; do not block thread that starts web server
54 | ::server/join? false
55 | ;; Routes can be a function that resolve routes,
56 | ;; we can use this to set the routes to be reloadable
57 | ::server/routes #(route/expand-routes
58 | (::routes (activate-vase (deref #'service/routes)
59 | (::vase/api-root service/service)
60 | (mapv (fn [res-str]
61 | (str "resources/" res-str))
62 | (::vase/spec-resources service/service))
63 | vase/load-edn-file)))
64 | ;; all origins are allowed in dev mode
65 | ::server/allowed-origins {:creds true :allowed-origins (constantly true)}})
66 | ;; Wire up interceptor chains
67 | server/default-interceptors
68 | server/dev-interceptors
69 | server/create-server
70 | server/start))
71 |
72 | (defn -main
73 | "The entry-point for 'lein run'"
74 | [& args]
75 | (println "\nCreating your server...")
76 | (server/start runnable-service))
77 |
78 | ;; If you package the service up as a WAR,
79 | ;; some form of the following function sections is required (for io.pedestal.servlet.ClojureVarServlet).
80 |
81 | ;;(defonce servlet (atom nil))
82 | ;;
83 | ;;(defn servlet-init
84 | ;; [_ config]
85 | ;; ;; Initialize your app here.
86 | ;; (reset! servlet (server/servlet-init service/service nil)))
87 | ;;
88 | ;;(defn servlet-service
89 | ;; [_ request response]
90 | ;; (server/servlet-service @servlet request response))
91 | ;;
92 | ;;(defn servlet-destroy
93 | ;; [_]
94 | ;; (server/servlet-destroy @servlet)
95 | ;; (reset! servlet nil))
96 |
--------------------------------------------------------------------------------
/samples/vasebi/src/vasebi/service.clj:
--------------------------------------------------------------------------------
1 | (ns vasebi.service
2 | (:require [io.pedestal.http :as http]
3 | [io.pedestal.http.route :as route]
4 | [io.pedestal.http.body-params :as body-params]
5 | [ring.util.response :as ring-resp]
6 | [com.cognitect.vase :as vase]
7 | [clojure.java.io :as io]))
8 |
9 | (defn about-page
10 | [request]
11 | (ring-resp/response (format "Clojure %s - served from %s"
12 | (clojure-version)
13 | (route/url-for ::about-page))))
14 |
15 |
16 | (def pivot-response {:status 200
17 | :headers {}
18 | :body (slurp (io/resource "public/index.html"))})
19 | (defn home-page
20 | [request]
21 | pivot-response)
22 |
23 | ;; Defines "/" and "/about" routes with their associated :get handlers.
24 | ;; The interceptors defined after the verb map (e.g., {:get home-page}
25 | ;; apply to / and its children (/about).
26 | (def common-interceptors [(body-params/body-params) http/html-body])
27 |
28 | ;; Tabular routes
29 | (def routes #{["/" :get (conj common-interceptors `home-page)]
30 | ["/about" :get (conj common-interceptors `about-page)]})
31 |
32 | (def service
33 | {:env :prod
34 | ;; You can bring your own non-default interceptors. Make
35 | ;; sure you include routing and set it up right for
36 | ;; dev-mode. If you do, many other keys for configuring
37 | ;; default interceptors will be ignored.
38 | ;; ::http/interceptors []
39 |
40 | ;; Uncomment next line to enable CORS support, add
41 | ;; string(s) specifying scheme, host and port for
42 | ;; allowed source(s):
43 | ;;
44 | ;; "http://localhost:8080"
45 | ;;
46 | ;;::http/allowed-origins ["scheme://host:port"]
47 |
48 | ::route-set routes
49 | ::vase/api-root "/api"
50 | ::vase/spec-resources ["vasebi_service.edn"]
51 |
52 | ;; Root for resource interceptor that is available by default.
53 | ::http/resource-path "/public"
54 | ;; We need to relax the secure headers a bit (specifically the CSP controls)
55 | ::http/secure-headers {:content-security-policy-settings {:object-src "none"}}
56 |
57 | ;; Either :jetty, :immutant or :tomcat (see comments in project.clj)
58 | ::http/type :jetty
59 | ;;::http/host "localhost"
60 | ::http/port 8080
61 | ;; Options to pass to the container (Jetty)
62 | ::http/container-options {:h2c? true
63 | :h2? false
64 | ;:keystore "test/hp/keystore.jks"
65 | ;:key-password "password"
66 | ;:ssl-port 8443
67 | :ssl? false}})
68 |
69 |
--------------------------------------------------------------------------------
/samples/vasebi/test/vasebi/service_test.clj:
--------------------------------------------------------------------------------
1 | (ns vasebi.service-test
2 | (:require [clojure.test :refer :all]
3 | [io.pedestal.test :refer :all]
4 | [io.pedestal.http :as http]
5 | [vasebi.test-helper :as helper]
6 | [vasebi.service :as service]))
7 |
8 | ;; To test your service, call `(helper/service` to get a new service instance.
9 | ;; If you need a constant service over multiple calls, use `(helper/with-service ...)
10 | ;; All generated services will have randomized, consistent in-memory Datomic DBs
11 | ;; if required by the service
12 | ;;
13 | ;; `helper` also contains shorthands for common `response-for` patterns,
14 | ;; like GET, POST, post-json, post-edn, and others
15 |
16 | (deftest about-page-test
17 | (helper/with-service service/service
18 | (is (.contains (:body (response-for (helper/service) :get "/about"))
19 | "Clojure 1.9"))
20 | (is (= (:headers (helper/GET "/about"))
21 | {"Content-Type" "text/html;charset=UTF-8"
22 | "Strict-Transport-Security" "max-age=31536000; includeSubdomains"
23 | "X-Frame-Options" "DENY"
24 | "X-Content-Type-Options" "nosniff"
25 | "X-XSS-Protection" "1; mode=block"
26 | "X-Download-Options" "noopen"
27 | "X-Permitted-Cross-Domain-Policies" "none"
28 | "Content-Security-Policy" "object-src none"}))))
29 |
30 |
--------------------------------------------------------------------------------
/samples/vasebi/test/vasebi/test_helper.clj:
--------------------------------------------------------------------------------
1 | (ns vasebi.test-helper
2 | (:require [io.pedestal.test :refer [response-for]]
3 | [io.pedestal.http :as http]
4 | [io.pedestal.interceptor.chain :as chain]
5 | [io.pedestal.log :as log]
6 | [com.cognitect.vase :as vase]
7 | [com.cognitect.vase.util :as util]
8 | [com.cognitect.vase.datomic :as datomic]
9 | [vasebi.server :as server]
10 | [vasebi.service]))
11 |
12 | (def write-edn pr-str)
13 |
14 | (defn new-service
15 | "This generates a new testable service for use with io.pedestal.test/response-for.
16 | It will also create a new Datomic DB (randomized URI)."
17 | ([] (new-service vasebi.service/service))
18 | ([service-map]
19 | (let [db-table (volatile! {})
20 | vase-service-map (server/vase-service
21 | service-map
22 | (fn [spec-path]
23 | (let [spec (vase/load-edn-resource spec-path)
24 | prod-db-uri (:datomic-uri spec)
25 | new-db-uri (when-let [db-uri (and prod-db-uri (get @db-table prod-db-uri (datomic/new-db-uri)))]
26 | (vswap! db-table assoc prod-db-uri db-uri)
27 | db-uri)]
28 | (if prod-db-uri
29 | (assoc spec :datomic-uri new-db-uri)
30 | spec))))]
31 | (::http/service-fn (http/create-servlet vase-service-map)))))
32 |
33 | (def ^:dynamic *current-service* nil)
34 |
35 | (defmacro with-service
36 | "Executes all requests in the body with the same service (using a thread-local binding)"
37 | [srv-map & body]
38 | `(binding [*current-service* (new-service ~srv-map)]
39 | ~@body))
40 |
41 | (defn service
42 | [& args]
43 | (or *current-service* (apply new-service args)))
44 |
45 | (defn GET
46 | "Make a GET request on our service using response-for."
47 | [& args]
48 | (apply response-for (service) :get args))
49 |
50 | (defn POST
51 | "Make a POST request on our service using response-for."
52 | [& args]
53 | (apply response-for (service) :post args))
54 |
55 | (defn DELETE
56 | "Make a DELETE request on our service using response-for."
57 | [& args]
58 | (apply response-for (service) :delete args))
59 |
60 | (defn json-request
61 | ([verb url payload]
62 | (json-request verb url payload {}))
63 | ([verb url payload opts]
64 | (response-for (service)
65 | verb url
66 | :headers (merge {"Content-Type" "application/json"}
67 | (:headers opts))
68 | :body (util/write-json payload))))
69 |
70 | (defn post-json
71 | "Makes a POST request to URL-path expecting a payload to submit as JSON.
72 |
73 | Options:
74 | * :headers: Additional headers to send with the request."
75 | ([URL-path payload]
76 | (post-json URL-path payload {}))
77 | ([URL-path payload opts]
78 | (json-request :post URL-path payload opts)))
79 |
80 | (defn post-edn
81 | "Makes a POST request to URL-path expecting a payload to submit as edn.
82 |
83 | Options:
84 | * :headers: Additional headers to send with the request."
85 | ([URL-path payload]
86 | (post-edn URL-path payload {}))
87 | ([URL-path payload opts]
88 | (response-for (service)
89 | :post URL-path
90 | :headers (merge {"Content-Type" "application/edn"}
91 | (:headers opts))
92 | :body (write-edn payload))))
93 |
94 | (defn response-data
95 | "Return the parsed payload data from a vase api http response."
96 | ([response] (response-data response util/read-json))
97 | ([response reader]
98 | (-> response
99 | :body
100 | reader)))
101 |
102 | (defn run-interceptor
103 | ([i] (run-interceptor {} i))
104 | ([ctx i] (chain/execute (chain/enqueue* ctx i))))
105 |
106 | (defn new-req-ctx
107 | [& {:as headers}]
108 | {:request {:headers headers}})
109 |
--------------------------------------------------------------------------------
/script/pre-release-check.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | #
3 | # Check for version hygiene and samples all pass before releasing
4 | #
5 |
6 | echo "==== Checking top-level for dependencies up to date"
7 | lein ancient
8 |
9 | echo "==== Checking samples for dependencies up to date"
10 | pushd samples
11 |
12 | for d in *; do
13 | pushd $d
14 | echo "===== $d"
15 | lein ancient
16 | popd
17 | done
18 | popd
19 |
20 | echo "==== Running tests in samples"
21 | pushd samples
22 |
23 | for d in *; do
24 | pushd $d
25 | echo "===== $d"
26 | lein test
27 | popd
28 | done
29 | popd
30 |
31 |
32 | echo "==== Look at all project.clj version declarations"
33 | find . -path ./template/src -prune -o -name "project.clj" -print | xargs grep defproject
34 |
35 | echo "==== Check copyright declarations"
36 | CURRENT_YEAR=`date +%Y`
37 | find . \( -path ./.git -o -path ./samples/pet-store/resources -o -path ./samples/petstore-full/resources -o -path ./script \) -prune -o -type f -print | xargs grep -i "copyright.*20" | grep -v Relevance | grep -v $CURRENT_YEAR
38 |
--------------------------------------------------------------------------------
/src/com/cognitect/vase.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase
2 | (:require [clojure.spec.alpha :as spec]
3 | [io.pedestal.http.route :as route]
4 | [com.cognitect.vase.datomic :as datomic]
5 | [com.cognitect.vase.edn :as edn]
6 | [com.cognitect.vase.literals :as literals]
7 | [com.cognitect.vase.routes :as routes]
8 | [com.cognitect.vase.spec :as vase.spec]))
9 |
10 | (defn load-edn-resource
11 | "Given a resource name, loads a descriptor or app-spec,
12 | using the proper readers to get support for Vase literals."
13 | [res]
14 | (if (coll? res)
15 | res
16 | (edn/from-resource res)))
17 |
18 | (defn load-edn-file
19 | "Given a path, loads a descriptor using the proper readers to get
20 | support for Vase literals."
21 | [file-path]
22 | (if (coll? file-path)
23 | file-path
24 | (edn/from-file file-path)))
25 |
26 | (defn ensure-schema
27 | "Given an api-spec or a collection of app-specs,
28 | extract the schema norms, ensure they conform, and idempotently
29 | transact them into the Datomic DB.
30 | Returns a map of {'db-uri' {:connection datomic-conn, :norms {... all merged norms ..}}}."
31 | [spec-or-specs]
32 | (let [edn-specs (if (sequential? spec-or-specs) spec-or-specs [spec-or-specs])
33 | uri-norms (reduce (fn [acc spec]
34 | (let [uri (:datomic-uri spec)
35 | norms (get-in spec [:descriptor :vase/norms])]
36 | (if (contains? acc uri)
37 | ;; It is expected that norm chunks are complete.
38 | ;; A single chunk cannot be spread across files,
39 | ;; which is why we're using `merge` and not `merge-with concat`
40 | (update-in acc [uri] #(merge % norms))
41 | (assoc acc uri norms))))
42 | {}
43 | edn-specs)]
44 | (reduce (fn [acc [uri norms]]
45 | (let [conn (datomic/connect uri)]
46 | (datomic/ensure-schema conn norms)
47 | (assoc acc uri {:connection conn
48 | :norms norms})))
49 | {}
50 | uri-norms)))
51 |
52 | (defn specs
53 | "Given a app-spec or collection of app-specs,
54 | extract all defined Clojure specs and evaluate them,
55 | placing them in spec's registry."
56 | [spec-or-specs]
57 | (let [edn-specs (if (sequential? spec-or-specs) spec-or-specs [spec-or-specs])
58 | descriptors (map :descriptor edn-specs)]
59 | (doseq [descriptor descriptors]
60 | (when-let [specs (:vase/specs descriptor)]
61 | (doseq [[k specv] specs]
62 | (let [sv (cond
63 | (spec/spec? specv) specv
64 | (list? specv) (eval specv)
65 | (symbol? specv) (resolve specv)
66 | :else specv)]
67 | (eval `(spec/def ~k ~sv))))))))
68 |
69 | (defn routes
70 | "Return a seq of route vectors for Pedestal's table routing syntax. Routes
71 | will all begin with `api-root/:api-namespace/api-name-tag`.
72 |
73 | `spec-or-specs` is either a single app-spec (as a map) or a collection of app-specs.
74 |
75 | The routes will support all the operations defined in the
76 | spec. Callers should treat the format of these routes as
77 | opaque. They may change in number, quantity, or layout."
78 | [api-root spec-or-specs]
79 | (let [specs (if (sequential? spec-or-specs) spec-or-specs [spec-or-specs])
80 | ;; We need to "unpack" all the :activated-apis
81 | ;; From this part onward, :activated-apis is a single, scalar; a keyword
82 | expanded-specs (mapcat (fn [spec]
83 | (if (sequential? (:activated-apis spec))
84 | (mapv #(assoc spec :activated-apis %) (:activated-apis spec))
85 | [spec]))
86 | specs)
87 | routes (mapcat (partial routes/spec-routes api-root) expanded-specs)
88 | api-route (routes/api-description-route
89 | api-root
90 | routes
91 | :describe-apis)]
92 | (cons api-route routes)))
93 |
94 | (spec/fdef routes
95 | :args (spec/cat :api-route vase.spec/valid-uri?
96 | :spec-or-specs (spec/or :single-spec ::vase.spec/spec
97 | :multiple-specs (spec/* ::vase.spec/spec)))
98 | :ret ::vase.spec/route-table)
99 |
--------------------------------------------------------------------------------
/src/com/cognitect/vase/api.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.api
2 | "Public functions for submitting a data structure to Vase and
3 | getting back routes, specs, and even a whole Pedestal service map."
4 | (:require [clojure.spec.alpha :as s]
5 | [clojure.string :as str]
6 | [io.pedestal.http :as http]
7 | [io.pedestal.interceptor :as i]
8 | [io.pedestal.interceptor.chain :as chain]))
9 |
10 | (s/def ::path string?)
11 |
12 | (defn- interceptor? [v]
13 | (satisfies? i/IntoInterceptor v))
14 |
15 | (s/def ::interceptor interceptor?)
16 | (s/def ::interceptors (s/coll-of ::interceptor :min-count 1))
17 | (s/def ::route-name keyword?)
18 | (s/def ::route (s/cat :path ::path
19 | :verb #{:get :put :post :delete :head :options :patch}
20 | :interceptors (s/or :single ::interceptor
21 | :vector ::interceptors)
22 | :route-name (s/? ::route-name)))
23 | (s/def ::routes (s/coll-of ::route :min-count 1))
24 | (s/def ::on-startup (s/nilable ::interceptors))
25 | (s/def ::on-request (s/nilable ::interceptors))
26 | (s/def ::api (s/keys :req-un [::on-startup ::on-request ::routes ::path]))
27 | (s/def ::apis (s/coll-of ::api :min-count 1))
28 | (s/def ::service-map (s/keys))
29 | (s/def ::service (s/keys :req-un [::apis] :opt-un [::service-map]))
30 |
31 | (defn- base-route
32 | [base {:keys [path]}]
33 | (let [s (str base path)]
34 | (str/replace s #"//" "/")))
35 |
36 | (defn- base-interceptors
37 | [on-request {:keys [interceptors]}]
38 | (let [[one-or-many intc] interceptors]
39 | (case one-or-many
40 | :single (conj on-request intc)
41 | :vector (into on-request intc))))
42 |
43 | (defn- routes-for-api
44 | [api]
45 | (let [base (:path api "/")
46 | on-req (or (:on-request api) [])]
47 | (mapv
48 | (fn [route]
49 | (cond-> [(base-route base route)
50 | (:verb route)
51 | (base-interceptors on-req route)]
52 | (some? (:route-name route))
53 | (into [:route-name (:route-name route)])))
54 | (:routes api #{}))))
55 |
56 | (defn- collect-routes
57 | [spec]
58 | (reduce into #{}
59 | (map routes-for-api (:apis spec))))
60 |
61 | (defn- add-routes
62 | [service-map all-routes]
63 | (assoc service-map ::http/routes all-routes))
64 |
65 | (defn- collect-startups
66 | [spec]
67 | (reduce into []
68 | (map #(:on-startup %) (:apis spec))))
69 |
70 | (defn- add-startups
71 | [service-map startups]
72 | (assoc service-map ::startups startups))
73 |
74 | (defn dev-mode
75 | [service-map]
76 | (assoc service-map ::http/join? false))
77 |
78 | (def default-service-map
79 | {::http/type :jetty
80 | ::http/port 80
81 | ::http/routes #{}})
82 |
83 | (defn service-map
84 | "Given a spec, return a Pedestal service-map
85 | or :clojure.spec/invalid. If starter-map is provided, the spec's
86 | settings will be merged into it. If starter-map is not provided then
87 | the `default-service-map` is used as a starter."
88 | ([spec]
89 | (service-map spec default-service-map))
90 | ([spec starter-map]
91 | (let [conformed (s/conform ::service spec)]
92 | (if (s/invalid? conformed)
93 | (throw (ex-info (str "Can't create service map.\n" (s/explain ::service spec)) {}))
94 | (-> starter-map
95 | (merge (:service-map conformed))
96 | (add-routes (collect-routes conformed))
97 | (add-startups (collect-startups conformed)))))))
98 |
99 | (defn execute-startups
100 | [service-map]
101 | (let [startups (map i/-interceptor (get service-map ::startups []))]
102 | (chain/execute service-map startups)))
103 |
104 | (defn start-service
105 | [service-map]
106 | (-> service-map
107 | execute-startups
108 | http/create-server
109 | http/start))
110 |
--------------------------------------------------------------------------------
/src/com/cognitect/vase/datomic.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.datomic
2 | (:require [datomic.api :as d]
3 | [io.rkn.conformity :as c]
4 | [io.pedestal.interceptor :as i]))
5 |
6 | (defn new-db-uri []
7 | (str "datomic:mem://" (d/squuid)))
8 |
9 | (defn connect
10 | "Given a Datomic URI, attempt to create the database and connect to it,
11 | returning the connection."
12 | [uri]
13 | (d/create-database uri)
14 | (d/connect uri))
15 |
16 | (defn normalize-norm-keys
17 | [norms]
18 | (reduce
19 | (fn [acc [k-title v-map]]
20 | (assoc acc
21 | k-title (reduce
22 | (fn [norm-acc [nk nv]]
23 | (assoc norm-acc
24 | (keyword (name nk)) nv))
25 | {} v-map)))
26 | {}
27 | norms))
28 |
29 | (defn ensure-schema
30 | [conn norms]
31 | (c/ensure-conforms conn (normalize-norm-keys norms)))
32 |
33 | (defn insert-datomic
34 | "Provide a Datomic conn and db in all incoming requests"
35 | [conn]
36 | (i/interceptor
37 | {:name ::insert-datomic
38 | :enter (fn [context]
39 | (-> context
40 | (assoc-in [:request :conn] conn)
41 | (assoc-in [:request :db] (d/db conn))))}))
42 |
43 |
--------------------------------------------------------------------------------
/src/com/cognitect/vase/edn.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.edn
2 | (:require [clojure.edn :as edn]
3 | [clojure.java.io :as io]
4 | [clojure.string :as cstr])
5 | (:import (java.io File)))
6 |
7 | (defn read
8 | "Converts an edn string into Clojure data. `args` are clojure.edn `opts`
9 | `:readers` defaults to `*data-readers*`"
10 | [string & args]
11 | (let [e (edn/read-string (merge {:readers *data-readers*}
12 | (apply hash-map args))
13 | string)]
14 | (if (instance? clojure.lang.IObj e)
15 | (with-meta e {:vase/src string})
16 | e)))
17 |
18 | (defn from-resource
19 | "Load an EDN resource file and read its contents. The only required argument
20 | is `file-path`, which is the path of a file relative the projects resources
21 | directory (`resources/` or, for tests, `test/resources/`).
22 |
23 | Optional arguments:
24 |
25 | * `fallback-path` - A \"default\" path to check if file-path is actually an
26 | empty string. Useful in places you load a `file-path` from a config and its
27 | value might be absent.
28 | * `process-path-fn` - The function to use for getting the URL of the file. By
29 | default uses `clojure.java.io/resource`."
30 | ([file-path]
31 | (from-resource file-path "" io/resource))
32 | ([file-path fallback-path]
33 | (from-resource file-path fallback-path io/resource))
34 | ([file-path fallback-path process-path-fn]
35 | (let [trimmed-path (or (not-empty (cstr/trim file-path))
36 | (not-empty (cstr/trim fallback-path)))
37 | contents (some->>
38 | trimmed-path
39 | process-path-fn
40 | slurp)]
41 | (if contents
42 | (read contents)
43 | (throw (ex-info
44 | (str "Failed to read an EDN file: " file-path " :: trimmed to: " trimmed-path)
45 | {:file-path file-path
46 | :trimmed-path trimmed-path}))))))
47 |
48 | (defn from-file
49 | [file-path]
50 | (from-resource file-path "" (fn [^String x] (io/as-url (File. x)))))
51 |
--------------------------------------------------------------------------------
/src/com/cognitect/vase/interceptor.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.interceptor
2 | (:require [io.pedestal.interceptor.helpers :as helpers :refer [defon-request]]
3 | [io.pedestal.interceptor :as i]
4 | [clojure.stacktrace :as ctrace]
5 | [clj-time.core :as clj-time]
6 | [com.cognitect.vase.util :as util]))
7 |
8 | (def request-id-header "vaserequest-id")
9 |
10 | (def attach-received-time
11 | (i/-interceptor
12 | {:name ::attach-received-time
13 | :doc "Attaches a timestamp to every request."
14 | :enter (fn [ctx] (assoc-in ctx [:request :received-time] (clj-time/now)))}))
15 |
16 | (def attach-request-id
17 | (i/-interceptor
18 | {:name ::attach-request-id
19 | :doc "Attaches a request ID to every request;
20 | If there's a 'request_id' header, it will use that value, otherwise it will generate a short hash"
21 | :enter (fn [{:keys [request] :as ctx}]
22 | (let [req-id (get-in request [:headers request-id-header] (util/short-hash))]
23 | (-> ctx
24 | (assoc-in [:request :request-id] req-id)
25 | (assoc-in [:request :headers request-id-header] req-id))))}))
26 |
27 | (defn forward-headers
28 | [headers]
29 | (i/-interceptor
30 | {:name ::forward-headers
31 | :doc "Given an interceptor name and list of headers to forward,
32 | return an interceptor that attaches those headers to reponses IFF
33 | they are in the request"
34 | :leave (fn [context]
35 | (update-in context [:response :headers]
36 | #(merge (select-keys (get-in context [:request :headers]) headers) %)))}))
37 |
--------------------------------------------------------------------------------
/src/com/cognitect/vase/main.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.main
2 | (:require [com.cognitect.vase.try :as try :refer [try->]]
3 | [fern.easy :as fe]
4 | [com.cognitect.vase.fern :as fern]
5 | [com.cognitect.vase.api :as a])
6 | (:gen-class))
7 |
8 | (def vase-fern-url "https://github.com/cognitect-labs/vase/blob/master/docs/vase_and_fern.md")
9 |
10 | (def usage
11 | (str
12 | "Usage: vase _filename_\n\nVase takes exactly one filename, which must be in Fern format.\nSee "
13 | vase-fern-url
14 | " for details."))
15 |
16 | (defn- parse-args
17 | [[filename & stuff]]
18 | (if (or (not filename) (not (empty? stuff)))
19 | (throw (ex-info usage {:filename filename}))
20 | {:filename filename}))
21 |
22 | (defn -main
23 | [& args]
24 | (let [file (try-> args
25 | parse-args
26 | (:! clojure.lang.ExceptionInfo ei (fe/print-other-exception ei))
27 | :filename)]
28 | (when (and file (not= ::try/exit file))
29 | (try-> file
30 | fern/load-from-file
31 | (:! java.io.FileNotFoundException fnfe (fe/print-error-message (str "File not found: " (pr-str (.getMessage fnfe)))))
32 |
33 | fern/prepare-service
34 | (:! Throwable t (fe/print-evaluation-exception t file))
35 |
36 | a/start-service
37 | (:! Throwable t (fe/print-other-exception t file))))))
38 |
--------------------------------------------------------------------------------
/src/com/cognitect/vase/response.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.response)
2 |
3 | (defn- complete-with-errors?
4 | [response errors]
5 | (and (not (nil? response)) (seq errors)))
6 |
7 | (defn- bad-request?
8 | [response errors]
9 | (and (nil? response) (seq errors)))
10 |
11 | (defn- exception?
12 | [response]
13 | (instance? Throwable response))
14 |
15 | (defn status-code
16 | [response errors]
17 | (cond
18 | (complete-with-errors? response errors) 205
19 | (bad-request? response errors) 400
20 | (exception? response) 500
21 | :else 200))
22 |
23 | (defn response
24 | [body headers status]
25 | {:body (or body "")
26 | :headers (or headers {})
27 | :status (or status 200)})
28 |
--------------------------------------------------------------------------------
/src/com/cognitect/vase/routes.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.routes
2 | (:require [io.pedestal.http :as http]
3 | [io.pedestal.interceptor :as i]
4 | [io.pedestal.http.body-params :as body-params]
5 | [com.cognitect.vase.datomic :as datomic]
6 | [com.cognitect.vase.interceptor :as interceptor]
7 | [clojure.string :as string]))
8 |
9 | (defn- describe-api
10 | "Return a list of all active routes.
11 | Optionally filter the list with the query param, `f`, which is a fuzzy match
12 | string value"
13 | [routes]
14 | (i/interceptor
15 | {:name (keyword "vase" (str (gensym "describe-api-")))
16 | :enter (fn [context]
17 | (let [{:keys [f sep edn]
18 | :or {f "" sep "
" edn false}} (get-in context [:request :query-params])
19 | results (mapv #(take 2 %) routes)]
20 | (assoc context :response
21 | (cond
22 | edn (http/edn-response results)
23 | (string/starts-with? sep "<") {:status 200
24 | :headers {"Content-Type" "text/html"}
25 | :body (string/join sep (map #(string/join " " %) results))}
26 | :else {:status 200
27 | :body (string/join sep (map #(string/join " " %) results))}))))}))
28 |
29 | (def ^:private common-api-interceptors
30 | [interceptor/attach-received-time
31 | interceptor/attach-request-id
32 | http/json-body])
33 |
34 | (defn- app-interceptors
35 | [spec]
36 | (let [{:keys [descriptor activated-apis datomic-uri]} spec
37 | datomic-conn (datomic/connect datomic-uri)
38 | headers-to-forward (get-in descriptor [:vase/apis activated-apis :vase.api/forward-headers] [])
39 | headers-to-forward (conj headers-to-forward interceptor/request-id-header)
40 | version-interceptors (mapv i/interceptor (get-in descriptor [:vase/apis activated-apis :vase.api/interceptors] []))
41 | base-interceptors (conj common-api-interceptors
42 | (datomic/insert-datomic datomic-conn)
43 | (body-params/body-params (body-params/default-parser-map :edn-options {:readers *data-readers*}))
44 | (interceptor/forward-headers headers-to-forward))]
45 | (into base-interceptors
46 | version-interceptors)))
47 |
48 | (defn- specified-routes
49 | [spec]
50 | (let [{:keys [activated-apis descriptor]} spec]
51 | (get-in descriptor [:vase/apis activated-apis :vase.api/routes])))
52 |
53 | (defn- api-routes
54 | "Given a descriptor map, an app-name keyword, and a version keyword,
55 | return route vectors in Pedestal's tabular format. Routes will all be
56 | subordinated under `base`"
57 | [base spec]
58 | (let [common (app-interceptors spec)]
59 | (for [[path verb-map] (specified-routes spec)
60 | [verb action] verb-map
61 | :let [interceptors (if (vector? action)
62 | (into common (map i/interceptor action))
63 | (conj common (i/interceptor action)))]]
64 | (if (= path "/")
65 | [(str base) verb interceptors]
66 | [(str base path) verb interceptors]))))
67 |
68 | (defn- api-base
69 | [api-root spec]
70 | (let [{:keys [activated-apis]} spec]
71 | (str api-root "/" (namespace activated-apis) "/" (name activated-apis))))
72 |
73 | (defn- api-description-route-name
74 | [spec]
75 | (let [{:keys [activated-apis]} spec]
76 | (keyword (str (namespace activated-apis)
77 | "." (name activated-apis))
78 | "describe")))
79 |
80 | (defn api-description-route
81 | [api-root routes route-name]
82 | [api-root
83 | :get
84 | (conj common-api-interceptors (describe-api routes))
85 | :route-name
86 | route-name])
87 |
88 | (defn spec-routes
89 | "Return a seq of route vectors from a single specification"
90 | [api-root spec]
91 | (if (nil? (:activated-apis spec))
92 | []
93 | (let [app-version-root (api-base api-root spec)
94 | app-version-routes (api-routes app-version-root spec)
95 | app-api-route (api-description-route app-version-root
96 | app-version-routes
97 | (api-description-route-name spec))]
98 | (cons app-api-route app-version-routes))))
99 |
--------------------------------------------------------------------------------
/src/com/cognitect/vase/spec.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.spec
2 | "Contains the clojure.spec.alpha definitions for the Vase
3 | application specification."
4 | (:require [io.pedestal.interceptor :as interceptor]
5 | [clojure.spec.alpha :as s]
6 | [clojure.spec.gen.alpha :as gen]))
7 |
8 | ;; -- Predicates --
9 | (defn valid-uri?
10 | "Returns true if v is a non-empty string representation of a uri."
11 | [v]
12 | (try
13 | (boolean (and (string? v) (not-empty v) (java.net.URI. v)))
14 | (catch java.net.URISyntaxException e false)))
15 |
16 | ;; -- Pedestal-specs --
17 | (s/def ::interceptor #(satisfies? interceptor/IntoInterceptor %))
18 | (s/def ::route-table-route (s/cat :path valid-uri?
19 | :verb #{:any :get :put :post :delete :patch :options :head}
20 | :handler (s/alt :fn fn? :interceptors (s/coll-of ::interceptor :kind vector?))
21 | :route-name (s/? (s/cat :_ #(= :route-name %) :name keyword?))
22 | :constraints (s/? (s/cat :_ #(= :constraints %)
23 | :constraints (s/map-of keyword?
24 | #(instance? java.util.regex.Pattern %))))))
25 | (s/def ::route-table (s/* (s/spec ::route-table-route)))
26 |
27 | ;; -- Vase app-specs --
28 | (s/def ::activated-apis (s/or :base keyword?
29 | :top-level (s/+ keyword?)))
30 | (s/def ::datomic-uri (s/with-gen (s/and string? valid-uri? #(.startsWith % "datomic"))
31 | #(gen/return (str "datomic:mem://" (java.util.UUID/randomUUID)))))
32 |
33 | ;; -- descriptor apis specs --
34 | (s/def :vase.api/schemas (s/+ qualified-keyword?))
35 | (s/def :vase.api/forward-headers (s/+ string?))
36 |
37 | ;; -- routes --
38 | (s/def ::get (s/or :one ::interceptor
39 | :many (s/+ ::interceptor)))
40 | (s/def ::put (s/or :one ::interceptor
41 | :many (s/+ ::interceptor)))
42 | (s/def ::post (s/or :one ::interceptor
43 | :many (s/+ ::interceptor)))
44 | (s/def ::delete (s/or :one ::interceptor
45 | :many (s/+ ::interceptor)))
46 | (s/def ::head (s/or :one ::interceptor
47 | :many (s/+ ::interceptor)))
48 | (s/def ::options (s/or :one ::interceptor
49 | :many (s/+ ::interceptor)))
50 |
51 | (s/def ::action (s/and (s/keys :opt-un [::get ::put ::post ::delete ::head ::options])
52 | #(not-empty (select-keys % [:get :put :post :delete :head :options]))))
53 | (s/def :vase.api/routes (s/map-of valid-uri? ::action))
54 |
55 | (s/def :vase.api/interceptors (s/+ ::interceptor))
56 |
57 | (s/def :vase/apis (s/map-of qualified-keyword?
58 | (s/keys :req [:vase.api/routes]
59 | :opt [:vase.api/schemas
60 | :vase.api/forward-headers
61 | :vase.api/interceptors])))
62 |
63 | ;; -- descriptor app specs --
64 | ;; -- norms --
65 | (s/def ::tx (s/* (s/or :_ vector? :_ map?)))
66 | (s/def :vase.norm/txes (s/* (s/spec ::tx)))
67 | (s/def :vase.norm/requires (s/* qualified-keyword?))
68 | (s/def :vase/norms (s/map-of qualified-keyword? (s/keys :req [:vase.norm/txes]
69 | :opt [:vase.norm/requires])))
70 |
71 | (s/def :vase/specs (s/map-of qualified-keyword? any?))
72 |
73 | ;; -- The descriptor spec --
74 | (s/def ::descriptor (s/keys :req [:vase/apis]
75 | :opt [:vase/norms
76 | :vase/specs]))
77 |
78 | ;; -- Vase app-spec --
79 | (s/def ::spec (s/keys :req-un [::activated-apis ::descriptor ::datomic-uri]))
80 |
--------------------------------------------------------------------------------
/src/com/cognitect/vase/try.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.try)
2 |
3 | (defmacro try->
4 | "Thread the first expression through the remaining expressions.
5 |
6 | A form like (:? classname var ,,,) catches exceptions from
7 | the preceding code. It continues threading with the value of the
8 | body expressions as the replacement value.
9 |
10 | A form like (:! classname var ,,,) catches exceptions from the
11 | preceding code, but does _not_ continue threading. It aborts and the
12 | entire expression returns ::exit"
13 | [x & forms]
14 | (loop [x x
15 | forms forms]
16 | (if forms
17 | (let [form (first forms)
18 | threaded (cond
19 | (and (seq? form) (= :! (first form)))
20 | (with-meta `(try ~x (catch ~@(rest form) ::exit)) (meta form))
21 |
22 | (and (seq? form) (= :? (first form)))
23 | (with-meta `(try ~x (catch ~@(rest form))) (meta form))
24 |
25 | (seq? form)
26 | (with-meta `(let [v# ~x]
27 | (if (= ::exit v#)
28 | ::exit
29 | (~(first form) v# ~@(next form)))) (meta form))
30 |
31 | :else
32 | `(let [v# ~x]
33 | (if (= ::exit v#)
34 | ::exit
35 | (~form v#))))]
36 | (recur threaded (next forms)))
37 | x)))
38 |
--------------------------------------------------------------------------------
/src/com/cognitect/vase/util.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.util
2 | (:require [clojure.edn :as edn]
3 | [clojure.java.io :as io]
4 | [clojure.string :as cstr]
5 | [cheshire.core :as json]
6 | [cognitect.transit :as transit])
7 | (:import (java.io ByteArrayInputStream
8 | FileInputStream
9 | File)
10 | (java.util Base64)))
11 |
12 | (defn map-vals
13 | [f m]
14 | (reduce-kv (fn [m k v] (assoc m k (f v))) m m))
15 |
16 | (defn str->inputstream
17 | ([^String text]
18 | (str->inputstream text "UTF-8"))
19 | ([^String text ^String encoding]
20 | (ByteArrayInputStream. (.getBytes text encoding))))
21 |
22 | (defn short-hash []
23 | (subs
24 | (.encodeToString (Base64/getEncoder)
25 | (byte-array (loop [i 0
26 | ret (transient [])]
27 | (if (< i 8)
28 | (recur (inc i) (conj! ret (.byteValue ^Long (long (rand 100)))))
29 | (persistent! ret)))))
30 | 0 11))
31 |
32 | ;; This function is useful when writing your own action literals,
33 | ;; allowing you to expand symbol names within the descriptors.
34 | ;; It's not used within the Vase source, but has been used on projects
35 | ;; built with Vase.
36 | (defn fully-qualify-symbol
37 | ([sym] (fully-qualify-symbol *ns* sym))
38 | ([ns sym]
39 | (if-let [ns-alias? (namespace sym)]
40 | (let [ns-aliases (ns-aliases ns)]
41 | (if-let [fqns (ns-aliases (symbol ns-alias?))]
42 | (symbol (name (ns-name fqns)) (name sym))
43 | sym))
44 | sym)))
45 |
46 | (defn ensure-keyword [x]
47 | (cond
48 | (keyword? x) x
49 | (string? x) (keyword x)
50 | (symbol? x) (keyword (namespace x) (name x))
51 | :else (keyword (str x))))
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | (defn read-json
62 | "Converts json string to Clojure data. By default, keys are keywordized."
63 | [string & args]
64 | (apply json/parse-string string keyword args))
65 |
66 | (defn write-json
67 | "Writes json string given Clojure data. By default, unicode is not escaped."
68 | [data & args]
69 | (json/generate-string data (apply hash-map args)))
70 |
71 | (defn read-transit-json
72 | [transit-json-str]
73 | (-> transit-json-str
74 | str->inputstream
75 | (transit/reader :json)
76 | transit/read))
77 |
--------------------------------------------------------------------------------
/src/data_readers.clj:
--------------------------------------------------------------------------------
1 | {vase/respond com.cognitect.vase.literals/respond
2 | vase/redirect com.cognitect.vase.literals/redirect
3 | vase/conform com.cognitect.vase.literals/conform
4 | vase/query com.cognitect.vase.literals/query
5 | vase/transact com.cognitect.vase.literals/transact
6 | vase/validate com.cognitect.vase.literals/validate
7 | vase/intercept com.cognitect.vase.literals/intercept
8 | vase/schema-tx com.cognitect.vase.literals/schema-tx
9 |
10 | vase.datomic/query com.cognitect.vase.literals/query
11 | vase.datomic/transact com.cognitect.vase.literals/transact
12 | vase.datomic.cloud/query com.cognitect.vase.literals/query-cloud
13 | vase.datomic.cloud/transact com.cognitect.vase.literals/transact-cloud}
14 |
--------------------------------------------------------------------------------
/template/.gitignore:
--------------------------------------------------------------------------------
1 | # Project related
2 | generated-js/
3 |
4 | # Java related
5 | pom.xml
6 | pom.xml.asc
7 | *jar
8 | *.class
9 |
10 | # Leiningen
11 | classes/
12 | lib/
13 | native/
14 | checkouts/
15 | target/
16 | .lein-deps-sum
17 | .lein-failures
18 | .lein-repl-history
19 | .lein-cljsbuild-repl
20 | .lein-plugins/
21 | repl-port
22 | .nrepl-port
23 |
24 | # Temp Files
25 | tmp/
26 | *.orig
27 | *~
28 | .*.swp
29 | .*.swo
30 | *.tmp
31 | *.bak
32 |
33 | # Editors (IntelliJ / Eclipse)
34 | */.idea
35 | */.classpath
36 | */.project
37 | */.settings
38 |
39 | # OS X
40 | .DS_Store
41 |
42 | # Logging
43 | *.log
44 | logs/
45 |
46 | # Docs
47 | autodoc/
48 | docs
49 | codox
50 |
--------------------------------------------------------------------------------
/template/README.md:
--------------------------------------------------------------------------------
1 | # Pedestal + Vase Service Template
2 |
3 | Generate a new Pedestal Service with Vase hooked up.
4 |
5 | ## Usage
6 |
7 | To generate a new app: `lein new vase my-app`. You will have a new app in my-app! To
8 | explore further, read the readme in your generated app.
9 |
10 | ## Developer Notes
11 |
12 | There are two ways to try out local changes to this template:
13 |
14 | 1. Run `lein new vase NAME` in this directory.
15 | 2. `lein install` in this directory; ensure the correct version of the template is in :plugins of your
16 | `~/.lein/profiles.clj`; generate a new app.
17 |
18 | Test this template by running `lein test` from within the `template/` directory
19 |
20 |
21 |
--------------------------------------------------------------------------------
/template/project.clj:
--------------------------------------------------------------------------------
1 | ; Copyright 2017 Cognitect, Inc.
2 |
3 | ; The use and distribution terms for this software are covered by the
4 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0)
5 | ; which can be found in the file epl-v10.html at the root of this distribution.
6 | ;
7 | ; By using this software in any fashion, you are agreeing to be bound by
8 | ; the terms of this license.
9 | ;
10 | ; You must not remove this notice, or any other, from this software.
11 |
12 | (defproject vase/lein-template "0.9.3-SNAPSHOT"
13 | :description "A Pedestal Service + Vase template."
14 | :url "https://github.com/pedestal/pedestal"
15 | :scm "https://github.com/pedestal/pedestal"
16 | :license {:name "Eclipse Public License"
17 | :url "http://www.eclipse.org/legal/epl-v10.html"}
18 | :min-lein-version "2.0.0"
19 | :eval-in-leiningen true
20 | :test-selectors {:travis (complement :not-travis)})
21 |
--------------------------------------------------------------------------------
/template/src/leiningen/new/vase.clj:
--------------------------------------------------------------------------------
1 | ; Copyright 2017 Cognitect, Inc.
2 |
3 | ; The use and distribution terms for this software are covered by the
4 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0)
5 | ; which can be found in the file epl-v10.html at the root of this distribution.
6 | ;
7 | ; By using this software in any fashion, you are agreeing to be bound by
8 | ; the terms of this license.
9 | ;
10 | ; You must not remove this notice, or any other, from this software.
11 |
12 | (ns leiningen.new.vase
13 | (:require [leiningen.new.templates :refer [renderer name-to-path ->files
14 | project-name sanitize-ns]]))
15 |
16 | (defn vase
17 | "A Pedestal service + Vase project template."
18 | [name & args]
19 | (let [render (renderer "vase")
20 | main-ns (sanitize-ns name)
21 | data {:raw-name name
22 | :name (project-name name)
23 | :namespace main-ns
24 | :sanitized (name-to-path main-ns)}]
25 | (println (str "Generating a Vase application called " name "."))
26 | (->files data
27 | ["README.md" (render "README.md" data)]
28 | ["project.clj" (render "project.clj" data)]
29 | ["build.boot" (render "build.boot" data)]
30 | ["boot.properties" (render "boot.properties" data)]
31 | ["Capstanfile" (render "Capstanfile" data)]
32 | ["Dockerfile" (render "Dockerfile" data)]
33 | [".gitignore" (render ".gitignore" data)]
34 | ["src/{{sanitized}}/service.clj" (render "service.clj" data)]
35 | ["resources/{{namespace}}_service.fern" (render "vase_service.fern" data)]
36 | ["config/logback.xml" (render "logback.xml" data)]
37 | ["test/{{sanitized}}/service_test.clj" (render "service_test.clj" data)])))
38 |
--------------------------------------------------------------------------------
/template/src/leiningen/new/vase/.gitignore:
--------------------------------------------------------------------------------
1 | # Project related
2 |
3 | # Java related
4 | pom.xml
5 | pom.xml.asc
6 | *jar
7 | *.class
8 |
9 | # Leiningen
10 | classes/
11 | lib/
12 | native/
13 | checkouts/
14 | target/
15 | .lein-*
16 | repl-port
17 | .nrepl-port
18 | .repl
19 |
20 | # Temp Files
21 | *.orig
22 | *~
23 | .*.swp
24 | .*.swo
25 | *.tmp
26 | *.bak
27 |
28 | # OS X
29 | .DS_Store
30 |
31 | # Logging
32 | *.log
33 | /logs/
34 |
35 | # Builds
36 | out/
37 | build/
38 |
39 |
--------------------------------------------------------------------------------
/template/src/leiningen/new/vase/Capstanfile:
--------------------------------------------------------------------------------
1 |
2 | #
3 | # Name of the base image. Capstan will download this automatically from
4 | # Cloudius S3 repository.
5 | #
6 | #base: cloudius/osv
7 | base: cloudius/osv-openjdk8
8 |
9 | #
10 | # The command line passed to OSv to start up the application.
11 | #
12 | #cmdline: /java.so -cp /{{name}}/app.jar clojure.main -m {{namespace}}
13 | cmdline: /java.so -jar /{{name}}/app.jar
14 |
15 | #
16 | # The command to use to build the application.
17 | # You can use any build tool/command (make/rake/lein/boot) - this runs locally on your machine
18 | #
19 | # For Leiningen, you can use:
20 | #build: lein uberjar
21 | # For Boot, you can use:
22 | #build: boot build
23 |
24 | #
25 | # List of files that are included in the generated image.
26 | #
27 | files:
28 | /{{name}}/app.jar: ./target/{{name}}-0.0.1-SNAPSHOT-standalone.jar
29 |
30 |
--------------------------------------------------------------------------------
/template/src/leiningen/new/vase/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM java:8-alpine
2 | MAINTAINER Your Name
3 |
4 | ADD target/{{name}}-0.0.1-SNAPSHOT-standalone.jar /{{name}}/app.jar
5 |
6 | EXPOSE 8080
7 |
8 | CMD ["java", "-jar", "/{{name}}/app.jar"]
9 |
--------------------------------------------------------------------------------
/template/src/leiningen/new/vase/README.md:
--------------------------------------------------------------------------------
1 | # {{name}}
2 |
3 | FIXME
4 |
5 | ## Getting Started
6 |
7 | 1. Start the application: `lein run resources/{{namespace}}_service.fern`
8 | 2. Read your app's source code at src/{{sanitized}}/service.clj. Explore the docs of functions
9 | that define routes and responses.
10 | 3. See your Vase API Specification at `resources/{{namespace}}_service.fern`.
11 | 4. Learn more! See the [Links section below](#links).
12 |
13 |
14 | ## Configuration
15 |
16 | To configure logging see config/logback.xml. By default, the app logs to stdout and logs/.
17 | To learn more about configuring Logback, read its [documentation](http://logback.qos.ch/documentation.html).
18 |
19 |
20 | ## Developing your service
21 |
22 | 1. Start a new REPL: `lein repl`
23 | 2. Start your service in dev-mode: `(def dev-serv (run-dev))`
24 | 3. Connect your editor to the running REPL session.
25 | Re-evaluated code will be seen immediately in the service.
26 | 4. All changes to your Vase Service Descriptor will be loaded - no re-evaluation
27 | needed.
28 |
29 | ### [Docker](https://www.docker.com/) container support
30 |
31 | 1. Build an uberjar of your service: `lein uberjar`
32 | 2. Build a Docker image: `sudo docker build -t {{name}} .`
33 | 3. Run your Docker image: `docker run -p 8080:8080 {{name}}`
34 |
35 | ### [OSv](http://osv.io/) unikernel support with [Capstan](http://osv.io/capstan/)
36 |
37 | 1. Build and run your image: `capstan run -f "8080:8080"`
38 |
39 | Once the image it built, it's cached. To delete the image and build a new one:
40 |
41 | 1. `capstan rmi {{name}}; capstan build`
42 |
43 |
44 | ## Links
45 |
46 | * [Pedestal examples](https://github.com/pedestal/samples)
47 | * [Vase examples](https://github.com/cognitect-labs/vase/samples)
48 |
--------------------------------------------------------------------------------
/template/src/leiningen/new/vase/boot.properties:
--------------------------------------------------------------------------------
1 | #http://boot-clj.com
2 | BOOT_CLOJURE_NAME=org.clojure/clojure
3 | BOOT_CLOJURE_VERSION=1.9.0
4 |
--------------------------------------------------------------------------------
/template/src/leiningen/new/vase/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
8 |
9 |
10 |
11 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
12 |
13 |
14 |
15 |
16 | logs/{{name}}-%d{yyyy-MM-dd}.%i.log
17 |
18 | 64 MB
19 |
20 |
21 |
22 | true
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | %-5level %logger{36} - %msg%n
31 |
32 |
33 |
34 | INFO
35 |
36 |
37 |
38 |
39 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/template/src/leiningen/new/vase/project.clj:
--------------------------------------------------------------------------------
1 | (defproject {{raw-name}} "0.0.1-SNAPSHOT"
2 | :description "FIXME: write description"
3 | :url "http://example.com/FIXME"
4 | :license {:name "Eclipse Public License"
5 | :url "http://www.eclipse.org/legal/epl-v10.html"}
6 | :dependencies [[org.clojure/clojure "1.9.0"]
7 | [io.pedestal/pedestal.service "0.5.4"]
8 | [com.cognitect/pedestal.vase "0.9.4-SNAPSHOT"]
9 |
10 | ;; Remove this line and uncomment one of the next lines to
11 | ;; use Immutant or Tomcat instead of Jetty:
12 | [io.pedestal/pedestal.jetty "0.5.4"]
13 | ;; [io.pedestal/pedestal.immutant "0.5.4"]
14 | ;; [io.pedestal/pedestal.tomcat "0.5.4"]
15 |
16 | [ch.qos.logback/logback-classic "1.2.3" :exclusions [org.slf4j/slf4j-api]]
17 | [org.slf4j/jul-to-slf4j "1.7.25"]
18 | [org.slf4j/jcl-over-slf4j "1.7.25"]
19 | [org.slf4j/log4j-over-slf4j "1.7.25"]]
20 | :min-lein-version "2.0.0"
21 | :resource-paths ["config", "resources"]
22 | ;; If you use HTTP/2 or ALPN, use the java-agent to pull in the correct alpn-boot dependency
23 | ;:java-agents [[org.mortbay.jetty.alpn/jetty-alpn-agent "2.0.3"]]
24 | :profiles {:dev {:aliases {"run-dev" ["trampoline" "run" "-m" "{{namespace}}.server/run-dev"]}
25 | :dependencies [[io.pedestal/pedestal.service-tools "0.5.4"]]}
26 | :uberjar {:aot [{{namespace}}.service]}}
27 | :main ^{:skip-aot true} {{namespace}}.service)
28 |
--------------------------------------------------------------------------------
/template/src/leiningen/new/vase/service.clj:
--------------------------------------------------------------------------------
1 | (ns {{namespace}}.service
2 | (:require [com.cognitect.vase.try :as try :refer [try->]]
3 | [fern.easy :as fe]
4 | [com.cognitect.vase.fern :as fern]
5 | [com.cognitect.vase.api :as a]
6 | [io.pedestal.http :s server]
7 | [clojure.java.io :as io])
8 | (:gen-class))
9 |
10 | (defn run-server
11 | [filename & {:as opts}]
12 | (try-> filename
13 | fern/load-from-file
14 | (:! java.io.FileNotFoundException fnfe (fe/print-error-message (str "File not found: " (pr-str (.getMessage fnfe)))))
15 |
16 | fern/prepare-service
17 | (:! Throwable t (fe/print-evaluation-exception t filename))
18 |
19 | (merge opts)
20 | a/start-service
21 | (:! Throwable t (fe/print-other-exception t filename))))
22 |
23 | (defn run-dev []
24 | (run-server (io/resource "{{namespace}}_service.fern") :io.pedestal.http/join? false))
25 |
26 | (def vase-fern-url "https://github.com/cognitect-labs/vase/blob/master/docs/vase_and_fern.md")
27 |
28 | (def usage
29 | (str
30 | "Usage: vase _filename_\n\nVase takes exactly one filename, which must be in Fern format.\nSee "
31 | vase-fern-url
32 | " for details."))
33 |
34 | (defn- parse-args
35 | [[filename & stuff]]
36 | (if (or (not filename) (not (empty? stuff)))
37 | (throw (ex-info usage {:filename filename}))
38 | {:filename filename}))
39 |
40 | (defn -main
41 | [& args]
42 | (let [file (try-> args
43 | parse-args
44 | (:! clojure.lang.ExceptionInfo ei (fe/print-other-exception ei))
45 | :filename)]
46 | (when (and file (not= ::try/exit file))
47 | (run-server file))))
48 |
--------------------------------------------------------------------------------
/template/src/leiningen/new/vase/service_test.clj:
--------------------------------------------------------------------------------
1 | (ns {{namespace}}.service-test
2 | (:require [{{namespace}}.service :as service]
3 | [clojure.test :refer :all]))
4 |
--------------------------------------------------------------------------------
/template/src/leiningen/new/vase/vase_service.fern:
--------------------------------------------------------------------------------
1 | {vase/service (fern/lit vase/service
2 | {:apis []
3 | :service-map @http-options})
4 | http-options {:io.pedestal.http/port 8080
5 | :io.pedestal.http/type :jetty
6 | :io.pedestal.http/resource-path "public"}}
7 |
--------------------------------------------------------------------------------
/template/test/com/cognitect/vase/new_server_integration_test.clj:
--------------------------------------------------------------------------------
1 | ; Copyright 2013 Relevance, Inc.
2 | ; Copyright 2014-2017 Cognitect, Inc.
3 |
4 | ; The use and distribution terms for this software are covered by the
5 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0)
6 | ; which can be found in the file epl-v10.html at the root of this distribution.
7 | ;
8 | ; By using this software in any fashion, you are agreeing to be bound by
9 | ; the terms of this license.
10 | ;
11 | ; You must not remove this notice, or any other, from this software.
12 |
13 | (ns com.cognitect.vase.new-server-integration-test
14 | (:require [clojure.test :refer :all]
15 | [clojure.java.shell :as sh]
16 | [clojure.string :as string]
17 | [clojure.java.io :as io]))
18 |
19 | (def lein (or (System/getenv "LEIN_CMD") "lein"))
20 |
21 | (def project-dir (io/file "."))
22 |
23 | ;; This code was heavily inspired by Overtone's version, thanks!
24 | ;; https://github.com/overtone/overtone/blob/e3de1f7ac59af7fa3cf75d696fbcfc2a15830594/src/overtone/helpers/file.clj#L360
25 | (defn mk-tmp-dir!
26 | "Creates a unique temporary directory on the filesystem. Typically in /tmp on
27 | *NIX systems. Returns a File object pointing to the new directory. Raises an
28 | exception if the directory couldn't be created after 10000 tries."
29 | []
30 | (let [base-dir (io/file (System/getProperty "java.io.tmpdir"))
31 | base-name (str (java.util.UUID/randomUUID))
32 | tmp-base (str base-dir java.io.File/separator base-name)
33 | max-attempts 100]
34 | (loop [num-attempts 1]
35 | (if (= num-attempts max-attempts)
36 | (throw (Exception. (str "Failed to create temporary directory after " max-attempts " attempts.")))
37 | (let [tmp-dir-name (str tmp-base num-attempts)
38 | tmp-dir (io/file tmp-dir-name)]
39 | (if (.mkdir tmp-dir)
40 | tmp-dir
41 | (recur (inc num-attempts))))))))
42 |
43 | (def tempdir (mk-tmp-dir!))
44 |
45 | (defn- sh-exits-successfully
46 | [full-app-name & args]
47 | (let [sh-result (sh/with-sh-dir full-app-name (apply sh/sh args))]
48 | (println (:out sh-result))
49 | (is (zero? (:exit sh-result))
50 | (format "Expected `%s` to exit successfully" (string/join " " args)))))
51 |
52 | (deftest ^:not-travis generated-app-has-correct-files
53 | (let [app-name "test-app"
54 | full-app-name (.getPath (io/file tempdir app-name))]
55 | (println (sh/with-sh-dir project-dir (sh/sh lein "install")))
56 | (println (sh/with-sh-dir tempdir (sh/sh lein "new" "vase" app-name "--snapshot")))
57 | (println "Created app at" full-app-name)
58 | (is (.exists (io/file full-app-name "project.clj")))
59 | (is (.exists (io/file full-app-name "README.md")))
60 | (is (.exists (io/file full-app-name "src" "test_app" "service.clj")))
61 | (sh-exits-successfully full-app-name lein "test")
62 | (sh-exits-successfully full-app-name lein "with-profile" "production" "compile" ":all")
63 | (sh/sh "rm" "-rf" full-app-name)))
64 |
65 | (deftest ^:not-travis generated-app-with-namespace-has-correct-files
66 | (let [app-name "pedestal.test/test-ns-app"
67 | full-app-name (.getPath (io/file tempdir "test-ns-app"))]
68 | (println (sh/with-sh-dir project-dir (sh/sh lein "install")))
69 | (println (sh/with-sh-dir tempdir (sh/sh lein "new" "vase" app-name)))
70 | (println "Created app at" full-app-name)
71 | (is (.exists (io/file full-app-name "project.clj")))
72 | (is (.exists (io/file full-app-name "README.md")))
73 | (is (.exists (io/file full-app-name "src" "pedestal" "test" "test_ns_app" "service.clj")))
74 | (sh-exits-successfully full-app-name lein "test")
75 | (sh-exits-successfully full-app-name lein "with-profile" "production" "compile" ":all")
76 | (sh/sh "rm" "-rf" full-app-name)))
77 |
--------------------------------------------------------------------------------
/test/com/cognitect/vase/actions_test.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.actions-test
2 | (:require [clojure.test :refer :all]
3 | [com.cognitect.vase.test-helper :as helper]
4 | [io.pedestal.interceptor :as interceptor]
5 | [com.cognitect.vase.actions :as actions]))
6 |
7 | (defn expect-response
8 | [actual status body headers]
9 | (is (= status (:status actual)))
10 | (is (= body (:body actual)))
11 | (is (= headers (select-keys (:headers actual) (keys headers)))))
12 |
13 | (defn with-query-params
14 | ([p]
15 | (with-query-params {} p))
16 | ([ctx p]
17 | (-> ctx
18 | (update-in [:request :query-params] merge p)
19 | (update-in [:request :params] merge p))))
20 |
21 | (defn execute-and-expect
22 | ([action status body headers]
23 | (expect-response (:response (helper/run-interceptor action)) status body headers))
24 | ([ctx action status body headers]
25 | (expect-response (:response (helper/run-interceptor ctx action)) status body headers)))
26 |
27 | (deftest dynamic-interceptor-creation
28 | (testing "at least one function is required"
29 | (is (thrown? AssertionError (actions/dynamic-interceptor :without-functions [] {})))
30 |
31 | (are [x] (interceptor/interceptor? x)
32 | (actions/dynamic-interceptor :enter-only [] {:enter (fn [ctx] ctx)})
33 | (actions/dynamic-interceptor :enter-only [] {:enter identity})
34 | (actions/dynamic-interceptor :leave-only [] {:leave identity})
35 | (actions/dynamic-interceptor :error-only [] {:error identity}))))
36 |
37 | (deftest builtin-actions-are-interceptors
38 | (are [x] (interceptor/interceptor? (interceptor/-interceptor x))
39 | (actions/map->RespondAction {})
40 | (actions/map->RedirectAction {})
41 | (actions/map->ValidateAction {})
42 | (actions/map->QueryAction {})
43 | (actions/map->TransactAction {})))
44 |
--------------------------------------------------------------------------------
/test/com/cognitect/vase/conform_test.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.conform-test
2 | (:require [clojure.test :refer :all]
3 | [com.cognitect.vase.actions :as actions]
4 | [com.cognitect.vase.test-helper :as helper]
5 | [clojure.spec.alpha :as s]
6 | [io.pedestal.interceptor :as i]))
7 |
8 | (s/def ::a string?)
9 | (s/def ::b boolean?)
10 | (s/def ::request-body (s/keys :req-un #{::a} :opt-un #{::b}))
11 |
12 | (defn make-conformer
13 | [from spec to explain-to]
14 | (i/-interceptor
15 | (actions/->ConformAction :conformer from spec to explain-to "")))
16 |
17 | (deftest conform-action
18 | (testing "Happy path"
19 | (is (not= ::s/invalid
20 | (-> {:query-data {:a 1 :b true}}
21 | (helper/run-interceptor (make-conformer :query-data ::request-body :shaped nil))
22 | (get :shaped)))))
23 |
24 | (testing "Non-conforming inputs"
25 | (is (= ::s/invalid
26 | (-> {:query-data {:a 1 :b "string-not-allowed"}}
27 | (helper/run-interceptor (make-conformer :query-data ::request-body :shaped nil))
28 | (get :shaped)))))
29 |
30 | (testing "Explain goes to :com.cognitect.vase.actions/explain-data by default"
31 | (is (not
32 | (nil?
33 | (-> {:query-data {:a 1 :b "string-not-allowed"}}
34 | (helper/run-interceptor (make-conformer :query-data ::request-body :shaped nil))
35 | (get :com.cognitect.vase.actions/explain-data))))))
36 |
37 |
38 | (testing "The 'from' part can be a vector to get nested data out of the context"
39 | (is (= ::s/invalid
40 | (-> {:context {:request {:query-data {:a 1 :b "string-not-allowed"}}}}
41 | (helper/run-interceptor (make-conformer [:context :request :query-data] ::request-body :shaped nil))
42 | (get :shaped))))
43 |
44 | (is (= {:path [:b] :pred `boolean? :val "string-not-allowed" :via [::request-body ::b] :in [:b]}
45 | (-> {:context {:request {:query-data {:a 1 :b "string-not-allowed"}}}}
46 | (helper/run-interceptor (make-conformer [:context :request :query-data] ::request-body :shaped nil))
47 | :com.cognitect.vase.actions/explain-data
48 | :clojure.spec.alpha/problems
49 | first)))))
50 |
--------------------------------------------------------------------------------
/test/com/cognitect/vase/interceptor_test.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.interceptor-test
2 | (:require [clojure.test :refer :all]
3 | [com.cognitect.vase.test-helper :as helper :refer [run-interceptor new-ctx]]
4 | [com.cognitect.vase.interceptor :refer :all]))
5 |
6 | (deftest stock-interceptors
7 | (testing "attach-received-time"
8 | (is (some-> (run-interceptor attach-received-time) :request :received-time)))
9 |
10 | (testing "attach-request-id"
11 | (testing "default request id is supplied"
12 | (is (some-> (run-interceptor attach-request-id) :request :request-id)))
13 |
14 | (testing "an explicit request id is returned"
15 | (let [original-id "1234554321"
16 | ctx (new-ctx "vaserequest-id" "1234554321")
17 | resulting-id (some-> (run-interceptor ctx attach-request-id) :request :request-id)]
18 | (is (= original-id resulting-id)))))
19 |
20 | (testing "forward-headers"
21 | (let [fh (forward-headers ["vaserequest-id" "custom-header"])
22 | ctx (new-ctx "vaserequest-id" "1234554321" "custom-header" "Any string value")
23 | result (run-interceptor ctx fh)]
24 | (is (= "1234554321" (some-> result :response :headers (get "vaserequest-id"))))
25 | (is (= "Any string value" (some-> result :response :headers (get "custom-header")))))))
26 |
--------------------------------------------------------------------------------
/test/com/cognitect/vase/query_test.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.query-test
2 | (:require [clojure.test :refer :all]
3 | [io.pedestal.interceptor :as interceptor]
4 | [com.cognitect.vase.actions :as actions]
5 | [com.cognitect.vase.test-helper :as helper]
6 | [com.cognitect.vase.test-db-helper :as db-helper]
7 | [clojure.spec.alpha :as s]
8 | [datomic.api :as d]))
9 |
10 | (defn empty-db-entity-count
11 | []
12 | (let [conn (:connection (db-helper/new-database []))
13 | db (d/db conn)]
14 | (count
15 | (d/q '[:find ?e ?v :where [?e :db/ident ?v]] db))))
16 |
17 | (defn make-query
18 | [query variables coercions constants to]
19 | (interceptor/-interceptor
20 | (actions/->QueryAction :query variables query coercions constants {} to "")))
21 |
22 | (defn- context-with-db []
23 | (let [conn (db-helper/connection)]
24 | (-> (helper/new-ctx)
25 | (assoc-in [:request :db] (d/db conn))
26 | (assoc-in [:request :conn] conn))))
27 |
28 | (defn- with-path-params
29 | ([p]
30 | (with-path-params {} p))
31 | ([ctx p]
32 | (-> ctx
33 | (update-in [:request :path-params] merge p)
34 | (update-in [:request :params] merge p))))
35 |
36 | (deftest query-action
37 | (db-helper/with-database db-helper/query-test-txes
38 | (testing "Simple query, no parameters"
39 | (let [action (make-query '[:find ?e ?v :where [?e :db/ident ?v]] [] [] [] nil)
40 | response (:response (helper/run-interceptor (context-with-db) action))
41 | query-results (:body response)]
42 | (is (vector? query-results))
43 | (is (< (empty-db-entity-count) (count query-results)))
44 | (is (= '(2) (distinct (map count query-results))))))
45 |
46 | (testing "with one coerced query parameter"
47 | (let [action (make-query '[:find ?id ?email :in $ ?id :where [?e :user/userId ?id] [?e :user/userEmail ?email]] '[id] '[id] [] nil)
48 | response (:response (helper/run-interceptor
49 | (with-path-params
50 | (context-with-db)
51 | {:id "100"})
52 | action))
53 | query-results (:body response)]
54 | (is (vector? query-results))
55 | (is (< 0 (count query-results)))))
56 |
57 | (testing "with two coerced query parameters"
58 | (let [action (make-query '[:find ?id ?email :in $ ?id :where [?e :user/userId ?id] [?e :user/userEmail ?email]] '[id email] '[email id] [] nil)
59 | response (:response (helper/run-interceptor
60 | (with-path-params
61 | (context-with-db)
62 | {:id "100"
63 | :email "paul@cognitect.com"})
64 | action))
65 | query-results (:body response)]
66 | (is (vector? query-results))
67 | (is (< 0 (count query-results)))))
68 |
69 | (testing "with scalar result"
70 | (let [action (make-query '[:find ?e . :in $ ?id :where [?e :user/userId ?id]] '[id] '[id] [] nil)
71 | response (:response (helper/run-interceptor
72 | (with-path-params
73 | (context-with-db)
74 | {:id "100"})
75 | action))
76 | query-result (:body response)]
77 | (is (number? query-result))))
78 |
79 | (testing "with nil params"
80 | (let [action (make-query '[:find ?e :in $ ?id :where [?e :user/userId ?id]] '[id] '[id] [] nil)
81 | response (:response (helper/run-interceptor
82 | (with-path-params
83 | (context-with-db)
84 | {})
85 | action))
86 | query-results (:body response)]
87 | (is (string? query-results))
88 | (is (re-matches #"Missing required query parameters.*" query-results))))
89 |
90 | (testing "scalar query with no results"
91 | (let [action (make-query '[:find ?e . :in $ ?id :where [?e :user/userId ?id]] '[id] '[id] [] nil)
92 | response (:response (helper/run-interceptor
93 | (with-path-params
94 | (context-with-db)
95 | {:id 999})
96 | action))
97 | query-results (:body response)]
98 | (is (= nil (read-string {:eof nil} query-results)))))))
99 |
--------------------------------------------------------------------------------
/test/com/cognitect/vase/redirect_test.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.redirect-test
2 | (:require [clojure.test :refer :all]
3 | [io.pedestal.interceptor :as interceptor]
4 | [com.cognitect.vase.actions :as actions]
5 | [com.cognitect.vase.test-helper :as helper]
6 | [com.cognitect.vase.test-db-helper :as db-helper]
7 | [com.cognitect.vase.actions-test :refer [expect-response with-query-params execute-and-expect]]
8 | [clojure.spec.alpha :as s]
9 | [datomic.api :as d]))
10 |
11 | (defn make-redirect
12 | ([params status body headers url]
13 | (interceptor/-interceptor
14 | (actions/->RedirectAction :redirector params body status headers url))))
15 |
16 | (deftest redirect-action
17 | (testing "static redirect"
18 | (are [status body headers action] (execute-and-expect action status body headers)
19 | 302 "" {"Location" "http://www.example.com"} (make-redirect [] 302 "" {} "http://www.example.com"))
20 |
21 | (are [headers action ctx] (execute-and-expect ctx action 302 "" headers)
22 | {"Location" "https://donotreply.com"} (make-redirect '[p1] 302 "" {} 'p1) (with-query-params {:p1 "https://donotreply.com"}))))
23 |
--------------------------------------------------------------------------------
/test/com/cognitect/vase/respond_test.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.respond-test
2 | (:require [clojure.test :refer :all]
3 | [io.pedestal.interceptor :as interceptor]
4 | [com.cognitect.vase.actions :as actions]
5 | [com.cognitect.vase.test-helper :as helper]
6 | [com.cognitect.vase.actions-test :refer [expect-response with-query-params execute-and-expect]]))
7 |
8 | (defn make-respond
9 | ([params coercions exprs]
10 | (make-respond params coercions exprs 200))
11 | ([params coercions exprs status]
12 | (make-respond params coercions exprs status {}))
13 | ([params coercions exprs status headers]
14 | (interceptor/-interceptor
15 | (actions/->RespondAction :responder params coercions exprs status headers ""))))
16 |
17 | (deftest respond-action
18 | (testing "static response"
19 | (are [ status body headers action] (execute-and-expect action status body headers)
20 | 202 "respond-only" {} (make-respond [] [] "respond-only" 202)
21 | 201 "with-header" {"a" "b"} (make-respond [] [] "with-header" 201 {"a" "b"})
22 | 200 "two-headers" {"a" "b" "c" "d"} (make-respond [] [] "two-headers" 200 {"a" "b" "c" "d"})))
23 |
24 | (testing "with parameters"
25 | (are [expected-body action ctx] (execute-and-expect ctx action 200 expected-body {})
26 | "p1: foo" (make-respond '[p1] [] '(str "p1: " p1)) (with-query-params {:p1 "foo"})
27 | "p1: &" (make-respond '[p1] [] '(str "p1: " p1)) (with-query-params {:p1 "&"})
28 | "foo.bar." (make-respond '[p1 zort] [] '(format "%s.%s." p1 zort)) (with-query-params {:p1 "foo" :zort "bar"})))
29 |
30 | (testing "with parameters and defaults"
31 | (are [expected-body action ctx] (execute-and-expect ctx action 200 expected-body {})
32 | "p1: foobar" (make-respond '[[p1 "foobar"]] [] '(str "p1: " p1)) (with-query-params {})
33 | "p1: & p2: +" (make-respond '[p1 [p2 "+"]] [] '(str "p1: " p1 " p2: " p2)) (with-query-params {:p1 "&"})
34 | "foo.bar." (make-respond '[[p1 "baz"] [zort "bar"]] [] '(format "%s.%s." p1 zort)) (with-query-params {:p1 "foo"})))
35 |
36 | (testing "with parameters and coercions"
37 | (are [expected-body action ctx] (execute-and-expect ctx action 200 expected-body {})
38 | "p1's foo: 1" (make-respond '[p1] '[p1] '(str "p1's foo: " (:foo p1))) (with-query-params {:p1 "{:foo 1}"})
39 | "p1: A" (make-respond '[p1] '[p1] '(str "p1: " p1)) (with-query-params {:p1 "\"A\""})
40 | "Sum: 3" (make-respond '[p1 zort] '[p1 zort] '(str "Sum: " (+ p1 zort))) (with-query-params {:p1 "1" :zort "2"}))))
41 |
--------------------------------------------------------------------------------
/test/com/cognitect/vase/service_route_table.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.service-route-table
2 | (:require [io.pedestal.http :as http]
3 | [io.pedestal.http.route :as route]
4 | [io.pedestal.http.route.definition.table :as table]
5 | [com.cognitect.vase :as vase])
6 | (:import [java.util UUID]))
7 |
8 | (defn make-master-routes
9 | [spec]
10 | (table/table-routes
11 | {}
12 | (vase/routes "/api" spec)))
13 |
14 | (defn test-spec
15 | []
16 | {:activated-apis [:example/v1 :example/v2]
17 | :descriptor (vase/load-edn-resource "test_descriptor.edn")
18 | :datomic-uri (str "datomic:mem://" (UUID/randomUUID))})
19 |
20 | (defn service-map
21 | "Return a new, fully initialized service map"
22 | []
23 | (let [{:keys [activated-apis
24 | descriptor
25 | datomic-uri] :as app-spec} (test-spec)
26 | conns (vase/ensure-schema app-spec)]
27 | (vase/specs app-spec)
28 | {:env :prod
29 | ::http/routes (make-master-routes app-spec)
30 | ::http/resource-path "/public"
31 | ::http/type :jetty
32 | ::http/port 8080}))
33 |
34 | (comment
35 |
36 | (service-map)
37 | (let [s (test-spec)]
38 | (vase.datomic/normalize-norm-keys (get-in s [:descriptor :vase/norms])))
39 |
40 | (vase/routes "/api" (test-spec)))
41 |
--------------------------------------------------------------------------------
/test/com/cognitect/vase/service_test.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.service-test
2 | (:require [clojure.test :refer :all]
3 | [io.pedestal.test :refer :all]
4 | [com.cognitect.vase.test-helper :as helper]
5 | [com.cognitect.vase :as vase]
6 | [com.cognitect.vase.service-route-table :as srt]))
7 |
8 | (defn selected-headers
9 | "Return a map with selected-keys out of the headers of a request to url"
10 | [verb url selected-keys]
11 | (-> url
12 | verb
13 | :headers
14 | (select-keys selected-keys)))
15 |
16 | (deftest request-tracing-test
17 | (is (=
18 | (selected-headers #(helper/GET % :headers {"vaserequest-id" "yes"})
19 | "/api/example/v1/hello"
20 | ["Content-Type" "vaserequest-id"])
21 | {"Content-Type" "text/plain"
22 | "vaserequest-id" "yes"}))
23 | (is (string? (get-in (helper/GET "/api/example/v1/hello") [:headers "vaserequest-id"]))))
24 |
25 | (deftest self-describing-test
26 | (helper/with-service (srt/service-map)
27 | (is (= 200 (:status (helper/GET "/api?edn=true"))))
28 | (is (= 200 (:status (helper/GET "/api"))))
29 | (is (= 200 (:status (helper/GET "/api/example/v1?edn=true"))))
30 | (is (= 200 (:status (helper/GET "/api/example/v1"))))
31 | (is (= 200 (:status (helper/GET "/api/example/v2?edn=true"))))
32 | (is (= 200 (:status (helper/GET "/api/example/v2"))))))
33 |
34 | (def known-route-names
35 | #{:describe-apis
36 | :example.v1/describe
37 | :example.v1/simple-response
38 | :example.v1/r-page
39 | :example.v1/ar-page
40 | :example.v1/url-param-example
41 | :example.v1/validate-page
42 | :example.v1/db-page
43 | :example.v1/users-page
44 | :example.v1/user-id-page
45 | :example.v1/user-create
46 | :example.v1/user-delete
47 | :example.v1/user-page
48 | :example.v1/fogus-page
49 | :example.v1/foguspaul-page
50 | :example.v1/fogussomeone-page
51 | :example.v2/describe
52 | :example.v2/hello
53 | :example.v2/intercept})
54 |
55 | (deftest all-route-names-present
56 | (let [service (srt/service-map)
57 | routes (:io.pedestal.http/routes service)
58 | route-names (set (map :route-name routes))]
59 | (is (= known-route-names route-names))))
60 |
--------------------------------------------------------------------------------
/test/com/cognitect/vase/spec_test.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.spec-test
2 | (:require [com.cognitect.vase.spec :as vase.spec]
3 | [com.cognitect.vase :as vase]
4 | [com.cognitect.vase.service-route-table :as srt]
5 | [clojure.spec.alpha :as s]
6 | [clojure.spec.test.alpha :as stest]
7 | [clojure.test :refer :all]
8 | [io.pedestal.interceptor :as interceptor]))
9 |
10 | (deftest route-table-spec-tests
11 | (let [handler-fn (fn [req] {:status 200 :body "foo"})]
12 | (is (s/valid? ::vase.spec/route-table
13 | [["/a" :get handler-fn]
14 | ["/b" :get [(interceptor/interceptor {:enter handler-fn})]]
15 | ["/c" :get handler-fn :route-name :c]
16 | ["/d/:id" :get handler-fn :route-name :d :constraints {:id #"[0-9]+"}]]))
17 | (testing "invalid routes"
18 | (are [v] (not (s/valid? ::vase.spec/route-table-route v))
19 | []
20 | ["/a"]
21 | ["/a" :get]
22 | ["/a" :get handler-fn :route-name]
23 | ["/a" :get handler-fn :not-route-name-k :bar]
24 | ["/a" :get handler-fn :route-name :bar :constraints]
25 | ["/a" :get handler-fn :route-name :bar :constraints 1]))))
26 |
27 | (def test-spec (srt/test-spec))
28 | (def sample-spec (vase/load-edn-resource "sample_payload.edn"))
29 |
30 | (deftest vase-spec-tests
31 | (testing "full vase spec"
32 | (doseq [vspec [test-spec sample-spec]]
33 | (is (s/valid? ::vase.spec/spec vspec))))
34 |
35 | (testing "descriptors"
36 | (doseq [d ["sample_descriptor.edn"
37 | "small_descriptor.edn"]]
38 | (is (s/valid? ::vase.spec/descriptor (vase/load-edn-resource d))
39 | (format "%s is not valid!" d)))))
40 |
41 | (use-fixtures :once (fn [f]
42 | (stest/instrument `vase/routes)
43 | (f)
44 | (stest/unstrument `vase/routes)))
45 |
46 | (deftest vase-routes-fn-tests
47 | (is (vase/routes "/api" test-spec))
48 | (is (vase/routes "/api" []))
49 | (is (vase/routes "/api" [test-spec]))
50 | (is (vase/routes "/api" [test-spec sample-spec]))
51 | (is (thrown-with-msg? clojure.lang.ExceptionInfo #"did not conform" (vase/routes "" test-spec)))
52 | (is (thrown-with-msg? clojure.lang.ExceptionInfo #"did not conform" (vase/routes :not-a-path test-spec)))
53 | (is (thrown-with-msg? clojure.lang.ExceptionInfo #"did not conform" (vase/routes "/api" (:descriptor test-spec)))))
54 |
--------------------------------------------------------------------------------
/test/com/cognitect/vase/test_db_helper.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.test-db-helper
2 | (:require [clojure.test :as t]
3 | [datomic.api :as d])
4 | (:import [java.util UUID]))
5 |
6 | (defn new-database
7 | "This generates a new, empty Datomic database for use within unit tests."
8 | [txes]
9 | (let [uri (str "datomic:mem://test" (UUID/randomUUID))
10 | _ (d/create-database uri)
11 | conn (d/connect uri)]
12 | (doseq [t txes]
13 | @(d/transact conn t))
14 | {:uri uri
15 | :connection conn}))
16 |
17 | (def ^:dynamic *current-db-connection* nil)
18 | (def ^:dynamic *current-db-uri* nil)
19 |
20 | (defmacro with-database
21 | "Executes all requests in the body with the same database."
22 | [txes & body]
23 | `(let [dbm# (new-database ~txes)]
24 | (binding [*current-db-uri* (:uri dbm#)
25 | *current-db-connection* (:connection dbm#)]
26 | ~@body)))
27 |
28 | (defn connection
29 | ([]
30 | (or *current-db-connection* (:connection (new-database []))))
31 | ([txes]
32 | (or *current-db-connection* (:connection (new-database txes)))))
33 |
34 |
35 | (def query-test-txes
36 | [[{:db/id #db/id[:db.part/db]
37 | :db/ident :company/name
38 | :db/unique :db.unique/value
39 | :db/valueType :db.type/string
40 | :db/cardinality :db.cardinality/one
41 | :db.install/_attribute :db.part/db}
42 | {:db/id #db/id[:db.part/db]
43 | :db/ident :user/userId
44 | :db/valueType :db.type/long
45 | :db/cardinality :db.cardinality/one
46 | :db.install/_attribute :db.part/db
47 | :db/doc "A Users unique identifier"
48 | :db/unique :db.unique/identity}
49 | {:db/id #db/id[:db.part/db]
50 | :db/ident :user/userEmail
51 | :db/valueType :db.type/string
52 | :db/cardinality :db.cardinality/one
53 | :db.install/_attribute :db.part/db
54 | :db/doc "The users email"
55 | :db/unique :db.unique/value}
56 | {:db/id #db/id[:db.part/db]
57 | :db/ident :user/userBio
58 | :db/valueType :db.type/string
59 | :db/cardinality :db.cardinality/one
60 | :db.install/_attribute :db.part/db
61 | :db/doc "A short blurb about the user"
62 | :db/fulltext true :db/index true}
63 | {:db/id #db/id[:db.part/db]
64 | :db/ident :user/userActive?
65 | :db/valueType :db.type/boolean
66 | :db/cardinality :db.cardinality/one
67 | :db.install/_attribute :db.part/db
68 | :db/doc "User active flag."
69 | :db/index true}
70 | {:db/id #db/id[:db.part/db]
71 | :db/ident :loanOffer/loanId
72 | :db/valueType :db.type/long
73 | :db/cardinality :db.cardinality/one
74 | :db.install/_attribute :db.part/db
75 | :db/doc "The unique offer ID"
76 | :db/unique :db.unique/value}
77 | {:db/id #db/id[:db.part/db]
78 | :db/ident :loanOffer/fees
79 | :db/valueType :db.type/long
80 | :db/cardinality :db.cardinality/one
81 | :db.install/_attribute :db.part/db
82 | :db/doc "All of the loan fees"
83 | :db/index true}
84 | {:db/id #db/id[:db.part/db]
85 | :db/ident :loanOffer/notes
86 | :db/valueType :db.type/string
87 | :db/cardinality :db.cardinality/many
88 | :db.install/_attribute :db.part/db
89 | :db/doc "Notes about the loan"}
90 | {:db/id #db/id[:db.part/db]
91 | :db/ident :user/loanOffers
92 | :db/valueType :db.type/ref
93 | :db/cardinality :db.cardinality/many
94 | :db.install/_attribute :db.part/db
95 | :db/doc "The collection of loan offers"}]
96 | [{:db/id #db/id[:db.part/user]
97 | :user/userId 100
98 | :user/userEmail "paul@cognitect.com"}
99 | {:db/id #db/id[:db.part/user]
100 | :user/userId 101
101 | :user/userEmail "fogus@cognitect.com"}
102 | {:db/id #db/id[:db.part/user]
103 | :user/userId 102
104 | :user/userEmail "mtnygard@cognitect.com"}]])
105 |
--------------------------------------------------------------------------------
/test/com/cognitect/vase/test_helper.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.test-helper
2 | (:require [io.pedestal.test :refer [response-for]]
3 | [io.pedestal.http :as http]
4 | [io.pedestal.log :as log]
5 | [com.cognitect.vase.interceptor :as interceptor]
6 | [com.cognitect.vase.util :as util]
7 | [com.cognitect.vase.service-route-table :as srt]
8 | [io.pedestal.interceptor.chain :as chain]))
9 |
10 | (def write-edn pr-str)
11 |
12 | (defn new-service
13 | "This generates a new testable service for use with io.pedestal.test/response-for."
14 | ([] (new-service (srt/service-map)))
15 | ([service-map] (::http/service-fn (http/create-servlet service-map))))
16 |
17 | (def ^:dynamic *current-service* nil)
18 |
19 | (defmacro with-service
20 | "Executes all requests in the body with the same service (using a thread-local binding)"
21 | [srv-map & body]
22 | `(binding [*current-service* (new-service ~srv-map)]
23 | ~@body))
24 |
25 | (defn service
26 | [& args]
27 | (or *current-service* (apply new-service args)))
28 |
29 | (defn GET
30 | "Make a GET request on our service using response-for."
31 | [& args]
32 | (apply response-for (service) :get args))
33 |
34 | (defn POST
35 | "Make a POST request on our service using response-for."
36 | [& args]
37 | (apply response-for (service) :post args))
38 |
39 | (defn DELETE
40 | "Make a DELETE request on our service using response-for."
41 | [& args]
42 | (apply response-for (service) :delete args))
43 |
44 | (defn json-request
45 | ([verb url payload]
46 | (json-request verb url payload {}))
47 | ([verb url payload opts]
48 | (response-for (service)
49 | verb url
50 | :headers (merge {"Content-Type" "application/json"}
51 | (:headers opts))
52 | :body (util/write-json payload))))
53 |
54 | (defn post-json
55 | "Makes a POST request to URL-path expecting a payload to submit as JSON.
56 |
57 | Options:
58 | * :headers: Additional headers to send with the request."
59 | ([URL-path payload]
60 | (post-json URL-path payload {}))
61 | ([URL-path payload opts]
62 | (json-request :post URL-path payload opts)))
63 |
64 | (defn post-edn
65 | "Makes a POST request to URL-path expecting a payload to submit as edn.
66 |
67 | Options:
68 | * :headers: Additional headers to send with the request."
69 | ([URL-path payload]
70 | (post-edn URL-path payload {}))
71 | ([URL-path payload opts]
72 | (response-for (service)
73 | :post URL-path
74 | :headers (merge {"Content-Type" "application/edn"}
75 | (:headers opts))
76 | :body (write-edn payload))))
77 |
78 | (defn response-data
79 | "Return the parsed payload data from a vase api http response."
80 | ([response] (response-data response util/read-json))
81 | ([response reader]
82 | (-> response
83 | :body
84 | reader)))
85 |
86 | (defn run-interceptor
87 | ([i] (run-interceptor {} i))
88 | ([ctx i] (chain/execute (chain/enqueue* ctx i))))
89 |
90 | (defn new-ctx
91 | [& {:as headers}]
92 | {:request {:headers headers}})
93 |
--------------------------------------------------------------------------------
/test/com/cognitect/vase/validate_test.clj:
--------------------------------------------------------------------------------
1 | (ns com.cognitect.vase.validate-test
2 | (:require [clojure.test :refer :all]
3 | [io.pedestal.interceptor :as interceptor]
4 | [com.cognitect.vase.actions :as actions]
5 | [com.cognitect.vase.test-helper :as helper]
6 | [com.cognitect.vase.test-db-helper :as db-helper]
7 | [com.cognitect.vase.actions-test :refer [expect-response with-query-params execute-and-expect]]
8 | [clojure.spec.alpha :as s]))
9 |
10 | (defn make-validate
11 | [spec]
12 | (interceptor/-interceptor
13 | (actions/->ValidateAction :validator [] {} spec nil "")))
14 |
15 | (defn- with-body [body]
16 | (assoc-in (helper/new-ctx) [:request :edn-params] body))
17 |
18 | (s/def ::a string?)
19 | (s/def ::b boolean?)
20 | (s/def ::request-body (s/keys :req-un #{::a} :opt-un #{::b}))
21 |
22 | (deftest validate-action
23 | (testing "Passing validation"
24 | (are [body-out body-in action] (execute-and-expect (with-body body-in) action 200 body-out {})
25 | '() {:a "one"} (make-validate `(s/keys :req-un #{::a} :opt-un #{::b}))
26 | '() {:a "one" :b false} (make-validate `(s/keys :req-un #{::a} :opt-un #{::b}))
27 | '() {:a "one" :b false} (make-validate `::request-body)))
28 |
29 | (testing "Failing validation"
30 | (are [body-out body-in action] (execute-and-expect (with-body body-in) action 200 body-out {})
31 | '({:path [] :val {} :via [] :in []}) {} (make-validate `(s/keys :req-un #{::a} :opt-un #{::b}))
32 | '({:path [] :val {:b 12345} :via [] :in []}
33 | {:path [:b] :val 12345 :via [::b] :in [:b]}) {:b 12345} (make-validate `(s/keys :req-un #{::a} :opt-un #{::b}))
34 | '({:path [] :val {:b "false"} :via [::request-body] :in []}
35 | {:path [:b] :val "false" :via [::request-body ::b] :in [:b]}) {:b "false"} (make-validate `::request-body))))
36 |
--------------------------------------------------------------------------------
/test/resources/small_descriptor.edn:
--------------------------------------------------------------------------------
1 |
2 | ;; Idempotent Schema Datoms (norms)
3 | ;; --------------------------------
4 | {:vase/norms
5 | {:example/base-schema
6 | ;; Supports full/long Datomic schemas
7 | {:vase.norm/txes [[{:db/id #db/id[:db.part/db]
8 | :db/ident :company/name
9 | :db/unique :db.unique/value
10 | :db/valueType :db.type/string
11 | :db/cardinality :db.cardinality/one
12 | :db.install/_attribute :db.part/db}]]}
13 | ;; End :example/base-schema
14 |
15 | :example/user-schema
16 | ;; Also supports schema dependencies
17 | {:vase.norm/requires [:example/base-schema]
18 | ;; and supports short/basic schema definitions
19 | :vase.norm/txes [#vase/schema-tx [[:user/userId :one :long :unique "A Users unique identifier"]
20 | [:user/userEmail :one :string :unique "The users email"]
21 | ;; :fulltext also implies :index
22 | [:user/userBio :one :string :fulltext "A short blurb about the user"]]]}}
23 |
24 | ;; API Tags/Versions
25 | ;; ------------------
26 | :vase/apis
27 | {:example/v1
28 | {:vase.api/routes
29 | {"/hello" {:get #vase/respond {:name :example.v1/simple-response
30 | :body "Hello World"}}
31 | "/redirect-to-google" {:get #vase/redirect {:name :example-v1/r-page
32 | :url "http://www.google.com"}}
33 | "/capture-s/:url-thing" {:get #vase/respond {:name :example-v1/url-param-example
34 | ;; URL parameters are also bound in :params
35 | :params [url-thing]
36 | :edn-coerce [url-thing] ;; parse a param as an edn string
37 | :body (str "You said: " url-thing " which is a " (type url-thing))}}
38 | "/users" {:get #vase/query {:name :example.v1/user-page
39 | :params [email]
40 | :query [:find ?e
41 | :in $ ?email
42 | :where
43 | [?e :user/userEmail ?email]]}
44 | :post #vase/transact {:name :example.v1/user-create
45 | :properties [:db/id
46 | :user/userId
47 | :user/userEmail
48 | :user/userBio]}}
49 | "/users/:id" {:get #vase/query {:name :example.v1/user-id-page
50 | :params [id]
51 | :edn-coerce [id]
52 | :query [:find ?e
53 | :in $ ?id
54 | :where
55 | [?e :user/userId ?id]]}}}
56 | :vase.api/schemas [:example/user-schema]
57 | :vase.api/forward-headers ["vaserequest-id"]}
58 | :example/v2
59 | {:vase.api/routes
60 | {"/hello" {:get #vase/respond {:name :example.v2/hello
61 | :enforce-format true
62 | :body "Another Hello World Route"}}}}}}
63 |
64 |
--------------------------------------------------------------------------------