From a44bf306da1fcc1463ad81401829d45228b6663b Mon Sep 17 00:00:00 2001 From: gered Date: Tue, 21 Dec 2021 18:35:39 -0500 Subject: [PATCH] initial commit --- .gitignore | 17 ++ LICENSE | 21 ++ README.md | 32 +++ project.clj | 21 ++ .../new/simple_web_service/config.edn | 3 + .../new/simple_web_service/gitignore | 17 ++ .../new/simple_web_service/project.clj | 36 ++++ .../simple_web_service/resources/logback.xml | 12 ++ .../simple_web_service/src/root_ns/core.clj | 189 ++++++++++++++++++ .../test/root_ns/core_test.clj | 4 + src/leiningen/new/simple_web_service.clj | 26 +++ 11 files changed, 378 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 project.clj create mode 100644 resources/leiningen/new/simple_web_service/config.edn create mode 100644 resources/leiningen/new/simple_web_service/gitignore create mode 100644 resources/leiningen/new/simple_web_service/project.clj create mode 100644 resources/leiningen/new/simple_web_service/resources/logback.xml create mode 100644 resources/leiningen/new/simple_web_service/src/root_ns/core.clj create mode 100644 resources/leiningen/new/simple_web_service/test/root_ns/core_test.clj create mode 100644 src/leiningen/new/simple_web_service.clj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..278bb6d --- /dev/null +++ b/.gitignore @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2347d9c --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4179e9e --- /dev/null +++ b/README.md @@ -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. diff --git a/project.clj b/project.clj new file mode 100644 index 0000000..4af1a2a --- /dev/null +++ b/project.clj @@ -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"]] + + ) diff --git a/resources/leiningen/new/simple_web_service/config.edn b/resources/leiningen/new/simple_web_service/config.edn new file mode 100644 index 0000000..076f152 --- /dev/null +++ b/resources/leiningen/new/simple_web_service/config.edn @@ -0,0 +1,3 @@ +{:nrepl {:port 7000 :bind "127.0.0.1"} + :http {:port 8080 :bind "0.0.0.0"} + :dev? true} diff --git a/resources/leiningen/new/simple_web_service/gitignore b/resources/leiningen/new/simple_web_service/gitignore new file mode 100644 index 0000000..278bb6d --- /dev/null +++ b/resources/leiningen/new/simple_web_service/gitignore @@ -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 diff --git a/resources/leiningen/new/simple_web_service/project.clj b/resources/leiningen/new/simple_web_service/project.clj new file mode 100644 index 0000000..f31ed04 --- /dev/null +++ b/resources/leiningen/new/simple_web_service/project.clj @@ -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"]]}) diff --git a/resources/leiningen/new/simple_web_service/resources/logback.xml b/resources/leiningen/new/simple_web_service/resources/logback.xml new file mode 100644 index 0000000..0e58a40 --- /dev/null +++ b/resources/leiningen/new/simple_web_service/resources/logback.xml @@ -0,0 +1,12 @@ + + + + + %d{ISO8601} %-5p [%c] - %m%n + + + + + + + diff --git a/resources/leiningen/new/simple_web_service/src/root_ns/core.clj b/resources/leiningen/new/simple_web_service/src/root_ns/core.clj new file mode 100644 index 0000000..bc96201 --- /dev/null +++ b/resources/leiningen/new/simple_web_service/src/root_ns/core.clj @@ -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!")) diff --git a/resources/leiningen/new/simple_web_service/test/root_ns/core_test.clj b/resources/leiningen/new/simple_web_service/test/root_ns/core_test.clj new file mode 100644 index 0000000..1f829a5 --- /dev/null +++ b/resources/leiningen/new/simple_web_service/test/root_ns/core_test.clj @@ -0,0 +1,4 @@ +(ns {{root-ns}}.core-test + (:require + [clojure.test :refer :all] + [{{root-ns}}.core :refer :all])) diff --git a/src/leiningen/new/simple_web_service.clj b/src/leiningen/new/simple_web_service.clj new file mode 100644 index 0000000..ed4469b --- /dev/null +++ b/src/leiningen/new/simple_web_service.clj @@ -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)])))