├── resources ├── public │ ├── css │ │ └── .gitkeep │ └── js │ │ └── theme.js ├── database │ └── migrations │ │ └── V1_1__create_weather_forecaast_table.sql └── config.edn ├── http-client.env.json ├── .clj-kondo ├── metosin │ ├── malli │ │ └── config.edn │ └── malli-types-clj │ │ └── config.edn ├── hiccup │ └── hiccup │ │ ├── config.edn │ │ └── hiccup │ │ └── hooks.clj └── com.github.seancorfield │ └── next.jdbc │ ├── config.edn │ └── hooks │ └── com │ └── github │ └── seancorfield │ └── next_jdbc.clj_kondo ├── .github ├── FUNDING.yml └── workflows │ └── dependencies.yaml ├── input.css ├── requests.http ├── .mise.toml ├── .gitignore ├── tailwind.config.js ├── src └── clojure_service_template │ ├── utils.clj │ ├── types.clj │ ├── components │ ├── database.clj │ ├── kinde_client.clj │ └── web_server.clj │ ├── queries.clj │ ├── svg.clj │ ├── core.clj │ └── handlers │ ├── weather.clj │ ├── index.clj │ └── kinde.clj ├── test └── clojure_service_template │ ├── http_test.clj │ └── test_support.clj ├── CHANGELOG.md ├── Dockerfile ├── dev └── dev.clj ├── README.md ├── bb.edn ├── project.clj └── LICENSE /resources/public/css/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /http-client.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "local": { 3 | "host": "http://localhost:8080" 4 | } 5 | } -------------------------------------------------------------------------------- /.clj-kondo/metosin/malli/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {malli.experimental/defn schema.core/defn} 2 | :linters {:unresolved-symbol {:exclude [(malli.core/=>)]}}} 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: andfadeev 4 | ko_fi: andreyfadeev 5 | buy_me_a_coffee: andrey.fadeev 6 | -------------------------------------------------------------------------------- /.clj-kondo/hiccup/hiccup/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {hiccup.def/defhtml clojure.core/defn} 2 | :hooks {:analyze-call {hiccup.def/defelem hiccup.hooks/defelem}}} 3 | -------------------------------------------------------------------------------- /resources/database/migrations/V1_1__create_weather_forecaast_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE weather_forecast 2 | ( 3 | time TEXT, 4 | temperature TEXT, 5 | updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 6 | ); 7 | -------------------------------------------------------------------------------- /input.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Titillium+Web:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700&display=swap'); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; -------------------------------------------------------------------------------- /requests.http: -------------------------------------------------------------------------------- 1 | ### GET index page request 2 | 3 | GET {{host}}/ 4 | 5 | ### GET login page request 6 | 7 | GET {{host}}/login 8 | 9 | ### GET weather forecast (example of 3rd party HTTP call) 10 | 11 | GET {{host}}/api/weather/forecast -------------------------------------------------------------------------------- /.mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | clj-kondo = "2024.09.27" 3 | java = "21" 4 | cljfmt = "0.13.0" 5 | babashka = "1.4.192" 6 | lein = "2.10.0" 7 | 8 | [alias] 9 | cljfmt = "https://github.com/b-social/asdf-cljfmt" 10 | lein = "https://github.com/miorimmax/asdf-lein" 11 | -------------------------------------------------------------------------------- /.github/workflows/dependencies.yaml: -------------------------------------------------------------------------------- 1 | name: dependencies 2 | on: 3 | push: 4 | schedule: 5 | - cron: '0 21 * * *' 6 | jobs: 7 | antq: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: liquidz/antq-action@main -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | /.prepl-port 12 | .hgignore 13 | .hg/ 14 | .idea 15 | .clj-kondo 16 | *.iml 17 | resources/public/css/output.css 18 | .lsp -------------------------------------------------------------------------------- /.clj-kondo/com.github.seancorfield/next.jdbc/config.edn: -------------------------------------------------------------------------------- 1 | {:hooks 2 | {:analyze-call 3 | {next.jdbc/with-transaction 4 | hooks.com.github.seancorfield.next-jdbc/with-transaction 5 | next.jdbc/with-transaction+options 6 | hooks.com.github.seancorfield.next-jdbc/with-transaction+options}} 7 | :lint-as {next.jdbc/on-connection clojure.core/with-open 8 | next.jdbc/on-connection+options clojure.core/with-open}} 9 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | // Use safelist for dynamic class names 4 | // safelist: [ 5 | // "bg-red-500", 6 | // "text-lg", 7 | // "font-bold", 8 | // ], 9 | darkMode: 'selector', 10 | content: [ 11 | "./src/**/*.{clj,cljs,cljc}" 12 | ], 13 | theme: { 14 | extend: { 15 | container: { 16 | center: true, 17 | padding: '1rem', 18 | screens: { 19 | DEFAULT: '600px', 20 | }, 21 | }, 22 | fontFamily: { 23 | sans: ['Titillium Web', 'sans-serif'], 24 | }, 25 | }, 26 | }, 27 | plugins: [], 28 | }; 29 | -------------------------------------------------------------------------------- /src/clojure_service_template/utils.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-service-template.utils 2 | (:require [charred.api :as charred] 3 | [hiccup2.core :as html])) 4 | 5 | (defn request->user 6 | [request] 7 | (-> request :session :user)) 8 | 9 | (defn request->database 10 | [request] 11 | (let [database 12 | (-> request 13 | :dependencies 14 | :database)] 15 | (database))) 16 | 17 | (defn json 18 | ([body status] 19 | {:status status 20 | :body (charred/write-json-str body) 21 | :headers {"Content-Type" "application/json"}}) 22 | ([body] 23 | (json body 200))) 24 | 25 | (defn html 26 | ([body] 27 | (html body 200)) 28 | ([body status] 29 | {:status status 30 | :headers {"Content-Type" "text/html"} 31 | :body (-> body (html/html) (str))})) -------------------------------------------------------------------------------- /test/clojure_service_template/http_test.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-service-template.http-test 2 | (:require [charred.api :as charred] 3 | [clojure.test :refer :all] 4 | [clojure-service-template.test-support :as test-support] 5 | [clojure-service-template.core :refer :all] 6 | [clojure-service-template.queries :as queries] 7 | [babashka.http-client :as http])) 8 | 9 | (use-fixtures 10 | :each 11 | test-support/with-postgresql-test-container 12 | (test-support/with-system test-support/test-configuration)) 13 | 14 | (deftest weather-api-test 15 | (is (= 1 (-> (http/get "http://localhost:8080/api/weather/forecast") 16 | :body 17 | (charred/read-json)))) 18 | (is (= [] (queries/get-weather-forecast-entries (test-support/database))))) 19 | -------------------------------------------------------------------------------- /resources/config.edn: -------------------------------------------------------------------------------- 1 | {:web-server {:host #env CST_WEB_SERVER_HOST 2 | :port #long #env CST_WEB_SERVER_PORT 3 | :cookie-store-key [124 39 -128 97 79 -21 34 16 -81 -24 -81 86 -110 62 23 118]} 4 | :database {:user #env CST_DATABASE_USER 5 | :port #long #env CST_DATABASE_PORT 6 | :password #env CST_DATABASE_PASSWORD 7 | :name #env CST_DATABASE_NAME 8 | :host #env CST_DATABASE_HOST} 9 | :weather-api {:base-url #env CST_WEATHER_API_BASE_URL} 10 | :kinde-client {:domain #env CST_KINDE_CLIENT_DOMAIN 11 | :client-id #env CST_KINDE_CLIENT_CLIENT_ID 12 | :client-secret #env CST_KINDE_CLIENT_CLIENT_SECRET 13 | :redirect-uri #env CST_KINDE_CLIENT_REDIRECT_URI 14 | :logout-redirect-uri #env CST_KINDE_CLIENT_LOGOUT_REDIRECT_URI}} -------------------------------------------------------------------------------- /src/clojure_service_template/types.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-service-template.types) 2 | 3 | (def WebServerComponentConfig 4 | [:map {:closed true} 5 | [:host :string] 6 | [:port :int] 7 | [:cookie-store-key [:vector :int]]]) 8 | 9 | (def KindeClientComponentConfig 10 | [:map {:closed true} 11 | [:domain :string] 12 | [:client-id :string] 13 | [:client-secret :string] 14 | [:redirect-uri :string] 15 | [:logout-redirect-uri :string]]) 16 | 17 | (def DatabaseComponentConfig 18 | [:map {:closed true} 19 | [:username :string] 20 | [:password :string] 21 | [:jdbcUrl :string]]) 22 | 23 | (def ServiceConfig 24 | [:map {:closed true} 25 | [:web-server WebServerComponentConfig] 26 | [:database DatabaseComponentConfig] 27 | [:kinde-client KindeClientComponentConfig] 28 | [:weather-api 29 | [:map 30 | [:base-url :string]]]]) -------------------------------------------------------------------------------- /src/clojure_service_template/components/database.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-service-template.components.database 2 | (:require 3 | [clojure.tools.logging :as log] 4 | [clojure-service-template.types :as t] 5 | [next.jdbc.connection :as connection]) 6 | (:import 7 | (com.zaxxer.hikari HikariDataSource) 8 | (org.flywaydb.core Flyway))) 9 | 10 | (defn init-fn 11 | [datasource] 12 | (log/info "Running database migrations...") 13 | (.migrate 14 | (.. (Flyway/configure) 15 | (dataSource datasource) 16 | (locations (into-array String ["classpath:database/migrations"])) 17 | (table "schema_version") 18 | (load)))) 19 | 20 | (defn new-database 21 | {:malli/schema [:=> [:cat t/DatabaseComponentConfig] :any]} 22 | [component-config] 23 | (connection/component 24 | HikariDataSource 25 | (assoc component-config :init-fn init-fn))) 26 | -------------------------------------------------------------------------------- /resources/public/js/theme.js: -------------------------------------------------------------------------------- 1 | function isDark() { 2 | return localStorage.theme === 'dark' 3 | || (!('theme' in localStorage) 4 | && window.matchMedia('(prefers-color-scheme: dark)').matches) 5 | } 6 | 7 | document.addEventListener('alpine:init', () => { 8 | Alpine.store('theme', { 9 | init() { 10 | document.documentElement.classList.toggle('dark', isDark()) 11 | this.dark = isDark() 12 | }, 13 | dark: isDark(), 14 | darkTheme() { 15 | this.dark = true; 16 | localStorage.theme = 'dark'; 17 | document.documentElement.classList.add('dark'); 18 | }, 19 | lightTheme() { 20 | this.dark = false; 21 | localStorage.theme = 'light'; 22 | document.documentElement.classList.remove('dark'); 23 | } 24 | }); 25 | }); 26 | 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased] 5 | ### Changed 6 | - Add a new arity to `make-widget-async` to provide a different widget shape. 7 | 8 | ## [0.1.1] - 2024-06-14 9 | ### Changed 10 | - Documentation on how to make the widgets. 11 | 12 | ### Removed 13 | - `make-widget-sync` - we're all async, all the time. 14 | 15 | ### Fixed 16 | - Fixed widget maker to keep working when daylight savings switches over. 17 | 18 | ## 0.1.0 - 2024-06-14 19 | ### Added 20 | - Files from the new template. 21 | - Widget maker public API - `make-widget-sync`. 22 | 23 | [Unreleased]: https://sourcehost.site/your-name/clojure-service-template/compare/0.1.1...HEAD 24 | [0.1.1]: https://sourcehost.site/your-name/clojure-service-template/compare/0.1.0...0.1.1 25 | -------------------------------------------------------------------------------- /src/clojure_service_template/queries.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-service-template.queries 2 | (:require [honey.sql :as honey] 3 | [next.jdbc :as jdbc])) 4 | 5 | (defn execute! 6 | [{:keys [database]} query] 7 | (jdbc/execute! 8 | database 9 | (honey/format query) 10 | jdbc/unqualified-snake-kebab-opts)) 11 | 12 | (defn execute-one! 13 | [{:keys [database]} query] 14 | (jdbc/execute-one! 15 | database 16 | (honey/format query) 17 | jdbc/unqualified-snake-kebab-opts)) 18 | 19 | (defn get-weather-forecast-entries 20 | [dependencies] 21 | (execute! 22 | dependencies 23 | {:select [:*] 24 | :from :weather-forecast})) 25 | 26 | (defn insert-weather-forecast-entry! 27 | {:malli/schema [:=> [:cat :any :any] :any]} 28 | [dependencies weather-forecast-entry] 29 | (execute-one! 30 | dependencies 31 | {:insert-into :weather-forecast 32 | :values [weather-forecast-entry] 33 | :returning [:*]})) 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.21 AS tailwind-builder 2 | 3 | WORKDIR /app 4 | COPY . . 5 | 6 | RUN apk update 7 | RUN apk upgrade 8 | RUN apk add bash 9 | 10 | RUN apk add gcompat build-base 11 | RUN wget https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 12 | RUN chmod +x tailwindcss-linux-x64 13 | RUN mv tailwindcss-linux-x64 /bin/tailwindcss 14 | RUN alias tailwindcss=/bin/tailwindcss 15 | 16 | RUN /bin/tailwindcss -i ./input.css -o ./resources/public/css/output.css --minify 17 | 18 | FROM clojure:temurin-21-lein-alpine AS clj-builder 19 | RUN mkdir -p /usr/src/app 20 | WORKDIR /usr/src/app 21 | COPY project.clj /usr/src/app/ 22 | RUN lein deps 23 | COPY . /usr/src/app 24 | COPY --from=tailwind-builder /app/resources/public/css/output.css /usr/src/app/resources/public/css/output.css 25 | RUN ["lein", "uberjar"] 26 | 27 | FROM eclipse-temurin:21-alpine AS runtime 28 | COPY --from=clj-builder /usr/src/app/target/uberjar/clojure-service-template-0.1.0-SNAPSHOT-standalone.jar /opt/service/clojure-service-template.jar 29 | RUN apk add --no-cache curl 30 | CMD ["java", "-jar", "/opt/service/clojure-service-template.jar"] -------------------------------------------------------------------------------- /src/clojure_service_template/svg.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-service-template.svg) 2 | 3 | (defn sun 4 | [] 5 | [:svg 6 | {:xmlns "http://www.w3.org/2000/svg" 7 | :width "24" 8 | :height "24" 9 | :fill "none" 10 | :viewBox "0 0 24 24" 11 | :stroke-width "1.5" 12 | :stroke "currentColor" 13 | :class "shrink-0"} 14 | [:path {:stroke-linecap "round" 15 | :stroke-linejoin "round" 16 | :d "M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z"}]]) 17 | 18 | (defn moon 19 | [] 20 | [:svg 21 | {:xmlns "http://www.w3.org/2000/svg" 22 | :width "24" 23 | :height "24" 24 | :fill "none" 25 | :viewBox "0 0 24 24" 26 | :stroke-width "1.5" 27 | :stroke "currentColor" 28 | :class "shrink-0"} 29 | [:path {:stroke-linecap "round" 30 | :stroke-linejoin "round" 31 | :d "M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z"}]]) 32 | -------------------------------------------------------------------------------- /.clj-kondo/hiccup/hiccup/hiccup/hooks.clj: -------------------------------------------------------------------------------- 1 | (ns hiccup.hooks 2 | (:require [clj-kondo.hooks-api :as api] 3 | [clojure.set :as set])) 4 | 5 | ;; See https://github.com/clj-kondo/clj-kondo/blob/master/doc/hooks.md 6 | 7 | (defn- parse-defn [elems] 8 | (let [[fhead fbody] (split-with #(not (or (api/vector-node? %) 9 | (api/list-node? %))) 10 | elems) 11 | arities (if (api/vector-node? (first fbody)) 12 | (list (api/list-node fbody)) 13 | fbody)] 14 | [fhead arities])) 15 | 16 | (defn- count-args [arity] 17 | (let [args (first (api/sexpr arity))] 18 | (if (= '& (fnext (reverse args))) 19 | true ; unbounded args 20 | (count args)))) 21 | 22 | (defn- dummy-arity [arg-count] 23 | (api/list-node 24 | (list 25 | (api/vector-node 26 | (vec (repeat arg-count (api/token-node '_))))))) 27 | 28 | (defn defelem [{:keys [node]}] 29 | (let [[_ & rest] (:children node) 30 | [fhead arities] (parse-defn rest) 31 | arg-counts (set (filter number? (map count-args arities))) 32 | dummy-arg-counts (set/difference (set (map inc arg-counts)) arg-counts) 33 | dummy-arities (for [n dummy-arg-counts] (dummy-arity n))] 34 | {:node 35 | (api/list-node 36 | (list* 37 | (api/token-node 'clojure.core/defn) 38 | (concat fhead arities dummy-arities)))})) 39 | -------------------------------------------------------------------------------- /.clj-kondo/com.github.seancorfield/next.jdbc/hooks/com/github/seancorfield/next_jdbc.clj_kondo: -------------------------------------------------------------------------------- 1 | (ns hooks.com.github.seancorfield.next-jdbc 2 | (:require [clj-kondo.hooks-api :as api])) 3 | 4 | (defn with-transaction 5 | "Expands (with-transaction [tx expr opts] body) 6 | to (let [tx expr] opts body) per clj-kondo examples." 7 | [{:keys [:node]}] 8 | (let [[binding-vec & body] (rest (:children node)) 9 | [sym val opts] (:children binding-vec)] 10 | (when-not (and sym val) 11 | (throw (ex-info "No sym and val provided" {}))) 12 | (let [new-node (api/list-node 13 | (list* 14 | (api/token-node 'let) 15 | (api/vector-node [sym val]) 16 | opts 17 | body))] 18 | {:node new-node}))) 19 | 20 | (defn with-transaction+options 21 | "Expands (with-transaction+options [tx expr opts] body) 22 | to (let [tx expr] opts body) per clj-kondo examples." 23 | [{:keys [:node]}] 24 | (let [[binding-vec & body] (rest (:children node)) 25 | [sym val opts] (:children binding-vec)] 26 | (when-not (and sym val) 27 | (throw (ex-info "No sym and val provided" {}))) 28 | (let [new-node (api/list-node 29 | (list* 30 | (api/token-node 'let) 31 | (api/vector-node [sym val]) 32 | opts 33 | body))] 34 | {:node new-node}))) 35 | -------------------------------------------------------------------------------- /dev/dev.clj: -------------------------------------------------------------------------------- 1 | (ns dev 2 | #_{:clj-kondo/ignore [:unused-referred-var]} 3 | (:require [com.stuartsierra.component.repl :as component-repl :refer [reset stop]] 4 | [clojure-service-template.core :as core] 5 | [malli.dev :as md]) 6 | (:import (org.testcontainers.containers PostgreSQLContainer))) 7 | 8 | (defonce postgresql-test-container 9 | (-> (PostgreSQLContainer. "postgres:16-alpine") 10 | (.withReuse true))) 11 | 12 | (md/start!) 13 | 14 | (component-repl/set-init 15 | (fn [_] 16 | (.start postgresql-test-container) 17 | 18 | (core/build-system 19 | {:web-server {:host "0.0.0.0" 20 | :port 8080 21 | :cookie-store-key [124 39 -128 97 79 -21 34 16 -81 -24 -81 86 -110 62 23 118]} 22 | :database {:jdbcUrl (.getJdbcUrl postgresql-test-container) 23 | :username (.getUsername postgresql-test-container) 24 | :password (.getPassword postgresql-test-container)} 25 | :weather-api {:base-url "https://api.open-meteo.com"} 26 | :kinde-client {:domain "https://andreyfadeev-local.uk.kinde.com" 27 | :client-id "4d1c2c7eb9954ddf94c63191cb7ffe5e" 28 | :client-secret (or (System/getenv "CST_KINDE_CLIENT_CLIENT_SECRET") 29 | "kinde-client-secret") 30 | :redirect-uri "http://localhost:8080/kinde/callback" 31 | :logout-redirect-uri "http://localhost:8080/"}}))) 32 | -------------------------------------------------------------------------------- /src/clojure_service_template/components/kinde_client.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-service-template.components.kinde-client 2 | (:require [clojure-service-template.types :as t] 3 | [com.stuartsierra.component :as component] 4 | [clojure.tools.logging :as log]) 5 | (:import (com.kinde KindeClientBuilder) 6 | (com.kinde.authorization AuthorizationType))) 7 | 8 | (defrecord KindeClientComponent 9 | [component-config] 10 | component/Lifecycle 11 | (start [component] 12 | (log/info "Starting KindeClientComponent") 13 | (let [{:keys [domain 14 | client-id 15 | client-secret 16 | redirect-uri 17 | logout-redirect-uri]} component-config] 18 | (assoc component :kinde-client 19 | (-> (KindeClientBuilder/builder) 20 | (.domain domain) 21 | (.clientId client-id) 22 | (.clientSecret client-secret) 23 | (.redirectUri redirect-uri) 24 | (.logoutRedirectUri logout-redirect-uri) 25 | (.grantType AuthorizationType/CODE) 26 | (.addScope "openid email profile") 27 | (.build))))) 28 | (stop [component] 29 | (log/info "Stopping KindeClientComponent") 30 | (assoc component :kinde-client nil))) 31 | 32 | (defn new-kinde-client 33 | {:malli/schema [:=> [:cat t/KindeClientComponentConfig] :any]} 34 | [component-config] 35 | (map->KindeClientComponent 36 | {:component-config component-config})) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clojure-service-template 2 | 3 | 4 | ### Build TailwindCSS 5 | Template is configured to use `tailwindcss-cli` to build production-ready CSS files: 6 | ```shell 7 | brew install tailwindcss 8 | 9 | bb tailwindcss:build 10 | bb tailwind:watch 11 | bb tailwind:minify 12 | ``` 13 | Build, watch or minify will generate CSS file: `resources/public/css/output.css`. 14 | 15 | By default, it's configured to discover Tailwind classes in clj, cljc, and cljs files: 16 | ```js 17 | content: [ 18 | "./src/**/*.{clj,cljs,cljc}" 19 | ] 20 | ``` 21 | You can change that in the `tailwind.config.js` file, for example, if you have plain HTML with TailwindCSS. 22 | 23 | If you have dynamic class names, something that's not a string-like literal - you'll have to use `safelist` option, example in the `tailwind.config.js`: 24 | ```js 25 | safelist: [ 26 | "bg-red-500", 27 | "text-lg", 28 | "font-bold", 29 | ] 30 | ``` 31 | 32 | ### Developer Experience: 33 | Babashka is used as the tasks runner: 34 | ```shell 35 | bb tasks 36 | 37 | The following tasks are available: 38 | 39 | app:precommit Run all checks and tests 40 | app:lint Lint with `clj-kondo` 41 | app:fmt:check Check code format with `cljfmt` 42 | app:fmt:fix Reformat code with `cljfmt` 43 | app:run Start application 44 | app:run:prod Start application in production 45 | app:test Run all tests 46 | app:start Start local instance of service. 47 | app:antq Check for outdated dependencies with `antq` 48 | app:deps Install all dependencies 49 | tailwind:watch Build TailwindCSS in watch mode 50 | tailwind:build Build TailwindCSS output 51 | tailwind:minify Build & minify TailwindCSS 52 | ``` 53 | 54 | -------------------------------------------------------------------------------- /src/clojure_service_template/core.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-service-template.core 2 | (:gen-class) 3 | (:require [clojure-service-template.types :as t] 4 | [clojure.java.io :as io] 5 | [clojure.tools.logging :as log] 6 | [com.stuartsierra.component :as component] 7 | [clojure-service-template.components 8 | [web-server :as web-server] 9 | [database :as database] 10 | [kinde-client :as kinde-client]] 11 | [aero.core :as aero] 12 | [malli.instrument :as mi])) 13 | 14 | (defn build-system 15 | {:malli/schema [:=> [:cat t/ServiceConfig] :any]} 16 | [config] 17 | (component/system-map 18 | :config config 19 | :kinde-client (kinde-client/new-kinde-client 20 | (:kinde-client config)) 21 | :database (database/new-database 22 | (:database config)) 23 | :web-server (component/using 24 | (web-server/new-web-server 25 | (:web-server config)) 26 | [:database 27 | :kinde-client 28 | :config]))) 29 | 30 | (defn read-config 31 | [] 32 | (-> "config.edn" 33 | (io/resource) 34 | (aero/read-config))) 35 | 36 | (defn start-system 37 | [] 38 | (->> (read-config) 39 | (build-system) 40 | (component/start-system))) 41 | 42 | (defn -main [] 43 | (log/info "Starting system") 44 | (mi/instrument!) 45 | (let [system (start-system)] 46 | (log/info "System started") 47 | (.addShutdownHook 48 | (Runtime/getRuntime) 49 | (new Thread (fn [] 50 | (log/info "Stopping system") 51 | (component/stop-system system) 52 | (log/info "System stopped")))))) 53 | -------------------------------------------------------------------------------- /test/clojure_service_template/test_support.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-service-template.test-support 2 | (:require [clojure-service-template.core :as core] 3 | [clojure.test :refer :all] 4 | [com.stuartsierra.component :as component]) 5 | (:import (org.testcontainers.containers PostgreSQLContainer))) 6 | 7 | (def ^:dynamic *test-system* nil) 8 | 9 | (defn database 10 | [] 11 | {:database ((-> (into {} *test-system*) :database))}) 12 | 13 | (defonce postgresql-test-container 14 | (-> (PostgreSQLContainer. "postgres:16") 15 | (.withReuse true))) 16 | 17 | (defn with-postgresql-test-container 18 | [f] 19 | (.start postgresql-test-container) 20 | (f)) 21 | 22 | (defn test-configuration 23 | [] 24 | {:web-server {:host "0.0.0.0" 25 | :port 8080 26 | :cookie-store-key [124 39 -128 97 79 -21 34 16 -81 -24 -81 86 -110 62 23 118]} 27 | :database {:jdbcUrl (.getJdbcUrl postgresql-test-container) 28 | :username (.getUsername postgresql-test-container) 29 | :password (.getPassword postgresql-test-container)} 30 | :weather-api {:base-url "https://api.open-meteo.com"} 31 | :kinde-client {:domain "https://andreyfadeev-local.uk.kinde.com" 32 | :client-id "***" 33 | :client-secret "***" 34 | :redirect-uri "http://localhost:8080/kinde/callback" 35 | :logout-redirect-uri "http://localhost:8080/"}}) 36 | 37 | (defn with-system 38 | [configuration-fn] 39 | (fn 40 | [test-fn] 41 | (binding [*test-system* (-> (configuration-fn) 42 | (core/build-system) 43 | (component/start-system))] 44 | (try 45 | (test-fn) 46 | (finally 47 | (component/stop *test-system*)))))) 48 | 49 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:tasks 2 | {:init (do (defn sh [& args] 3 | (binding [*out* *err*] 4 | (apply println "+" args)) 5 | (apply shell args)) 6 | (defn tailwindcss 7 | [cmd] 8 | (sh (str "tailwindcss -i input.css -o ./resources/public/css/output.css " cmd)))) 9 | docker:build {:doc "Build Docker image" 10 | :task (sh "docker" "build" "--platform" "linux/amd64" "-t" "clojure-service-template:local" ".")} 11 | app:precommit {:doc "Run all checks and tests" 12 | :depends [app:lint app:fmt:check app:test]} 13 | app:lint {:doc "Lint with `clj-kondo`" 14 | :task (sh "clj-kondo" "--parallel" "--lint" "src:test:dev")} 15 | app:fmt:check {:doc "Check code format with `cljfmt`" 16 | :task (sh "cljfmt" "check")} 17 | app:fmt:fix {:doc "Reformat code with `cljfmt`" 18 | :task (sh "cljfmt" "fix")} 19 | app:run {:doc "Start application" 20 | :task (sh "lein run")} 21 | app:run:prod {:doc "Start application in production" 22 | :task (sh "lein trampoline run")} 23 | app:test {:doc "Run all tests" 24 | :task (sh "lein kaocha")} 25 | app:start {:doc "Start local instance of service." 26 | :task (sh "lein run")} 27 | app:antq {:doc "Check for outdated dependencies with `antq`" 28 | :task (sh "lein antq")} 29 | app:deps {:doc "Install all dependencies" 30 | :task (sh "lein with-profiles +kaocha deps")} 31 | tailwind:watch {:doc "Build TailwindCSS in watch mode" 32 | :task (tailwindcss "--watch")} 33 | tailwind:build {:doc "Build TailwindCSS output" 34 | :task (tailwindcss nil)} 35 | tailwind:minify {:doc "Build & minify TailwindCSS" 36 | :task (tailwindcss "--minify")}}} 37 | -------------------------------------------------------------------------------- /src/clojure_service_template/handlers/weather.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-service-template.handlers.weather 2 | (:require [babashka.http-client :as http-client] 3 | [clojure.string :as str] 4 | [camel-snake-kebab.core :as csk] 5 | [clojure-service-template.utils :as utils] 6 | [charred.api :as charred] 7 | [clojure-service-template.queries :as queries] 8 | [next.jdbc :as jdbc])) 9 | 10 | (defn request->weather-api-base-url 11 | [request] 12 | (-> request :dependencies :config :weather-api :base-url)) 13 | 14 | (defn get-weather-forecast-handler 15 | [request] 16 | (let [uri (str/join "/" [(request->weather-api-base-url request) 17 | "v1" 18 | "forecast"]) 19 | query-params {"hourly" "temperature_2m" 20 | "latitude" "52.52" 21 | "longitude" "13.41"} 22 | response (http-client/get uri {:query-params query-params})] 23 | 24 | (when-not (= 200 (:status response)) 25 | (throw (ex-info "Failed to get weather forecast" 26 | {:uri uri 27 | :query-params query-params 28 | :response response}))) 29 | 30 | (let [response (charred/read-json 31 | (:body response) 32 | :key-fn csk/->kebab-case-keyword) 33 | {:keys [time temperature-2m]} (:hourly response) 34 | weather-forecast-entries (into (sorted-map) 35 | (map (fn [time temperature] 36 | [time temperature]) 37 | time temperature-2m))] 38 | 39 | (jdbc/with-transaction 40 | [tnx (utils/request->database request)] 41 | (let [dependencies (assoc (:dependencies request) :database tnx)] 42 | (doseq [weather-forecast-entry weather-forecast-entries] 43 | (queries/insert-weather-forecast-entry! 44 | dependencies 45 | weather-forecast-entry)))) 46 | 47 | (utils/json {:weather-forecast-entries weather-forecast-entries})))) -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject clojure-service-template "0.1.0-SNAPSHOT" 2 | :description "Opinionated Clojure Service Template" 3 | :url "http://example.com/FIXME" 4 | :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" 5 | :url "https://www.eclipse.org/legal/epl-2.0/"} 6 | :dependencies [[org.clojure/clojure "1.12.1"] 7 | [metosin/reitit "0.9.1"] 8 | [ring/ring-core "1.14.2"] 9 | [ring/ring-jetty-adapter "1.14.2"] 10 | [com.stuartsierra/component "1.1.0"] 11 | [aero "1.1.6"] 12 | [metosin/malli "0.19.1"] 13 | [hiccup/hiccup "2.0.0-RC5"] 14 | [com.kinde/kinde-core "2.0.1"] 15 | 16 | [camel-snake-kebab "0.4.3"] 17 | [org.babashka/http-client "0.4.23"] 18 | [com.cnuernber/charred "1.037"] 19 | 20 | [com.github.seancorfield/next.jdbc "1.3.1048"] 21 | [com.layerware/hugsql "0.5.3"] 22 | [com.github.seancorfield/honeysql "2.7.1310"] 23 | [org.postgresql/postgresql "42.7.7"] 24 | [org.flywaydb/flyway-core "11.9.1"] 25 | [org.flywaydb/flyway-database-postgresql "11.9.1"] 26 | 27 | [com.zaxxer/HikariCP "6.3.0"] 28 | 29 | [org.clojure/tools.logging "1.3.0"] 30 | [org.slf4j/slf4j-api "2.1.0-alpha1"] 31 | [org.slf4j/slf4j-simple "2.1.0-alpha1"]] 32 | :main ^:skip-aot clojure-service-template.core 33 | :target-path "target/%s" 34 | :plugins [[com.github.liquidz/antq "2.11.1276"]] 35 | :antq {} 36 | :repl-options {:init-ns dev} 37 | :profiles {:dev {:dependencies [[com.stuartsierra/component.repl "1.0.0"] 38 | [nrepl/nrepl "1.3.1"] 39 | [org.testcontainers/testcontainers "1.21.1"] 40 | [org.testcontainers/postgresql "1.21.1"]] 41 | :source-paths ["dev"]} 42 | :kaocha {:dependencies [[lambdaisland/kaocha "1.91.1392"]]} 43 | :uberjar {:aot :all 44 | :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}}) 45 | -------------------------------------------------------------------------------- /src/clojure_service_template/handlers/index.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-service-template.handlers.index 2 | (:require [clojure-service-template.utils :as utils] 3 | [hiccup.page :as hiccup-page] 4 | [clojure-service-template.svg :as svg])) 5 | 6 | (defn service-version 7 | [] 8 | (or (System/getenv "SERVICE_VERSION") 9 | (random-uuid))) 10 | 11 | (defn with-service-version 12 | [path] 13 | (str path "?v=" (service-version))) 14 | 15 | (defn layout 16 | [body] 17 | [:head 18 | [:title "Clojure Service Template"] 19 | (hiccup-page/include-css 20 | (with-service-version "/assets/css/output.css")) 21 | [:script {:src (with-service-version "/assets/js/theme.js")}] 22 | [:script {:src "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"}] 23 | [:script {:src "https://unpkg.com/htmx.org@2.0.4" 24 | :integrity "sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" 25 | :crossorigin "anonymous"}] 26 | body]) 27 | 28 | (defn theme-toggle 29 | [] 30 | [:div 31 | [:button {"@click" "$store.theme.lightTheme()" 32 | :class "text-white rounded p-1 hover:text-yellow-500" 33 | :x-show "$store.theme.dark"} 34 | (svg/sun)] 35 | 36 | [:button {"@click" "$store.theme.darkTheme()" 37 | :class "text-gray-400 rounded p-1 hover:text-sky-900" 38 | :x-show "!$store.theme.dark"} 39 | (svg/moon)]]) 40 | 41 | (defn nav 42 | [] 43 | [:nav {:class "flex justify-between container py-5 items-center"} 44 | [:a {:class "text-xl font-bold"} "Clojure Service Template"] 45 | (theme-toggle)]) 46 | 47 | (defn index 48 | [request] 49 | (let [user (utils/request->user request) 50 | body [:body {:class "bg-white dark:bg-black dark:text-white"} 51 | (nav) 52 | [:div.container 53 | (if user 54 | [:div 55 | [:h1.text-lg "Welcome"] 56 | [:div (str user)] 57 | [:div [:a {:href "/logout"} "Logout"]]] 58 | [:div 59 | [:h1 {:class "text-xl font-bold text-green-500 dark:text-red-500"} "Login or Signup"] 60 | [:div [:a {:href "/login"} "Login"]] 61 | [:div [:a {:href "/signup"} "Signup"]]])]]] 62 | (->> body 63 | (layout) 64 | (utils/html)))) 65 | -------------------------------------------------------------------------------- /src/clojure_service_template/components/web_server.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-service-template.components.web-server 2 | (:require [clojure-service-template.types :as t] 3 | [clojure.tools.logging :as log] 4 | [reitit.ring :as ring] 5 | [ring.adapter.jetty :as jetty] 6 | [reitit.ring.middleware.parameters :as parameters] 7 | [ring.middleware.keyword-params :as keyword-params] 8 | [reitit.coercion.malli :as coercion-malli] 9 | [reitit.ring.coercion :as ring-coercion] 10 | [ring.middleware.session :as session] 11 | [ring.middleware.session.cookie :as cookie-store] 12 | [com.stuartsierra.component :as component] 13 | [clojure-service-template.handlers 14 | [kinde :as kinde] 15 | [index :as index] 16 | [weather :as weather]]) 17 | (:import (org.eclipse.jetty.server Server))) 18 | 19 | (defn wrap-dependencies 20 | [handler context] 21 | (fn [request] 22 | (-> request 23 | (assoc :dependencies context) 24 | (handler)))) 25 | 26 | (defn component->cookie-store-key 27 | [component] 28 | (-> component 29 | :component-config 30 | :cookie-store-key 31 | (byte-array))) 32 | 33 | (defn component->cookie-store 34 | [component] 35 | {:cookie-attrs {:secure true 36 | :max-age 604800} 37 | :store (cookie-store/cookie-store 38 | {:key (component->cookie-store-key component)})}) 39 | 40 | (defn app 41 | [component] 42 | (ring/ring-handler 43 | (ring/router 44 | [["/" {:get index/index}] 45 | ["/login" {:get kinde/login}] 46 | ["/signup" {:get kinde/signup}] 47 | ["/logout" {:get kinde/logout}] 48 | ["/api" 49 | ["/weather/forecast" {:get weather/get-weather-forecast-handler}]] 50 | ["/kinde/callback" {:get kinde/kinde-callback}] 51 | ["/assets/*" (ring/create-resource-handler)]] 52 | {:data {:coercion coercion-malli/coercion 53 | :middleware [[wrap-dependencies component] 54 | parameters/parameters-middleware 55 | keyword-params/wrap-keyword-params 56 | ring-coercion/coerce-request-middleware 57 | ring-coercion/coerce-response-middleware 58 | [session/wrap-session 59 | (component->cookie-store component)]]}}) 60 | (ring/routes 61 | (ring/redirect-trailing-slash-handler) 62 | (ring/create-default-handler 63 | {;:not-found error-pages/not-found-page 64 | :method-not-allowed (constantly {:status 405 65 | :body ""}) 66 | :not-acceptable (constantly {:status 406 67 | :body ""})})))) 68 | 69 | (defrecord WebServerComponent 70 | [component-config] 71 | component/Lifecycle 72 | (start [component] 73 | (log/info "Starting WebServerComponent" component-config) 74 | (let [web-server (jetty/run-jetty 75 | (app component) 76 | {:port (:port component-config) 77 | :join? false})] 78 | (assoc component :web-server web-server))) 79 | (stop [{:keys [web-server] :as component}] 80 | (when web-server 81 | (log/info "Stopping WebServerComponent") 82 | (.stop ^Server web-server)) 83 | (assoc component :web-server nil))) 84 | 85 | (defn new-web-server 86 | {:malli/schema [:=> [:cat t/WebServerComponentConfig] :any]} 87 | [component-config] 88 | (map->WebServerComponent 89 | {:component-config component-config})) -------------------------------------------------------------------------------- /src/clojure_service_template/handlers/kinde.clj: -------------------------------------------------------------------------------- 1 | (ns clojure-service-template.handlers.kinde 2 | (:require [jsonista.core :as json] 3 | [ring.util.response :as response]) 4 | (:import (com.kinde KindeClient) 5 | (com.kinde.authorization AuthorizationUrl) 6 | (com.kinde.token KindeTokens) 7 | (com.kinde.user UserInfo) 8 | (com.nimbusds.oauth2.sdk.pkce CodeVerifier) 9 | (java.net URI))) 10 | 11 | (def KINDE_STATE_COOKIE "kinde-state") 12 | 13 | (defn authorization-url->cookie-string 14 | [_request ^AuthorizationUrl authorization-url] 15 | {:value (json/write-value-as-string 16 | {:url (.toString (.getUrl authorization-url)) 17 | :code-verifier (.getValue (.getCodeVerifier authorization-url))})}) 18 | ;; TODO: configure properties for prod 19 | ;:path - the subpath the cookie is valid for 20 | ;:domain - the domain the cookie is valid for 21 | ;:max-age - the maximum age in seconds of the cookie 22 | ;:expires - a date string at which the cookie will expire 23 | ;:secure - set to true if the cookie requires HTTPS, prevent HTTP access 24 | ;:http-only - set to true if the cookie is valid for HTTP and HTTPS only 25 | ;(ie. prevent JavaScript access) 26 | ;:same-site - set to :strict or :lax to set SameSite attribute of the cookie 27 | 28 | (defn request->authorization-url 29 | ^AuthorizationUrl 30 | [request] 31 | (let [state-cookie (json/read-value (get-in request [:cookies KINDE_STATE_COOKIE :value]))] 32 | (AuthorizationUrl. 33 | (.toURL (URI. (get state-cookie "url"))) 34 | (CodeVerifier. (get state-cookie "code-verifier"))))) 35 | 36 | (defn request->kinde-client 37 | ^KindeClient [request] 38 | (-> request :dependencies :kinde-client :kinde-client)) 39 | 40 | (defn login 41 | [request] 42 | (let [kinde-client (request->kinde-client request) 43 | kinde-client-session (.clientSession kinde-client) 44 | authorization-url (.login kinde-client-session)] 45 | 46 | (assoc (response/redirect (.toString (.getUrl authorization-url))) 47 | :cookies {KINDE_STATE_COOKIE (authorization-url->cookie-string request authorization-url)}))) 48 | 49 | (defn signup 50 | [request] 51 | (let [authorization-url (.register 52 | (.clientSession 53 | (request->kinde-client request)))] 54 | (assoc (response/redirect (.toString (.getUrl authorization-url))) 55 | :cookies {KINDE_STATE_COOKIE (authorization-url->cookie-string request authorization-url)}))) 56 | 57 | (defn logout 58 | [request] 59 | (let [authorization-url (.logout 60 | (.clientSession 61 | (request->kinde-client request)))] 62 | 63 | (assoc (response/redirect (.toString (.getUrl authorization-url))) 64 | :session nil))) 65 | 66 | (defn kinde-callback 67 | [request] 68 | (let [{:keys [code]} (:params request) 69 | ^KindeTokens tokens (-> (.initClientSession 70 | (request->kinde-client request) 71 | code (request->authorization-url request)) 72 | (.retrieveTokens)) 73 | ^UserInfo user-info (-> (.initClientSession 74 | (request->kinde-client request) 75 | (.getAccessToken tokens)) 76 | (.retrieveUserInfo))] 77 | (assoc (response/redirect "/") 78 | :session {:user {:id (.getSubject user-info) 79 | :email (.getEmail user-info) 80 | :picture (.getPicture user-info)}}))) 81 | -------------------------------------------------------------------------------- /.clj-kondo/metosin/malli-types-clj/config.edn: -------------------------------------------------------------------------------- 1 | {:linters {:unresolved-symbol {:exclude [(malli.core/=>)]}, 2 | :type-mismatch {:namespaces {clojure-service-template.core {build-system {:arities {1 {:args [{:op :keys, 3 | :req {:web-server {:op :keys, 4 | :req {:host :string, 5 | :port :int, 6 | :cookie-store-key :vector}}, 7 | :database {:op :keys, 8 | :req {:username :string, 9 | :password :string, 10 | :jdbcUrl :string}}, 11 | :kinde-client {:op :keys, 12 | :req {:domain :string, 13 | :client-id :string, 14 | :client-secret :string, 15 | :redirect-uri :string, 16 | :logout-redirect-uri :string}}, 17 | :weather-api {:op :keys, 18 | :req {:base-url :string}}}}], 19 | :ret :any}}}}, 20 | clojure-service-template.queries {insert-weather-forecast-entry! {:arities {2 {:args [:any 21 | :any], 22 | :ret :any}}}}, 23 | clojure-service-template.components.web-server {new-web-server {:arities {1 {:args [{:op :keys, 24 | :req {:host :string, 25 | :port :int, 26 | :cookie-store-key :vector}}], 27 | :ret :any}}}}, 28 | clojure-service-template.components.database {new-database {:arities {1 {:args [{:op :keys, 29 | :req {:username :string, 30 | :password :string, 31 | :jdbcUrl :string}}], 32 | :ret :any}}}}, 33 | clojure-service-template.components.kinde-client {new-kinde-client {:arities {1 {:args [{:op :keys, 34 | :req {:domain :string, 35 | :client-id :string, 36 | :client-secret :string, 37 | :redirect-uri :string, 38 | :logout-redirect-uri :string}}], 39 | :ret :any}}}}}}}} 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: GNU General Public 267 | License as published by the Free Software Foundation, either version 2 268 | of the License, or (at your option) any later version, with the GNU 269 | Classpath Exception which is available at 270 | https://www.gnu.org/software/classpath/license.html." 271 | 272 | Simply including a copy of this Agreement, including this Exhibit A 273 | is not sufficient to license the Source Code under Secondary Licenses. 274 | 275 | If it is not possible or desirable to put the notice in a particular 276 | file, then You may include the notice in a location (such as a LICENSE 277 | file in a relevant directory) where a recipient would be likely to 278 | look for such a notice. 279 | 280 | You may add additional accurate notices of copyright ownership. 281 | --------------------------------------------------------------------------------