diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..2631f9a --- /dev/null +++ b/.envrc @@ -0,0 +1,4 @@ +if ! has nix_direnv_version || ! nix_direnv_version 2.3.0; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.3.0/direnvrc" "sha256-Dmd+j63L84wuzgyjITIfSxSD57Tx7v51DMxVZOsiUD8=" +fi +use flake diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..ca8510a --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,19 @@ +on: + # Allows to run this workflow manually from the Actions tab + workflow_dispatch: + pull_request: + push: + branches: [main] + +jobs: + lints: + name: Build + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + - name: Build package + run: nix build . + - name: Build example + run: nix build --no-write-lock-file ./example#nixosConfigurations.example.config.system.build.toplevel diff --git a/.github/workflows/update-deps-lock.yaml b/.github/workflows/update-deps-lock.yaml new file mode 100644 index 0000000..ad7052a --- /dev/null +++ b/.github/workflows/update-deps-lock.yaml @@ -0,0 +1,23 @@ +on: + # Allows to run this workflow manually from the Actions tab + workflow_dispatch: + push: + paths: deps.edn + +jobs: + ci: + name: Update deps-lock + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + - name: Update deps-lock.json + run: nix run github:jlesquembre/clj-nix#deps-lock + - name: Commit changes + run: | + git config user.name "sohalt" + git config user.email 'sohalt@users.noreply.github.com' + git add deps-lock.json + git commit -m "Update deps-lock.json" || true + git push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c18780 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +.DS_Store +.idea +*.log +tmp/ + +/target +/classes +/checkouts +profiles.clj +pom.xml +pom.xml.asc +*.jar +*.class +/.lein-* +/.nrepl-port +/.prepl-port +/.cpcache + +.clj-kondo/ +.lsp/ +.clerk/ + +.direnv/ + +result diff --git a/README.md b/README.md new file mode 100644 index 0000000..f5773c8 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# Solisplit + +Solisplit is an application to share costs in a more solidaric way. + +See [about](./resources/about.md) for an explanation on how it works. + +## Hosted deployment + +There is a hosted instance at https://solisplit.soha.lt. + +## Self-hosting + +The easiest way to self host is using [NixOS](https://nixos.org/) with [flakes](https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html). +See `example/flake.nix` for an example deployment. + +Alternantively you can also get an executable using `nix build github:sohalt/solisplit`, which you can start any way you like. diff --git a/bb.edn b/bb.edn new file mode 100644 index 0000000..cd73691 --- /dev/null +++ b/bb.edn @@ -0,0 +1,2 @@ +{:tasks + {dev {:task (clojure "-X:dev")}}} diff --git a/deps-lock.json b/deps-lock.json new file mode 100644 index 0000000..c52de1a --- /dev/null +++ b/deps-lock.json @@ -0,0 +1,1153 @@ +{ + "lock-version": 3, + "git-deps": [], + "mvn-deps": [ + { + "mvn-path": "applied-science/js-interop/0.3.3/js-interop-0.3.3.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-1bVv447Y4Lf/IzCzIAeWCBFQcVqbTO69HOwTw4xpy1U=" + }, + { + "mvn-path": "applied-science/js-interop/0.3.3/js-interop-0.3.3.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-lKlMACVEhOobAqY2WatGFygJuCvmvOmb0VqNAJV9ou0=" + }, + { + "mvn-path": "appliedscience/js-interop/0.2.6-MOVED/js-interop-0.2.6-MOVED.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-cfVgSwFP2TBPIhgFqk4rxCu79PZjJf3WZLaSWRwGWis=" + }, + { + "mvn-path": "appliedscience/js-interop/0.2.6-MOVED/js-interop-0.2.6-MOVED.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-hBL9F5QIVRf9YvHEOElzl8w53AwGqQ0iD5oU9gUQkxA=" + }, + { + "mvn-path": "babashka/fs/0.2.14/fs-0.2.14.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-jjtc2JKtS7+FmXshxrDtdVMaEToOa68bot1OGoyJPGE=" + }, + { + "mvn-path": "babashka/fs/0.2.14/fs-0.2.14.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-/o21k35neeAvObbm15KjQcetPquWAvcQjw5CE57HRAU=" + }, + { + "mvn-path": "babashka/process/0.4.16/process-0.4.16.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-tjf2dKOvaLnfbVZhc1CiBvZTiTiO2FQ9h122thL4Uno=" + }, + { + "mvn-path": "babashka/process/0.4.16/process-0.4.16.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-roypsD6aZWNhJICYMm3fJf9U1FSQ9wwIvxgM8r8NtCI=" + }, + { + "mvn-path": "borkdude/dynaload/0.3.5/dynaload-0.3.5.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-CIfAzbmvzs18SW6iWKMuQ6py52bz8GMuG9D1JFyowkw=" + }, + { + "mvn-path": "borkdude/dynaload/0.3.5/dynaload-0.3.5.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-XBwijxopsG0KfQNJD15k+vcTo8YWcpi6Fxz/wzz57Rg=" + }, + { + "mvn-path": "borkdude/edamame/1.0.16/edamame-1.0.16.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-fnSjYI2xlPLzlX8Ci1NGRhTzBN8gIDkLsHPuP6EI1Ls=" + }, + { + "mvn-path": "borkdude/edamame/1.3.23/edamame-1.3.23.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-nm6BmZ66C/z3D61MlxA1TaNV37nSQAwepLpGK9Tv9VA=" + }, + { + "mvn-path": "borkdude/edamame/1.3.23/edamame-1.3.23.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-FSyVB9jpXsj9TeHIQLvurP9BeXwFoi6z5LAPGeUfwp4=" + }, + { + "mvn-path": "com/bhauman/spell-spec/0.1.2/spell-spec-0.1.2.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-ux1waVVJJ2hH61sgWNwXc5LWharNvgky+dWnM+J7/eM=" + }, + { + "mvn-path": "com/bhauman/spell-spec/0.1.2/spell-spec-0.1.2.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-CHzQYPxRUXL5tdXVJbSIzE8VJaslGlo3UCtVQV44fP4=" + }, + { + "mvn-path": "com/cognitect/transit-clj/1.0.324/transit-clj-1.0.324.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-W7OrnSRQkevGWkWLeUF0uV195ZMWdAQJOWYnx386HPM=" + }, + { + "mvn-path": "com/cognitect/transit-clj/1.0.324/transit-clj-1.0.324.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-2KiGaliPynFU05XvzSsW3xy7WHRrUofA/tZ+XcIVPiM=" + }, + { + "mvn-path": "com/cognitect/transit-java/1.0.343/transit-java-1.0.343.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-lbX8MSVmBSvdadyHWd9zh6RY/MThrYAEbjp97Tfx25g=" + }, + { + "mvn-path": "com/cognitect/transit-java/1.0.343/transit-java-1.0.343.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-n3B2zwEVioKorScnBtCtwSMN22AFs+VZ/Yb7Q6N9JwA=" + }, + { + "mvn-path": "com/fasterxml/jackson/core/jackson-annotations/2.15.1/jackson-annotations-2.15.1.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-NEwbND2YGdXdQ5XyUHl3uRuyRjd4zy73sNAXJOyP+rI=" + }, + { + "mvn-path": "com/fasterxml/jackson/core/jackson-annotations/2.15.1/jackson-annotations-2.15.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-DKy1qFN8cteOQSWdbX5PCf/g96+ehsWIU21/WOg34uc=" + }, + { + "mvn-path": "com/fasterxml/jackson/core/jackson-core/2.15.1/jackson-core-2.15.1.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-nZhDGcyXboXEn0DakoAhrNLXc+JZqf+fgy7c2saSDu0=" + }, + { + "mvn-path": "com/fasterxml/jackson/core/jackson-core/2.15.1/jackson-core-2.15.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-fHKqaK699Qz1EBA2TYWJa6gYjRX79O55b4oGFNYTtoY=" + }, + { + "mvn-path": "com/fasterxml/jackson/core/jackson-databind/2.15.1/jackson-databind-2.15.1.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-wfA0fOJpbJT1MdCVzZ7yLmSOwJDpscPe4Xk4sHod8XY=" + }, + { + "mvn-path": "com/fasterxml/jackson/core/jackson-databind/2.15.1/jackson-databind-2.15.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-7cmvJRy1QoPCy0FZ3bM4CZJ/DIkS7UIeKQLC064GIYQ=" + }, + { + "mvn-path": "com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.14.1/jackson-datatype-jsr310-2.14.1.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-C2U2E/UdbhU5bStlzP7JU5sqtHHFhC9bEaxId8vVH3I=" + }, + { + "mvn-path": "com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.14.1/jackson-datatype-jsr310-2.14.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-yRM4B2Gdzeb1Jc1UNJZSk/0BcUccj4R+D6GVu6GttNc=" + }, + { + "mvn-path": "com/fasterxml/jackson/jackson-base/2.14.1/jackson-base-2.14.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-GAFdG6y6mhRiWovxlBH1v62C0AYN83snvQLngTLEZ24=" + }, + { + "mvn-path": "com/fasterxml/jackson/jackson-base/2.15.1/jackson-base-2.15.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-qdc9e+y/QhF0Vow4KrFik2EYD6lvwEHCzUokX2ljsJ0=" + }, + { + "mvn-path": "com/fasterxml/jackson/jackson-bom/2.14.1/jackson-bom-2.14.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-eP35nlBQ/EhfQRfauMzL+2+mxoOF6184oJtlU3HUpsw=" + }, + { + "mvn-path": "com/fasterxml/jackson/jackson-bom/2.15.1/jackson-bom-2.15.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-xRdH0RSll5SfgRQHJ9nmzWPj9DkI82R++qcLGelNqk0=" + }, + { + "mvn-path": "com/fasterxml/jackson/jackson-parent/2.14/jackson-parent-2.14.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-CQat2FWuOfkjV9Y/SFiJsI/KTEOl/kM1ItdTROB1exk=" + }, + { + "mvn-path": "com/fasterxml/jackson/jackson-parent/2.15/jackson-parent-2.15.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-bN+XvGbzifY+NoUNL1UtEhZoj45aWHJ9P2qY7fhnXN4=" + }, + { + "mvn-path": "com/fasterxml/jackson/module/jackson-modules-java8/2.14.1/jackson-modules-java8-2.14.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-CyfB7lgSDFHyo4cXwseUr/qa91wSNNcpe5zkLof8Il4=" + }, + { + "mvn-path": "com/fasterxml/oss-parent/48/oss-parent-48.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-EbuiLYYxgW4JtiOiAHR0U9ZJGmbqyPXAicc9ordJAU8=" + }, + { + "mvn-path": "com/fasterxml/oss-parent/50/oss-parent-50.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-9dpV3XuI+xcMRoAdF3dKZS+y9FgftbHQpfyGqhgrhXc=" + }, + { + "mvn-path": "com/googlecode/json-simple/json-simple/1.1.1/json-simple-1.1.1.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-TmlpaJK4i0HFXUmrL9zCHurZK/VKzFiMAFBZbDt1GZw=" + }, + { + "mvn-path": "com/googlecode/json-simple/json-simple/1.1.1/json-simple-1.1.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-Zl9jWQ3vtj1irdIdNSU2LPk3z2ocBeSwFFuujailf4M=" + }, + { + "mvn-path": "com/ibm/icu/icu4j/70.1/icu4j-70.1.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-K02NTgmOhqpfkF7IHEZ1HSGLFq/T9/wCtk+A3SD/+iA=" + }, + { + "mvn-path": "com/ibm/icu/icu4j/70.1/icu4j-70.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-kWzAJX9qZqGLf3rW7FEKEAL8VyHNsT4pTtu4R1oqgMU=" + }, + { + "mvn-path": "com/nextjournal/beholder/1.0.2/beholder-1.0.2.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-iQOMTbidIDu1sSpfsa2qkpXgWirEsk+u2ZbJNOqJ0Xo=" + }, + { + "mvn-path": "com/nextjournal/beholder/1.0.2/beholder-1.0.2.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-LRJkSGdQPfLoeGjLwU3deFKoaum2dW/QJ46lCK0d4u4=" + }, + { + "mvn-path": "com/pngencoder/pngencoder/0.13.1/pngencoder-0.13.1.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-6V7eMmUDcuOOL92cjW+8fjEiYrbn/S6O04XGwceftwA=" + }, + { + "mvn-path": "com/pngencoder/pngencoder/0.13.1/pngencoder-0.13.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-duLrS3lgIiKuJQIKk+rw1caR2nILsRxA87VuoA073tk=" + }, + { + "mvn-path": "com/taoensso/encore/3.23.0/encore-3.23.0.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-kzQqFBs/jscGs0g/Ta6dmbM2KSsdQCKauyhUzSkli7U=" + }, + { + "mvn-path": "com/taoensso/encore/3.23.0/encore-3.23.0.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-WnwqxaGwJzXsWGO0UZH3/YH+g3fiwcoUXtHQUlG2iWU=" + }, + { + "mvn-path": "com/taoensso/nippy/3.2.0/nippy-3.2.0.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-3p6/MGx+WXe9NzzJMnLw/5uMZ5iY/3m1acH1qwxDqEs=" + }, + { + "mvn-path": "com/taoensso/nippy/3.2.0/nippy-3.2.0.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-jrSUx6+IOAoN9QG89Jm7XUImzYqWloGIMtnYD6GTcYA=" + }, + { + "mvn-path": "com/taoensso/truss/1.6.0/truss-1.6.0.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-TvDkfabocHLAnMezlPJ0jGIvcYTJZrN3AlXMGQPhtXw=" + }, + { + "mvn-path": "com/taoensso/truss/1.6.0/truss-1.6.0.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-J04Z8q0QEU+BVjI0+SNbQmk39CM3A+NKHbZ30oRj8/A=" + }, + { + "mvn-path": "commons-codec/commons-codec/1.15/commons-codec-1.15.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-s+n21jp5AQm/DQVmEfvtHPaQVYJt7+uYlKcTadJG7WM=" + }, + { + "mvn-path": "commons-codec/commons-codec/1.15/commons-codec-1.15.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-yG7hmKNaNxVIeGD0Gcv2Qufk2ehxR3eUfb5qTjogq1g=" + }, + { + "mvn-path": "commons-fileupload/commons-fileupload/1.5/commons-fileupload-1.5.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-Ufez3LTlDHZimU2i9HIxUZ/5lwelx/t7BfTE06FyjBQ=" + }, + { + "mvn-path": "commons-fileupload/commons-fileupload/1.5/commons-fileupload-1.5.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-zHIPHgWDV52f5Tk8iE7kbQ10+Z0fm6AXsXKzHqGJ4rE=" + }, + { + "mvn-path": "commons-io/commons-io/2.11.0/commons-io-2.11.0.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-lhsvbYfbrMXVSr9Fq3puJJX4m3VZiWLYxyPOqbwhCQg=" + }, + { + "mvn-path": "commons-io/commons-io/2.11.0/commons-io-2.11.0.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-LgFv1+MkS18sIKytg02TqkeQSG7h5FZGQTYaPoMe71k=" + }, + { + "mvn-path": "crypto-equality/crypto-equality/1.0.1/crypto-equality-1.0.1.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-7hXMC3bc9z+PyG0C6e/+1xYKimtAsvDp3nLj3SXtS98=" + }, + { + "mvn-path": "crypto-equality/crypto-equality/1.0.1/crypto-equality-1.0.1.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-Yy5XAtXTVhk9GKUN4KJhoZwUqtYIc05GToWjYA509Es=" + }, + { + "mvn-path": "crypto-random/crypto-random/1.2.1/crypto-random-1.2.1.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-XBKIRte2bVPF6XJnc7hy9+AoRP9lGplqXjRSoVI65Sw=" + }, + { + "mvn-path": "crypto-random/crypto-random/1.2.1/crypto-random-1.2.1.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-2OgLA0KFMl6QX1RkmhWYtoe5pKmaOk9LlO7TWXyyEEg=" + }, + { + "mvn-path": "expound/expound/0.9.0/expound-0.9.0.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-1qNyYJkY4DUb+mqL1pPRi8GZ6Lp6r67BHola+uAY+Vw=" + }, + { + "mvn-path": "expound/expound/0.9.0/expound-0.9.0.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-kJSODD3MvE8aCvaABWue2JizAcvtLd4/9CR5eWmXxdk=" + }, + { + "mvn-path": "fi/metosin/reitit-openapi/0.7.0-alpha7/reitit-openapi-0.7.0-alpha7.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-jhMijEyjSwJuzDC4ADP/D2y3oSQxHDCISzEabk0KBFU=" + }, + { + "mvn-path": "fi/metosin/reitit-openapi/0.7.0-alpha7/reitit-openapi-0.7.0-alpha7.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-VH+Gp7a1wBggGPK/8brR1adDOyVSa8Ozd2cGWn5FucY=" + }, + { + "mvn-path": "fipp/fipp/0.6.26/fipp-0.6.26.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-98tpbM5Vr9dMg41UQUGcfl9tSRrxhajlY9+nl5aFcoM=" + }, + { + "mvn-path": "fipp/fipp/0.6.26/fipp-0.6.26.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-p+xjV7gTIRMv0HwvV+/rAhFEFVlDY9g6FDE6GU9fVTU=" + }, + { + "mvn-path": "hiccup/hiccup/2.0.0-RC2/hiccup-2.0.0-RC2.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-N68rIhwWsZRngzn+K4VmYNaVxuAALk1h8Cr7+4Dtw3o=" + }, + { + "mvn-path": "hiccup/hiccup/2.0.0-RC2/hiccup-2.0.0-RC2.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-n3qR8COqb33JF/5OlzXSzTj1kGBZpMrZLWinyMTSyxk=" + }, + { + "mvn-path": "http-kit/http-kit/2.8.0-SNAPSHOT/http-kit-2.8.0-20231011.124634-3.jar", + "snapshot": "http-kit-2.8.0-SNAPSHOT.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-kYQu+xi6N47xBd0TWSTObZrJy8TkiVSpxbZt5mAk8n4=" + }, + { + "mvn-path": "http-kit/http-kit/2.8.0-SNAPSHOT/http-kit-2.8.0-20231011.124634-3.pom", + "snapshot": "http-kit-2.8.0-SNAPSHOT.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-b1qoXjmER6AUB7oQxYFWBf2L/B5oRoW6W9dwOG8Y3pE=" + }, + { + "mvn-path": "io/github/nextjournal/clerk/0.15.957/clerk-0.15.957.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-dAj3f95n5Td8IOm36cuRr32X3ZDgDsZwGyT5Yo1TcQQ=" + }, + { + "mvn-path": "io/github/nextjournal/clerk/0.15.957/clerk-0.15.957.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-HMYRGQ7+N0lFIJZ6bTayknJ5kFXkPsQV6XZVHJGKEso=" + }, + { + "mvn-path": "io/github/nextjournal/markdown/0.5.146/markdown-0.5.146.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-s8GE3caUgO4FnjQIZN4/sfBFDFxe6cbGILWnCnOf3po=" + }, + { + "mvn-path": "io/github/nextjournal/markdown/0.5.146/markdown-0.5.146.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-C/26rL7VyLTBl8vEuNk8kSEtiCsKJsZJiL81mz/k+7Y=" + }, + { + "mvn-path": "io/methvin/directory-watcher/0.17.3/directory-watcher-0.17.3.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-BSPdOKOTpjt15DIP9SMDSgrDzqaX9hUaesiok8MiUx4=" + }, + { + "mvn-path": "io/methvin/directory-watcher/0.17.3/directory-watcher-0.17.3.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-Y/C4zR1No0m2Dfk4kdHgDF0/0sm9M9jlvu0HWijNiFQ=" + }, + { + "mvn-path": "javax/xml/bind/jaxb-api-parent/2.3.0/jaxb-api-parent-2.3.0.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-dreqxkEyadC920e4uON7sxRXicmppC34CSeuta7v+3I=" + }, + { + "mvn-path": "javax/xml/bind/jaxb-api/2.3.0/jaxb-api-2.3.0.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-iDAHmJ03PRnzUrqXkrJd7CHcfQ4gWnEKk6OBUQG7PQM=" + }, + { + "mvn-path": "javax/xml/bind/jaxb-api/2.3.0/jaxb-api-2.3.0.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-Rm9ue5kB6VKuGtJPDpSUpWgJYfBOQiObWuZOjcr2KXo=" + }, + { + "mvn-path": "juji/editscript/0.6.2/editscript-0.6.2.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-CM+3NETqOsMwIoO5qCrnuEWYZqmpQXr8CKEj7kZhYOY=" + }, + { + "mvn-path": "juji/editscript/0.6.2/editscript-0.6.2.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-qP0V3ltZEf0ecySEPCrxNjVYZYiMgNfLOp1mjZttsM0=" + }, + { + "mvn-path": "lambdaisland/deep-diff/0.0-47/deep-diff-0.0-47.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-falL4yyvcE4PHTuanJq/7G2nS1u4BwuU0d+R/Ns9h54=" + }, + { + "mvn-path": "lambdaisland/deep-diff/0.0-47/deep-diff-0.0-47.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-1PXzUUHstr5hI/+oPIQUGogO744bynAw2Zoo5HSjTbE=" + }, + { + "mvn-path": "meta-merge/meta-merge/1.0.0/meta-merge-1.0.0.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-1/7i4+PXBuDlRWLnKqNxIQjXAYahLLwJDhBoBYLrAsc=" + }, + { + "mvn-path": "meta-merge/meta-merge/1.0.0/meta-merge-1.0.0.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-sAogZY/OzCvRNBAx85T1LWjFP7SAxEVBNMyqwgTqWTE=" + }, + { + "mvn-path": "metosin/jsonista/0.3.7/jsonista-0.3.7.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-ggbyC9QlVe49UZTKwAB7GQciX0jNZlJBpXN6HcQaYcY=" + }, + { + "mvn-path": "metosin/jsonista/0.3.7/jsonista-0.3.7.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-J4FWrDxfoVPDI8oQGwJlyoTmBjvGgPEXs3UQ37qVpzg=" + }, + { + "mvn-path": "metosin/malli/0.12.0/malli-0.12.0.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-Lzrin8v1SZgNJ5iuokym3gun4YsBj8UobOINeN4FxyY=" + }, + { + "mvn-path": "metosin/malli/0.12.0/malli-0.12.0.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-Co8/1bBvIr7vb9PVbDsKrP+8wwlHYvkUxTJy4JODLOs=" + }, + { + "mvn-path": "metosin/muuntaja/0.6.8/muuntaja-0.6.8.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-pj41Oi7ezDvwOrzMk5VIZ7HkDTdPOr+vKb0TtBdZWAQ=" + }, + { + "mvn-path": "metosin/muuntaja/0.6.8/muuntaja-0.6.8.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-pOKMydh8sWSV4JAgpctfTqjwZfgCAnR9kb+mRpY+6t8=" + }, + { + "mvn-path": "metosin/reitit-core/0.7.0-alpha7/reitit-core-0.7.0-alpha7.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-61q2xNRaWiimz0pv5JJXLaCALtJRo8Cvp+qVj2pDl68=" + }, + { + "mvn-path": "metosin/reitit-core/0.7.0-alpha7/reitit-core-0.7.0-alpha7.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-ObfRNIslsJHrFl3X8RX0rsGi50jDMjimSrwWqajZ0G4=" + }, + { + "mvn-path": "metosin/reitit-dev/0.7.0-alpha7/reitit-dev-0.7.0-alpha7.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-DLY/7b/wm3DXBzB9hi1pDC4gRD02ZFyPV1LYFR9gUsE=" + }, + { + "mvn-path": "metosin/reitit-dev/0.7.0-alpha7/reitit-dev-0.7.0-alpha7.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-hWtKgTzNmBaUY/h2+qb7S2s7kfLmoUlXwALlr8GJ4rA=" + }, + { + "mvn-path": "metosin/reitit-frontend/0.7.0-alpha7/reitit-frontend-0.7.0-alpha7.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-3NI2u/qW7Ovs2V+tqGpY0P/L4FK0DLCyEED8CMGFn0E=" + }, + { + "mvn-path": "metosin/reitit-frontend/0.7.0-alpha7/reitit-frontend-0.7.0-alpha7.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-cUuR/HJZlxFchkJgD26u8UONrQibVgU7m6/Zm9RjWEo=" + }, + { + "mvn-path": "metosin/reitit-http/0.7.0-alpha7/reitit-http-0.7.0-alpha7.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-s+0kHe3EWB3mCOWO2r1npamVX9ipX/XLANteTvzn6OU=" + }, + { + "mvn-path": "metosin/reitit-http/0.7.0-alpha7/reitit-http-0.7.0-alpha7.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-cb7wPmiqOF2FHDG8kAudD0+zvES5xIGPgCOPQ6efKKo=" + }, + { + "mvn-path": "metosin/reitit-interceptors/0.7.0-alpha7/reitit-interceptors-0.7.0-alpha7.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-Iu0+3lmJpUklNG+fCz6aFTVfhML7IMTR1/d19GbukFI=" + }, + { + "mvn-path": "metosin/reitit-interceptors/0.7.0-alpha7/reitit-interceptors-0.7.0-alpha7.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-zZLgUpqqz+JojttJq/cTwli347ExeSHnndeOaevCEds=" + }, + { + "mvn-path": "metosin/reitit-malli/0.7.0-alpha7/reitit-malli-0.7.0-alpha7.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-s+06fuYXwSb9hE2ZmbXxuaiduBDKqF+ObfCCj1NiIQM=" + }, + { + "mvn-path": "metosin/reitit-malli/0.7.0-alpha7/reitit-malli-0.7.0-alpha7.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-FUV26Ip9rugPW8TeF7wOmvVREtZY9AMDFNl6cDcXoOw=" + }, + { + "mvn-path": "metosin/reitit-middleware/0.7.0-alpha7/reitit-middleware-0.7.0-alpha7.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-/8/qkkCX20xHOalaWT+dKh/PV81Wi7h6C+2NAcFRgPE=" + }, + { + "mvn-path": "metosin/reitit-middleware/0.7.0-alpha7/reitit-middleware-0.7.0-alpha7.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-BbAdVdneheUuhC85oXbkPRrIEIuqf2W7lfw80jjVnAM=" + }, + { + "mvn-path": "metosin/reitit-ring/0.7.0-alpha7/reitit-ring-0.7.0-alpha7.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-9FXwzDTOxe09sxj5xk/ZGibv6n63rDmLLKjB7jjbA7c=" + }, + { + "mvn-path": "metosin/reitit-ring/0.7.0-alpha7/reitit-ring-0.7.0-alpha7.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-oBA5hJGNbdT+rf4dd9Tpa/Ey6gplTQyJyHeTqQHRNVk=" + }, + { + "mvn-path": "metosin/reitit-schema/0.7.0-alpha7/reitit-schema-0.7.0-alpha7.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-6uC5XezXszd8L37c3f8uQDgPFhxoomSffkZDMCwkEv0=" + }, + { + "mvn-path": "metosin/reitit-schema/0.7.0-alpha7/reitit-schema-0.7.0-alpha7.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-dMHrh+jE9QhFyfrVmYv10W1g248P8IK4nwModowab+0=" + }, + { + "mvn-path": "metosin/reitit-sieppari/0.7.0-alpha7/reitit-sieppari-0.7.0-alpha7.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-WJa/Yet1pjXE+Ls0bfhnCPdaZdfi6OSkOMO4pczUUzw=" + }, + { + "mvn-path": "metosin/reitit-sieppari/0.7.0-alpha7/reitit-sieppari-0.7.0-alpha7.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-BTEl5Eqee+oxRZ+RcgQ1hSYbqQuBcOElghlC8Mqq78w=" + }, + { + "mvn-path": "metosin/reitit-spec/0.7.0-alpha7/reitit-spec-0.7.0-alpha7.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-8xXgAew0NN93C5jdAi7kVPNBk861Zg6Q9Mn/ou/IqGE=" + }, + { + "mvn-path": "metosin/reitit-spec/0.7.0-alpha7/reitit-spec-0.7.0-alpha7.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-0zhi8L9t5sOwqoilCxj5pBARKBINMdz7+djJpZDPUH0=" + }, + { + "mvn-path": "metosin/reitit-swagger-ui/0.7.0-alpha7/reitit-swagger-ui-0.7.0-alpha7.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-s0C50+92ARoQs/yW0amTXMh1c415y639YxO1Of+bWqo=" + }, + { + "mvn-path": "metosin/reitit-swagger-ui/0.7.0-alpha7/reitit-swagger-ui-0.7.0-alpha7.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-Wrcov3t87aZ1Dx7MSwf1NR6e/bBIkv1xYEG6J72ptC8=" + }, + { + "mvn-path": "metosin/reitit-swagger/0.7.0-alpha7/reitit-swagger-0.7.0-alpha7.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-/m5iaqaO9DMVqC/mlNtqJmBalWTiqCkt/gkG5iAlheE=" + }, + { + "mvn-path": "metosin/reitit-swagger/0.7.0-alpha7/reitit-swagger-0.7.0-alpha7.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-JM8gRnSNbm7j7XF+f1G+u2wkGPt93EQ898sQ/jopvAU=" + }, + { + "mvn-path": "metosin/reitit/0.7.0-alpha7/reitit-0.7.0-alpha7.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-cwKqNA51kVIoDj5lzC4O3F4vhb/OU9q02vsELhCJvE4=" + }, + { + "mvn-path": "metosin/reitit/0.7.0-alpha7/reitit-0.7.0-alpha7.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-8FkZCdYBhsXUcbxip8rhWAIZpst2uYeCc7E6/bfxu0s=" + }, + { + "mvn-path": "metosin/ring-swagger-ui/4.19.1/ring-swagger-ui-4.19.1.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-rG/DpzkGyiEaR9/oHQt6elSZhp2Y2rDUoxWLG5IgDwc=" + }, + { + "mvn-path": "metosin/ring-swagger-ui/4.19.1/ring-swagger-ui-4.19.1.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-xgjOFrWYbnfMmvEyDaDIZuQ1UgCQFvd4X1ZwIITk10Q=" + }, + { + "mvn-path": "metosin/schema-tools/0.13.1/schema-tools-0.13.1.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-fGQBvcjQyDRqz64GQ2hKTovprY4lb2JFH/Z4aH8jumI=" + }, + { + "mvn-path": "metosin/schema-tools/0.13.1/schema-tools-0.13.1.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-J/pYO3QRubM3IBvnGHiIfhcaB41df1LkuLf9CZHwvpA=" + }, + { + "mvn-path": "metosin/sieppari/0.0.0-alpha13/sieppari-0.0.0-alpha13.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-LTLkMBx8NyTQNAgrG4nwuNbbil/amMYF/rT6TtBvZTc=" + }, + { + "mvn-path": "metosin/sieppari/0.0.0-alpha13/sieppari-0.0.0-alpha13.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-KtIhqq/oyvTwjLfZ7xmcN77k88lPsfdIrLvgImiXj6Q=" + }, + { + "mvn-path": "metosin/spec-tools/0.10.5/spec-tools-0.10.5.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-HELBkNFwpoAIQi+wHMMrgGTVb7/QkuA42PHFky0KPa8=" + }, + { + "mvn-path": "metosin/spec-tools/0.10.5/spec-tools-0.10.5.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-/5Pe3pdc8ciOXM0+LkV645q59U5xCT+6mrx/ShuhBO8=" + }, + { + "mvn-path": "mvxcvi/alphabase/2.1.1/alphabase-2.1.1.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-KvUA/uCHjQrgdJlyduV6VHZf+RgWEn0iJX/8bcUYpro=" + }, + { + "mvn-path": "mvxcvi/alphabase/2.1.1/alphabase-2.1.1.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-91aJaevLtTaHzgyjB+io7slwC2qJvFGtYGPPqgUEmZs=" + }, + { + "mvn-path": "mvxcvi/arrangement/2.1.0/arrangement-2.1.0.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-83JROF0iDfiVHmjVJVq7UZetvL2PxrPT/KhyojOfOcg=" + }, + { + "mvn-path": "mvxcvi/arrangement/2.1.0/arrangement-2.1.0.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-fWiMhmYYFAo78Am00FcK7acJA0h7dlH7VNBHf5TT2Is=" + }, + { + "mvn-path": "mvxcvi/multiformats/0.3.107/multiformats-0.3.107.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-qqmkZTLDzSCtcJKqsh3uzf3LiWwmtthhqyA3IcPmo3g=" + }, + { + "mvn-path": "mvxcvi/multiformats/0.3.107/multiformats-0.3.107.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-SLI4zIC2cWJ+X+uugoYtpq90Yl3rY4vCDqTCmViQ6aQ=" + }, + { + "mvn-path": "mvxcvi/puget/1.1.2/puget-1.1.2.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-7i5fjiJhNogHcc2ZaD1NsjU70gSF++gh9re+jOZmRRU=" + }, + { + "mvn-path": "mvxcvi/puget/1.1.2/puget-1.1.2.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-LjYHVXDmCU2iBMeOwiacuvmPvMJ8uOS4n973O47Br7s=" + }, + { + "mvn-path": "net/java/dev/jna/jna/5.12.1/jna-5.12.1.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-kagUrE9A1g3ukdhC4aith0xiGXmEQD0OPDDTnlXPU7M=" + }, + { + "mvn-path": "net/java/dev/jna/jna/5.12.1/jna-5.12.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-Zf8lhJuthZVUtQMXeS9Wia20UprkAx6aUkYxnLK4U1Y=" + }, + { + "mvn-path": "net/java/jvnet-parent/5/jvnet-parent-5.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-GvaZ+Nndq2f5oNIC+9eRXrA2Klpt/V/8VMr6NGXJywo=" + }, + { + "mvn-path": "org/apache/apache/23/apache-23.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-vBBiTgYj82V3+sVjnKKTbTJA7RUvttjVM6tNJwVDSRw=" + }, + { + "mvn-path": "org/apache/apache/29/apache-29.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-PkkDcXSCC70N9jQgqXclWIY5iVTCoGKR+mH3J6w1s3c=" + }, + { + "mvn-path": "org/apache/commons/commons-parent/52/commons-parent-52.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-ddvo806Y5MP/QtquSi+etMvNO18QR9VEYKzpBtu0UC4=" + }, + { + "mvn-path": "org/apache/commons/commons-parent/56/commons-parent-56.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-VgxwUd3HaOE3LkCHlwdk5MATkDxdxutSwph3Nw2uJpQ=" + }, + { + "mvn-path": "org/babashka/cli/0.7.53/cli-0.7.53.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-n2GpmSZvwc3EFTsZ1S+yOQIAg5pT1zG+P6V6Os3RmQ4=" + }, + { + "mvn-path": "org/babashka/cli/0.7.53/cli-0.7.53.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-vJmtZNNep3lDzOQDp0I+4+h5QkJKVWE16PZ2wiOC9h4=" + }, + { + "mvn-path": "org/clojure/clojure/1.10.3/clojure-1.10.3.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-fxJHLa7Y9rUXSYqqKrE6ViR1w+31FHjkWBzHYemJeaM=" + }, + { + "mvn-path": "org/clojure/clojure/1.10.3/clojure-1.10.3.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-GJwAxDNAdJai+7DsyzeQjJSVXZHq0b5IFWdE7MGBbZQ=" + }, + { + "mvn-path": "org/clojure/clojure/1.11.0/clojure-1.11.0.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-PiH6daB+yd278bK1A1bPGAcQ0DmN6qT0TpHNYwRVWUc=" + }, + { + "mvn-path": "org/clojure/clojure/1.11.0/clojure-1.11.0.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-SQjMS0yeYsmoFJb5PLWsb2lBd8xkXc87jOXkkavOHro=" + }, + { + "mvn-path": "org/clojure/clojure/1.11.1/clojure-1.11.1.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-I4G26UI6tGUVFFWUSQPROlYkPWAGuRlK/Bv0+HEMtN4=" + }, + { + "mvn-path": "org/clojure/clojure/1.11.1/clojure-1.11.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-IMRaGr7b2L4grvk2BQrjGgjBZ0CzL4dAuIOM3pb/y4o=" + }, + { + "mvn-path": "org/clojure/core.cache/1.0.207/core.cache-1.0.207.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-j0TtpTM2iDtGH7nIfkixdDpNgmt9OvsEyI962ZdWXso=" + }, + { + "mvn-path": "org/clojure/core.cache/1.0.207/core.cache-1.0.207.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-cT6lL/F0jRLo7Fj/3Iz7u2vfDs2PFqcQz54plG6wwBs=" + }, + { + "mvn-path": "org/clojure/core.memoize/1.0.236/core.memoize-1.0.236.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-hJz3OP9Z7XTcxsxBxDsiD/Tk014GgItrwbF7kBgmVEE=" + }, + { + "mvn-path": "org/clojure/core.memoize/1.0.236/core.memoize-1.0.236.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-Y06JD65bM83SsVieEfjmt4WU1XQTwC+hF49oi88iCeg=" + }, + { + "mvn-path": "org/clojure/core.rrb-vector/0.0.14/core.rrb-vector-0.0.14.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-6dlGbfN/KRui0bACnQ7GWyj3VZ0l++71oeqsGtKDjdo=" + }, + { + "mvn-path": "org/clojure/core.rrb-vector/0.1.2/core.rrb-vector-0.1.2.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-UfmOunss1C7jDzgmkl3N6HkRZ/dvcSMprlG4gkToE44=" + }, + { + "mvn-path": "org/clojure/core.rrb-vector/0.1.2/core.rrb-vector-0.1.2.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-juK6yvw4QzWMznZRDXMyQhK7NRn61XgE7Oq9w3rFCR8=" + }, + { + "mvn-path": "org/clojure/core.specs.alpha/0.2.56/core.specs.alpha-0.2.56.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-/PRCveArBKhj8vzFjuaiowxM8Mlw99q4VjTwq3ERZrY=" + }, + { + "mvn-path": "org/clojure/core.specs.alpha/0.2.56/core.specs.alpha-0.2.56.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-AarxdIP/HHSCySoHKV1+e8bjszIt9EsptXONAg/wB0A=" + }, + { + "mvn-path": "org/clojure/core.specs.alpha/0.2.62/core.specs.alpha-0.2.62.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-Bu6owHC75FwVhWfkQ0OWgbyMRukSNBT4G/oyukLWy8g=" + }, + { + "mvn-path": "org/clojure/core.specs.alpha/0.2.62/core.specs.alpha-0.2.62.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-F3i70Ti9GFkLgFS+nZGdG+toCfhbduXGKFtn1Ad9MA4=" + }, + { + "mvn-path": "org/clojure/data.json/2.4.0/data.json-2.4.0.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-7D8vmU4e7dQgMTxFK6VRjF9cl75RUt/tVlC8ZhFIat8=" + }, + { + "mvn-path": "org/clojure/data.json/2.4.0/data.json-2.4.0.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-pC6nDxe1F2Zq2EkqG/qRfeXe+se0fFFvbQ1NicJ4DPQ=" + }, + { + "mvn-path": "org/clojure/data.priority-map/1.0.0/data.priority-map-1.0.0.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-sRg+WD9ZJWTm3bDA1SkdqShD5KclMqWAMUQtiNGXy/Q=" + }, + { + "mvn-path": "org/clojure/data.priority-map/1.0.0/data.priority-map-1.0.0.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-oelmVnOmC8I3A8FMT0fAIecgHL8yzUpZ4sJJyJ9bExU=" + }, + { + "mvn-path": "org/clojure/java.classpath/1.0.0/java.classpath-1.0.0.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-wU4OEDBKXlz9LMdC+976wfUpPuxgcML/6JA/tcf+fW8=" + }, + { + "mvn-path": "org/clojure/java.classpath/1.0.0/java.classpath-1.0.0.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-C+AThRRX/CTENM5FU0ZD8iblwQgASGJT/Tc/LglUXig=" + }, + { + "mvn-path": "org/clojure/pom.contrib/0.2.2/pom.contrib-0.2.2.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-4OoifEnFw+MHVM0m/MV75+Telz/kOqXMZmdAHsXBAyM=" + }, + { + "mvn-path": "org/clojure/pom.contrib/0.3.0/pom.contrib-0.3.0.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-fxgrOypUPgV0YL+T/8XpzvasUn3xoTdqfZki6+ee8Rk=" + }, + { + "mvn-path": "org/clojure/pom.contrib/1.0.0/pom.contrib-1.0.0.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-EBH6rlyeSWhY5MZQujNxOr1Gml1S4Arrf1sBoryvR+k=" + }, + { + "mvn-path": "org/clojure/pom.contrib/1.1.0/pom.contrib-1.1.0.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-EOzku1+YKQENwWVh9C67g7ry9HYFtR+RBbkvPKoIlxU=" + }, + { + "mvn-path": "org/clojure/spec.alpha/0.2.194/spec.alpha-0.2.194.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-z2iZ+YUpjGSxPqEplGrZAo3uja3w6rmuGORVAn04JJw=" + }, + { + "mvn-path": "org/clojure/spec.alpha/0.2.194/spec.alpha-0.2.194.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-WhHw4eizwFLmUcSYxpRbRNs1Nb8sGHGf3PZd8fiLE+Y=" + }, + { + "mvn-path": "org/clojure/spec.alpha/0.3.218/spec.alpha-0.3.218.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-Z+yJjrVcZqlXpVJ53YXRN2u5lL2HZosrDeHrO5foquA=" + }, + { + "mvn-path": "org/clojure/spec.alpha/0.3.218/spec.alpha-0.3.218.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-bY3hTDrIdXYMX/kJVi/5hzB3AxxquTnxyxOeFp/pB1g=" + }, + { + "mvn-path": "org/clojure/test.check/1.1.1/test.check-1.1.1.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-lFEYFjssNVpfq5bJ69AShxUlVbsulkH1zZ7Qf+S8UHg=" + }, + { + "mvn-path": "org/clojure/test.check/1.1.1/test.check-1.1.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-lqAC7Edw1pxWlVEJykIewELOc0+jdnf6rgvi8708xxc=" + }, + { + "mvn-path": "org/clojure/tools.analyzer.jvm/1.1.0/tools.analyzer.jvm-1.1.0.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-36tXEmBdmBZdFWbHj91XE2l+FqN5yjD1qdnXJoJljk0=" + }, + { + "mvn-path": "org/clojure/tools.analyzer.jvm/1.1.0/tools.analyzer.jvm-1.1.0.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-oURBkMmdK5+OfSLhJgya+uVu2eiaZMgJl5XYK5CD3jM=" + }, + { + "mvn-path": "org/clojure/tools.analyzer/1.1.0/tools.analyzer-1.1.0.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-E2i2vDvd98OY1XhNEFSPRMTtLXwB6hBawO/enPXg3yE=" + }, + { + "mvn-path": "org/clojure/tools.analyzer/1.1.0/tools.analyzer-1.1.0.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-NyBxL7knYaNclNDuQV1r8VhB70afBzZGd2h1553JtwY=" + }, + { + "mvn-path": "org/clojure/tools.reader/1.3.2/tools.reader-1.3.2.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-sc3Czi6cUhEEfa7M0kuo1bE2yOmYDDiZ6k4rovDZ0dg=" + }, + { + "mvn-path": "org/clojure/tools.reader/1.3.4/tools.reader-1.3.4.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-o6I8B7TIjnvx2Jw2esqFG3CZRdcCfpHT29pIgNz5kXk=" + }, + { + "mvn-path": "org/clojure/tools.reader/1.3.4/tools.reader-1.3.4.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-ShJn32aM7BwpKtBs5bp8PMSS0M/Xsf2TnoeOc4/5WV4=" + }, + { + "mvn-path": "org/clojure/tools.reader/1.3.6/tools.reader-1.3.6.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-EdGzHyxlwzVbKSu5tEuPyv2lS0TaY+NKuXt5qKs7uOA=" + }, + { + "mvn-path": "org/clojure/tools.reader/1.3.6/tools.reader-1.3.6.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-rvXugot8sUocWPRbn4oQ/zQMV2mSXqDvXDXR5J2SC+o=" + }, + { + "mvn-path": "org/flatland/ordered/1.15.11/ordered-1.15.11.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-4pTU/plTQDI6waSfaEYmrWCILMAN7AlvrijCn0BZC+k=" + }, + { + "mvn-path": "org/flatland/ordered/1.15.11/ordered-1.15.11.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-xAT9kGW7+BVkzXv9ADdB7P5YYdboxY8pRCJJ9BucBWQ=" + }, + { + "mvn-path": "org/graalvm/js/js/21.3.2.1/js-21.3.2.1.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-5NMkzVkE0YLyHVuKbkbPKsEYiEgkJ5AnNdKleghHeAk=" + }, + { + "mvn-path": "org/graalvm/js/js/21.3.2.1/js-21.3.2.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-8hc9QiDWGXE4M63bjFK3rT9/zdNpXF2Dt1WEkd5jtUI=" + }, + { + "mvn-path": "org/graalvm/regex/regex/21.3.2.1/regex-21.3.2.1.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-PSNvUrq/9LroRFMAroybCaFCmFy5jqA5zDyK+xkrK3U=" + }, + { + "mvn-path": "org/graalvm/regex/regex/21.3.2.1/regex-21.3.2.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-Fbx4IBGJBxGic+BOcNAfajeUrg4wHz0fbrGTn+h06hQ=" + }, + { + "mvn-path": "org/graalvm/sdk/graal-sdk/21.3.2.1/graal-sdk-21.3.2.1.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-8UA+qWC8q1ub7OzqnpEIy3b6Uz4z9hpC/EJMCXmv2Bk=" + }, + { + "mvn-path": "org/graalvm/sdk/graal-sdk/21.3.2.1/graal-sdk-21.3.2.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-+T+p27J9xXlbXDzWVDtipXwUorZ7WTzkE3b6AvzqXmc=" + }, + { + "mvn-path": "org/graalvm/truffle/truffle-api/21.3.2.1/truffle-api-21.3.2.1.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-TrofDKFcK3h+bQxAnxPi4Ttbxmjpuyx4n7VREc9FOYM=" + }, + { + "mvn-path": "org/graalvm/truffle/truffle-api/21.3.2.1/truffle-api-21.3.2.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-xqWMV7jnK72vFRexggjNBIlJ59p4MAtwgrfGk/qBNy8=" + }, + { + "mvn-path": "org/iq80/snappy/snappy/0.4/snappy-0.4.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-RqDIfVBM6dYGPh/25NIHOP60nYq/hbUHGn0Y308Rusk=" + }, + { + "mvn-path": "org/iq80/snappy/snappy/0.4/snappy-0.4.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-pwnOFxEeQUnZt5pSlWRODNWoNVrsSy70wENqunsl0Io=" + }, + { + "mvn-path": "org/javassist/javassist/3.18.1-GA/javassist-3.18.1-GA.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-P7cSMa/QmLsPk/Xrl6qCkcjQVWN5El5Zb5Lsj5RMYWI=" + }, + { + "mvn-path": "org/javassist/javassist/3.18.1-GA/javassist-3.18.1-GA.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-WrGVb+jEXzXUal5H8yB0TZ/E9YV82pMRs3GJxdNT2g8=" + }, + { + "mvn-path": "org/junit/junit-bom/5.7.2/junit-bom-5.7.2.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-zRSqqGmZH4ICHFhdVw0x/zQry6WLtEIztwGTdxuWSHs=" + }, + { + "mvn-path": "org/junit/junit-bom/5.9.1/junit-bom-5.9.1.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-sWPBz8j8H9WLRXoA1YbATEbphtdZBOnKVMA6l9ZbSWw=" + }, + { + "mvn-path": "org/junit/junit-bom/5.9.2/junit-bom-5.9.2.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-LtB9ZYRRMfUzaoZHbJpAVrWdC1i5gVqzZ5uw82819wU=" + }, + { + "mvn-path": "org/lz4/lz4-java/1.8.0/lz4-java-1.8.0.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-10ozNPs1GVAJszipUfkYID1rvKPR01kDPcM+3Rytye8=" + }, + { + "mvn-path": "org/lz4/lz4-java/1.8.0/lz4-java-1.8.0.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-DbittR4TJFSlxAbeuy8aDfgfk91Z++IMuUcQKZRokDQ=" + }, + { + "mvn-path": "org/msgpack/msgpack/0.6.12/msgpack-0.6.12.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-4JymXYUgSI6ApdxCaEior8z9QPSi6zuWRgQlldO9m14=" + }, + { + "mvn-path": "org/msgpack/msgpack/0.6.12/msgpack-0.6.12.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-lEl9jwL43oFZpbfVE24BD1f12axliGES7O2GlcUFbe4=" + }, + { + "mvn-path": "org/ow2/asm/asm-parent/5.2/asm-parent-5.2.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-y/rNXdgS2xbckQmUAH/ljZQColUH+BocIhjhXCwb7fo=" + }, + { + "mvn-path": "org/ow2/asm/asm/5.2/asm-5.2.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-Pl6g19osUVXvT0cNkJLULeNOP1PbZYnHwH1nIa30uj4=" + }, + { + "mvn-path": "org/ow2/asm/asm/5.2/asm-5.2.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-KJ9/u9ewxXbVKG6K3PjQc1E005Fe/qZDbivjXBH88FA=" + }, + { + "mvn-path": "org/ow2/ow2/1.3/ow2-1.3.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-USFcZ9LAaNi30vb4D1E3KgmAdd7MxEjUvde5h7qDKPs=" + }, + { + "mvn-path": "org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-0+9XXj5JeWeNwBvx3M5RAhSTtNEft/G+itmCh3wWocA=" + }, + { + "mvn-path": "org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-+wRqnCKUN5KLsRwtJ8i113PriiXmDL0lPZhSEN7cJoQ=" + }, + { + "mvn-path": "org/slf4j/slf4j-parent/1.7.36/slf4j-parent-1.7.36.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-uziNN/vN083mTDzt4hg4aTIY3EUfBAQMXfNgp47X6BI=" + }, + { + "mvn-path": "org/sonatype/oss/oss-parent/7/oss-parent-7.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-tR+IZ8kranIkmVV/w6H96ne9+e9XRyL+kM5DailVlFQ=" + }, + { + "mvn-path": "org/tukaani/xz/1.9/xz-1.9.jar", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-IRswbPxE+Plt86Cj3a91uoxSie7XfWDXL4ibuFX1NeU=" + }, + { + "mvn-path": "org/tukaani/xz/1.9/xz-1.9.pom", + "mvn-repo": "https://repo1.maven.org/maven2/", + "hash": "sha256-CTvhsDMxvOKTLWglw36YJy12Ieap6fuTKJoAJRi43Vo=" + }, + { + "mvn-path": "prismatic/schema/1.1.12/schema-1.1.12.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-t/uQ9AKwdu5wKracrzn0SiGq+i4TOZFiWLgsiaeXHbg=" + }, + { + "mvn-path": "prismatic/schema/1.1.12/schema-1.1.12.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-bUkp9AkJtNLYxBjJekPaQnvJQSp5zz61OPFbpxTCJSo=" + }, + { + "mvn-path": "rewrite-clj/rewrite-clj/1.1.45/rewrite-clj-1.1.45.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-RdiztNjxqnlGyHx2VjCu6GWLNjwAjZ+ZzB5YVp68yOA=" + }, + { + "mvn-path": "rewrite-clj/rewrite-clj/1.1.45/rewrite-clj-1.1.45.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-2tGJhWfuZlNy4+tNdHY9JGTHmR0CSC0L9Qionkr7rPI=" + }, + { + "mvn-path": "ring/ring-codec/1.2.0/ring-codec-1.2.0.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-b9itBQBiBVtXtzgRBnH2j0NXdEQ9GCbmL07GTSxFZcI=" + }, + { + "mvn-path": "ring/ring-codec/1.2.0/ring-codec-1.2.0.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-de3pMoKzj49m+yTFILdNGDfQsbtdpUIW+AOglmzp2s4=" + }, + { + "mvn-path": "ring/ring-core/1.10.0/ring-core-1.10.0.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-890nOHNp+7xifHrtBMLP3Vllo0ni6/tU6Lm/lisgxYo=" + }, + { + "mvn-path": "ring/ring-core/1.10.0/ring-core-1.10.0.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-u2TefpyESbB9MB079SA+VCs0GIQcDjTeR3STK1gJosk=" + }, + { + "mvn-path": "tech/droit/clj-diff/1.0.1/clj-diff-1.0.1.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-ZnTq41/jMQHEdHLgs6JGuh2aFwvOzOTuSaxkbcdOpLc=" + }, + { + "mvn-path": "tech/droit/clj-diff/1.0.1/clj-diff-1.0.1.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-V7Sh1HoKMFlY7HNgBEpuF4nKhGJq4wIRKemo1UJDtgo=" + }, + { + "mvn-path": "weavejester/dependency/0.2.1/dependency-0.2.1.jar", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-sK5v+lwZjBQtEPvI3MYVVTHl5CQuz1hxeUcQHS9Hq/Q=" + }, + { + "mvn-path": "weavejester/dependency/0.2.1/dependency-0.2.1.pom", + "mvn-repo": "https://repo.clojars.org/", + "hash": "sha256-aK2mnRAeEby9h4sNiwO3bsmBJebRvr13mcY0XpHdu68=" + } + ] +} diff --git a/deps.edn b/deps.edn new file mode 100644 index 0000000..0b6d20c --- /dev/null +++ b/deps.edn @@ -0,0 +1,11 @@ +{:paths ["src" "resources"] + :deps {hiccup/hiccup {:mvn/version "2.0.0-RC2"} + metosin/reitit {:mvn/version "0.7.0-alpha7"} + http-kit/http-kit {:mvn/version "2.8.0-SNAPSHOT"} + org.babashka/cli {:mvn/version "0.7.53"} + io.github.nextjournal/markdown {:mvn/version "0.5.146"}} + :aliases + {:dev {:extra-deps {io.github.nextjournal/clerk {:mvn/version "0.15.957"}} + :extra-paths ["dev"]} + :test {:extra-deps {} + :extra-paths ["test"]}}} diff --git a/dev/git-hooks/pre-commit b/dev/git-hooks/pre-commit new file mode 100755 index 0000000..3a59795 --- /dev/null +++ b/dev/git-hooks/pre-commit @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +# Automatically update deps-lock.json when committing a changed deps.edn + +# Redirect output to stderr. +exec 1>&2 + +if ! git diff --quiet --cached -- deps.edn; then + if which deps-lock > /dev/null; then + if ! deps-lock --deps-include ./deps.edn; then + echo "Failed to create deps-lock.json" + echo "Aborting commit" + exit 1 + fi + git add deps-lock.json + else + echo "WARN: Updated deps.edn but cannot find deps-lock command to update deps-lock.json" + fi +fi diff --git a/dev/user.clj b/dev/user.clj new file mode 100644 index 0000000..bc4ce2d --- /dev/null +++ b/dev/user.clj @@ -0,0 +1,10 @@ +(ns user + (:require [nextjournal.clerk :as clerk] + [net.sohalt.solisplit.main :as main] + [org.httpkit.server :as server])) + +(clerk/serve! {:browse? true}) +(clerk/show! 'nextjournal.clerk.tap) +(let [server (main/-main) + port (server/server-port server)] + (clojure.java.browse/browse-url (format "http://localhost:%s" port))) diff --git a/example/flake.nix b/example/flake.nix new file mode 100644 index 0000000..9869e0b --- /dev/null +++ b/example/flake.nix @@ -0,0 +1,40 @@ +{ + description = "An example deployment of solisplit using caddy as a reverse proxy"; + #inputs.solisplit.url = "github:sohalt/solisplit"; # <- use this in your standalone flake + inputs.solisplit.url = ".."; # <- refering to the flake in parent directory + outputs = { + nixpkgs, + solisplit, + ... + }: { + nixosConfigurations.example = nixpkgs.lib.nixosSystem { + modules = [ + ({config, ...}: { + # --- genenric setup --- + # make a container config, so we don't need to think about bootloader, filesystems, etc + boot.isContainer = true; + nixpkgs.hostPlatform = "x86_64-linux"; + + # --- import flake --- + # make services.solisplit module available + imports = [solisplit.nixosModules.solisplit]; + # make solisplit package available + nixpkgs.overlays = [solisplit.overlays.default]; + + # --- configure service --- + # enable solisplit + services.solisplit.enable = true; + # configure reverse proxy + services.caddy = { + enable = true; + virtualHosts = { + "solisplit.example.com" = { + extraConfig = "reverse_proxy 127.0.0.1:${toString config.services.solisplit.port}"; + }; + }; + }; + }) + ]; + }; + }; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..fff5004 --- /dev/null +++ b/flake.lock @@ -0,0 +1,229 @@ +{ + "nodes": { + "clj-nix": { + "inputs": { + "devshell": "devshell", + "nix-fetcher-data": "nix-fetcher-data", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1695760857, + "narHash": "sha256-cVKY2YyI/hKM+muWRtE8yuw+hXyGupv4OtvQJaDS7rk=", + "owner": "jlesquembre", + "repo": "clj-nix", + "rev": "51caf63bfac477f98450694141dbc894b0a27614", + "type": "github" + }, + "original": { + "owner": "jlesquembre", + "repo": "clj-nix", + "type": "github" + } + }, + "devshell": { + "inputs": { + "nixpkgs": [ + "clj-nix", + "nixpkgs" + ], + "systems": "systems" + }, + "locked": { + "lastModified": 1695195896, + "narHash": "sha256-pq9q7YsGXnQzJFkR5284TmxrLNFc0wo4NQ/a5E93CQU=", + "owner": "numtide", + "repo": "devshell", + "rev": "05d40d17bf3459606316e3e9ec683b784ff28f16", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, + "flake-part": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1685546676, + "narHash": "sha256-XDbjJyAg6odX5Vj0Q22iI/gQuFvEkv9kamsSbQ+npaI=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "6ef2707776c6379bc727faf3f83c0dd60b06e0c6", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib_2" + }, + "locked": { + "lastModified": 1685546676, + "narHash": "sha256-XDbjJyAg6odX5Vj0Q22iI/gQuFvEkv9kamsSbQ+npaI=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "6ef2707776c6379bc727faf3f83c0dd60b06e0c6", + "type": "github" + }, + "original": { + "id": "flake-parts", + "type": "indirect" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nix-fetcher-data": { + "inputs": { + "flake-part": "flake-part", + "flake-parts": "flake-parts", + "nixpkgs": [ + "clj-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1685572850, + "narHash": "sha256-lYKEqFG9F84xu51H1rM1u+Ip88cINL0+W26sT+vFEZc=", + "owner": "jlesquembre", + "repo": "nix-fetcher-data", + "rev": "f14967db6c92c79b77419f52c22a698518c91120", + "type": "github" + }, + "original": { + "owner": "jlesquembre", + "repo": "nix-fetcher-data", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1695644571, + "narHash": "sha256-asS9dCCdlt1lPq0DLwkVBbVoEKuEuz+Zi3DG7pR/RxA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "6500b4580c2a1f3d0f980d32d285739d8e156d92", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "dir": "lib", + "lastModified": 1682879489, + "narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0", + "type": "github" + }, + "original": { + "dir": "lib", + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib_2": { + "locked": { + "dir": "lib", + "lastModified": 1682879489, + "narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0", + "type": "github" + }, + "original": { + "dir": "lib", + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1697059129, + "narHash": "sha256-9NJcFF9CEYPvHJ5ckE8kvINvI84SZZ87PvqMbH6pro0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5e4c2ada4fcd54b99d56d7bd62f384511a7e2593", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "clj-nix": "clj-nix", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs_2" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..dfbeb48 --- /dev/null +++ b/flake.nix @@ -0,0 +1,50 @@ +{ + description = "A webapp to split cost on a pay-what-you-can basis"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + clj-nix.url = "github:jlesquembre/clj-nix"; + }; + + outputs = { + self, + nixpkgs, + flake-utils, + clj-nix, + }: + flake-utils.lib.eachDefaultSystem (system: let + pkgs = import nixpkgs { + inherit system; + overlays = [self.overlays.default]; + }; + in { + packages = { + default = pkgs.solisplit; + solisplit = pkgs.solisplit; + }; + devShells.default = pkgs.mkShell { + packages = with pkgs; [ + clojure + babashka + clj-nix.packages.${system}.deps-lock + ]; + }; + }) + // { + overlays.default = final: prev: { + solisplit = clj-nix.lib.mkCljApp { + pkgs = final; + modules = [ + { + projectSrc = ./.; + name = "net.sohalt/solisplit"; + main-ns = "net.sohalt.solisplit.main"; + java-opts = ["-Dversion=\"${self.shortRev or "dev"}\""]; + } + ]; + }; + }; + nixosModules.solisplit = import ./module/solisplit.nix; + }; +} diff --git a/module/solisplit.nix b/module/solisplit.nix new file mode 100644 index 0000000..f4e914d --- /dev/null +++ b/module/solisplit.nix @@ -0,0 +1,30 @@ +{ + config, + lib, + pkgs, + ... +}: let + cfg = config.services.solisplit; +in + with lib; { + options.services.solisplit = { + enable = mkEnableOption "solisplit"; + port = mkOption { + type = types.port; + default = 8000; + description = "port to listen on"; + }; + server-address = mkOption { + type = types.str; + description = "Full address (including scheme) where the service is running."; + example = "https://solisplit.example.com"; + }; + }; + config = mkIf cfg.enable { + systemd.services.solisplit = { + wantedBy = ["multi-user.target"]; + script = "${pkgs.solisplit}/bin/solisplit --port ${toString cfg.port}"; + environment.SERVER_ADDRESS = cfg.server-address; + }; + }; + } diff --git a/resources/about.md b/resources/about.md new file mode 100644 index 0000000..a205a0b --- /dev/null +++ b/resources/about.md @@ -0,0 +1,29 @@ +# Solisplit + +Solisplit is an application to share costs in a more solidaric way. + +## How it works + +1. You enter the total cost that you want to split. +2. You enter the names of everyone who wants to share the cost. +3. Everyone gets a personalized link to a form where they can enter the maximum amount they would be willing to contribute to the shared expense. +4. After everyone has entered their answer, you can check the result to see the actual share that everyone should pay: + - if the sum of everyone's maximum contribution exceeds the total cost that you want to split, the total cost is divided among everyone, proportional to their answers. This means that everyone pays at most the amount they were comfortable with and the cost is split fairly, based on how much someone was willing to contribute. + - if the sum of everyone's maximum contribution does not reach the total cost, the program cannot calculate a fair distribution and instead shows how much more money would be needed to reach the total cost. You can then start over from the beginning and hope that people will increase their maximum contribution or try reducing the total cost if your situation allows for it. + +### Example + +Let's say Alice, Bob, and Charlie live together in a shared appartment and want to buy a new fridge for 600€. + +Alice offers to pay up to 250€, Bob offers 150€ and Charlie offers 150€. +Since the total of 250+150+150 = 550 is below the required 600, we cannot compute a fair distribution and instead show that they are 50€ short of reaching their total. + +The three decide to do a second round and hope that everyone can increase their share a bit. + +This time Alice offers to pay up to 400€, Bob and Charlie each offer to pay up to 200€. Since the total of 400+200+200 = 800 exceeds the required 600, we now split the 600 proportional to the offers. This results in the following shares: + +- Alice: 600 * 400/800 = 300 +- Bob: 600 * 200/800 = 150 +- Charlie: 600 * 200/800 = 150 + +As we can see Bob and Charlie pay the same, since they entered the same maximum. Alice pays double what Bob or Charlie are paying, since her maximum was twice that of Bob or Charlie. Also everyone pays less than their stated maximum and the sum of everyone's share adds up to the required total. diff --git a/resources/public/js/main.js b/resources/public/js/main.js new file mode 100644 index 0000000..2e005f8 --- /dev/null +++ b/resources/public/js/main.js @@ -0,0 +1,20 @@ +window.onload = () =>{ + let names = document.getElementById("names"); + let row = names.children[0]; + let input = row.querySelector("input"); + input.addEventListener("input",onChange); + let rowTemplate = row.cloneNode(true); + rowTemplate.querySelector("input").removeAttribute("required"); + + function addRow() { + let row = rowTemplate.cloneNode(true); + let input = row.querySelector("input"); + input.addEventListener("input",onChange); + names.appendChild(row) + } + + function onChange(e){ + e.target.removeEventListener("input", onChange); + addRow(); + } +}; diff --git a/src/net/sohalt/solisplit/about.clj b/src/net/sohalt/solisplit/about.clj new file mode 100644 index 0000000..86b7eff --- /dev/null +++ b/src/net/sohalt/solisplit/about.clj @@ -0,0 +1,19 @@ +(ns net.sohalt.solisplit.about + (:require [clojure.java.io :as io] + [clojure.walk :as walk] + [nextjournal.markdown :as md] + [nextjournal.markdown.transform :as transform])) + +(defn remove-plain-nodes [ast] + (walk/postwalk (fn [x] (if (map? x) + (let [{:keys [type content]} x] + (if (= type :plain) + (first content) + x)) + x)) ast)) +(defn project-description [] + (-> (io/resource "about.md") + slurp + md/parse + remove-plain-nodes + transform/->hiccup)) diff --git a/src/net/sohalt/solisplit/main.clj b/src/net/sohalt/solisplit/main.clj new file mode 100644 index 0000000..fa3a120 --- /dev/null +++ b/src/net/sohalt/solisplit/main.clj @@ -0,0 +1,24 @@ +(ns net.sohalt.solisplit.main + (:require + [babashka.cli :as cli] + [org.httpkit.server :as server] + [net.sohalt.solisplit.routes :as routes]) + (:gen-class)) + +(defonce !server (atom nil)) + +(defn start! [options] + (if (and (some? @!server) (= :running (server/server-status @!server))) + {:error "Server already running"} + (let [server (reset! !server (server/run-server #'routes/dev-app (merge options {:legacy-return-value? false})))] + (println (format "Started server on port %s" (server/server-port server))) + server))) + +(defn stop! [] + (if (not= :running (server/server-status @!server)) + {:error "Server not running"} + (server/server-stop! @!server))) + +(defn -main [& args] + (println (format "Starting solisplit (version %s)" (or (System/getProperty "version") "dev"))) + (start! (cli/parse-opts (or *command-line-args* args)))) diff --git a/src/net/sohalt/solisplit/routes.clj b/src/net/sohalt/solisplit/routes.clj new file mode 100644 index 0000000..b8722d6 --- /dev/null +++ b/src/net/sohalt/solisplit/routes.clj @@ -0,0 +1,367 @@ +(ns net.sohalt.solisplit.routes + (:require + [hiccup.def :refer [defelem]] + [hiccup.page :as page] + [reitit.core :as r] + [reitit.ring :as rr] + [ring.middleware.params :as params] + [ring.middleware.content-type :as ct] + [clojure.string :as str] + [net.sohalt.solisplit.about :as about])) + +(defonce !shares (atom {})) +(defonce !person->share (atom {})) + +(def server-address (or (System/getenv "SERVER_ADDRESS") "http://localhost:8090")) + +(defn person->share [person-id] + (let [share-id (@!person->share person-id)] + (@!shares share-id))) + +(comment + (reset! !shares {}) + (reset! !person->share {})) + +(defn add-share [shares {:as share :keys [id]}] + (assoc shares id share)) + +(defn currency? [x] + ;FIXME use precise type + (double? x)) + +(defn parse-currency [s] + (parse-double s)) + +(def currency-symbol "€") + +(defn format-currency [x] + (format "%.2f%s" (double x) currency-symbol)) + +(comment + (format-currency 0) + (let [x (double (/ (rand-int 1000) 100))] + (= x (parse-currency (format-currency x))))) + +(defelem currency-input + ([name] + (currency-input name nil)) + ([name placeholder] + [:input {:type "number" + :name name + :placeholder placeholder + :step "any" + :min 0 + :class ["rounded" "border-2" "border-dotted" "p-2"]}])) + +(defelem link [to text] + [:a {:href to + :class ["underline"]} text]) + +(defelem label [name text] + [:label {:for name} text]) + +(defelem text-field + ([name] + (text-field name nil)) + ([name value] + [:input {:type "text", + :name name, + :id name, + :placeholder name, + :value value, + :class ["rounded" "border-2" "border-dotted" "p-2"]}])) + +(defelem text-area + ([name] + (text-area name nil)) + ([name value] + [:textarea {:name name, + :id name, + :placeholder name, + :value value, + :class ["rounded" "border-2" "border-dotted" "p-2"]}])) + +(defelem button [text] + [:input.rounded-lg.border.p-2 {:type "submit" :value text}]) + +(defn redirect [target] + {:status 302 + :headers {"Location" target}}) + +(defn redirect-to-share [{:keys [id]}] + (redirect (str "/share/" id "/"))) + +(defn create-person [name] + (let [id (random-uuid)] + {:id id + :name name})) + +(defn create-share [{:as share :keys [total names]}] + (assert (currency? total) "A project needs a total") + (assert (seq names) "A project needs at least one person") + (let [id (random-uuid)] + (-> share + (dissoc :names) + (assoc :id id) + (assoc :people (into {} (map (fn [name] + (let [{:as person :keys [id]} (create-person name)] + [id person])) + names)))))) + +(defn header [] + [:h1.bg-teal-800.text-white.text-center.text-4xl.p-2 [:a {:href "/"} "Solisplit"]]) + +(defn html-response [body] + {:status 200 + :headers {"Content-Type" "text/html; charset=utf-8"} + :body body}) + +(defn not-found-response [] + {:status 404 + :body "not found"}) + +(defn handle-healthcheck [req] + {:status 200}) + +(defmacro page [& body] + `(page/html5 + (page/include-js "/js/main.js") + (page/include-js "https://cdn.tailwindcss.com") + [:meta {:name "viewport" + :content "width=device-width, initial-scale=1"}] + (header) + [:div.w-full.flex.flex-row.justify-center.bg-grey-100 + [:div.max-w-5xl.align-self-center.drop-shadow.bg-white.rounded-b.p-5 + ~@body]])) + +(defn create-share-form [] + [:form.flex.flex-col.md:grid.md:grid-cols-2.font-sans.gap-2 {:method "post"} + (label {:class ["mt-3"]} "title" "What do you want to split the cost for?") + (text-field "title") + (label {:class ["mt-3"]} "description" "If you want you can provide a bit more detail:") + (text-area "description") + (label {:class ["mt-3"]} "total" "What is the total cost that you want to split?") + (currency-input {:required true} "total" "total") + [:p.col-span-2.mt-3 "What are the names of the people you want to split the expense with?"] + [:div#names.col-start-2 + [:div.mb-2 (text-field {:class (concat ["p-2" "w-full"] ["rounded" "border-2" "border-dotted" "p-2"]) + :required true} "name")]] + (button {:class ["col-span-2" "bg-teal-800" "text-white"]} "create")]) + +(defn handle-create-share [{:as req :keys [form-params]}] + (let [{:strs [title description total name]} form-params + total (parse-currency total) + names (filter (comp not str/blank?) name) + share (create-share (cond-> {:total total + :names names} + (not (str/blank? title)) (assoc :title title ) + (not (str/blank? description)) (assoc :description description))) + people-ids (keys (:people share))] + (swap! !shares add-share share) + (swap! !person->share merge (into {} (map (fn [person-id] [person-id (:id share)]) people-ids))) + (redirect-to-share share))) + +(defn create-project-form [req] + (html-response + (page + [:div.text-sm] + (create-share-form)))) + +(defn everyone-submitted-bid? [{:keys [people]}] + (every? :bid (vals people))) + +(defn share-view [router {:as share :keys [title description total people]}] + [:div.flex.flex-col.font-medium.font-sans + [:p.pb-5 (str "We want to split " (format-currency total) (when title (str " for " title ".")))] + (when description [:p.pb-5 description]) + [:p.pb-5 "Share the following links with the people you want to split the cost with:"] + [:div.grid.gap-5 {:style {"grid-template-columns" "auto auto"}} + (mapcat (fn [{:keys [id bid name]}] + [[:p (str "Link for " name) (when bid [:span.text-sm "(submitted)"]) ":"] + [:p (let [person-link (str server-address (r/match->path (r/match-by-name router :person {:person-id (str id)})))] + (link person-link person-link))]]) + (vals people))]]) + +(defn handle-view-share [{::r/keys [router] :keys [path-params]}] + (let [id (parse-uuid (:share-id path-params))] + (if-let [share (@!shares id)] + (html-response (page (share-view router share))) + (not-found-response)))) + +(defn deep-merge [a b] + (reduce (fn [acc k] (update acc k merge (b k))) a (keys a))) + +(comment + (deep-merge {"foo" {:id "foo" :name "foo"} + "bar" {:id "bar" :name "bar"}} + {"foo" {:bid 2}}) + (deep-merge {"foo" {:id "foo" :name "foo"} + "bar" {:id "bar" :name "bar"}} + {"foo" {:bid 2} + "bar" {:bid 3}}) + (deep-merge {"foo" {:id "foo" :name "foo"} + "bar" {:id "bar" :name "bar"}} + {"foo" {:bid 2} + "baz" {:bid 3}})) + +(defn handle-update-share [{:keys [path-params form-params]}] + (let [id (parse-uuid (:share-id path-params)) + bids (into {} (filter identity (map (fn [[id bid]] (when-let [bid (parse-currency bid)] + [(parse-uuid id) {:bid bid}])) + form-params)))] + (if-let [share (@!shares id)] + (do + (swap! !shares (fn [shares] (update-in shares [id :people] deep-merge bids))) + (redirect-to-share share)) + (not-found-response)))) + +(defn total-committed [{:keys [people]}] + (reduce + (map :bid (vals people)))) + +(defn goal-reached? [{:as share :keys [total]}] + (>= (total-committed share) total)) + +(defn compute-distribution [{:as share :keys [total people]}] + (assert (goal-reached? share)) + (let [tc (total-committed share) + ratio (/ total tc)] + (into {} (map (fn [[id {:keys [bid]}]] [id (* bid ratio)]) people)))) + +(comment + (def share {:id #uuid "3552075c-6258-4c76-98c4-5333b707dc89" + :total 7 + :people {#uuid "ec0f24ac-0bda-4bb7-b286-dd364c2c8af1" + {:id #uuid "ec0f24ac-0bda-4bb7-b286-dd364c2c8af1" + :name "foo" + :bid 2} + #uuid "d7a3f660-ffc9-4cf4-a51c-4fa0f5877d04" + {:id #uuid "d7a3f660-ffc9-4cf4-a51c-4fa0f5877d04" + :name "bar" + :bid 3}}}) + (def share2 (update share :people assoc #uuid "c455da09-ee73-4ca0-b652-c741231c1627" + {:id #uuid "c455da09-ee73-4ca0-b652-c741231c1627" + :name "baz" + :bid 2})) + (= 5 (total-committed share)) + (= false (goal-reached? share)) + (= true (goal-reached? share2)) + (= {#uuid "ec0f24ac-0bda-4bb7-b286-dd364c2c8af1" 2, + #uuid "d7a3f660-ffc9-4cf4-a51c-4fa0f5877d04" 3, + #uuid "c455da09-ee73-4ca0-b652-c741231c1627" 2} + (compute-distribution share2)) + (render-distribution share2)) + +(defn render-distribution [{:as share :keys [people]}] + (into [:div.flex.flex-col.font-medium.font-sans + [:p "Here is what everyone should pay:"]] + (for [[id contribution] (compute-distribution share)] + (let [name (get-in share [:people id :name])] + [:div.flex.flex-row.mb-2 + [:span.flex-1 name] [:span.flex-1 (format-currency contribution)]])))) + +(defn render-not-reached [{:as share :keys [total people]}] + (let [tc (total-committed share) + missing (- total tc) + missing-per-person (/ missing (count people))] + [:div [:p (str "We are " (format-currency missing) " short (" (format-currency missing-per-person) " per person, when splitting equally).")]])) + +(defn handle-check [{:keys [path-params]}] + (let [id (parse-uuid (:share-id path-params))] + (if-let [{:as share :keys [people]} (@!shares id)] + (html-response + (page + (if (everyone-submitted-bid? share) + (if (goal-reached? share) + (render-distribution share) + (render-not-reached share)) + [:p "Not everyone has submitted a bid yet"]))) + (not-found-response)))) + +(defn render-contribution-form [{:keys [name bid]} {:keys [title description total people]}] + [:div.max-w-3xl + [:p.pb-2 "Hello " name "!"] + (when bid + (list [:p.pb-2 (str "Thank you " name " for your submission. Check back later to see what share you should pay.")] + [:p.pb-2 "If you want to update the amount you'd be willing to contribute, you can resubmit the form below."])) + [:p.pb-2 (str "We want to split " (format-currency total) " among " (count people) " people " (str " (" (format-currency (/ total (count people))) " per person, when splitting equally)") (when title (str " for " title)) ".")] + (when description [:p.pb-2 description]) + [:form.flex.flex-col.md:grid.md:grid-cols-2.font-sans.gap-2 + {:method "post"} + (label "bid" [:div.flex.flex-col [:span "How much would you be willing to contribute?"] [:span.text-sm "(you will probably end up paying a bit less)"]]) + (currency-input {:required true} "bid") + (button {:class ["col-span-2" "bg-teal-800" "text-white"]} "submit")]]) + +(defn person-view [{:keys [path-params]}] + (let [person-id (parse-uuid (:person-id path-params))] + (if-let [share (person->share person-id)] + (html-response + (page + (if (everyone-submitted-bid? share) + (if (goal-reached? share) + [:p (str "Your share is " (format-currency ((compute-distribution share) person-id)) ".")] + (render-not-reached share)) + (render-contribution-form (get-in share [:people person-id]) share)))) + (not-found-response)))) + +(defn handle-submit-contribution [{:as req :keys [path-params form-params]}] + (let [person-id (parse-uuid (:person-id path-params)) + share-id (@!person->share person-id) + bid (parse-currency (get form-params "bid"))] + (swap! !shares (fn [shares] (assoc-in shares [share-id :people person-id :bid] bid))) + (redirect (str "/contribute/" person-id)))) + +(defn home [req] + (html-response (page + [:p.pb-2.mt-5 [:span.font-bold "Solisplit"] " is an application to share costs in a more " [:span.font-bold "solidaric"] " way."] + [:h2.text-xl.pb-2.mt-5 "How it works"] + [:ol.list-decimal.p-5 + [:li "You enter the total cost that you want to split."] + [:li "You enter the names of all the people who want to share the cost."] + [:li "Everyone gets a personalized link to a form, where they can enter the maximum amount they would be willing to contribute to the shared expense."] + [:li "After everyone has entered their answer, you can check the result to see the actual share that everyone should pay:" + [:ul.list-disc.p-5 + [:li "If the sum of everyone's maximum contribution exceeds the total cost that you want to split, the total cost is divided among everyone, proportional to their answers. This means that everyone pays at most the amount they were comfortable with and the cost is split fairly, based on how much someone was willing to contribute."] + [:li "If the sum of everyone's maximum contribution does not reach the total cost, the program cannot calculate a fair distribution and instead shows how much more money would be needed to reach the total cost. You can then start over from the beginning and hope that people will increase their maximum contribution or try reducing the total cost if your situation allows for it."]]]] + [:h2.text-xl.pb-2.mt-5 "Example"] + [:p.pb-2 "Alice, Bob, and Charlie live together in a shared appartment and want to buy a new fridge for 600€."] + [:p.pb-2 "Alice is an engineer with a good salary and offers to pay up to 250€. Bob is a nurse and can pay at most 150€. Charlie is a student and also offers to pay 150€."] + [:p.pb-2 "Since the total of 250+150+150 = 550 is below the required 600, we cannot compute a fair distribution and instead show that they are 50€ short of reaching their total."] + [:p.pb-2 "The three decide to do a second round and hope that everyone can increase their share a bit."] + [:p.pb-2 "This time Alice offers to pay up to 400€, Bob and Charlie each offer to pay up to 200€."] + [:p.pb-2 "Since the total of 400+200+200 = 800 exceeds the required 600, we now split the 600 proportional to the offers. This results in the following shares:"] + [:ul.list-disc.p-5 + [:li "Alice: 600 * 400/800 = 300"] + [:li "Bob: 600 * 200/800 = 150"] + [:li "Charlie: 600 * 200/800 = 150"]] + [:p.pb-2 "As we can see, Bob and Charlie pay the same, since they entered the same maximum. Alice pays double what Bob or Charlie are paying, since her maximum was twice that of Bob or Charlie. Also everyone pays less than their stated maximum and the sum of everyone's share adds up to the required total."] + [:div.flex.justify-center + [:a.rounded-lg.border.p-2.bg-teal-800.text-white.text-lg {:href "/share/"} "Split expense"]]))) + + +(defn routes [] + [["/healthcheck" {:get handle-healthcheck}] + ["/" {:get home}] + ["/share" + ["/" {:get create-project-form + :post handle-create-share}] + ["/:share-id" + ["/" {:get handle-view-share + :post handle-update-share}] + ["/check" {:get handle-check}]]] + ["/contribute/:person-id" {:name :person + :get person-view + :post handle-submit-contribution}]]) + +(def dev-router #(rr/router (routes))) +(def prod-router (constantly (rr/router (routes)))) + +(defn dev-app [req] ((rr/ring-handler + (dev-router) + (rr/create-resource-handler {:path "/"}) + {:middleware [params/wrap-params + ct/wrap-content-type]}) req)) +(def prod-app (rr/ring-handler + (prod-router) + (rr/create-resource-handler {:path "/"}) + {:middleware [params/wrap-params + ct/wrap-content-type]}))