initial commit
This commit is contained in:
commit
a44bf306da
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
.DS_Store
|
||||||
|
/target
|
||||||
|
/classes
|
||||||
|
/checkouts
|
||||||
|
pom.xml
|
||||||
|
pom.xml.asc
|
||||||
|
*.jar
|
||||||
|
*.class
|
||||||
|
/.lein-*
|
||||||
|
/.nrepl-port
|
||||||
|
.settings/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2021 Gered King
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
32
README.md
Normal file
32
README.md
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Leiningen Template: Simple Clojure Web Service
|
||||||
|
|
||||||
|
A Leiningen template intended for creating new Clojure web service projects utilizing [reitit](https://github.com/metosin/reitit).
|
||||||
|
|
||||||
|
This template primarily exists for my own personal use, so some stuff is definitely more oriented towards
|
||||||
|
my own particular preferences regarding setup and organization of a Clojure project.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ lein new net.gered/simple-web-service [your-project-name-here]
|
||||||
|
```
|
||||||
|
|
||||||
|
The resulting project starts up via a `main` function and during startup expects to be able to read an EDN
|
||||||
|
configuration file located in the current working directory called `config.edn`.
|
||||||
|
|
||||||
|
The project can be run simply by:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ lein run
|
||||||
|
```
|
||||||
|
|
||||||
|
A nREPL server will be started which can be connected to on port 7000 (configured via the aforementioned `config.edn`).
|
||||||
|
|
||||||
|
The web service's endpoints will be accessible over port 8080 (again, configured via the aforementioned `config.edn`).
|
||||||
|
The Swagger UI page will be available at `/api-docs/` e.g. http://localhost:8080/api-docs/
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright © 2021 Gered King
|
||||||
|
|
||||||
|
Distributed under the the MIT License. See LICENSE for more details.
|
21
project.clj
Normal file
21
project.clj
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
(defproject net.gered/lein-template.simple-web-service "0.1.0-SNAPSHOT"
|
||||||
|
:description "Simple Clojure web service project template."
|
||||||
|
:url "https://github.com/gered/simple-web-service-template"
|
||||||
|
:license {:name "MIT License"
|
||||||
|
:url "http://opensource.org/licenses/MIT"}
|
||||||
|
|
||||||
|
:eval-in-leiningen true
|
||||||
|
|
||||||
|
:deploy-repositories [["releases" :clojars]
|
||||||
|
["snapshots" :clojars]]
|
||||||
|
|
||||||
|
:release-tasks [["vcs" "assert-committed"]
|
||||||
|
["change" "version" "leiningen.release/bump-version" "release"]
|
||||||
|
["vcs" "commit"]
|
||||||
|
["vcs" "tag" "v" "--no-sign"]
|
||||||
|
["deploy" "clojars"]
|
||||||
|
["change" "version" "leiningen.release/bump-version"]
|
||||||
|
["vcs" "commit"]
|
||||||
|
["vcs" "push"]]
|
||||||
|
|
||||||
|
)
|
3
resources/leiningen/new/simple_web_service/config.edn
Normal file
3
resources/leiningen/new/simple_web_service/config.edn
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{:nrepl {:port 7000 :bind "127.0.0.1"}
|
||||||
|
:http {:port 8080 :bind "0.0.0.0"}
|
||||||
|
:dev? true}
|
17
resources/leiningen/new/simple_web_service/gitignore
Normal file
17
resources/leiningen/new/simple_web_service/gitignore
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
.DS_Store
|
||||||
|
/target
|
||||||
|
/classes
|
||||||
|
/checkouts
|
||||||
|
pom.xml
|
||||||
|
pom.xml.asc
|
||||||
|
*.jar
|
||||||
|
*.class
|
||||||
|
/.lein-*
|
||||||
|
/.nrepl-port
|
||||||
|
.settings/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
36
resources/leiningen/new/simple_web_service/project.clj
Normal file
36
resources/leiningen/new/simple_web_service/project.clj
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
(defproject {{name}} "0.1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
:description "FIXME: write description"
|
||||||
|
:url "http://example.com/FIXME"
|
||||||
|
:license {:name "MIT License"
|
||||||
|
:url "http://opensource.org/licenses/MIT"}
|
||||||
|
|
||||||
|
:dependencies [[ch.qos.logback/logback-classic "1.2.7"]
|
||||||
|
[cprop "0.1.19"]
|
||||||
|
[hiccup "1.0.5"]
|
||||||
|
[http-kit "2.5.3"]
|
||||||
|
[javax.servlet/servlet-api "2.5"]
|
||||||
|
[metosin/reitit "0.5.15"]
|
||||||
|
[metosin/ring-http-response "0.9.3"]
|
||||||
|
[mount "0.1.16"]
|
||||||
|
[nrepl "0.9.0"]
|
||||||
|
[org.clojure/clojure "1.10.0"]
|
||||||
|
[org.clojure/tools.logging "1.2.1"]
|
||||||
|
[ring/ring-devel "1.9.4"]]
|
||||||
|
|
||||||
|
:main {{root-ns}}.core
|
||||||
|
|
||||||
|
:repl-options {:init-ns {{root-ns}}.core}
|
||||||
|
|
||||||
|
:profiles {:dev {:source-paths ["env/dev/src"]
|
||||||
|
:resource-paths ["env/dev/resources"]
|
||||||
|
:dependencies [[pjstadig/humane-test-output "0.11.0"]]
|
||||||
|
:injections [(require 'pjstadig.humane-test-output)
|
||||||
|
(pjstadig.humane-test-output/activate!)]}
|
||||||
|
|
||||||
|
:uberjar {:source-paths ["env/prod/src"]
|
||||||
|
:resource-paths ["env/prod/resources"]
|
||||||
|
:omit-source true
|
||||||
|
:aot :all}}
|
||||||
|
|
||||||
|
:aliases {"uberjar" ["do" ["clean"] ["uberjar"]]})
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration>
|
||||||
|
<appender name="C" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{ISO8601} %-5p [%c] - %m%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="${ROOT_LEVEL:-INFO}">
|
||||||
|
<appender-ref ref="C"/>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
189
resources/leiningen/new/simple_web_service/src/root_ns/core.clj
Normal file
189
resources/leiningen/new/simple_web_service/src/root_ns/core.clj
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
{{=<% %>=}}
|
||||||
|
(ns <%root-ns%>.core
|
||||||
|
(:gen-class)
|
||||||
|
(:require
|
||||||
|
[clojure.tools.logging :as log]
|
||||||
|
[cprop.core :refer [load-config]]
|
||||||
|
[hiccup.page :refer [html5]]
|
||||||
|
[org.httpkit.server :as http-kit]
|
||||||
|
[mount.core :as mount :refer [defstate]]
|
||||||
|
[muuntaja.core :as m]
|
||||||
|
[nrepl.server :as nrepl]
|
||||||
|
[reitit.coercion.schema]
|
||||||
|
[reitit.ring :as ring]
|
||||||
|
[reitit.ring.coercion :as coercion]
|
||||||
|
[reitit.ring.middleware.exception :as exception]
|
||||||
|
[reitit.ring.middleware.multipart :as multipart]
|
||||||
|
[reitit.ring.middleware.muuntaja :as muuntaja]
|
||||||
|
[reitit.ring.middleware.parameters :as parameters]
|
||||||
|
[reitit.swagger :as swagger]
|
||||||
|
[reitit.swagger-ui :as swagger-ui]
|
||||||
|
[ring.middleware.reload :refer [wrap-reload]]
|
||||||
|
[ring.util.http-response :refer :all]
|
||||||
|
[schema.core :as s]))
|
||||||
|
|
||||||
|
(declare config)
|
||||||
|
|
||||||
|
;;
|
||||||
|
;; TODO: other app stuff goes here ...
|
||||||
|
;;
|
||||||
|
|
||||||
|
; example exception handler that logs all unhandled exceptions thrown by your routes
|
||||||
|
(def exception-middleware
|
||||||
|
(exception/create-exception-middleware
|
||||||
|
(merge
|
||||||
|
exception/default-handlers
|
||||||
|
{::exception/default
|
||||||
|
(fn [e {:keys [uri request-method remote-addr] :as request}]
|
||||||
|
(log/error e (format "Unhandled exception during request - %s %s from %s"
|
||||||
|
request-method uri remote-addr))
|
||||||
|
(exception/default-handler e request))})))
|
||||||
|
|
||||||
|
; example middleware to enforce request authorization via simple api token header
|
||||||
|
(defn wrap-auth-restrict
|
||||||
|
[handler]
|
||||||
|
(fn [{:keys [headers] :as request}]
|
||||||
|
(let [api-key (get headers "x-api-key")]
|
||||||
|
(if (= "secret" api-key)
|
||||||
|
(handler request)
|
||||||
|
(unauthorized "unauthorized!")))))
|
||||||
|
|
||||||
|
(defstate handler
|
||||||
|
:start
|
||||||
|
(ring/ring-handler
|
||||||
|
(ring/router
|
||||||
|
[["/status"
|
||||||
|
{:swagger {:tags ["Infrastructure"]}
|
||||||
|
:get {:summary "Tests and returns the current status of this web service."
|
||||||
|
:handler (fn [_]
|
||||||
|
; TODO: add your own real test and more detailed status response
|
||||||
|
(ok "up!"))}}]
|
||||||
|
|
||||||
|
["/api"
|
||||||
|
; TODO: replace these with your own endpoints ...
|
||||||
|
|
||||||
|
["/foo"
|
||||||
|
{:swagger {:security [{:my-auth []}]} ; tell swagger this route is restricted
|
||||||
|
:middleware [wrap-auth-restrict] ; apply auth middleware to this route
|
||||||
|
:get {:summary "Gets a foo"
|
||||||
|
:responses {200 {:body {:foo s/Str}}}
|
||||||
|
:handler (fn [_]
|
||||||
|
(let [foo {:foo (str "This is a foo that was generated on: " (java.util.Date.))}]
|
||||||
|
(log/info "Returning a foo:" foo)
|
||||||
|
(ok foo)))}
|
||||||
|
:post {:summary "Posts a foo"
|
||||||
|
:parameters {:body {:foo s/Str}}
|
||||||
|
:responses {200 {:body s/Str}}
|
||||||
|
:handler (fn [{{foo :body} :parameters}]
|
||||||
|
(log/info "Posted a foo: " foo)
|
||||||
|
(ok "Thanks for the foo!"))}}]
|
||||||
|
|
||||||
|
["/math"
|
||||||
|
{:get {:summary "Perform a simple math calculation"
|
||||||
|
:parameters {:query {:a s/Num
|
||||||
|
:b s/Num
|
||||||
|
:op s/Str}}
|
||||||
|
:responses {200 {:body {:result s/Num}}
|
||||||
|
400 {:body s/Str}}
|
||||||
|
:handler (fn [{{{:keys [a b op]} :query} :parameters}]
|
||||||
|
(log/info "Performing math calculation: " a op b)
|
||||||
|
(ok
|
||||||
|
{:result
|
||||||
|
(case op
|
||||||
|
"+" (+ a b)
|
||||||
|
"-" (- a b)
|
||||||
|
"*" (* a b)
|
||||||
|
"/" (/ a b)
|
||||||
|
(bad-request! {:what "Invalid operation"}))}))}}]
|
||||||
|
|
||||||
|
; ---
|
||||||
|
]
|
||||||
|
|
||||||
|
["" {:no-doc true
|
||||||
|
:swagger {:securityDefinitions
|
||||||
|
; tell swagger the details of our auth method(s)
|
||||||
|
{:my-auth {:type "apiKey"
|
||||||
|
:in "header"
|
||||||
|
:name "X-API-Key"}}}}
|
||||||
|
|
||||||
|
; default root handler
|
||||||
|
["/"
|
||||||
|
{:get {:handler (fn [_]
|
||||||
|
(ok (html5 [:h2 "<%name%>"])))}}]
|
||||||
|
|
||||||
|
["/swagger.json"
|
||||||
|
{:get {:swagger {:info {:title "<%name%> API"}}
|
||||||
|
:handler (swagger/create-swagger-handler)}}]
|
||||||
|
["/api-docs*"
|
||||||
|
(swagger-ui/create-swagger-ui-handler
|
||||||
|
{:config {}})]]
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
{:data {:coercion reitit.coercion.schema/coercion
|
||||||
|
:muuntaja m/instance
|
||||||
|
:middleware [parameters/parameters-middleware ; query-params & form-params
|
||||||
|
muuntaja/format-negotiate-middleware ; content-negotiation
|
||||||
|
muuntaja/format-response-middleware ; encoding response body
|
||||||
|
exception-middleware ; exception handling
|
||||||
|
muuntaja/format-request-middleware ; decoding request body
|
||||||
|
coercion/coerce-response-middleware ; coercing response body
|
||||||
|
coercion/coerce-request-middleware ; coercing request parameters
|
||||||
|
multipart/multipart-middleware ; multipart
|
||||||
|
]}})
|
||||||
|
(ring/routes
|
||||||
|
(ring/create-default-handler))))
|
||||||
|
|
||||||
|
(defn wrap-base
|
||||||
|
[handler]
|
||||||
|
(as-> handler h
|
||||||
|
(if (:dev? config) (wrap-reload h) h)
|
||||||
|
; TODO: other base middleware here
|
||||||
|
))
|
||||||
|
|
||||||
|
;;
|
||||||
|
|
||||||
|
(defstate ^{:on-reload :noop} config
|
||||||
|
:start
|
||||||
|
(do
|
||||||
|
(log/info "Loading config.edn")
|
||||||
|
(load-config :file "config.edn")))
|
||||||
|
|
||||||
|
(defstate ^{:on-reload :noop} repl-server
|
||||||
|
:start
|
||||||
|
(let [{:keys [port bind]
|
||||||
|
:or {port 7000
|
||||||
|
bind "127.0.0.1"}} (:nrepl config)
|
||||||
|
server (nrepl/start-server :port port :bind bind)]
|
||||||
|
(log/info (format "Starting nREPL server listening on %s:%d" bind port))
|
||||||
|
server)
|
||||||
|
:stop
|
||||||
|
(when repl-server
|
||||||
|
(log/info "Stopping nREPL server")
|
||||||
|
(nrepl/stop-server repl-server)))
|
||||||
|
|
||||||
|
(defstate ^{:on-reload :noop} http-server
|
||||||
|
:start
|
||||||
|
(let [{:keys [port bind]
|
||||||
|
:or {port 8080
|
||||||
|
bind "0.0.0.0"}} (:http-server config)
|
||||||
|
server (http-kit/run-server
|
||||||
|
(wrap-base #'handler)
|
||||||
|
{:port port
|
||||||
|
:ip bind
|
||||||
|
:server-header nil})]
|
||||||
|
(log/info (format "Started HTTP server listening on %s:%d" bind port))
|
||||||
|
server)
|
||||||
|
:stop
|
||||||
|
(when @http-server
|
||||||
|
(log/info "Stopping HTTP server")
|
||||||
|
(http-kit/server-stop! @http-server)
|
||||||
|
nil))
|
||||||
|
|
||||||
|
;;
|
||||||
|
|
||||||
|
(defn -main
|
||||||
|
[& args]
|
||||||
|
(log/info "<%name%> is starting up ...")
|
||||||
|
(mount/start-with-args args)
|
||||||
|
(log/info "Ready!"))
|
|
@ -0,0 +1,4 @@
|
||||||
|
(ns {{root-ns}}.core-test
|
||||||
|
(:require
|
||||||
|
[clojure.test :refer :all]
|
||||||
|
[{{root-ns}}.core :refer :all]))
|
26
src/leiningen/new/simple_web_service.clj
Normal file
26
src/leiningen/new/simple_web_service.clj
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
(ns leiningen.new.simple-web-service
|
||||||
|
(:require
|
||||||
|
[leiningen.new.templates :as t]
|
||||||
|
[leiningen.core.main :as main]))
|
||||||
|
|
||||||
|
(def render (t/renderer "simple_web_service"))
|
||||||
|
|
||||||
|
(defn simple-web-service
|
||||||
|
[name]
|
||||||
|
(let [data {:name name
|
||||||
|
:sanitized (t/sanitize name)
|
||||||
|
:root-ns (t/sanitize-ns name)
|
||||||
|
:root-ns-path (t/name-to-path name)}]
|
||||||
|
(main/info (str "Creating new project via net.gered/simple-web-service called \"" name "\" ..."))
|
||||||
|
(t/->files
|
||||||
|
data
|
||||||
|
"env/dev/resources"
|
||||||
|
"env/dev/src"
|
||||||
|
"env/prod/resources"
|
||||||
|
"env/prod/src"
|
||||||
|
["resources/logback.xml" (render "resources/logback.xml" data)]
|
||||||
|
["src/{{root-ns-path}}/core.clj" (render "src/root_ns/core.clj" data)]
|
||||||
|
["test/{{root-ns-path}}/core_test.clj" (render "test/root_ns/core_test.clj" data)]
|
||||||
|
[".gitignore" (render "gitignore" data)]
|
||||||
|
["config.edn" (render "config.edn" data)]
|
||||||
|
["project.clj" (render "project.clj" data)])))
|
Loading…
Reference in a new issue