Commit ce78cee3 authored by Ivar Refsdal's avatar Ivar Refsdal
Browse files

Default vase skeleton

# Project related
# Java related
# Leiningen
# Temp Files
# OS X
# Logging
# Builds
# Name of the base image. Capstan will download this automatically from
# Cloudius S3 repository.
#base: cloudius/osv
base: cloudius/osv-openjdk8
# The command line passed to OSv to start up the application.
#cmdline: / -cp /vase-transact-fn/app.jar clojure.main -m vase-transact-fn
cmdline: / -jar /vase-transact-fn/app.jar
# The command to use to build the application.
# You can use any build tool/command (make/rake/lein/boot) - this runs locally on your machine
# For Leiningen, you can use:
#build: lein uberjar
# For Boot, you can use:
#build: boot build
# List of files that are included in the generated image.
/vase-transact-fn/app.jar: ./target/vase-transact-fn-0.0.1-SNAPSHOT-standalone.jar
FROM java:8-alpine
ADD target/vase-transact-fn-0.0.1-SNAPSHOT-standalone.jar /vase-transact-fn/app.jar
CMD ["java", "-jar", "/vase-transact-fn/app.jar"]
# vase-transact-fn
## Getting Started
1. Start the application: `lein run`
2. Go to [localhost:8080](http://localhost:8080/) to see: `Hello World!`
3. Read your app's source code at src/vase_transact_fn/service.clj. Explore the docs of functions
that define routes and responses.
4. See your Vase API Specification at `resources/vase-transact-fn_service.edn`.
5. Run your app's tests with `lein test`. Read the tests at test/vase_transact_fn/service_test.clj.
6. Learn more! See the [Links section below](#links).
## Configuration
To configure logging see config/logback.xml. By default, the app logs to stdout and logs/.
To learn more about configuring Logback, read its [documentation](
## Developing your service
1. Start a new REPL: `lein repl`
2. Start your service in dev-mode: `(def dev-serv (run-dev))`
3. Connect your editor to the running REPL session.
Re-evaluated code will be seen immediately in the service.
4. All changes to your Vase Service Descriptor will be loaded - no re-evaluation
### [Docker]( container support
1. Build an uberjar of your service: `lein uberjar`
2. Build a Docker image: `sudo docker build -t vase-transact-fn .`
3. Run your Docker image: `docker run -p 8080:8080 vase-transact-fn`
### [OSv]( unikernel support with [Capstan](
1. Build and run your image: `capstan run -f "8080:8080"`
Once the image it built, it's cached. To delete the image and build a new one:
1. `capstan rmi vase-transact-fn; capstan build`
## Links
* [Pedestal examples](
* [Vase examples](
(def proj '{:app {:project vase-transact-fn
:version "0.0.1-SNAPSHOT"
:description "FIXME: write description"
:license {:name "Eclipse Public License"
:url ""}}
:source-paths #{"src"}
:test-paths #{"test"}
:resource-paths #{"resources" "config"}
:dependencies [[org.clojure/clojure "1.9.0-alpha14"]
[io.pedestal/pedestal.service "0.5.2"]
[com.cognitect/pedestal.vase "0.9.1"]
;; Remove this line and uncomment one of the next lines to
;; use Immutant or Tomcat instead of Jetty:
[io.pedestal/pedestal.jetty "0.5.2"]
;; [io.pedestal/pedestal.immutant "0.5.2-SNAPSHOT"]
;; [io.pedestal/pedestal.tomcat "0.5.2-SNAPSHOT"]
[ch.qos.logback/logback-classic "1.1.8" :exclusions [org.slf4j/slf4j-api]]
[org.slf4j/jul-to-slf4j "1.7.22"]
[org.slf4j/jcl-over-slf4j "1.7.22"]
[org.slf4j/log4j-over-slf4j "1.7.22"]]
:dev-dependencies [[io.pedestal/pedestal.service-tools "0.5.2"]
[org.clojure/tools.namespace "0.3.0-alpha3"]]
:external {:nrepl-options {:bind ""
:reply true}
:main vase-transact-fn.server}})
(set-env! :source-paths (set (:source-paths proj))
:test-paths (set (:test-paths proj))
:resource-paths (set (:resource-paths proj))
;:repositories (:repositories proj)
:dependencies (:dependencies proj))
(task-options! pom {:project (get-in proj [:app :project])
:version (str (get-in proj [:app :version]) "-standalone")
:description (get-in proj [:app :description])
:license (get-in proj [:app :license])})
;; == Testing tasks ========================================
(deftask with-test
"Add test to source paths"
(set-env! :source-paths #(clojure.set/union % (get-env :test-paths)))
(set-env! :dependencies #(into % (:dev-dependencies proj)))
;(set-env! :dependencies #(conj % '[it.frbracch/boot-marginalia "0.1.3-1" :scope "test"]))
;(set-env! :dependencies #(conj % '[boot-codox "0.9.6" :scope "test"]))
;; Include test/ in REPL sources
[r repl] (fn [& xs] (with-test) (apply r xs)))
(require '[clojure.test :refer [run-tests]])
(deftask test
"Run project tests"
(with-test) ;; test source paths and test/dev deps added
(require '[ :refer [find-namespaces-in-dir]])
(let [find-namespaces-in-dir (resolve '
test-nses (->> (get-env :test-paths)
(mapcat #(find-namespaces-in-dir ( %)))
(doseq [tns test-nses] (require tns))
(apply clojure.test/run-tests test-nses)))
;; == Dev tasks ============================================
(deftask dumbrepl
"Launch a standard Clojure REPL"
;; Include test/ in REPL sources
(clojure.main/repl :init (fn []
(use 'clojure.core)
(use 'clojure.repl))))
;(deftask docs
; "Generate API (Codox) and Literate (Marginalia) docs"
; []
; (with-test)
; (require '[codox.boot :refer [codox]])
; (require '[it.frbracch.boot-marginalia :refer [marginalia]])
; (let [codox (resolve 'codox.boot/codox)
; marginalia (resolve 'it.frbracch.boot-marginalia/marginalia)]
; (comp (codox :name (name (get-in proj [:app :project]))
; :description (str (get-in proj [:app :description])
; "\n -- Also: [Literate docs](./uberdoc.html)"))
; (marginalia :dir "doc"
; :desc (str (get-in proj [:app :description])
; "\n -- Also: <a href=\"./index.html\">API docs</a>"))
; (target))))
;; == Server Tasks =========================================
(deftask build
"Build my project."
(comp (aot :namespace #{(get-in proj [:external :main])})
(jar :main (get-in proj [:external :main]))))
<!-- Logback configuration. See -->
<!-- Scanning is currently turned on; This will impact performance! -->
<configuration scan="true" scanPeriod="10 seconds">
<!-- Silence Logback's own status messages about config parsing
<statusListener class="ch.qos.logback.core.status.NopStatusListener" /> -->
<!-- Simple file output -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- encoder defaults to ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- rollover daily -->
<!-- or whenever the file size reaches 64 MB -->
<maxFileSize>64 MB</maxFileSize>
<!-- Safely log to the same file from multiple JVMs. Degrades performance! -->
<!-- Console output -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoder defaults to ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<pattern>%-5level %logger{36} - %msg%n</pattern>
<!-- Only log level INFO and above -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- Enable FILE and STDOUT appenders for all log messages.
By default, only log at level INFO and above. -->
<root level="INFO">
<appender-ref ref="FILE" />
<appender-ref ref="STDOUT" />
<!-- For loggers in the these namespaces, log at all levels. -->
<logger name="user" level="ALL" />
<!-- To log pedestal internals, enable this and change ThresholdFilter to DEBUG
<logger name="io.pedestal" level="ALL" />
(defproject vase-transact-fn "0.0.1-SNAPSHOT"
:description "FIXME: write description"
:url ""
:license {:name "Eclipse Public License"
:url ""}
:dependencies [[org.clojure/clojure "1.9.0-alpha14"]
[io.pedestal/pedestal.service "0.5.2"]
[com.cognitect/pedestal.vase "0.9.1"]
;; Remove this line and uncomment one of the next lines to
;; use Immutant or Tomcat instead of Jetty:
[io.pedestal/pedestal.jetty "0.5.2"]
;; [io.pedestal/pedestal.immutant "0.5.2-SNAPSHOT"]
;; [io.pedestal/pedestal.tomcat "0.5.2-SNAPSHOT"]
[ch.qos.logback/logback-classic "1.1.8" :exclusions [org.slf4j/slf4j-api]]
[org.slf4j/jul-to-slf4j "1.7.22"]
[org.slf4j/jcl-over-slf4j "1.7.22"]
[org.slf4j/log4j-over-slf4j "1.7.22"]]
:min-lein-version "2.0.0"
:resource-paths ["config", "resources"]
;; If you use HTTP/2 or ALPN, use the java-agent to pull in the correct alpn-boot dependency
;:java-agents [[org.mortbay.jetty.alpn/jetty-alpn-agent "2.0.3"]]
:profiles {:dev {:aliases {"run-dev" ["trampoline" "run" "-m" "vase-transact-fn.server/run-dev"]}
:dependencies [[io.pedestal/pedestal.service-tools "0.5.2"]]}
:uberjar {:aot [vase-transact-fn.server]}}
:main ^{:skip-aot true} vase-transact-fn.server)
{:activated-apis [:vase-transact-fn/v1]
:datomic-uri "datomic:mem://example"
;; Datomic Schema Norms
;; --------------------
;; Supports full/long Datomic schemas
{:vase.norm/txes [[{:db/id #db/id[:db.part/db]
:db/ident :company/name
:db/unique :db.unique/value
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db.install/_attribute :db.part/db}]]}
{:vase.norm/requires [:vase-transact-fn/base-schema] ;; Also supports schema dependencies
;; and supports short/basic schema definitions
:vase.norm/txes [#vase/schema-tx [[:user/userId :one :long :identity "A User's unique identifier"]
[:user/userEmail :one :string :unique "The user's email"]
;; :fulltext also implies :index
[:user/userBio :one :string :fulltext "A short blurb about the user"]
[:user/company :one :ref "The user's employer"]]]}}
;; Global Specs for the API
;; ------------------------
{:vase-transact-fn.test/age (fn [age] (> age 21))
:vase-transact-fn.test/name (clojure.spec/and string? not-empty)
:vase-transact-fn.test/person (clojure.spec/keys :req-un [:vase-transact-fn.test/name
;; API Tagged Chunks/Versions
;; --------------------------
{"/hello" {:get #vase/respond {:name :vase-transact-fn.v1/simple-response
:body "Hello World"}}
"/hello2" {:get #vase/respond {:name :vase-transact-fn.v1/param-response
;; POST bodies and query string args are bound in :params
:params [user]
;; `edn-coerce` will edn/read-string on params, with all active data readers
:edn-coerce [user]
:body (if user
(str "Hello " user ". You are a: " (type user))
"Hello World!")}}
"/redirect-to-google" {:get #vase/redirect {:name :vase-transact-fn.v1/r-page
:url ""}}
"/redirect-to-param" {:get #vase/redirect {:name :vase-transact-fn.v1/ar-page
:params [someurl]
:url someurl}}
;; Validate (with clojure.spec) happens on the entire `param` map
"/validate" {:post #vase/validate {:name :vase-transact-fn.v1/validate-page
:spec :vase-transact-fn.test/person}}
;; Just use datomic queries
"/db" {:get #vase/query {:name :vase-transact-fn.v1/db-page
:params []
:query [:find ?e ?v
:where [?e :db/ident ?v]]}}
"/users" {:get #vase/query {:name :vase-transact-fn.v1/users-page
:params []
:query [:find ?id ?email
[?e :user/userId ?id]
[?e :user/userEmail ?email]]}}
"/users/:id" {:get #vase/query {:name :vase-transact-fn.v1/user-id-page
:params [id]
:edn-coerce [id]
:query [:find ?e
:in $ ?id
[?e :user/userId ?id]]}}
"/user" {:get #vase/query {:name :vase-transact-fn.v1/user-page
;; All params are required to perform the query
:params [email]
:query [:find ?e
:in $ ?email
[?e :user/userEmail ?email]]}
:post #vase/transact {:name :vase-transact-fn.v1/user-create
;; `:properties` are pulled from the parameters
:properties [:db/id
:delete #vase/transact {:name :vase-transact-fn.v1/user-delete
:db-op :vase/retract-entity
;; :vase/retract-entity requires :db/id to be supplied
:properties [:db/id]}}
"/jane-and-someone" {:get #vase/query {:name :vase-transact-fn.v1/fogussomeone-page
;; Params can have default values, using the "default pair" notation
:params [[someone ""]]
:constants [""]
:query [:find ?e
:in $ ?someone ?jane
[(list ?someone ?jane) [?emails ...]]
[?e :user/userEmail ?emails]]}}}
;:vase.api/interceptors [] ;; Any extra interceptors to apply to this API chunk/version
:vase.api/schemas [:vase-transact-fn/user-schema]
:vase.api/forward-headers ["vaserequest-id"]}}}}
(ns vase-transact-fn.server
(:gen-class) ; for -main method in uberjar
(:require [io.pedestal.http :as server]
[io.pedestal.http.route :as route]
[com.cognitect.vase :as vase]
[vase-transact-fn.service :as service]))
(defn activate-vase
([base-routes api-root spec-paths]
(activate-vase base-routes api-root spec-paths vase/load-edn-resource))
([base-routes api-root spec-paths vase-load-fn]
(let [vase-specs (mapv vase-load-fn spec-paths)]
(when (seq vase-specs)
(vase/ensure-schema vase-specs)
(vase/specs vase-specs))
{::routes (if (empty? vase-specs)
(into base-routes (vase/routes api-root vase-specs)))
::specs vase-specs})))
(defn vase-service
"Optionally given a default service map and any number of string paths
to Vase API Specifications,
Return a Pedestal Service Map with all Vase APIs parsed, ensured, and activated."
(vase-service service/service))
(vase-service service-map vase/load-edn-resource))
([service-map vase-load-fn]
(merge {:env :prod
::server/routes (::routes (activate-vase
(::service/route-set service-map)
(::vase/api-root service-map)
(::vase/spec-resources service-map)
;; This is an adapted service map, that can be started and stopped
;; From the REPL you can call server/start and server/stop on this service
(defonce runnable-service (server/create-server (vase-service)))
(defn run-dev
"The entry-point for 'lein run-dev'"
[& args]
(println "\nCreating your [DEV] server...")
(-> service/service ;; start with production configuration
(merge {:env :dev
;; do not block thread that starts web server
::server/join? false
;; Routes can be a function that resolve routes,
;; we can use this to set the routes to be reloadable
::server/routes #(route/expand-routes
(::routes (activate-vase (deref #'service/routes)
(::vase/api-root service/service)
(mapv (fn [res-str]
(str "resources/" res-str))
(::vase/spec-resources service/service))
;; all origins are allowed in dev mode
::server/allowed-origins {:creds true :allowed-origins (constantly true)}})
;; Wire up interceptor chains
(defn -main
"The entry-point for 'lein run'"
[& args]
(println "\nCreating your server...")
(server/start runnable-service))
;; If you package the service up as a WAR,
;; some form of the following function sections is required (for io.pedestal.servlet.ClojureVarServlet).
;;(defonce servlet (atom nil))
;;(defn servlet-init
;; [_ config]
;; ;; Initialize your app here.
;; (reset! servlet (server/servlet-init service/service nil)))
;;(defn servlet-service
;; [_ request response]
;; (server/servlet-service @servlet request response))
;;(defn servlet-destroy
;; [_]
;; (server/servlet-destroy @servlet)
;; (reset! servlet nil))
(ns vase-transact-fn.service
(:require [io.pedestal.http :as http]
[io.pedestal.http.route :as route]
[io.pedestal.http.body-params :as body-params]
[ring.util.response :as ring-resp]
[com.cognitect.vase :as vase]))
(defn about-page
(ring-resp/response (format "Clojure %s - served from %s"
(route/url-for ::about-page))))
(defn home-page
(ring-resp/response "Hello World!"))
;; Defines "/" and "/about" routes with their associated :get handlers.
;; The interceptors defined after the verb map (e.g., {:get home-page}
;; apply to / and its children (/about).
(def common-interceptors [(body-params/body-params) http/html-body])
;; Tabular routes
(def routes #{["/" :get (conj common-interceptors `home-page)]
["/about" :get (conj common-interceptors `about-page)]})
(def service
{:env :prod
;; You can bring your own non-default interceptors. Make
;; sure you include routing and set it up right for
;; dev-mode. If you do, many other keys for configuring
;; default interceptors will be ignored.
;; ::http/interceptors []
;; Uncomment next line to enable CORS support, add
;; string(s) specifying scheme, host and port for
;; allowed source(s):
;; "http://localhost:8080"
;;::http/allowed-origins ["scheme://host:port"]
::route-set routes
::vase/api-root "/api"
::vase/spec-resources ["vase-transact-fn_service.edn"]
;; Root for resource interceptor that is available by default.
::http/resource-path "/public"
;; Either :jetty, :immutant or :tomcat (see comments in project.clj)
::http/type :jetty
;;::http/host "localhost"
::http/port 8080
;; Options to pass to the container (Jetty)
::http/container-options {:h2c? true
:h2? false
;:keystore "test/hp/keystore.jks"
;:key-password "password"
;:ssl-port 8443
:ssl? false}})
(ns vase-transact-fn.service-test
(:require [clojure.test :refer :all]
[io.pedestal.test :refer :all]
[io.pedestal.http :as http]
[vase-transact-fn.test-helper :as helper]
[vase-transact-fn.service :as service]))
;; To test your service, call `(helper/service` to get a new service instance.
;; If you need a constant service over multiple calls, use `(helper/with-service ...)
;; All generated services will have randomized, consistent in-memory Datomic DBs
;; if required by the service
;; `helper` also contains shorthands for common `response-for` patterns,
;; like GET, POST, post-json, post-edn, and others
(deftest home-page-test
(is (= (:body (response-for (helper/service) :get "/"))
"Hello World!"))
(is (= (:headers (helper/GET "/"))
{"Content-Type" "text/html;charset=UTF-8"
"Strict-Transport-Security" "max-age=31536000; includeSubdomains"
"X-Frame-Options" "DENY"
"X-Content-Type-Options" "nosniff"
"X-XSS-Protection" "1; mode=block"
"X-Download-Options" "noopen"
"X-Permitted-Cross-Domain-Policies" "none"
"Content-Security-Policy" "object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;"})))