Initial Kit project with PostgreSQL, HTMX, Hiccup, and Tailwind

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 11:45:09 +01:00
co-authored by Claude Opus 4.6
commit c1f386ef44
37 changed files with 1920 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
kit.git-config.edn
.DS_Store
.nrepl-port
hs_err_pid*.log
.cpcache/
target
log/
.shadow-cljs/
node_modules/
modules/
resources/public/css/output.css
# IntelliJ
*.iml
.idea/
*.bak
# clj-kondo
.clj-kondo/.cache/
.calva/output-window/
.lsp/.cache/
+15
View File
@@ -0,0 +1,15 @@
# syntax = docker/dockerfile:1.2
FROM clojure:openjdk-17 AS build
WORKDIR /
COPY . /
RUN clj -Sforce -T:build all
FROM azul/zulu-openjdk-alpine:17
COPY --from=build /target/elprice-standalone.jar /elprice/elprice-standalone.jar
EXPOSE $PORT
ENTRYPOINT exec java $JAVA_OPTS -jar /elprice/elprice-standalone.jar
+14
View File
@@ -0,0 +1,14 @@
clean:
rm -rf target
run:
clj -M:dev
repl:
clj -M:dev:nrepl
test:
clj -M:test
uberjar:
clj -T:build all
+37
View File
@@ -0,0 +1,37 @@
# elprice
Start a [REPL](#repls) in your editor or terminal of choice.
Start the server with:
```clojure
(go)
```
The default API is available under http://localhost:3000/api
System configuration is available under `resources/system.edn`.
To reload changes:
```clojure
(reset)
```
## REPLs
### Cursive
Configure a [REPL following the Cursive documentation](https://cursive-ide.com/userguide/repl.html). Using the default "Run with IntelliJ project classpath" option will let you select an alias from the ["Clojure deps" aliases selection](https://cursive-ide.com/userguide/deps.html#refreshing-deps-dependencies).
### CIDER
Use the `cider` alias for CIDER nREPL support (run `clj -M:dev:cider`). See the [CIDER docs](https://docs.cider.mx/cider/basics/up_and_running.html) for more help.
Note that this alias runs nREPL during development. To run nREPL in production (typically when the system starts), use the kit-nrepl library through the +nrepl profile as described in [the documentation](https://kit-clj.github.io/docs/profiles.html#profiles).
### Command Line
Run `clj -M:dev:nrepl` or `make repl`.
Note that, just like with [CIDER](#cider), this alias runs nREPL during development. To run nREPL in production (typically when the system starts), use the kit-nrepl library through the +nrepl profile as described in [the documentation](https://kit-clj.github.io/docs/profiles.html#profiles).
+27
View File
@@ -0,0 +1,27 @@
{:min-bb-version "0.8.156"
:deps {failjure/failjure {:mvn/version "2.3.0"}}
:tasks {:requires ([babashka.fs :as fs]
[babashka.tasks :refer [shell]])
run {:doc "starts the app"
:task (if (fs/windows?)
(clojure {:dir "."} "-M:dev")
(shell {:dir "."} "clj -M:dev"))}
nrepl {:doc "starts the nREPL"
:task (clojure {:dir "."} "-M:dev:nrepl")}
cider {:doc "starts the cider"
:task (clojure {:dir "."} "-M:dev:cider")}
test {:doc "runs tests"
:task (clojure {:dir "."} "-M:test")}
uberjar {:doc "builds the uberjar"
:task (clojure {:dir "."} "-T:build all")}
format {:doc "Formats codebase"
:task (try
(shell {:dir "src"} "cljstyle fix")
(catch Exception _
(clojure {:dir "src"} "-M:dev -m cljstyle.main fix")))}}}
+41
View File
@@ -0,0 +1,41 @@
(ns build
(:require [clojure.string :as string]
[clojure.tools.build.api :as b]))
(def lib 'pmagnus/elprice)
(def main-cls (string/join "." (filter some? [(namespace lib) (name lib) "core"])))
(def version (format "0.0.1-SNAPSHOT"))
(def target-dir "target")
(def class-dir (str target-dir "/" "classes"))
(def uber-file (format "%s/%s-standalone.jar" target-dir (name lib)))
(def basis (b/create-basis {:project "deps.edn"}))
(defn clean
"Delete the build target directory"
[_]
(println (str "Cleaning " target-dir))
(b/delete {:path target-dir}))
(defn prep [_]
(println "Writing Pom...")
(b/write-pom {:class-dir class-dir
:lib lib
:version version
:basis basis
:src-dirs ["src/clj"]})
(b/copy-dir {:src-dirs ["src/clj" "resources" "env/prod/resources" "env/prod/clj"]
:target-dir class-dir}))
(defn uber [_]
(println "Compiling Clojure...")
(b/compile-clj {:basis basis
:src-dirs ["src/clj" "resources" "env/prod/resources" "env/prod/clj"]
:class-dir class-dir})
(println "Making uberjar...")
(b/uber {:class-dir class-dir
:uber-file uber-file
:main main-cls
:basis basis}))
(defn all [_]
(do (clean nil) (prep nil) (uber nil)))
+69
View File
@@ -0,0 +1,69 @@
{:paths ["src/clj"
"resources"]
:deps {org.clojure/clojure {:mvn/version "1.12.3"}
;; Routing
metosin/reitit {:mvn/version "0.9.2"}
;; Ring
metosin/ring-http-response {:mvn/version "0.9.5"}
ring/ring-core {:mvn/version "1.15.3"}
ring/ring-defaults {:mvn/version "0.7.0"}
;; Logging
ch.qos.logback/logback-classic {:mvn/version "1.5.20"}
;; Data coercion
luminus-transit/luminus-transit {:mvn/version "0.1.6"
:exclusions [com.cognitect/transit-clj]}
metosin/muuntaja {:mvn/version "0.6.11"}
;; kit Libs
io.github.kit-clj/kit-core {:mvn/version "1.0.9"}
io.github.kit-clj/kit-undertow {:mvn/version "1.0.10"}
io.github.kit-clj/kit-postgres {:mvn/version "1.0.7"}
io.github.kit-clj/kit-sql-conman {:mvn/version "1.10.5"}
io.github.kit-clj/kit-sql-migratus {:mvn/version "1.0.5"}
hiccup/hiccup {:mvn/version "2.0.0"}}
:aliases {:build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.11"}}
:ns-default build}
:dev {:extra-deps {com.lambdaisland/classpath {:mvn/version "0.6.58"}
criterium/criterium {:mvn/version "0.4.6"}
expound/expound {:mvn/version "0.9.0"}
integrant/repl {:mvn/version "0.5.0"}
mvxcvi/cljstyle {:mvn/version "0.17.642"}
pjstadig/humane-test-output {:mvn/version "0.11.0"}
ring/ring-devel {:mvn/version "1.15.3"}
ring/ring-mock {:mvn/version "0.6.2"}
io.github.kit-clj/kit-generator {:mvn/version "0.2.5"}
org.clojure/tools.namespace {:mvn/version "1.5.0"}
}
:extra-paths ["env/dev/clj" "env/dev/resources" "test/clj"]}
:nrepl {:extra-deps {nrepl/nrepl {:mvn/version "1.5.1"}}
:main-opts ["-m" "nrepl.cmdline" "-i"]}
:cider {:extra-deps {nrepl/nrepl {:mvn/version "1.5.1"}
cider/cider-nrepl {:mvn/version "0.58.0"}}
:main-opts ["-m" "nrepl.cmdline" "--middleware" "[cider.nrepl/cider-middleware]" "-i"]}
:test {:extra-deps {criterium/criterium {:mvn/version "0.4.6"}
expound/expound {:mvn/version "0.9.0"}
integrant/repl {:mvn/version "0.5.0"}
io.github.cognitect-labs/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git"
:git/tag "v0.5.1"
:git/sha "dfb30dd"}
pjstadig/humane-test-output {:mvn/version "0.11.0"}
ring/ring-devel {:mvn/version "1.15.3"}
ring/ring-mock {:mvn/version "0.6.2"}
io.github.kit-clj/kit-generator {:mvn/version "0.2.5"}
org.clojure/tools.namespace {:mvn/version "1.5.0"}
peridot/peridot {:mvn/version "0.5.4"}
org.clj-commons/byte-streams {:mvn/version "0.3.4"}
com.lambdaisland/classpath {:mvn/version "0.6.58"}}
:exec-fn cognitect.test-runner.api/test
:extra-paths ["env/dev/clj" "env/dev/resources" "env/test/resources" "test/clj"]
:main-opts ["-e" "(require 'pjstadig.humane-test-output) (pjstadig.humane-test-output/activate!)"
"-m" "cognitect.test-runner"]}}
}
+14
View File
@@ -0,0 +1,14 @@
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: elprice
POSTGRES_USER: elprice
POSTGRES_PASSWORD: elprice
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
+5
View File
@@ -0,0 +1,5 @@
(ns pmagnus.elprice.dev-middleware)
(defn wrap-dev [handler _opts]
(-> handler
))
+14
View File
@@ -0,0 +1,14 @@
(ns pmagnus.elprice.env
(:require
[clojure.tools.logging :as log]
[pmagnus.elprice.dev-middleware :refer [wrap-dev]]))
(def defaults
{:init (fn []
(log/info "\n-=[elprice starting using the development or test profile]=-"))
:start (fn []
(log/info "\n-=[elprice started successfully using the development or test profile]=-"))
:stop (fn []
(log/info "\n-=[elprice has shut down successfully]=-"))
:middleware wrap-dev
:opts {:profile :dev}})
+59
View File
@@ -0,0 +1,59 @@
(ns user
"Userspace functions you can run by default in your local REPL."
(:require
[clojure.pprint]
[clojure.spec.alpha :as s]
[clojure.tools.namespace.repl :as repl]
[criterium.core :as c] ;; benchmarking
[expound.alpha :as expound]
[integrant.core :as ig]
[integrant.repl :refer [clear go halt prep init reset reset-all]]
[integrant.repl.state :as state]
[kit.api :as kit]
[lambdaisland.classpath.watch-deps :as watch-deps] ;; hot loading for deps
[pmagnus.elprice.core :refer [start-app]]))
;; uncomment to enable hot loading for deps
(watch-deps/start! {:aliases [:dev :test]})
(alter-var-root #'s/*explain-out* (constantly expound/printer))
(add-tap (bound-fn* clojure.pprint/pprint))
(defn dev-prep!
[]
(integrant.repl/set-prep! (fn []
(-> (pmagnus.elprice.config/system-config {:profile :dev})
(ig/expand)))))
(defn test-prep!
[]
(integrant.repl/set-prep! (fn []
(-> (pmagnus.elprice.config/system-config {:profile :test})
(ig/expand)))))
;; Can change this to test-prep! if want to run tests as the test profile in your repl
;; You can run tests in the dev profile, too, but there are some differences between
;; the two profiles.
(dev-prep!)
(repl/set-refresh-dirs "src/clj")
(def refresh repl/refresh)
(defn reset-db []
(migratus.core/reset (:db.sql/migrations state/system)))
(defn rollback []
(migratus.core/rollback (:db.sql/migrations state/system)))
(defn migrate []
(migratus.core/migrate (:db.sql/migrations state/system)))
(def query-fn (:db.sql/query-fn state/system))
(comment
(go)
(reset))
+38
View File
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds">
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<charset>UTF-8</charset>
<pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>log/pmagnus.elprice.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>log/pmagnus.elprice.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- keep 30 days of history -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<charset>UTF-8</charset>
<pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern>
</encoder>
</appender>
<logger name="org.eclipse.aether" level="warn" />
<logger name="io.methvin.watcher" level="warn" />
<logger name="org.eclipse.jgit" level="warn" />
<logger name="com.zaxxer.hikari" level="warn" />
<logger name="org.apache.http" level="warn" />
<logger name="org.xnio.nio" level="warn" />
<logger name="io.undertow" level="warn" />
<root level="DEBUG">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>
+12
View File
@@ -0,0 +1,12 @@
(ns pmagnus.elprice.env
(:require [clojure.tools.logging :as log]))
(def defaults
{:init (fn []
(log/info "\n-=[elprice starting]=-"))
:start (fn []
(log/info "\n-=[elprice started successfully]=-"))
:stop (fn []
(log/info "\n-=[elprice has shut down successfully]=-"))
:middleware (fn [handler _] handler)
:opts {:profile :prod}})
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.zaxxer.hikari" level="error" />
<logger name="org.apache.http" level="error" />
<logger name="org.xnio.nio" level="error" />
<logger name="io.undertow" level="error" />
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-5relative %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.zaxxer.hikari" level="error" />
<logger name="org.apache.http" level="error" />
<logger name="org.xnio.nio" level="error" />
<logger name="io.undertow" level="error" />
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
+8
View File
@@ -0,0 +1,8 @@
{:full-name "pmagnus/elprice"
:ns-name "pmagnus.elprice"
:sanitized "pmagnus/elprice"
:name "elprice"
:modules {:root "modules"
:repositories [{:url "https://github.com/kit-clj/modules.git"
:tag "master"
:name "kit-modules"}]}}
+1055
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"name": "elprice",
"version": "1.0.0",
"description": "Start a [REPL](#repls) in your editor or terminal of choice.",
"main": "index.js",
"directories": {
"test": "test"
},
"scripts": {
"css:build": "npx @tailwindcss/cli -i resources/css/input.css -o resources/public/css/output.css --minify",
"css:watch": "npx @tailwindcss/cli -i resources/css/input.css -o resources/public/css/output.css --watch"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"@tailwindcss/cli": "^4.1.18",
"tailwindcss": "^4.1.18"
}
}
+1
View File
@@ -0,0 +1 @@
@import "tailwindcss";
+1
View File
@@ -0,0 +1 @@
See https://github.com/yogthos/migratus
+2
View File
@@ -0,0 +1,2 @@
-- place your sql queries here
-- see https://www.hugsql.org/ for documentation
+69
View File
@@ -0,0 +1,69 @@
{:system/env
#profile {:dev :dev
:test :test
:prod :prod}
:server/http
{:port #long #or [#env PORT 3000]
:host #or [#env HTTP_HOST "0.0.0.0"]
:handler #ig/ref :handler/ring}
:handler/ring
{:router #ig/ref :router/core
:api-path "/api"
:cookie-secret #or [#env COOKIE_SECRET "JJCILTOQLMYRBFRU"]
;; from ring.middleware.defaults. anti-forgery `false` by default because services may not require it
:site-defaults-config {:params {:urlencoded true
:multipart true
:nested true
:keywordize true}
:cookies true
:session {:flash true
:cookie-name "pmagnus.elprice"
:cookie-attrs {:max-age 86400
:http-only true
:same-site :strict}}
:security {:anti-forgery false
:xss-protection {:enable? true,
:mode :block}
:frame-options :sameorigin
:content-type-options :nosniff}
:static {:resources "public"}
:responses {:not-modified-responses true
:absolute-redirects true
:content-types true
:default-charset "utf-8"}}}
:reitit.routes/api
{:base-path "/api"
:env #ig/ref :system/env
:query-fn #ig/ref :db.sql/query-fn}
:router/routes
{:routes #ig/refset :reitit/routes}
:router/core
{:routes #ig/ref :router/routes
:env #ig/ref :system/env}
:db.sql/connection
#profile {:dev {:jdbc-url "jdbc:postgresql://localhost:5432/elprice?user=elprice&password=elprice"}
:test {:jdbc-url "jdbc:postgresql://localhost:5432/elprice?user=elprice&password=elprice"}
:prod {:jdbc-url #env JDBC_URL
:init-size 1
:min-idle 1
:max-idle 8
:max-active 32}}
:db.sql/query-fn
{:conn #ig/ref :db.sql/connection
:options {}
:filename "queries.sql"
:env #ig/ref :system/env}
:db.sql/migrations
{:store :database
:db {:datasource #ig/ref :db.sql/connection}
:migrate-on-init? true}
:reitit.routes/ui {:base-path "",
:env #ig/ref :system/env}}
+9
View File
@@ -0,0 +1,9 @@
(ns pmagnus.elprice.config
(:require
[kit.config :as config]))
(def ^:const system-filename "system.edn")
(defn system-config
[options]
(config/read-config system-filename options))
+43
View File
@@ -0,0 +1,43 @@
(ns pmagnus.elprice.core
(:require
[clojure.tools.logging :as log]
[integrant.core :as ig]
[pmagnus.elprice.config :as config]
[pmagnus.elprice.env :refer [defaults]]
;; Edges
[kit.edge.db.sql.conman]
[kit.edge.db.sql.migratus]
[kit.edge.db.postgres]
[kit.edge.db.mysql]
[kit.edge.server.undertow]
[pmagnus.elprice.web.handler]
;; Routes
[pmagnus.elprice.web.routes.api]
[pmagnus.elprice.web.routes.ui])
(:gen-class))
;; log uncaught exceptions in threads
(Thread/setDefaultUncaughtExceptionHandler
(fn [thread ex]
(log/error {:what :uncaught-exception
:exception ex
:where (str "Uncaught exception on" (.getName thread))})))
(defonce system (atom nil))
(defn stop-app []
((or (:stop defaults) (fn [])))
(some-> (deref system) (ig/halt!)))
(defn start-app [& [params]]
((or (:start params) (:start defaults) (fn [])))
(->> (config/system-config (or (:opts params) (:opts defaults) {}))
(ig/expand)
(ig/init)
(reset! system)))
(defn -main [& _]
(start-app)
(.addShutdownHook (Runtime/getRuntime) (Thread. (fn [] (stop-app) (shutdown-agents)))))
@@ -0,0 +1,13 @@
(ns pmagnus.elprice.web.controllers.health
(:require
[ring.util.http-response :as http-response])
(:import
[java.util Date]))
(defn healthcheck!
[req]
(http-response/ok
{:time (str (Date. (System/currentTimeMillis)))
:up-since (str (Date. (.getStartTime (java.lang.management.ManagementFactory/getRuntimeMXBean))))
:app {:status "up"
:message ""}}))
+45
View File
@@ -0,0 +1,45 @@
(ns pmagnus.elprice.web.handler
(:require
[pmagnus.elprice.web.middleware.core :as middleware]
[integrant.core :as ig]
[ring.util.http-response :as http-response]
[reitit.ring :as ring]
[reitit.swagger-ui :as swagger-ui]))
(defmethod ig/init-key :handler/ring
[_ {:keys [router api-path] :as opts}]
(ring/ring-handler
(router)
(ring/routes
;; Handle trailing slash in routes - add it + redirect to it
;; https://github.com/metosin/reitit/blob/master/doc/ring/slash_handler.md
(ring/redirect-trailing-slash-handler)
(ring/create-resource-handler {:path "/"})
(when (some? api-path)
(swagger-ui/create-swagger-ui-handler {:path api-path
:url (str api-path "/swagger.json")}))
(ring/create-default-handler
{:not-found
(constantly (-> {:status 404, :body "Page not found"}
(http-response/content-type "text/plain")))
:method-not-allowed
(constantly (-> {:status 405, :body "Not allowed"}
(http-response/content-type "text/plain")))
:not-acceptable
(constantly (-> {:status 406, :body "Not acceptable"}
(http-response/content-type "text/plain")))}))
{:middleware [(middleware/wrap-base opts)]}))
(defmethod ig/init-key :router/routes
[_ {:keys [routes]}]
(mapv (fn [route]
(if (fn? route)
(route)
route))
routes))
(defmethod ig/init-key :router/core
[_ {:keys [routes env] :as opts}]
(if (= env :dev)
#(ring/router ["" opts routes])
(constantly (ring/router ["" opts routes]))))
+25
View File
@@ -0,0 +1,25 @@
(ns pmagnus.elprice.web.htmx
(:require
[ring.util.http-response :as http-response]
[hiccup.core :as h]
[hiccup.page :as p]))
(defmacro page [opts & content]
`(-> (p/html5 ~opts
[:head
[:meta {:charset "UTF-8"}]
[:meta {:name "viewport"
:content "width=device-width, initial-scale=1, viewport-fit=cover"}]
[:link {:rel "stylesheet" :href "/css/output.css"}]
[:script {:src "https://unpkg.com/htmx.org@2.0.8/dist/htmx.min.js"
:defer true}]]
[:body {:class "bg-gray-50 min-h-screen"}
[:div {:class "mx-auto max-w-[390px] px-4 py-6"}
~@content]])
http-response/ok
(http-response/content-type "text/html")))
(defmacro fragment [opts & content]
`(-> (str (h/html ~opts ~@content))
http-response/ok
(http-response/content-type "text/html")))
@@ -0,0 +1,14 @@
(ns pmagnus.elprice.web.middleware.core
(:require
[pmagnus.elprice.env :as env]
[ring.middleware.defaults :as defaults]
[ring.middleware.session.cookie :as cookie]))
(defn wrap-base
[{:keys [metrics site-defaults-config cookie-secret] :as opts}]
(let [cookie-store (cookie/cookie-store {:key (.getBytes ^String cookie-secret)})]
(fn [handler]
(cond-> ((:middleware env/defaults) handler opts)
true (defaults/wrap-defaults
(assoc-in site-defaults-config [:session :store] cookie-store))
))))
@@ -0,0 +1,31 @@
(ns pmagnus.elprice.web.middleware.exception
(:require
[clojure.tools.logging :as log]
[reitit.ring.middleware.exception :as exception]))
(defn handler [message status exception request]
(when (>= status 500)
;; You can optionally use this to report error to an external service
(log/error exception))
{:status status
:body {:message message
:exception (.getClass exception)
:data (ex-data exception)
:uri (:uri request)}})
(def wrap-exception
(exception/create-exception-middleware
(merge
exception/default-handlers
{:system.exception/internal (partial handler "internal exception" 500)
:system.exception/business (partial handler "bad request" 400)
:system.exception/not-found (partial handler "not found" 404)
:system.exception/unauthorized (partial handler "unauthorized" 401)
:system.exception/forbidden (partial handler "forbidden" 403)
;; override the default handler
::exception/default (partial handler "default" 500)
;; print stack-traces for all exceptions
::exception/wrap (fn [handler e request]
(handler e request))})))
@@ -0,0 +1,14 @@
(ns pmagnus.elprice.web.middleware.formats
(:require
[luminus-transit.time :as time]
[muuntaja.core :as m]))
(def instance
(m/create
(-> m/default-options
(update-in
[:formats "application/transit+json" :decoder-opts]
(partial merge time/time-deserialization-handlers))
(update-in
[:formats "application/transit+json" :encoder-opts]
(partial merge time/time-serialization-handlers)))))
@@ -0,0 +1,52 @@
(ns pmagnus.elprice.web.routes.api
(:require
[pmagnus.elprice.web.controllers.health :as health]
[pmagnus.elprice.web.middleware.exception :as exception]
[pmagnus.elprice.web.middleware.formats :as formats]
[integrant.core :as ig]
[reitit.coercion.malli :as malli]
[reitit.ring.coercion :as coercion]
[reitit.ring.middleware.muuntaja :as muuntaja]
[reitit.ring.middleware.parameters :as parameters]
[reitit.swagger :as swagger]))
(def route-data
{:coercion malli/coercion
:muuntaja formats/instance
:swagger {:id ::api}
:middleware [;; query-params & form-params
parameters/parameters-middleware
;; content-negotiation
muuntaja/format-negotiate-middleware
;; encoding response body
muuntaja/format-response-middleware
;; exception handling
coercion/coerce-exceptions-middleware
;; decoding request body
muuntaja/format-request-middleware
;; coercing response bodys
coercion/coerce-response-middleware
;; coercing request parameters
coercion/coerce-request-middleware
;; exception handling
exception/wrap-exception]})
;; Routes
(defn api-routes [_opts]
[["/swagger.json"
{:get {:no-doc true
:swagger {:info {:title "pmagnus.elprice API"}}
:handler (swagger/create-swagger-handler)}}]
["/health"
;; note that use of the var is necessary
;; for reitit to reload routes without
;; restarting the system
{:get #'health/healthcheck!}]])
(derive :reitit.routes/api :reitit/routes)
(defmethod ig/init-key :reitit.routes/api
[_ {:keys [base-path]
:or {base-path ""}
:as opts}]
(fn [] [base-path route-data (api-routes opts)]))
+50
View File
@@ -0,0 +1,50 @@
(ns pmagnus.elprice.web.routes.ui
(:require
[pmagnus.elprice.web.middleware.exception :as exception]
[pmagnus.elprice.web.middleware.formats :as formats]
[pmagnus.elprice.web.routes.utils :as utils]
[pmagnus.elprice.web.htmx :refer [page fragment] :as htmx]
[integrant.core :as ig]
[reitit.ring.middleware.muuntaja :as muuntaja]
[reitit.ring.middleware.parameters :as parameters]))
(defn home [request]
(page {:lang "en"}
[:h1 {:class "text-2xl font-bold text-gray-900"}
"Welcome to elprice"]
[:p {:class "mt-2 text-gray-600"}
"Built with Kit, HTMX, Hiccup & Tailwind"]
[:button {:class "mt-4 rounded-lg bg-blue-600 px-4 py-2 text-white font-medium
active:bg-blue-700 transition-colors"
:hx-post "/clicked"
:hx-swap "outerHTML"}
"Click me!"]))
(defn clicked [request]
(fragment
[:div {:class "mt-4 rounded-lg bg-green-100 p-4 text-green-800"}
"Congratulations! You just clicked the button!"]))
;; Routes
(defn ui-routes [_opts]
[["/" {:get home}]
["/clicked" {:post clicked}]])
(def route-data
{:muuntaja formats/instance
:middleware
[;; Default middleware for ui
;; query-params & form-params
parameters/parameters-middleware
;; encoding response body
muuntaja/format-response-middleware
;; exception handling
exception/wrap-exception]})
(derive :reitit.routes/ui :reitit/routes)
(defmethod ig/init-key :reitit.routes/ui
[_ {:keys [base-path]
:or {base-path ""}
:as opts}]
(fn [] [base-path route-data (ui-routes opts)]))
@@ -0,0 +1,11 @@
(ns pmagnus.elprice.web.routes.utils)
(def route-data-path [:reitit.core/match :data])
(defn route-data
[req]
(get-in req route-data-path))
(defn route-data-key
[req k]
(get-in req (conj route-data-path k)))
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
content: ["./src/**/*.clj", "./resources/**/*.html"],
theme: { extend: {} },
plugins: []
}
+8
View File
@@ -0,0 +1,8 @@
(ns pmagnus.elprice.core-test
(:require
[pmagnus.elprice.test-utils :as utils]
[clojure.test :refer :all]))
(deftest example-test
(is (= 1 2)))
+32
View File
@@ -0,0 +1,32 @@
(ns pmagnus.elprice.test-utils
(:require
[pmagnus.elprice.core :as core]
[peridot.core :as p]
[byte-streams :as bs]
[integrant.repl.state :as state]))
(defn system-state
[]
(or @core/system state/system))
(defn system-fixture
[]
(fn [f]
(when (nil? (system-state))
(core/start-app {:opts {:profile :test}}))
(f)
(core/stop-app)))
(defn get-response [ctx]
(-> ctx
:response
(update :body (fnil bs/to-string ""))))
(defn GET [app path params headers]
(-> (p/session app)
(p/request path
:request-method :get
:content-type "application/edn"
:headers headers
:params params)
(get-response)))
@@ -0,0 +1,13 @@
(ns pmagnus.elprice.web.request-test
(:require [clojure.test :refer [deftest testing is use-fixtures]]
[pmagnus.elprice.test-utils :refer [system-state system-fixture GET]]))
(use-fixtures :once (system-fixture))
(deftest health-request-test []
(testing "happy path"
(let [handler (:handler/ring (system-state))
params {}
headers {}
response (GET handler "/api/health" params headers)]
(is (= 200 (:status response))))))