├── components
├── env
│ ├── resources
│ │ └── env
│ │ │ └── .keep
│ ├── Readme.md
│ ├── deps.edn
│ └── src
│ │ └── clojure
│ │ └── realworld
│ │ └── env
│ │ ├── interface.clj
│ │ └── core.clj
├── log
│ ├── resources
│ │ └── log
│ │ │ └── .keep
│ ├── Readme.md
│ ├── deps.edn
│ └── src
│ │ └── clojure
│ │ └── realworld
│ │ └── log
│ │ ├── core.clj
│ │ ├── interface.clj
│ │ └── config.clj
├── spec
│ ├── resources
│ │ └── spec
│ │ │ └── .keep
│ ├── Readme.md
│ ├── deps.edn
│ └── src
│ │ └── clojure
│ │ └── realworld
│ │ └── spec
│ │ ├── interface.clj
│ │ └── core.clj
├── tag
│ ├── resources
│ │ └── tag
│ │ │ └── .keep
│ ├── Readme.md
│ ├── src
│ │ └── clojure
│ │ │ └── realworld
│ │ │ └── tag
│ │ │ ├── interface.clj
│ │ │ └── core.clj
│ ├── deps.edn
│ └── test
│ │ └── clojure
│ │ └── realworld
│ │ └── tag
│ │ └── core_test.clj
├── user
│ ├── resources
│ │ └── user
│ │ │ └── .keep
│ ├── Readme.md
│ ├── src
│ │ └── clojure
│ │ │ └── realworld
│ │ │ └── user
│ │ │ ├── interface
│ │ │ └── spec.clj
│ │ │ ├── interface.clj
│ │ │ ├── store.clj
│ │ │ ├── spec.clj
│ │ │ └── core.clj
│ ├── deps.edn
│ └── test
│ │ └── clojure
│ │ └── realworld
│ │ └── user
│ │ ├── store_test.clj
│ │ └── core_test.clj
├── article
│ ├── resources
│ │ └── article
│ │ │ └── .keep
│ ├── Readme.md
│ ├── src
│ │ └── clojure
│ │ │ └── realworld
│ │ │ └── article
│ │ │ ├── interface
│ │ │ └── spec.clj
│ │ │ ├── interface.clj
│ │ │ ├── spec.clj
│ │ │ ├── core.clj
│ │ │ └── store.clj
│ ├── deps.edn
│ └── test
│ │ └── clojure
│ │ └── realworld
│ │ └── article
│ │ ├── core_test.clj
│ │ └── store_test.clj
├── comment
│ ├── resources
│ │ └── comment
│ │ │ └── .keep
│ ├── Readme.md
│ ├── src
│ │ └── clojure
│ │ │ └── realworld
│ │ │ └── comment
│ │ │ ├── interface
│ │ │ └── spec.clj
│ │ │ ├── interface.clj
│ │ │ ├── spec.clj
│ │ │ ├── store.clj
│ │ │ └── core.clj
│ ├── deps.edn
│ └── test
│ │ └── clojure
│ │ └── realworld
│ │ └── comment
│ │ ├── store_test.clj
│ │ └── core_test.clj
├── profile
│ ├── resources
│ │ └── profile
│ │ │ └── .keep
│ ├── Readme.md
│ ├── src
│ │ └── clojure
│ │ │ └── realworld
│ │ │ └── profile
│ │ │ ├── interface
│ │ │ └── spec.clj
│ │ │ ├── interface.clj
│ │ │ ├── spec.clj
│ │ │ ├── store.clj
│ │ │ └── core.clj
│ ├── deps.edn
│ └── test
│ │ └── clojure
│ │ └── realworld
│ │ └── profile
│ │ ├── store_test.clj
│ │ └── core_test.clj
└── database
│ ├── resources
│ └── database
│ │ └── .keep
│ ├── Readme.md
│ ├── deps.edn
│ └── src
│ └── clojure
│ └── realworld
│ └── database
│ ├── interface.clj
│ ├── core.clj
│ └── schema.clj
├── bases
└── rest-api
│ ├── resources
│ └── rest_api
│ │ └── .keep
│ ├── Readme.md
│ ├── deps.edn
│ ├── src
│ └── clojure
│ │ └── realworld
│ │ └── rest_api
│ │ ├── main.clj
│ │ ├── middleware.clj
│ │ ├── api.clj
│ │ └── handler.clj
│ └── test
│ └── clojure
│ └── realworld
│ └── rest_api
│ └── handler_test.clj
├── logo.png
├── .vscode
├── extensions.json
└── settings.json
├── env.edn
├── .media
├── how-to
│ ├── 05_base.png
│ ├── 01_workspace.png
│ ├── 03_components.png
│ ├── 02_dev_project.png
│ ├── 06_empty_project.png
│ ├── 10_repl_config.png
│ ├── 07_filled_project.png
│ ├── 08_workspace_info.png
│ ├── 09_workspace_info_after_commit.png
│ └── 04_components_added_to_development_project.png
├── readme
│ ├── 01_rest_api.png
│ ├── 02_polylith_info.png
│ └── 03_calva_jack_in.png
└── gitpod
│ ├── Gitpod-to-REPL.png
│ └── Gitpod-Polylith-RealWorld.png
├── development
└── src
│ └── dev
│ ├── server.clj
│ ├── furkan.clj
│ ├── getting_started.clj
│ └── hello_repl.clj
├── .gitignore
├── workspace.edn
├── api-tests
└── run-api-tests.sh
├── .idea
├── clojure-deps.xml
└── runConfigurations
│ └── REPL.xml
├── .gitpod.yml
├── .gitpod
└── Dockerfile
├── LICENSE
├── projects
└── realworld-backend
│ └── deps.edn
├── deps.edn
├── gitpod.md
├── .circleci
└── config.yml
├── how-to.md
└── readme.md
/components/env/resources/env/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/log/resources/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/spec/resources/spec/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/tag/resources/tag/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/user/resources/user/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bases/rest-api/resources/rest_api/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/article/resources/article/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/comment/resources/comment/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/profile/resources/profile/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/database/resources/database/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/spec/Readme.md:
--------------------------------------------------------------------------------
1 | # spec component
2 |
3 | Component containing specs.
--------------------------------------------------------------------------------
/bases/rest-api/Readme.md:
--------------------------------------------------------------------------------
1 | # rest-api base
2 |
3 | Base containing rest api endpoints and handlers.
4 |
--------------------------------------------------------------------------------
/components/log/Readme.md:
--------------------------------------------------------------------------------
1 | # log component
2 |
3 | Component handling functions related to logging.
4 |
--------------------------------------------------------------------------------
/components/tag/Readme.md:
--------------------------------------------------------------------------------
1 | # tag component
2 |
3 | Component handling functions related to tag domain.
4 |
--------------------------------------------------------------------------------
/components/user/Readme.md:
--------------------------------------------------------------------------------
1 | # user component
2 |
3 | Component handling functions related to user domain.
4 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/logo.png
--------------------------------------------------------------------------------
/components/article/Readme.md:
--------------------------------------------------------------------------------
1 | # article component
2 |
3 | Component handling functions related to article domain.
4 |
--------------------------------------------------------------------------------
/components/comment/Readme.md:
--------------------------------------------------------------------------------
1 | # comment component
2 |
3 | Component handling functions related to comment domain.
4 |
--------------------------------------------------------------------------------
/components/database/Readme.md:
--------------------------------------------------------------------------------
1 | # database component
2 |
3 | Component containing functions related to database.
4 |
--------------------------------------------------------------------------------
/components/env/Readme.md:
--------------------------------------------------------------------------------
1 | # env component
2 |
3 | Component containing functionality to read environment variables.
--------------------------------------------------------------------------------
/components/profile/Readme.md:
--------------------------------------------------------------------------------
1 | # profile component
2 |
3 | Component handling functions related to profile domain.
4 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "betterthantomorrow.calva",
4 | "djblue.portal"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/env.edn:
--------------------------------------------------------------------------------
1 | {:allowed-origins "http://localhost:3000,http://localhost:4100"
2 | :environment "LOCAL"
3 | :database "database.db"}
4 |
--------------------------------------------------------------------------------
/.media/how-to/05_base.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/05_base.png
--------------------------------------------------------------------------------
/.media/readme/01_rest_api.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/readme/01_rest_api.png
--------------------------------------------------------------------------------
/components/env/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps {}
3 | :aliases {:test {:extra-paths []
4 | :extra-deps []}}}
5 |
--------------------------------------------------------------------------------
/.media/how-to/01_workspace.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/01_workspace.png
--------------------------------------------------------------------------------
/.media/how-to/03_components.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/03_components.png
--------------------------------------------------------------------------------
/.media/gitpod/Gitpod-to-REPL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/gitpod/Gitpod-to-REPL.png
--------------------------------------------------------------------------------
/.media/how-to/02_dev_project.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/02_dev_project.png
--------------------------------------------------------------------------------
/.media/how-to/06_empty_project.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/06_empty_project.png
--------------------------------------------------------------------------------
/.media/how-to/10_repl_config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/10_repl_config.png
--------------------------------------------------------------------------------
/.media/readme/02_polylith_info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/readme/02_polylith_info.png
--------------------------------------------------------------------------------
/.media/readme/03_calva_jack_in.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/readme/03_calva_jack_in.png
--------------------------------------------------------------------------------
/.media/how-to/07_filled_project.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/07_filled_project.png
--------------------------------------------------------------------------------
/.media/how-to/08_workspace_info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/08_workspace_info.png
--------------------------------------------------------------------------------
/.media/gitpod/Gitpod-Polylith-RealWorld.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/gitpod/Gitpod-Polylith-RealWorld.png
--------------------------------------------------------------------------------
/components/env/src/clojure/realworld/env/interface.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.env.interface
2 | (:require [clojure.realworld.env.core :as core]))
3 |
4 | (def env core/env)
5 |
--------------------------------------------------------------------------------
/.media/how-to/09_workspace_info_after_commit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/09_workspace_info_after_commit.png
--------------------------------------------------------------------------------
/components/log/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps {com.taoensso/timbre {:mvn/version "6.0.4"}}
3 | :aliases {:test {:extra-paths []
4 | :extra-deps []}}}
5 |
--------------------------------------------------------------------------------
/components/spec/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps {metosin/spec-tools {:mvn/version "0.10.5"}}
3 | :aliases {:test {:extra-paths []
4 | :extra-deps []}}}
5 |
--------------------------------------------------------------------------------
/components/tag/src/clojure/realworld/tag/interface.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.tag.interface
2 | (:require [clojure.realworld.tag.core :as core]))
3 |
4 | (defn all-tags []
5 | (core/all-tags))
6 |
--------------------------------------------------------------------------------
/.media/how-to/04_components_added_to_development_project.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/furkan3ayraktar/clojure-polylith-realworld-example-app/HEAD/.media/how-to/04_components_added_to_development_project.png
--------------------------------------------------------------------------------
/components/profile/src/clojure/realworld/profile/interface/spec.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.profile.interface.spec
2 | (:require [clojure.realworld.profile.spec :as spec]))
3 |
4 | (def profile spec/profile)
5 |
--------------------------------------------------------------------------------
/components/comment/src/clojure/realworld/comment/interface/spec.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.comment.interface.spec
2 | (:require [clojure.realworld.comment.spec :as spec]))
3 |
4 | (def id spec/id)
5 |
6 | (def add-comment spec/add-comment)
7 |
--------------------------------------------------------------------------------
/components/tag/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps {com.github.seancorfield/honeysql {:mvn/version "2.4.980"}
3 | org.clojure/java.jdbc {:mvn/version "0.7.12"}}
4 | :aliases {:test {:extra-paths ["test"]
5 | :extra-deps []}}}
6 |
--------------------------------------------------------------------------------
/components/article/src/clojure/realworld/article/interface/spec.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.article.interface.spec
2 | (:require [clojure.realworld.article.spec :as spec]))
3 |
4 | (def create-article spec/create-article)
5 |
6 | (def update-article spec/update-article)
7 |
--------------------------------------------------------------------------------
/development/src/dev/server.clj:
--------------------------------------------------------------------------------
1 | (ns dev.server
2 | (:require [clojure.realworld.rest-api.main :as main]))
3 |
4 | (defn start! [port]
5 | (main/start! port))
6 |
7 | (defn stop! []
8 | (main/stop!))
9 |
10 | (comment
11 | (start! 6003)
12 | (stop!)
13 |
14 | )
15 |
--------------------------------------------------------------------------------
/components/database/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps {com.github.seancorfield/honeysql {:mvn/version "2.4.980"}
3 | org.clojure/java.jdbc {:mvn/version "0.7.12"}
4 | org.xerial/sqlite-jdbc {:mvn/version "3.41.0.0"}}
5 | :aliases {:test {:extra-paths []
6 | :extra-deps []}}}
7 |
--------------------------------------------------------------------------------
/components/profile/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps {com.github.seancorfield/honeysql {:mvn/version "2.4.980"}
3 | metosin/spec-tools {:mvn/version "0.10.5"}
4 | org.clojure/java.jdbc {:mvn/version "0.7.12"}}
5 | :aliases {:test {:extra-paths ["test"]
6 | :extra-deps []}}}
7 |
--------------------------------------------------------------------------------
/components/log/src/clojure/realworld/log/core.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.log.core
2 | (:require [taoensso.timbre :as timbre]))
3 |
4 | (defmacro info [args]
5 | `(timbre/log! :info :p ~args))
6 |
7 | (defmacro warn [args]
8 | `(timbre/log! :warn :p ~args))
9 |
10 | (defmacro error [args]
11 | `(timbre/log! :error :p ~args))
12 |
--------------------------------------------------------------------------------
/components/user/src/clojure/realworld/user/interface/spec.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.user.interface.spec
2 | (:require [clojure.realworld.user.spec :as spec]))
3 |
4 | (def login spec/login)
5 |
6 | (def register spec/register)
7 |
8 | (def update-user spec/update-user)
9 |
10 | (def user spec/user)
11 |
12 | (def visible-user spec/visible-user)
13 |
--------------------------------------------------------------------------------
/development/src/dev/furkan.clj:
--------------------------------------------------------------------------------
1 | (ns dev.furkan
2 | (:require [portal.api :as p]))
3 |
4 | (def portal-atom (atom nil))
5 |
6 | (defn launch-portal []
7 | (let [portal (p/open (or @portal-atom {:launcher :vs-code}))]
8 | (reset! portal-atom portal)
9 | (add-tap #'p/submit)
10 | portal))
11 |
12 | (comment
13 | (launch-portal)
14 |
15 | )
16 |
--------------------------------------------------------------------------------
/components/comment/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps {clj-time/clj-time {:mvn/version "0.15.2"}
3 | com.github.seancorfield/honeysql {:mvn/version "2.4.980"}
4 | metosin/spec-tools {:mvn/version "0.10.5"}
5 | org.clojure/java.jdbc {:mvn/version "0.7.12"}}
6 | :aliases {:test {:extra-paths ["test"]
7 | :extra-deps []}}}
8 |
--------------------------------------------------------------------------------
/components/spec/src/clojure/realworld/spec/interface.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.spec.interface
2 | (:require [clojure.realworld.spec.core :as core]))
3 |
4 | (def username? core/username?)
5 |
6 | (def non-empty-string? core/non-empty-string?)
7 |
8 | (def email? core/email?)
9 |
10 | (def uri-string? core/uri-string?)
11 |
12 | (def slug? core/slug?)
13 |
14 | (def password? core/password?)
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/classes
2 | **/target
3 | **/.cpcache
4 | **/.DS_Store
5 |
6 | # IntelliJ-related
7 | *.iws
8 | *.ipr
9 | *.iml
10 | .idea/*
11 | !.idea/runConfigurations/REPL.xml
12 | !.idea/clojure-deps.xml
13 |
14 | # REPL-related
15 | .nrepl-port
16 |
17 | # Project specific
18 | database.db
19 | test.db
20 |
21 | # VSCode-related
22 | **/.calva/*
23 | .clj-kondo/*
24 | out/*
25 | .lsp/*
26 | **/.portal
27 |
--------------------------------------------------------------------------------
/components/article/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps {clj-time/clj-time {:mvn/version "0.15.2"}
3 | com.github.seancorfield/honeysql {:mvn/version "2.4.980"}
4 | metosin/spec-tools {:mvn/version "0.10.5"}
5 | org.clojure/java.jdbc {:mvn/version "0.7.12"}
6 | slugger/slugger {:mvn/version "1.0.1"}}
7 | :aliases {:test {:extra-paths ["test"]
8 | :extra-deps []}}}
9 |
--------------------------------------------------------------------------------
/bases/rest-api/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps {compojure/compojure {:mvn/version "1.7.0"}
3 | org.clojure/data.json {:mvn/version "2.4.0"}
4 | ring/ring-json {:mvn/version "0.5.1"}
5 | ring/ring-jetty-adapter {:mvn/version "1.9.6"}
6 | ring-logger-timbre/ring-logger-timbre {:mvn/version "0.7.6"}}
7 | :aliases {:test {:extra-paths ["test"]
8 | :extra-deps []}}}
9 |
--------------------------------------------------------------------------------
/workspace.edn:
--------------------------------------------------------------------------------
1 | {:top-namespace "clojure.realworld"
2 | :interface-ns "interface"
3 | :default-profile-name "default"
4 | :compact-views #{}
5 | :vcs {:name "git"
6 | :auto-add true}
7 | :tag-patterns {:stable "stable-*"
8 | :release "v[0-9]*"}
9 | :projects {"development" {:alias "dev"}
10 | "realworld-backend" {:alias "rb"}}}
11 |
--------------------------------------------------------------------------------
/components/log/src/clojure/realworld/log/interface.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.log.interface
2 | (:require [clojure.realworld.log.config :as config]
3 | [clojure.realworld.log.core :as core]))
4 |
5 | (defn init []
6 | (config/init))
7 |
8 | (defmacro info [& args]
9 | `(core/info ~args))
10 |
11 | (defmacro warn [& args]
12 | `(core/warn ~args))
13 |
14 | (defmacro error [& args]
15 | `(core/error ~args))
16 |
--------------------------------------------------------------------------------
/components/profile/src/clojure/realworld/profile/interface.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.profile.interface
2 | (:require [clojure.realworld.profile.core :as core]))
3 |
4 | (defn fetch-profile [auth-user username]
5 | (core/fetch-profile auth-user username))
6 |
7 | (defn follow! [auth-user username]
8 | (core/follow! auth-user username))
9 |
10 | (defn unfollow! [auth-user username]
11 | (core/unfollow! auth-user username))
12 |
--------------------------------------------------------------------------------
/components/comment/src/clojure/realworld/comment/interface.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.comment.interface
2 | (:require [clojure.realworld.comment.core :as core]))
3 |
4 | (defn article-comments [auth-user slug]
5 | (core/article-comments auth-user slug))
6 |
7 | (defn add-comment! [auth-user slug comment]
8 | (core/add-comment! auth-user slug comment))
9 |
10 | (defn delete-comment! [auth-user id]
11 | (core/delete-comment! auth-user id))
12 |
--------------------------------------------------------------------------------
/components/tag/src/clojure/realworld/tag/core.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.tag.core
2 | (:require [clojure.java.jdbc :as jdbc]
3 | [clojure.realworld.database.interface :as database]
4 | [honey.sql :as sql]))
5 |
6 | (defn all-tags []
7 | (let [query {:select [:name]
8 | :from [:tag]}
9 | result (jdbc/query (database/db) (sql/format query))
10 | res {:tags (mapv :name result)}]
11 | [true res]))
12 |
--------------------------------------------------------------------------------
/components/user/deps.edn:
--------------------------------------------------------------------------------
1 | {:paths ["src" "resources"]
2 | :deps {clj-jwt/clj-jwt {:mvn/version "0.1.1"}
3 | clj-time/clj-time {:mvn/version "0.15.2"}
4 | crypto-password/crypto-password {:mvn/version "0.3.0"}
5 | com.github.seancorfield/honeysql {:mvn/version "2.4.980"}
6 | metosin/spec-tools {:mvn/version "0.10.5"}
7 | org.clojure/java.jdbc {:mvn/version "0.7.12"}}
8 | :aliases {:test {:extra-paths ["test"]
9 | :extra-deps []}}}
10 |
--------------------------------------------------------------------------------
/components/profile/src/clojure/realworld/profile/spec.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.profile.spec
2 | (:require [clojure.realworld.spec.interface :as spec]
3 | [spec-tools.data-spec :as ds]))
4 |
5 | (def profile
6 | (ds/spec {:name :core/profile
7 | :spec {:username spec/username?
8 | :following boolean?
9 | (ds/opt :image) (ds/maybe spec/uri-string?)
10 | (ds/opt :bio) (ds/maybe spec/non-empty-string?)}}))
11 |
--------------------------------------------------------------------------------
/components/database/src/clojure/realworld/database/interface.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.database.interface
2 | (:require [clojure.realworld.database.core :as core]
3 | [clojure.realworld.database.schema :as schema]))
4 |
5 | (defn db
6 | ([path]
7 | (core/db path))
8 | ([]
9 | (core/db)))
10 |
11 | (defn db-exists? []
12 | (core/db-exists?))
13 |
14 | (defn generate-db [db]
15 | (schema/generate-db db))
16 |
17 | (defn drop-db [db]
18 | (schema/drop-db db))
19 |
20 | (defn valid-schema? [db]
21 | (schema/valid-schema? db))
22 |
--------------------------------------------------------------------------------
/api-tests/run-api-tests.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -x
3 |
4 | SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
5 |
6 | APIURL=${APIURL:-https://conduit.productionready.io/api}
7 | USERNAME=${USERNAME:-u`date +%s`}
8 | EMAIL=${EMAIL:-$USERNAME@mail.com}
9 | PASSWORD=${PASSWORD:-password}
10 |
11 | npx newman run $SCRIPTDIR/Conduit.postman_collection.json \
12 | --delay-request 500 \
13 | --global-var "APIURL=$APIURL" \
14 | --global-var "USERNAME=$USERNAME" \
15 | --global-var "EMAIL=$EMAIL" \
16 | --global-var "PASSWORD=$PASSWORD"
--------------------------------------------------------------------------------
/.idea/clojure-deps.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/components/database/src/clojure/realworld/database/core.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.database.core
2 | (:require [clojure.java.io :as io]
3 | [clojure.realworld.env.interface :as env]))
4 |
5 | (defn- db-path []
6 | (if (contains? env/env :database)
7 | (env/env :database)
8 | "database.db"))
9 |
10 | (defn db
11 | ([path]
12 | {:classname "org.sqlite.JDBC"
13 | :subprotocol "sqlite"
14 | :subname path})
15 | ([]
16 | (db (db-path))))
17 |
18 | (defn db-exists? []
19 | (let [db-file (io/file "database.db")]
20 | (.exists db-file)))
21 |
--------------------------------------------------------------------------------
/components/user/src/clojure/realworld/user/interface.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.user.interface
2 | (:require [clojure.realworld.user.core :as core]
3 | [clojure.realworld.user.store :as store]))
4 |
5 | (defn login! [login-input]
6 | (core/login! login-input))
7 |
8 | (defn register! [register-input]
9 | (core/register! register-input))
10 |
11 | (defn user-by-token [token]
12 | (core/user-by-token token))
13 |
14 | (defn update-user! [auth-user user-input]
15 | (core/update-user! auth-user user-input))
16 |
17 | (defn find-by-username-or-id [username-or-id]
18 | (store/find-by-username-or-id username-or-id))
19 |
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | image:
2 | file: .gitpod/Dockerfile
3 |
4 | vscode:
5 | extensions:
6 | - betterthantomorrow.calva
7 | - djblue.portal
8 |
9 | ports:
10 | - port: 5900
11 | onOpen: ignore
12 | - port: 6080
13 | onOpen: ignore
14 | - port: 6003
15 | onOpen: ignore
16 |
17 | tasks:
18 | - name: Polylith Real World nREPL Server
19 | command: clojure -Sdeps '{:deps {nrepl/nrepl {:mvn/version,"1.0.0"},cider/cider-nrepl {:mvn/version,"0.28.5"}}}' -M:dev:test:default -m nrepl.cmdline --middleware "[cider.nrepl/cider-middleware]"
20 | - name: Open Getting Started
21 | command: code development/src/dev/getting_started.clj
22 | - name: poly shell
23 | command: poly shell
24 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/REPL.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.gitpod/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM gitpod/workspace-java-17
2 |
3 | ENV PATH="/home/gitpod/bin:/workspace/clojure-polylith-realworld-example-app/.gitpod:${PATH}"
4 | ENV JAVA_HOME="/home/gitpod/.sdkman/candidates/java/current"
5 | ENV JAVA_TOOL_OPTIONS="-Xmx3489m"
6 |
7 | RUN mkdir /home/gitpod/bin
8 | RUN wget -P /home/gitpod https://github.com/polyfy/polylith/releases/download/stable-master/poly-0.2.18-SNAPSHOT.jar
9 | RUN bash -c cat <<'EOF' > /home/gitpod/bin/poly && chmod +x /home/gitpod/bin/poly
10 | #!/bin/bash
11 |
12 | java $JVM_OPTS "-jar" "/home/gitpod/poly-0.2.18-SNAPSHOT.jar" $@
13 |
14 | EOF
15 | RUN curl -sL https://raw.githubusercontent.com/borkdude/deps.clj/master/install > install_clojure && chmod +x install_clojure && ./install_clojure --dir /home/gitpod/bin --as-clj && clojure -Sdeps '{:deps {nrepl/nrepl {:mvn/version,"1.0.0"},cider/cider-nrepl {:mvn/version,"0.28.5"}}}' -P
--------------------------------------------------------------------------------
/components/article/src/clojure/realworld/article/interface.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.article.interface
2 | (:require [clojure.realworld.article.core :as core]))
3 |
4 | (defn article [auth-user slug]
5 | (core/article auth-user slug))
6 |
7 | (defn create-article! [auth-user article-input]
8 | (core/create-article! auth-user article-input))
9 |
10 | (defn update-article! [auth-user slug article-input]
11 | (core/update-article! auth-user slug article-input))
12 |
13 | (defn delete-article! [auth-user slug]
14 | (core/delete-article! auth-user slug))
15 |
16 | (defn favorite-article! [auth-user slug]
17 | (core/favorite-article! auth-user slug))
18 |
19 | (defn unfavorite-article! [auth-user slug]
20 | (core/unfavorite-article! auth-user slug))
21 |
22 | (defn feed [auth-user limit offset]
23 | (core/feed auth-user limit offset))
24 |
25 | (defn articles [auth-user limit offset author tag favorited]
26 | (core/articles auth-user limit offset author tag favorited))
27 |
--------------------------------------------------------------------------------
/bases/rest-api/src/clojure/realworld/rest_api/main.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.rest-api.main
2 | (:require [clojure.realworld.log.interface :as log]
3 | [clojure.realworld.rest-api.api :as api]
4 | [ring.adapter.jetty :refer [run-jetty]])
5 | (:gen-class))
6 |
7 | (def ^:private server-ref (atom nil))
8 |
9 | (defn start!
10 | [port]
11 | (if-let [_server @server-ref]
12 | (log/warn "Server already running? (stop!) it first.")
13 | (do
14 | (log/info "Starting server on port: " port)
15 | (api/init)
16 | (reset! server-ref
17 | (run-jetty api/app
18 | {:port port
19 | :join? false})))))
20 |
21 | (defn stop! []
22 | (if-let [server @server-ref]
23 | (do (api/destroy)
24 | (.stop server)
25 | (reset! server-ref nil))
26 | (log/warn "No server")))
27 |
28 | (defn -main [& _args]
29 | (start! (Integer/valueOf
30 | (or (System/getenv "port")
31 | "6003")
32 | 10)))
33 |
--------------------------------------------------------------------------------
/components/comment/src/clojure/realworld/comment/spec.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.comment.spec
2 | (:require [clojure.realworld.spec.interface :as spec]
3 | [clojure.realworld.profile.interface.spec :as profile-spec]
4 | [spec-tools.core :as st]
5 | [spec-tools.data-spec :as ds]))
6 |
7 | (def id
8 | (st/spec {:spec pos-int?
9 | :type :long
10 | :description "A long spec that defines a comment id which is a positive integer"}))
11 |
12 | (def add-comment
13 | (ds/spec {:name :core/add-comment
14 | :spec {:body spec/non-empty-string?}}))
15 |
16 | (def comment-spec
17 | (ds/spec {:name :core/comment
18 | :spec {:id pos-int?
19 | :updatedAt string?
20 | :createdAt string?
21 | :body spec/non-empty-string?
22 | :author profile-spec/profile}}))
23 |
24 | (def visible-comment
25 | (ds/spec {:name :core/visible-comment
26 | :spec {:comment comment-spec}}))
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Furkan Bayraktar
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/components/comment/src/clojure/realworld/comment/store.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.comment.store
2 | (:require [clojure.java.jdbc :as jdbc]
3 | [clojure.realworld.database.interface :as database]
4 | [honey.sql :as sql]))
5 |
6 | (defn comments [article-id]
7 | (let [query {:select [:*]
8 | :from [:comment]
9 | :where [:= :articleId article-id]}]
10 | (jdbc/query (database/db) (sql/format query) {:identifiers identity})))
11 |
12 | (defn find-by-id [id]
13 | (let [query {:select [:*]
14 | :from [:comment]
15 | :where [:= :id id]}
16 | results (jdbc/query (database/db) (sql/format query) {:identifiers identity})]
17 | (first results)))
18 |
19 | (defn add-comment! [comment-input]
20 | (let [result (jdbc/insert! (database/db) :comment
21 | comment-input
22 | {:entities identity})]
23 | (-> result first first val)))
24 |
25 | (defn delete-comment! [id]
26 | (let [query {:delete-from :comment
27 | :where [:= :id id]}]
28 | (jdbc/execute! (database/db) (sql/format query))
29 | nil))
30 |
--------------------------------------------------------------------------------
/components/profile/src/clojure/realworld/profile/store.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.profile.store
2 | (:require [clojure.java.jdbc :as jdbc]
3 | [clojure.realworld.database.interface :as database]
4 | [honey.sql :as sql]))
5 |
6 | (defn following? [user-id followed-user-id]
7 | (let [query {:select [:*]
8 | :from [:userFollows]
9 | :where [:and [:= :userId user-id]
10 | [:= :followedUserId followed-user-id]]}
11 | results (jdbc/query (database/db) (sql/format query))]
12 | (-> results first nil? not)))
13 |
14 | (defn follow! [user-id followed-user-id]
15 | (when-not (following? user-id followed-user-id)
16 | (jdbc/insert! (database/db) :userFollows {:userId user-id
17 | :followedUserId followed-user-id})))
18 |
19 | (defn unfollow! [user-id followed-user-id]
20 | (when (following? user-id followed-user-id)
21 | (let [query {:delete-from :userFollows
22 | :where [:and [:= :userId user-id]
23 | [:= :followedUserId followed-user-id]]}]
24 | (jdbc/execute! (database/db) (sql/format query)))))
25 |
--------------------------------------------------------------------------------
/components/tag/test/clojure/realworld/tag/core_test.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.tag.core-test
2 | (:require [clojure.test :refer [deftest is use-fixtures]]
3 | [clojure.realworld.database.interface :as database]
4 | [clojure.realworld.tag.core :as core]
5 | [clojure.java.jdbc :as jdbc]))
6 |
7 | (defn test-db
8 | ([] {:classname "org.sqlite.JDBC"
9 | :subprotocol "sqlite"
10 | :subname "test.db"})
11 | ([_] (test-db)))
12 |
13 | (defn prepare-for-tests [f]
14 | (with-redefs [database/db test-db]
15 | (let [db (test-db)]
16 | (database/generate-db db)
17 | (f)
18 | (database/drop-db db))))
19 |
20 | (use-fixtures :each prepare-for-tests)
21 |
22 | (deftest all-tags--return-tags-response
23 | (let [_ (jdbc/insert-multi! (test-db) :tag [{:name "tag1"}
24 | {:name "tag2"}
25 | {:name "tag3"}
26 | {:name "tag4"}
27 | {:name "tag5"}])
28 | [ok? res] (core/all-tags)]
29 | (is (true? ok?))
30 | (is (= {:tags ["tag1" "tag2" "tag3" "tag4" "tag5"]} res))))
31 |
--------------------------------------------------------------------------------
/components/user/src/clojure/realworld/user/store.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.user.store
2 | (:require [clojure.java.jdbc :as jdbc]
3 | [clojure.realworld.database.interface :as database]
4 | [clojure.realworld.user.spec :as spec]
5 | [clojure.spec.alpha :as s]
6 | [honey.sql :as sql]))
7 |
8 | (defn find-by [key value]
9 | (let [query {:select [:*]
10 | :from [:user]
11 | :where [:= key value]}
12 | results (jdbc/query (database/db) (sql/format query))]
13 | (first results)))
14 |
15 | (defn find-by-email [email]
16 | (find-by :email email))
17 |
18 | (defn find-by-username [username]
19 | (find-by :username username))
20 |
21 | (defn find-by-id [id]
22 | (find-by :id id))
23 |
24 | (defn find-by-username-or-id [username-or-id]
25 | (if (s/valid? spec/id username-or-id)
26 | (find-by-id username-or-id)
27 | (find-by-username username-or-id)))
28 |
29 | (defn insert-user! [user-input]
30 | (jdbc/insert! (database/db) :user user-input))
31 |
32 | (defn update-user! [id user-input]
33 | (let [query {:update :user
34 | :set user-input
35 | :where [:= :id id]}]
36 | (jdbc/execute! (database/db) (sql/format query))))
37 |
--------------------------------------------------------------------------------
/components/env/src/clojure/realworld/env/core.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.env.core
2 | (:require [clojure.edn :as edn]
3 | [clojure.java.io :as io]
4 | [clojure.string :as str]))
5 |
6 | (defn keywordize [s]
7 | (when s
8 | (-> (str/lower-case s)
9 | (str/replace "_" "-")
10 | (str/replace "." "-")
11 | (keyword))))
12 |
13 | (defn read-system-env []
14 | (->> (System/getenv)
15 | (map (fn [[k v]] [(keywordize k) v]))
16 | (into {})))
17 |
18 | (defn read-system-props []
19 | (->> (System/getProperties)
20 | (map (fn [[k v]] [(keywordize k) v]))
21 | (into {})))
22 |
23 | (defn slurp-file [f]
24 | (when-let [f (io/file f)]
25 | (when (.exists f)
26 | (slurp f))))
27 |
28 | (defn read-env-file [f]
29 | (when-let [content (slurp-file f)]
30 | (try
31 | (let [parsed-content (edn/read-string content)]
32 | (if (map? parsed-content)
33 | parsed-content
34 | {}))
35 | (catch Exception _
36 | {}))))
37 |
38 | (def env
39 | (let [env-file (read-env-file "env.edn")
40 | system-props (read-system-props)
41 | system-env (read-system-env)]
42 | (merge system-props
43 | system-env
44 | env-file)))
45 |
--------------------------------------------------------------------------------
/components/profile/src/clojure/realworld/profile/core.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.profile.core
2 | (:require [clojure.realworld.profile.store :as store]
3 | [clojure.realworld.user.interface :as user]))
4 |
5 | (defn- create-profile [user following?]
6 | (let [profile (assoc (select-keys user [:username :bio :image])
7 | :following following?)]
8 | {:profile profile}))
9 |
10 | (defn fetch-profile [auth-user username-or-id]
11 | (let [user (user/find-by-username-or-id username-or-id)]
12 | (if (nil? user)
13 | [false {:errors {:username ["Cannot find a profile with given username."]}}]
14 | (let [following? (if (nil? auth-user)
15 | false
16 | (store/following? (:id auth-user) (:id user)))]
17 | [true (create-profile user following?)]))))
18 |
19 | (defn follow! [auth-user username]
20 | (if-let [user (user/find-by-username-or-id username)]
21 | (do
22 | (store/follow! (:id auth-user) (:id user))
23 | [true (create-profile user true)])
24 | [false {:errors {:username ["Cannot find a profile with given username."]}}]))
25 |
26 | (defn unfollow! [auth-user username]
27 | (if-let [user (user/find-by-username-or-id username)]
28 | (do
29 | (store/unfollow! (:id auth-user) (:id user))
30 | [true (create-profile user false)])
31 | [false {:errors {:username ["Cannot find a profile with given username."]}}]))
32 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://calva.io/connect-sequences/ for more info about Calva REPL Connect Sequences
3 | "calva.replConnectSequences": [
4 | {
5 | "projectType": "deps.edn",
6 | "afterCLJReplJackInCode": "(require '[dev.server] :reload) (in-ns 'dev.server) (start! 6003)",
7 | "name": "Polylith RealWorld Server REPL (start)",
8 | "autoSelectForJackIn": true,
9 | "projectRootPath": ["."],
10 | "menuSelections": {
11 | "cljAliases": ["dev", "test"]
12 | },
13 | },
14 | {
15 | "projectType": "deps.edn",
16 | "name": "Polylith RealWorld Server REPL (connect)",
17 | "afterCLJReplJackInCode": "(require '[dev.server] :reload) (in-ns 'dev.server) (start! 6003)",
18 | "autoSelectForConnect": true,
19 | "projectRootPath": ["."],
20 | }
21 | ],
22 |
23 | // When autoConnectRepl is set to `true`, you can start the REPL yourself and keep it running
24 | // using the command produced by: **Calva: Copy Jack-in Command Line to Clipboard**. This will
25 | // ensure that Calva's dependencies are met and Calva will connect to your REPL automatically
26 | // when the project is opened.
27 | "calva.autoConnectRepl": true,
28 |
29 | // Set this to `true` to make Calva show the Jack-in process in the Terminal pane.
30 | "calva.autoOpenJackInTerminal": false,
31 | // Enable this for a some startup messages from Calva.
32 | "calva.showCalvaSaysOnStart": false,
33 |
34 | // Calm down the hover eagerness a bit
35 | "editor.hover.delay": 1500
36 | }
37 |
--------------------------------------------------------------------------------
/components/article/src/clojure/realworld/article/spec.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.article.spec
2 | (:require [clojure.realworld.spec.interface :as spec]
3 | [clojure.realworld.profile.interface.spec :as profile-spec]
4 | [spec-tools.data-spec :as ds]))
5 |
6 | (def create-article
7 | (ds/spec {:name :core/create-article
8 | :spec {:title spec/non-empty-string?
9 | :description spec/non-empty-string?
10 | :body spec/non-empty-string?
11 | (ds/opt :tagList) [spec/non-empty-string?]}}))
12 |
13 | (def update-article
14 | (ds/spec {:name :core/update-article
15 | :spec {(ds/opt :title) spec/non-empty-string?
16 | (ds/opt :description) spec/non-empty-string?
17 | (ds/opt :body) spec/non-empty-string?}}))
18 |
19 | (def article
20 | (ds/spec {:name :core/article
21 | :spec {:id pos-int?
22 | :slug spec/slug?
23 | :title spec/non-empty-string?
24 | :description spec/non-empty-string?
25 | :body spec/non-empty-string?
26 | :updatedAt string?
27 | :createdAt string?
28 | :favorited boolean?
29 | :favoritesCount nat-int?
30 | :author profile-spec/profile
31 | (ds/opt :tagList) [spec/non-empty-string?]}}))
32 |
33 | (def visible-article
34 | (ds/spec {:name :core/visible-article
35 | :spec {:article article}}))
36 |
--------------------------------------------------------------------------------
/components/user/src/clojure/realworld/user/spec.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.user.spec
2 | (:require [clojure.realworld.spec.interface :as spec]
3 | [spec-tools.core :as st]
4 | [spec-tools.data-spec :as ds]))
5 |
6 | (def id
7 | (st/spec {:spec pos-int?
8 | :type :long
9 | :description "A long spec that defines a user id which is a positive integer"}))
10 |
11 | (def login
12 | (ds/spec {:name :core/login
13 | :spec {:email spec/email?
14 | :password spec/password?}}))
15 |
16 | (def register
17 | (ds/spec {:name :core/register
18 | :spec {:username spec/username?
19 | :email spec/email?
20 | :password spec/password?}}))
21 |
22 | (def update-user
23 | (ds/spec {:name :core/update-user
24 | :spec {:email spec/email?
25 | :username spec/username?
26 | :password spec/password?
27 | :image (ds/maybe spec/uri-string?)
28 | :bio (ds/maybe spec/non-empty-string?)}
29 | :keys-default ds/opt}))
30 |
31 | (def user-base
32 | {:id id
33 | :email spec/email?
34 | :username spec/username?
35 | (ds/opt :image) (ds/maybe spec/uri-string?)
36 | (ds/opt :bio) (ds/maybe spec/non-empty-string?)})
37 |
38 | (def user
39 | (ds/spec {:name :core/user
40 | :spec user-base}))
41 |
42 | (def visible-user
43 | (ds/spec {:name :core/visible-user
44 | :spec {:user (assoc user-base
45 | (ds/opt :token) spec/non-empty-string?)}}))
46 |
--------------------------------------------------------------------------------
/projects/realworld-backend/deps.edn:
--------------------------------------------------------------------------------
1 | {:ring {:init clojure.realworld.rest-api.api/init
2 | :destroy clojure.realworld.rest-api.api/destroy
3 | :handler clojure.realworld.rest-api.api/app
4 | :port 6003}
5 |
6 | :mvn/repos {"central" {:url "https://repo1.maven.org/maven2/"}
7 | "clojars" {:url "https://clojars.org/repo"}}
8 |
9 | :deps {poly/article {:local/root "../../components/article"}
10 | poly/comment {:local/root "../../components/comment"}
11 | poly/database {:local/root "../../components/database"}
12 | poly/env {:local/root "../../components/env"}
13 | poly/log {:local/root "../../components/log"}
14 | poly/profile {:local/root "../../components/profile"}
15 | poly/spec {:local/root "../../components/spec"}
16 | poly/tag {:local/root "../../components/tag"}
17 | poly/user {:local/root "../../components/user"}
18 | poly/rest-api {:local/root "../../bases/rest-api"}
19 |
20 | org.clojure/clojure {:mvn/version "1.11.1"}}
21 |
22 | :aliases {:test {:extra-paths []
23 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}}}
24 |
25 | :uberjar {:main clojure.realworld.rest-api.main}
26 |
27 | :ring {:extra-deps {org.slf4j/slf4j-nop {:mvn/version "2.0.3"}
28 | furkan3ayraktar/polylith-clj-deps-ring {:git/url "https://github.com/furkan3ayraktar/polylith-clj-deps-ring.git"
29 | :sha "7bb68846bb8a200a486a2886f1af95984538ec25"
30 | :deps/root "projects/core"}}
31 |
32 | :main-opts ["-m" "polylith.clj-deps-ring.cli.main" "start"]}}}
33 |
--------------------------------------------------------------------------------
/bases/rest-api/src/clojure/realworld/rest_api/middleware.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.rest-api.middleware
2 | (:require [clojure.string :as str]
3 | [clojure.realworld.log.interface :as log]
4 | [clojure.realworld.user.interface :as user]
5 | [clojure.realworld.env.interface :as env]))
6 |
7 | (defn wrap-auth-user [handler]
8 | (fn [req]
9 | (let [authorization (get (:headers req) "authorization")
10 | token (when authorization (-> (str/split authorization #" ") last))]
11 | (if-not (str/blank? token)
12 | (let [[ok? user] (user/user-by-token token)]
13 | (if ok?
14 | (handler (assoc req :auth-user (:user user)))
15 | (handler req)))
16 | (handler req)))))
17 |
18 | (defn wrap-authorization [handler]
19 | (fn [req]
20 | (if (:auth-user req)
21 | (handler req)
22 | {:status 401
23 | :body {:errors {:authorization "Authorization required."}}})))
24 |
25 | (defn wrap-exceptions [handler]
26 | (fn [req]
27 | (try
28 | (handler req)
29 | (catch Exception e
30 | (let [message (str "An unknown exception occurred.")]
31 | (log/error e message)
32 | {:status 500
33 | :body {:errors {:other [message]}}})))))
34 |
35 | (defn create-access-control-header [origin]
36 | (let [allowed-origins (or (env/env :allowed-origins) "")
37 | origins (str/split allowed-origins #",")
38 | allowed-origin (some #{origin} origins)]
39 | {"Access-Control-Allow-Origin" allowed-origin
40 | "Access-Control-Allow-Methods" "POST, GET, PUT, OPTIONS, DELETE"
41 | "Access-Control-Max-Age" "3600"
42 | "Access-Control-Allow-Headers" "Authorization, Content-Type, x-requested-with"}))
43 |
44 | (defn wrap-cors [handler]
45 | (fn [req]
46 | (let [origin (get (:headers req) "origin")
47 | response (handler req)]
48 | (assoc response :headers (merge (:headers response) (create-access-control-header origin))))))
49 |
--------------------------------------------------------------------------------
/components/comment/src/clojure/realworld/comment/core.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.comment.core
2 | (:require [clj-time.core :as t]
3 | [clojure.realworld.article.interface :as article]
4 | [clojure.realworld.comment.store :as store]
5 | [clojure.realworld.profile.interface :as profile]))
6 |
7 | (defn comment->visible-comment [auth-user comment]
8 | (let [user-id (:userId comment)
9 | [_ author] (profile/fetch-profile auth-user user-id)]
10 | (-> comment
11 | (dissoc :userId :articleId)
12 | (assoc :author (:profile author)))))
13 |
14 | (defn article-comments [auth-user slug]
15 | (let [[ok? {:keys [article]}] (article/article auth-user slug)]
16 | (if ok?
17 | (let [article-id (:id article)
18 | comments (store/comments article-id)
19 | res (mapv #(comment->visible-comment auth-user %) comments)]
20 | [true {:comments res}])
21 | [false {:errors {:slug ["Cannot find an article with given slug."]}}])))
22 |
23 | (defn add-comment! [auth-user slug {:keys [body]}]
24 | (let [[ok? {:keys [article]}] (article/article auth-user slug)]
25 | (if ok?
26 | (let [now (t/now)
27 | comment {:body body
28 | :articleId (:id article)
29 | :userId (:id auth-user)
30 | :createdAt now
31 | :updatedAt now}
32 | comment-id (store/add-comment! comment)]
33 | (if comment-id
34 | (let [added-comment (store/find-by-id comment-id)
35 | res (comment->visible-comment auth-user added-comment)]
36 | [true {:comment res}])
37 | [false {:errors {:other ["Cannot insert comment into db."]}}]))
38 | [false {:errors {:slug ["Cannot find an article with given slug."]}}])))
39 |
40 | (defn delete-comment! [auth-user id]
41 | (if-let [comment (store/find-by-id id)]
42 | (if (= (:id auth-user) (:userId comment))
43 | [true (store/delete-comment! id)]
44 | [false {:errors {:authorization ["You need to be author of this comment to delete it."]}}])
45 | [false {:errors {:id ["Cannot find a comment with given id."]}}]))
46 |
--------------------------------------------------------------------------------
/components/user/test/clojure/realworld/user/store_test.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.user.store-test
2 | (:require [clojure.java.jdbc :as jdbc]
3 | [clojure.realworld.database.interface :as database]
4 | [clojure.realworld.user.store :as store]
5 | [clojure.test :refer [deftest is use-fixtures]]))
6 |
7 | (defn test-db
8 | ([] {:classname "org.sqlite.JDBC"
9 | :subprotocol "sqlite"
10 | :subname "test.db"})
11 | ([_] (test-db)))
12 |
13 | (defn prepare-for-tests [f]
14 | (with-redefs [database/db test-db]
15 | (let [db (test-db)]
16 | (database/generate-db db)
17 | (f)
18 | (database/drop-db db))))
19 |
20 | (use-fixtures :each prepare-for-tests)
21 |
22 | (deftest find-by-key--test
23 | (let [_ (jdbc/insert! (test-db) :user {:email "test@test.com"
24 | :username "username"})
25 | res1 (store/find-by :email "test@test.com")
26 | user {:bio nil
27 | :email "test@test.com"
28 | :id 1
29 | :image nil
30 | :password nil
31 | :username "username"}
32 | res2 (store/find-by :username "username")]
33 | (is (= user res1))
34 | (is (= user res2))))
35 |
36 | (deftest insert-user!--test
37 | (let [user {:bio "bio"
38 | :email "test@test.com"
39 | :image "image"
40 | :password "password"
41 | :username "username"}
42 | _ (store/insert-user! user)
43 | res (store/find-by-email "test@test.com")]
44 | (is (= (assoc user :id 1) res))))
45 |
46 | (deftest update-user!--test
47 | (let [_ (store/insert-user! {:bio "bio"
48 | :email "test@test.com"
49 | :image "image"
50 | :password "password"
51 | :username "username"})
52 | user {:bio "updated-bio"
53 | :email "updated-test@test.com"
54 | :image "updated-image"
55 | :password "updated-password"
56 | :username "updated-username"}
57 | _ (store/update-user! 1 user)
58 | res (store/find-by-email "updated-test@test.com")]
59 | (is (= (assoc user :id 1) res))))
60 |
--------------------------------------------------------------------------------
/components/profile/test/clojure/realworld/profile/store_test.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.profile.store-test
2 | (:require [clojure.java.jdbc :as jdbc]
3 | [clojure.realworld.database.interface :as database]
4 | [clojure.realworld.profile.store :as store]
5 | [clojure.test :refer [deftest is use-fixtures]]))
6 |
7 | (defn test-db
8 | ([] {:classname "org.sqlite.JDBC"
9 | :subprotocol "sqlite"
10 | :subname "test.db"})
11 | ([_] (test-db)))
12 |
13 | (defn prepare-for-tests [f]
14 | (with-redefs [database/db test-db]
15 | (let [db (test-db)]
16 | (database/generate-db db)
17 | (f)
18 | (database/drop-db db))))
19 |
20 | (use-fixtures :each prepare-for-tests)
21 |
22 | (deftest following?--following--return-true
23 | (let [_ (jdbc/insert! (test-db) :userFollows {:userId 1
24 | :followedUserId 2})
25 | res (store/following? 1 2)]
26 | (is (true? res))))
27 |
28 | (deftest following?--not-following--return-false
29 | (let [res (store/following? 1 2)]
30 | (is (false? res))))
31 |
32 | (deftest follow!--currently-not-following--insert-user-follows
33 | (let [before-following? (store/following? 1 2)
34 | _ (store/follow! 1 2)
35 | after-following? (store/following? 1 2)]
36 | (is (false? before-following?))
37 | (is (true? after-following?))))
38 |
39 | (deftest follow!--currently-following--do-nothing
40 | (let [_ (store/follow! 1 2)
41 | before-following? (store/following? 1 2)
42 | _ (store/follow! 1 2)
43 | after-following? (store/following? 1 2)]
44 | (is (true? before-following?))
45 | (is (true? after-following?))))
46 |
47 | (deftest unfollow!--currently-following--delete-user-follows
48 | (let [_ (store/follow! 1 2)
49 | before-following? (store/following? 1 2)
50 | _ (store/unfollow! 1 2)
51 | after-following? (store/following? 1 2)]
52 | (is (true? before-following?))
53 | (is (false? after-following?))))
54 |
55 | (deftest unfollow!--currently-not-following--do-nothing
56 | (let [before-following? (store/following? 1 2)
57 | _ (store/unfollow! 1 2)
58 | after-following? (store/following? 1 2)]
59 | (is (false? before-following?))
60 | (is (false? after-following?))))
61 |
--------------------------------------------------------------------------------
/deps.edn:
--------------------------------------------------------------------------------
1 | {:mvn/repos {"central" {:url "https://repo1.maven.org/maven2/"}
2 | "clojars" {:url "https://clojars.org/repo"}}
3 |
4 | :aliases {:dev {:extra-paths ["development/src"]
5 |
6 | :extra-deps {; Components
7 | poly/article {:local/root "components/article"}
8 | poly/comment {:local/root "components/comment"}
9 | poly/database {:local/root "components/database"}
10 | poly/env {:local/root "components/env"}
11 | poly/log {:local/root "components/log"}
12 | poly/profile {:local/root "components/profile"}
13 | poly/spec {:local/root "components/spec"}
14 | poly/tag {:local/root "components/tag"}
15 | poly/user {:local/root "components/user"}
16 |
17 | ; Bases
18 | poly/rest-api {:local/root "bases/rest-api"}
19 |
20 | ; Development dependencies
21 | djblue/portal {:mvn/version "0.35.1"}
22 | clj-http/clj-http {:mvn/version "3.12.3"}
23 | org.clojure/clojure {:mvn/version "1.11.1"}
24 | org.slf4j/slf4j-nop {:mvn/version "2.0.3"}}}
25 |
26 | :test {:extra-paths ["components/article/test"
27 | "components/comment/test"
28 | "components/profile/test"
29 | "components/tag/test"
30 | "components/user/test"
31 |
32 | "bases/rest-api/test"]
33 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}}}
34 |
35 | :poly {:main-opts ["-m" "polylith.clj.core.poly-cli.core"]
36 | :extra-deps {polylith/clj-poly {:mvn/version "0.2.18-SNAPSHOT"}}}
37 |
38 | :build {:deps {io.github.clojure/tools.build {:mvn/version "0.9.3"}
39 | io.github.seancorfield/build-clj {:git/tag "v0.9.2"
40 | :git/sha "9c9f078"}
41 | org.clojure/tools.deps {:mvn/version "0.16.1281"}}
42 | :paths []
43 | :ns-default build}}}
44 |
--------------------------------------------------------------------------------
/gitpod.md:
--------------------------------------------------------------------------------
1 | # Hack on Real World Polylith in your Browser
2 |
3 | Using [Gitpod](https://www.gitpod.io) you can explore a [Polylith](https://polylith.gitbook.io/polylith/) implementation of [Real World](https://www.realworld.how/) from the Clojure REPL, without downloading or installing anything at all.
4 |
5 | 
6 |
7 | Don't click on this link just yet:
8 | https://gitpod.io/#https://github.com/furkan3ayraktar/clojure-polylith-realworld-example-app
9 |
10 | The link will take you to a full blown [VS Code](https://code.visualstudio.com/) running in your browser. The VS Code instance will have [Clojure](https://clojure.org) development support (through [Calva](https://calva.io)). The first time you use it, it may be quite a long wait. But then (after some little more waiting) you will be in the editor, connected to the REPL of the Polylith Real World server!
11 |
12 | ## Prerequisites
13 |
14 | * A Github account.
15 | * Curiosity
16 |
17 | That's it.
18 |
19 | ## From here to the Polylith REPL
20 |
21 | When you click the link you will first need to sign in to Gitpod using your Github account, then create the workspace, then wait, then wait a bit again. Then you will have the REPL under your fingertips. The process looks like so:
22 |
23 | 
24 |
25 | The Clojure file that opens will have further instructions and suggestions for what you can try at the REPL. There's also a file `src/hello_repl.clj` available for anyone unfamiliar with Calva and/or Clojure to start with.
26 |
27 | Now you can click that link above. 😄 (That said, see below about [considering forking first](#fork-first).)
28 |
29 | Happy coding! ❤️
30 |
31 | ## Fork first?
32 |
33 | If you just click the link above, things will work, but you will not be able to immediately push your work to your own repo. If you find that you want to do that, use your git skills to retarget the local (albeit Gitpod hosted) repo to a repo of your own.
34 |
35 | It could be a good idea to fork this repo first, of course.
36 |
37 | ## Gitpod Free hours are limited
38 |
39 | You may find it so fun to play with Polylith in the REPL that you run out of Gitpod hours. Then it is time to either:
40 |
41 | 1. Upgrade to a paid Gitpod plan
42 | 2. Run this repository locally
43 | * Check [the readme of this repository](readme.md) for instructions of how to run this example locally. (It is just a few steps more than running this on Gitpod.)
44 |
45 |
--------------------------------------------------------------------------------
/components/comment/test/clojure/realworld/comment/store_test.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.comment.store-test
2 | (:require [clj-time.core :as t]
3 | [clojure.java.jdbc :as jdbc]
4 | [clojure.realworld.comment.store :as store]
5 | [clojure.realworld.database.interface :as database]
6 | [clojure.test :refer [deftest is use-fixtures]]))
7 |
8 | (defn test-db
9 | ([] {:classname "org.sqlite.JDBC"
10 | :subprotocol "sqlite"
11 | :subname "test.db"})
12 | ([_] (test-db)))
13 |
14 | (defn prepare-for-tests [f]
15 | (with-redefs [database/db test-db]
16 | (let [db (test-db)]
17 | (database/generate-db db)
18 | (f)
19 | (database/drop-db db))))
20 |
21 | (use-fixtures :each prepare-for-tests)
22 |
23 | (deftest comments--no-comments--return-empty-vector
24 | (let [res (store/comments 1)]
25 | (is (= [] res))))
26 |
27 | (deftest comments--some-comments--return-all-comments
28 | (let [_ (jdbc/insert-multi! (test-db) :comment [{:articleId 1 :body "body1"}
29 | {:articleId 1 :body "body2"}
30 | {:articleId 1 :body "body3"}
31 | {:articleId 1 :body "body4"}])
32 | res (store/comments 1)]
33 | (is (= 4 (count res)))))
34 |
35 | (deftest find-by-id--comment-exists--return-comment
36 | (let [_ (jdbc/insert! (test-db) :comment {:body "body"})
37 | comment (store/find-by-id 1)]
38 | (is (= "body" (:body comment)))))
39 |
40 | (deftest find-by-id--comment-does-not-exist--return-nil
41 | (let [comment (store/find-by-id 1)]
42 | (is (nil? comment))))
43 |
44 | (deftest add-comment!--test
45 | (let [now (t/now)
46 | comment {:body "body"
47 | :createdAt now
48 | :updatedAt now
49 | :userId 1
50 | :articleId 1}
51 | res (store/add-comment! comment)
52 | added (store/find-by-id 1)]
53 | (is (= (assoc comment :id 1
54 | :createdAt (-> comment :createdAt str)
55 | :updatedAt (-> comment :updatedAt str))
56 | added))
57 | (is (= 1 res))))
58 |
59 | (deftest delete-comment!--test
60 | (let [now (t/now)
61 | _ (store/add-comment! {:body "body"
62 | :createdAt now
63 | :updatedAt now
64 | :userId 1
65 | :articleId 1})
66 | comment-before (store/find-by-id 1)
67 | _ (store/delete-comment! 1)
68 | comment-after (store/find-by-id 1)]
69 | (is (not (nil? comment-before)))
70 | (is (nil? comment-after))))
71 |
--------------------------------------------------------------------------------
/components/spec/src/clojure/realworld/spec/core.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.spec.core
2 | (:require [clojure.spec.alpha :as s]
3 | [clojure.spec.gen.alpha :as gen]
4 | [clojure.string :as str]
5 | [spec-tools.core :as st])
6 | (:import (java.util UUID)))
7 |
8 | (def ^:private email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$")
9 | (def ^:private uri-regex #"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)")
10 | (def ^:private slug-regex #"^[a-z0-9]+(?:-[a-z0-9]+)*$")
11 |
12 | (def non-empty-string?
13 | (st/spec {:spec (s/and string? #(not (str/blank? %)))
14 | :type :string
15 | :description "Non empty string spec. Checks with clojure.string/blank?"}))
16 |
17 | (def username?
18 | (st/spec {:spec non-empty-string?
19 | :type :string
20 | :description "A non empty string spec with a special username (UUID) generator."
21 | :gen #(gen/fmap (fn [_] (str (UUID/randomUUID)))
22 | (gen/string-alphanumeric))}))
23 |
24 | (def email?
25 | (st/spec {:spec (s/and string? #(re-matches email-regex %))
26 | :type :string
27 | :description "A string spec that conforms to email-regex."
28 | :gen #(gen/fmap (fn [[s1 s2]] (str s1 "@" s2 ".com"))
29 | (gen/tuple (gen/string-alphanumeric) (gen/string-alphanumeric)))}))
30 |
31 | (def uri-string?
32 | (st/spec {:spec (s/and string? #(re-matches uri-regex %))
33 | :type :string
34 | :description "A string spec that conforms to uri-regex."
35 | :gen #(gen/fmap (fn [[c1 c2]]
36 | (let [s1 (apply str c1)
37 | s2 (apply str c2)]
38 | (str "http://" s1 "." (subs s2 0 (if (< 3 (count s2)) 3 (count s2))))))
39 | (gen/tuple (gen/vector (gen/char-alpha) 2 100) (gen/vector (gen/char-alpha) 2 5)))}))
40 |
41 | (def slug?
42 | (st/spec {:spec (s/and string? #(re-matches slug-regex %))
43 | :type :string
44 | :description "A string spec that conforms to slug-regex."
45 | :gen #(gen/fmap (fn [[c1 c2]]
46 | (let [s1 (str/lower-case (apply str c1))
47 | s2 (str/lower-case (apply str c2))]
48 | (str s1 "-" s2)))
49 | (gen/tuple (gen/vector (gen/char-alpha) 2 10) (gen/vector (gen/char-alpha) 2 10)))}))
50 |
51 | (def password?
52 | (st/spec {:spec (s/and string? #(<= 8 (count %)))
53 | :type :string
54 | :description "A string spec with more than or equal to 8 characters."}))
55 |
--------------------------------------------------------------------------------
/bases/rest-api/src/clojure/realworld/rest_api/api.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.rest-api.api
2 | (:require [clojure.realworld.database.interface :as database]
3 | [clojure.realworld.rest-api.handler :as h]
4 | [clojure.realworld.rest-api.middleware :as m]
5 | [clojure.realworld.log.interface :as log]
6 | [compojure.core :refer [routes wrap-routes defroutes GET POST PUT DELETE ANY OPTIONS]]
7 | [ring.logger.timbre :as logger]
8 | [ring.middleware.json :as js]
9 | [ring.middleware.keyword-params :as kp]
10 | [ring.middleware.multipart-params :as mp]
11 | [ring.middleware.nested-params :as np]
12 | [ring.middleware.params :as pr]))
13 |
14 | (defroutes public-routes
15 | (OPTIONS "/**" [] h/options)
16 | (GET "/api/health" [] h/health)
17 | (POST "/api/users/login" [] h/login)
18 | (POST "/api/users" [] h/register)
19 | (GET "/api/profiles/:username" [] h/profile)
20 | (GET "/api/articles" [] h/articles)
21 | (GET "/api/articles/:slug" [] h/article)
22 | (GET "/api/articles/:slug/comments" [] h/comments)
23 | (GET "/api/tags" [] h/tags))
24 |
25 | (defroutes private-routes
26 | (GET "/api/user" [] h/current-user)
27 | (PUT "/api/user" [] h/update-user)
28 | (POST "/api/profiles/:username/follow" [] h/follow-profile)
29 | (DELETE "/api/profiles/:username/follow" [] h/unfollow-profile)
30 | (GET "/api/articles/feed" [] h/feed)
31 | (POST "/api/articles" [] h/create-article)
32 | (PUT "/api/articles/:slug" [] h/update-article)
33 | (DELETE "/api/articles/:slug" [] h/delete-article)
34 | (POST "/api/articles/:slug/comments" [] h/add-comment)
35 | (DELETE "/api/articles/:slug/comments/:id" [] h/delete-comment)
36 | (POST "/api/articles/:slug/favorite" [] h/favorite-article)
37 | (DELETE "/api/articles/:slug/favorite" [] h/unfavorite-article))
38 |
39 | (defroutes other-routes
40 | (ANY "/**" [] h/other))
41 |
42 | (def ^:private app-routes
43 | (routes
44 | (-> private-routes
45 | (wrap-routes m/wrap-authorization)
46 | (wrap-routes m/wrap-auth-user))
47 | (-> public-routes
48 | (wrap-routes m/wrap-auth-user))
49 | other-routes))
50 |
51 | (def app
52 | (-> app-routes
53 | logger/wrap-with-logger
54 | kp/wrap-keyword-params
55 | pr/wrap-params
56 | mp/wrap-multipart-params
57 | js/wrap-json-params
58 | np/wrap-nested-params
59 | m/wrap-exceptions
60 | js/wrap-json-response
61 | m/wrap-cors))
62 |
63 | (defn init []
64 | (try
65 | (log/init)
66 | (let [db (database/db)]
67 | (if (database/db-exists?)
68 | (if (database/valid-schema? db)
69 | (log/info "Database schema is valid.")
70 | (do
71 | (log/warn "Please fix database schema and restart")
72 | (System/exit 1)))
73 | (do
74 | (log/info "Generating database.")
75 | (database/generate-db db)
76 | (log/info "Database generated."))))
77 | (log/info "Initialized server.")
78 | (catch Exception e
79 | (log/error e "Could not start server."))))
80 |
81 | (defn destroy []
82 | (log/info "Destroyed server."))
83 |
--------------------------------------------------------------------------------
/components/log/src/clojure/realworld/log/config.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.log.config
2 | (:require [clojure.java.io :as io]
3 | [clojure.string :as str]
4 | [clojure.realworld.env.interface :as env]
5 | [taoensso.timbre :as timbre])
6 | (:import (java.util Calendar)
7 | (java.text SimpleDateFormat)
8 | (java.io File IOException)))
9 |
10 | (defn- rename-old-create-new-log [^File log ^File old-log]
11 | (.renameTo log old-log)
12 | (.createNewFile log))
13 |
14 | (defn- shift-log-period [log path prev-cal]
15 | (let [postfix (-> "yyyy-MM-dd" SimpleDateFormat. (.format (.getTime prev-cal)))
16 | last-index-of-dot (str/last-index-of path ".")
17 | file-name (if (>= last-index-of-dot 0) (subs path 0 last-index-of-dot) path)
18 | extension (if (>= last-index-of-dot 0) (subs path (+ 1 last-index-of-dot)) "")
19 | old-path (format "%s.%s.%s" file-name postfix extension)
20 | old-log (io/file old-path)]
21 | (if (.exists old-log)
22 | (loop [index 0]
23 | (let [index-path (format "%s.%d" old-path index)
24 | index-log (io/file index-path)]
25 | (if (.exists index-log)
26 | (recur (+ index 1))
27 | (rename-old-create-new-log log index-log))))
28 | (rename-old-create-new-log log old-log))))
29 |
30 | (defn- log-cal [date] (let [now (Calendar/getInstance)] (.setTime now date) now))
31 |
32 | (defn- prev-period-end-cal [date pattern]
33 | (let [cal (log-cal date)
34 | offset (case pattern
35 | :daily 1
36 | :weekly (.get cal Calendar/DAY_OF_WEEK)
37 | :monthly (.get cal Calendar/DAY_OF_MONTH)
38 | 0)]
39 | (.add cal Calendar/DAY_OF_MONTH (* -1 offset))
40 | (.set cal Calendar/HOUR_OF_DAY 23)
41 | (.set cal Calendar/MINUTE 59)
42 | (.set cal Calendar/SECOND 59)
43 | (.set cal Calendar/MILLISECOND 999)
44 | cal))
45 |
46 | (defn- rolling-appender
47 | "Returns a Rolling file appender. Opts:
48 | :path - logfile path.
49 | :pattern - frequency of rotation, e/o {:daily :weekly :monthly}."
50 | [& [{:keys [path pattern]
51 | :or {path "./timbre-rolling.log"
52 | pattern :daily}}]]
53 |
54 | {:enabled? true
55 | :async? false
56 | :min-level nil
57 | :rate-limit nil
58 | :output-fn :inherit
59 | :fn
60 | (fn [data]
61 | (let [{:keys [instant output_]} data
62 | output-str (force output_)
63 | prev-cal (prev-period-end-cal instant pattern)]
64 | (when-let [log (io/file path)]
65 | (try
66 | (when-not (.exists log)
67 | (io/make-parents log))
68 | (if (.exists log)
69 | (when (<= (.lastModified log) (.getTimeInMillis prev-cal))
70 | (shift-log-period log path prev-cal))
71 | (.createNewFile log))
72 | (spit path (with-out-str (println output-str)) :append true)
73 | (catch IOException _)))))})
74 |
75 | (defn init []
76 | (if (= "LOCAL" (env/env :environment))
77 | (timbre/info "Initialized logging. Using console to print logs.")
78 | (do
79 | (timbre/set-config! {:level :info
80 | :appenders {:rolling-file-adapter (rolling-appender {:path "/var/log/tomcat8/backend.log"})}})
81 | (timbre/info "Initialized logging. Using /var/log/tomcat8/backend.log path for rolling file adapter."))))
82 |
--------------------------------------------------------------------------------
/components/user/src/clojure/realworld/user/core.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.user.core
2 | (:require [clojure.realworld.user.store :as store]
3 | [crypto.password.pbkdf2 :as crypto]
4 | [clj-jwt.core :as jwt]
5 | [clj-time.coerce :as c]
6 | [clj-time.core :as t]
7 | [clojure.realworld.env.interface :as env]))
8 |
9 | (defn- token-secret []
10 | (if (contains? env/env :secret)
11 | (env/env :secret)
12 | "some-default-secret-do-not-use-it"))
13 |
14 | (defn- generate-token [email username]
15 | (let [now (t/now)
16 | claim {:sub username
17 | :iss email
18 | :exp (t/plus now (t/days 7))
19 | :iat now}]
20 | (-> claim jwt/jwt (jwt/sign :HS256 (token-secret)) jwt/to-str)))
21 |
22 | (defn- jwt-str->jwt [jwt-string]
23 | (try
24 | (jwt/str->jwt jwt-string)
25 | (catch Exception _
26 | nil)))
27 |
28 | (defn- expired? [jwt]
29 | (if-let [exp (-> jwt :claims :exp)]
30 | (try
31 | (t/before? (c/from-long (* 1000 exp)) (t/now))
32 | (catch Exception _
33 | true))
34 | true))
35 |
36 | (defn- token->claims [jwt-string]
37 | (when-let [jwt (jwt-str->jwt jwt-string)]
38 | (when (and (jwt/verify jwt (token-secret))
39 | (not (expired? jwt)))
40 | (:claims jwt))))
41 |
42 | (defn encrypt-password [password]
43 | (-> password crypto/encrypt str))
44 |
45 | (defn user->visible-user [user token]
46 | {:user (-> user
47 | (assoc :token token)
48 | (dissoc :password))})
49 |
50 | (defn login! [{:keys [email password]}]
51 | (if-let [user (store/find-by-email email)]
52 | (if (crypto/check password (:password user))
53 | (let [new-token (generate-token email (:username user))]
54 | [true (user->visible-user user new-token)])
55 | [false {:errors {:password ["Invalid password."]}}])
56 | [false {:errors {:email ["Invalid email."]}}]))
57 |
58 | (defn register! [{:keys [username email password]}]
59 | (if-let [_ (store/find-by-email email)]
60 | [false {:errors {:email ["A user exists with given email."]}}]
61 | (if-let [_ (store/find-by-username username)]
62 | [false {:errors {:username ["A user exists with given username."]}}]
63 | (let [new-token (generate-token email username)
64 | user-input {:email email
65 | :username username
66 | :password (encrypt-password password)}
67 | _ (store/insert-user! user-input)]
68 | (if-let [user (store/find-by-email email)]
69 | [true (user->visible-user user new-token)]
70 | [false {:errors {:other ["Cannot insert user into db."]}}])))))
71 |
72 | (defn user-by-token [token]
73 | (let [claims (token->claims token)
74 | username (:sub claims)
75 | user (store/find-by-username username)]
76 | (if user
77 | [true (user->visible-user user token)]
78 | [false {:errors {:token ["Cannot find a user with associated token."]}}])))
79 |
80 | (defn update-user! [auth-user {:keys [username email password image bio]}]
81 | (if (and (not (nil? email))
82 | (not= email (:email auth-user))
83 | (not (nil? (store/find-by-email email))))
84 | [false {:errors {:email ["A user exists with given email."]}}]
85 | (if (and (not (nil? username))
86 | (not= username (:username auth-user))
87 | (not (nil? (store/find-by-username username))))
88 | [false {:errors {:username ["A user exists with given username."]}}]
89 | (let [email-to-use (if email email (:email auth-user))
90 | optional-map (filter #(-> % val nil? not)
91 | {:password (when password (encrypt-password password))
92 | :email (when email email)
93 | :username (when username username)})
94 | user-input (merge {:image image
95 | :bio bio}
96 | optional-map)
97 | _ (store/update-user! (:id auth-user) user-input)]
98 | (if-let [updated-user (store/find-by-email email-to-use)]
99 | [true (user->visible-user updated-user (:token auth-user))]
100 | [false {:errors {:other ["Cannot update user."]}}])))))
101 |
--------------------------------------------------------------------------------
/development/src/dev/getting_started.clj:
--------------------------------------------------------------------------------
1 | (ns dev.getting-started
2 | (:require [clj-http.client :as http]))
3 |
4 | ;; Welcome to the REPL for:
5 | ;; Getting Started with the Polylith Real World example!
6 | ;;
7 | ;; Please have some little patience while the wheels spin up. You'll
8 | ;; know things are ready when you see timestamped log messages in
9 | ;; the repl/output pane that will split open to the right of this file ->
10 | ;; (We often refer to this as “The REPL/Output Window”.)
11 | ;;
12 | ;; If you are unfamiliar with Calva or Clojure, please open the file
13 | ;; development/src/dev/hello_repl.clj
14 | ;; To confirm that you know Calva well enough to continue this session,
15 | ;; 1. Load this file in the REPL
16 | ;; You should see a greeting printed in the output window
17 | ;; 2. Evaluate this string below
18 | ;; You should see the result (the string itself) appear both
19 | ;; inline in this pane and printed to the output window
20 |
21 | "Hello Polylith Real World!"
22 |
23 | ;; Worked? Great! Let's continue.
24 | ;; Didn't work as described? Please don't hesitate to file an issue.
25 | ;;
26 | ;; The guide is designed to run in Rich Comment Forms like the one
27 | ;; below. You are supposed to read instructions and evaluate the
28 | ;; top level forms in the RCF. If there is no instruction to evaluate
29 | ;; a top level form, consider it an implied instruction to do so.
30 | ;; Please don't hesitate to experiment with the code!
31 |
32 | (comment
33 | ;; The server is running, we should be able to grab some articles
34 |
35 | ;; This is the API base URL
36 | (def base-url "http://localhost:6003/api")
37 |
38 | ;; Fetch some articles
39 | (def articles (http/get (str base-url "/articles") {:as :auto}))
40 |
41 | ;; Look at them
42 | (:body articles)
43 |
44 | ;; Oh, no articles? We need to add one!
45 |
46 | ;; First we need to create a user, this is the API payload.
47 | (def register-payload {:user {:username "Eager Polylith Explorer"
48 | :email "polylith-is-cool@example.com"
49 | :password "battery-staple-horse"}})
50 |
51 | ;; Creating the user
52 | ;; We hold on to the token so that we can create articles later
53 | (def jwt-token (let [register-response (http/post (str base-url "/users")
54 | {:form-params register-payload
55 | :content-type :json
56 | :as :auto})]
57 | (->> register-response :body :user :token)))
58 | ;; Consider evaluating `jwt-token` above to “save” the token in the
59 | ;; repl/output window.
60 |
61 | ;; Now our first article
62 | (def article-payload {:article {:title "Polylith Real World example article"
63 | :description "My first article. And it is about Polylith of course!"
64 | :body "Yada, yada, yada. Too lazy right now"
65 | :tagList ["polylith" "clojure" "realworld"]}})
66 |
67 | (def article-request-headers {"Authorization" (str "Token " jwt-token)})
68 |
69 | ;; Happy with the copy? Let's post it!
70 | (http/post (str base-url "/articles")
71 | {:form-params article-payload
72 | :headers article-request-headers
73 | :content-type :json})
74 |
75 | ;; We define the request so that we can examine it later
76 | (def article-request *1)
77 |
78 |
79 | (def articles-again (http/get (str base-url "/articles") {:as :auto}))
80 | (:body articles-again)
81 | (->> articles-again :body :articlesCount)
82 |
83 | :rcf)
84 |
85 | ;; ======== The poly tool ========
86 | ;; We have started the tool in a terminal, open the terminal pane
87 | ;; and you should find a terminal named “poly shell”
88 | ;; Commands you can try from the poly shell:
89 | ;; * `check` - checks the integrity of the project
90 | ;; * `help`
91 | ;; * `info`
92 | ;; * `help info`
93 | ;; * `test :all`
94 | ;; * `help create`
95 | ;;
96 | ;; NB: Don't prepend your poly tool commands with `poly`, that's
97 | ;; for when you run the `poly` command from the operating system
98 | ;; shell (bash etcetera).
99 | ;; Learn about the poly tool here: https://polylith.gitbook.io/poly/
100 |
101 | (comment
102 |
103 | ;; TODO: @furkan3ayraktar and @tengstrand and @Misophistful, please fill in the blank space!
104 |
105 | :rcf)
106 |
107 |
108 | "Hello dear Polylith Real World explorer! You loaded this file in the REPL."
--------------------------------------------------------------------------------
/components/database/src/clojure/realworld/database/schema.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.database.schema
2 | (:require [clojure.java.jdbc :as jdbc]
3 | [clojure.realworld.log.interface :as log]
4 | [honey.sql :as sql]))
5 |
6 | (def user
7 | (jdbc/create-table-ddl :user
8 | [[:id :integer :primary :key :autoincrement]
9 | [:email :text :unique]
10 | [:username :text :unique]
11 | [:password :text]
12 | [:image :text]
13 | [:bio :text]]
14 | {:entities identity}))
15 |
16 | (def user-follows
17 | (jdbc/create-table-ddl :userFollows
18 | [[:userId :integer "references user(id)"]
19 | [:followedUserId :integer "references user(id)"]]
20 | {:entities identity}))
21 |
22 | (def article
23 | (jdbc/create-table-ddl :article
24 | [[:id :integer :primary :key :autoincrement]
25 | [:slug :text :unique]
26 | [:title :text]
27 | [:description :text]
28 | [:body :text]
29 | [:createdAt :datetime]
30 | [:updatedAt :datetime]
31 | [:userId :integer "references user(id)"]]
32 | {:entities identity}))
33 |
34 | (def tag
35 | (jdbc/create-table-ddl :tag
36 | [[:id :integer :primary :key :autoincrement]
37 | [:name :text :unique]]
38 | {:entities identity}))
39 |
40 | (def article-tags
41 | (jdbc/create-table-ddl :articleTags
42 | [[:articleId :integer "references article(id)"]
43 | [:tagId :integer "references tag(id)"]]
44 | {:entities identity}))
45 |
46 | (def favorite-articles
47 | (jdbc/create-table-ddl :favoriteArticles
48 | [[:articleId :integer "references article(id)"]
49 | [:userId :integer "references user(id)"]]
50 | {:entities identity}))
51 |
52 | (def comment-table
53 | (jdbc/create-table-ddl :comment
54 | [[:id :integer :primary :key :autoincrement]
55 | [:body :text]
56 | [:articleId :integer "references article(id)"]
57 | [:userId :integer "references user(id)"]
58 | [:createdAt :datetime]
59 | [:updatedAt :datetime]]
60 | {:entities identity}))
61 |
62 | (defn generate-db [db]
63 | (jdbc/db-do-commands db
64 | [user
65 | user-follows
66 | article
67 | tag
68 | article-tags
69 | favorite-articles
70 | comment-table]))
71 |
72 | (defn drop-db [db]
73 | (jdbc/db-do-commands db
74 | [(jdbc/drop-table-ddl :user)
75 | (jdbc/drop-table-ddl :userFollows)
76 | (jdbc/drop-table-ddl :article)
77 | (jdbc/drop-table-ddl :tag)
78 | (jdbc/drop-table-ddl :articleTags)
79 | (jdbc/drop-table-ddl :favoriteArticles)
80 | (jdbc/drop-table-ddl :comment)]))
81 |
82 | (defn table->schema-item [{:keys [tbl_name sql]}]
83 | [(keyword tbl_name) sql])
84 |
85 | (defn valid-schema? [db]
86 | (let [query {:select [:*]
87 | :from [:sqlite_master]
88 | :where [:= :type "table"]}
89 | tables (jdbc/query db (sql/format query) {:identifiers identity})
90 | current-schema (select-keys (into {} (map table->schema-item tables))
91 | [:user :userFollows :article :tag :articleTags :favoriteArticles :comment])
92 | valid-schema {:user user
93 | :userFollows user-follows
94 | :article article
95 | :tag tag
96 | :articleTags article-tags
97 | :favoriteArticles favorite-articles
98 | :comment comment-table}]
99 | (if (= valid-schema current-schema)
100 | true
101 | (do
102 | (log/warn "Current schema is invalid. Please correct it and restart the server.")
103 | false))))
104 |
105 |
--------------------------------------------------------------------------------
/components/comment/test/clojure/realworld/comment/core_test.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.comment.core-test
2 | (:require [clojure.java.jdbc :as jdbc]
3 | [clojure.realworld.comment.core :as core]
4 | [clojure.realworld.comment.spec :as spec]
5 | [clojure.realworld.database.interface :as database]
6 | [clojure.realworld.user.interface.spec :as user-spec]
7 | [clojure.spec.alpha :as s]
8 | [clojure.spec.gen.alpha :as gen]
9 | [clojure.test :refer [deftest is use-fixtures]]))
10 |
11 | (defn test-db
12 | ([] {:classname "org.sqlite.JDBC"
13 | :subprotocol "sqlite"
14 | :subname "test.db"})
15 | ([_] (test-db)))
16 |
17 | (def ^:private auth-user
18 | (assoc (gen/generate (s/gen user-spec/user)) :id 1))
19 |
20 | (defn prepare-for-tests [f]
21 | (with-redefs [database/db test-db]
22 | (let [db (test-db)]
23 | (database/generate-db db)
24 | (jdbc/insert! db :user auth-user)
25 | (f)
26 | (database/drop-db db))))
27 |
28 | (use-fixtures :each prepare-for-tests)
29 |
30 | (deftest article-comments--article-not-found--return-negative-response
31 | (let [[ok? res] (core/article-comments auth-user "slug")]
32 | (is (false? ok?))
33 | (is (= {:errors {:slug ["Cannot find an article with given slug."]}} res))))
34 |
35 | (deftest article-comments--no-comments-found--return-positive-response-with-empty-vector
36 | (let [_ (jdbc/insert! (test-db) :article {:slug "slug"})
37 | [ok? res] (core/article-comments auth-user "slug")]
38 | (is (true? ok?))
39 | (is (= {:comments []} res))))
40 |
41 | (deftest article-comments--comments-found--return-positive-response
42 | (let [_ (jdbc/insert! (test-db) :article {:slug "slug"})
43 | _ (jdbc/insert-multi! (test-db) :user (map-indexed #(assoc %2 :id (+ 2 %1)) (gen/sample (s/gen user-spec/user) 2)))
44 | _ (jdbc/insert-multi! (test-db) :comment [{:body "body1" :articleId 1 :userId 1}
45 | {:body "body2" :articleId 1 :userId 2}
46 | {:body "body3" :articleId 1 :userId 2}
47 | {:body "body4" :articleId 1 :userId 3}])
48 | [ok? res] (core/article-comments auth-user "slug")]
49 | (is (true? ok?))
50 | (is (= 4 (-> res :comments count)))))
51 |
52 | (deftest article-comments--comments-found-without-auth--return-positive-response
53 | (let [_ (jdbc/insert! (test-db) :article {:slug "slug"})
54 | _ (jdbc/insert-multi! (test-db) :user (map-indexed #(assoc %2 :id (+ 2 %1)) (gen/sample (s/gen user-spec/user) 2)))
55 | _ (jdbc/insert-multi! (test-db) :comment [{:body "body1" :articleId 1 :userId 1}
56 | {:body "body2" :articleId 1 :userId 2}
57 | {:body "body3" :articleId 1 :userId 2}
58 | {:body "body4" :articleId 1 :userId 3}])
59 | [ok? res] (core/article-comments nil "slug")]
60 | (is (true? ok?))
61 | (is (= 4 (-> res :comments count)))))
62 |
63 | (deftest add-comment!--test
64 | (let [_ (jdbc/insert! (test-db) :article {:slug "slug"})
65 | inputs (gen/sample (s/gen spec/add-comment) 20)
66 | results (map #(core/add-comment! auth-user "slug" %) inputs)]
67 | (is (every? true? (map first results)))
68 | (is (every? #(s/valid? spec/visible-comment (second %)) results))))
69 |
70 | (deftest delete-comment!--comment-not-found--return-negative-response
71 | (let [[ok? res] (core/delete-comment! auth-user 1)]
72 | (is (false? ok?))
73 | (is (= {:errors {:id ["Cannot find a comment with given id."]}} res))))
74 |
75 | (deftest delete-comment!--comment-is-not-owned-by-user--return-negative-response
76 | (let [_ (jdbc/insert! (test-db) :article {:slug "slug"})
77 | initial (gen/generate (s/gen spec/add-comment))
78 | [_ comment] (core/add-comment! auth-user "slug" initial)
79 | [ok? res] (core/delete-comment! (assoc auth-user :id 2)
80 | (-> comment :comment :id))]
81 | (is (false? ok?))
82 | (is (= {:errors {:authorization ["You need to be author of this comment to delete it."]}} res))))
83 |
84 | (deftest delete-comment!--input-is-ok--delete-comment-and-return-positive-response
85 | (let [_ (jdbc/insert! (test-db) :article {:slug "slug"})
86 | initial-inputs (gen/sample (s/gen spec/add-comment) 20)
87 | create-res (map #(core/add-comment! auth-user "slug" %) initial-inputs)
88 | update-res (map #(core/delete-comment! auth-user (-> % second :comment :id)) create-res)]
89 | (is (every? #(= [true nil] %) update-res))))
90 |
--------------------------------------------------------------------------------
/components/profile/test/clojure/realworld/profile/core_test.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.profile.core-test
2 | (:require [clojure.java.jdbc :as jdbc]
3 | [clojure.realworld.database.interface :as database]
4 | [clojure.realworld.profile.core :as core]
5 | [clojure.realworld.user.interface.spec :as user-spec]
6 | [clojure.spec.gen.alpha :as gen]
7 | [clojure.spec.alpha :as s]
8 | [clojure.test :refer [deftest is use-fixtures]]))
9 |
10 | (defn- test-db
11 | ([] {:classname "org.sqlite.JDBC"
12 | :subprotocol "sqlite"
13 | :subname "test.db"})
14 | ([_] (test-db)))
15 |
16 | (def ^:private auth-user
17 | (assoc (gen/generate (s/gen user-spec/user)) :id 1))
18 |
19 | (defn prepare-for-tests [f]
20 | (with-redefs [database/db test-db]
21 | (let [db (test-db)]
22 | (database/generate-db db)
23 | (jdbc/insert! db :user auth-user)
24 | (f)
25 | (database/drop-db db))))
26 |
27 | (use-fixtures :each prepare-for-tests)
28 |
29 | (deftest fetch-profile--profile-not-found--return-negative-result
30 | (let [[ok? res] (core/fetch-profile auth-user "username")]
31 | (is (false? ok?))
32 | (is (= {:errors {:username ["Cannot find a profile with given username."]}} res))))
33 |
34 | (deftest fetch-profile--not-logged-in--return-positive-result-with-false-following
35 | (let [_ (jdbc/insert! (database/db) :user {:username "username"
36 | :bio "bio"
37 | :image "image"})
38 | [ok? res] (core/fetch-profile nil "username")]
39 | (is (true? ok?))
40 | (is (= {:profile {:username "username"
41 | :bio "bio"
42 | :image "image"
43 | :following false}}
44 | res))))
45 |
46 | (deftest fetch-profile--logged-in-not-following--return-positive-result-with-false-following
47 | (let [_ (jdbc/insert! (database/db) :user {:username "username"
48 | :bio "bio"
49 | :image "image"})
50 | [ok? res] (core/fetch-profile auth-user "username")]
51 | (is (true? ok?))
52 | (is (= {:profile {:username "username"
53 | :bio "bio"
54 | :image "image"
55 | :following false}}
56 | res))))
57 |
58 | (deftest fetch-profile--logged-in-following--return-positive-result-with-true-following
59 | (let [_ (jdbc/insert! (database/db) :user {:username "username"
60 | :bio "bio"
61 | :image "image"})
62 | _ (jdbc/insert! (database/db) :userFollows {:userId 1 :followedUserId 2})
63 | [ok? res] (core/fetch-profile auth-user "username")]
64 | (is (true? ok?))
65 | (is (= {:profile {:username "username"
66 | :bio "bio"
67 | :image "image"
68 | :following true}}
69 | res))))
70 |
71 | (deftest follow!--profile-not-found--return-negative-result
72 | (let [[ok? res] (core/follow! auth-user "username")]
73 | (is (false? ok?))
74 | (is (= {:errors {:username ["Cannot find a profile with given username."]}} res))))
75 |
76 | (deftest follow!--profile-found--return-positive-result
77 | (let [_ (jdbc/insert! (database/db) :user {:username "username"
78 | :bio "bio"
79 | :image "image"})
80 | [ok? res] (core/follow! auth-user "username")]
81 | (is (true? ok?))
82 | (is (= {:profile {:username "username"
83 | :bio "bio"
84 | :image "image"
85 | :following true}}
86 | res))))
87 |
88 | (deftest unfollow!--profile-not-found--return-negative-result
89 | (let [[ok? res] (core/unfollow! auth-user "username")]
90 | (is (false? ok?))
91 | (is (= {:errors {:username ["Cannot find a profile with given username."]}} res))))
92 |
93 | (deftest unfollow!--logged-in-and-profile-found--return-positive-result
94 | (let [_ (jdbc/insert! (database/db) :user {:username "username"
95 | :bio "bio"
96 | :image "image"})
97 | _ (jdbc/insert! (database/db) :userFollows {:userId 1 :followedUserId 2})
98 | [ok? res] (core/unfollow! auth-user "username")]
99 | (is (true? ok?))
100 | (is (= {:profile {:username "username"
101 | :bio "bio"
102 | :image "image"
103 | :following false}}
104 | res))))
105 |
--------------------------------------------------------------------------------
/components/article/src/clojure/realworld/article/core.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.article.core
2 | (:require [clj-time.coerce :as c]
3 | [clj-time.core :as t]
4 | [clojure.realworld.article.store :as store]
5 | [clojure.realworld.profile.interface :as profile]
6 | [slugger.core :as slugger]))
7 |
8 | (defn article->visible-article [article auth-user]
9 | (let [user-id (:userId article)
10 | article-id (:id article)
11 | [_ author] (profile/fetch-profile auth-user user-id)
12 | favorited? (if auth-user (store/favorited? (:id auth-user) article-id) false)
13 | favorites-count (store/favorites-count article-id)
14 | tags (store/article-tags article-id)]
15 | (assoc (dissoc article :userId)
16 | :favorited favorited?
17 | :favoritesCount favorites-count
18 | :author (:profile author)
19 | :tagList tags)))
20 |
21 | (defn- create-slug [title now]
22 | (when title
23 | (let [slug (slugger/->slug title)]
24 | (if (store/find-by-slug slug)
25 | (str slug "-" (c/to-long now))
26 | slug))))
27 |
28 | (defn article [auth-user slug]
29 | (if-let [article (store/find-by-slug slug)]
30 | [true {:article (article->visible-article article auth-user)}]
31 | [false {:errors {:slug ["Cannot find an article with given slug."]}}]))
32 |
33 | (defn create-article! [auth-user {:keys [title description body tagList]}]
34 | (let [user-id (:id auth-user)
35 | now (t/now)
36 | slug (create-slug title now)
37 | article-input {:slug slug
38 | :title title
39 | :description description
40 | :body body
41 | :createdAt now
42 | :updatedAt now
43 | :userId user-id}
44 | _ (store/insert-article! article-input)]
45 | (if-let [article (store/find-by-slug slug)]
46 | (let [article-id (:id article)
47 | _ (store/update-tags! tagList)
48 | _ (store/add-tags-to-article! article-id tagList)]
49 | [true {:article (article->visible-article article auth-user)}])
50 | [false {:errors {:other ["Cannot insert article into db."]}}])))
51 |
52 | (defn update-article! [auth-user slug {:keys [title description body]}]
53 | (if-let [article (store/find-by-slug slug)]
54 | (if (= (:id auth-user) (:userId article))
55 | (let [now (t/now)
56 | slug (create-slug title now)
57 | article-input (into {} (filter #(-> % val nil? not)
58 | {:slug slug
59 | :title title
60 | :description description
61 | :body body
62 | :updatedAt now}))
63 | _ (store/update-article! (:id article) article-input)]
64 | (if-let [updated-article (store/find-by-slug (if slug slug (:slug article)))]
65 | [true {:article (article->visible-article updated-article auth-user)}]
66 | [false {:errors {:other ["Cannot insert article into db."]}}]))
67 | [false {:errors {:authorization ["You need to be author of this article to update it."]}}])
68 | [false {:errors {:slug ["Cannot find an article with given slug."]}}]))
69 |
70 | (defn delete-article! [auth-user slug]
71 | (if-let [article (store/find-by-slug slug)]
72 | (if (= (:id auth-user) (:userId article))
73 | [true (store/delete-article! (:id article))]
74 | [false {:errors {:authorization ["You need to be author of this article to delete it."]}}])
75 | [false {:errors {:slug ["Cannot find an article with given slug."]}}]))
76 |
77 | (defn favorite-article! [auth-user slug]
78 | (if-let [article (store/find-by-slug slug)]
79 | (do
80 | (store/favorite! (:id auth-user) (:id article))
81 | [true {:article (article->visible-article article auth-user)}])
82 | [false {:errors {:slug ["Cannot find an article with given slug."]}}]))
83 |
84 | (defn unfavorite-article! [auth-user slug]
85 | (if-let [article (store/find-by-slug slug)]
86 | (do
87 | (store/unfavorite! (:id auth-user) (:id article))
88 | [true {:article (article->visible-article article auth-user)}])
89 | [false {:errors {:slug ["Cannot find an article with given slug."]}}]))
90 |
91 | (defn feed [auth-user limit offset]
92 | (let [limit (or limit 20)
93 | offset (or offset 0)
94 | user-id (:id auth-user)
95 | articles (store/feed user-id limit offset)
96 | visible-articles (mapv #(article->visible-article % auth-user) articles)
97 | res {:articles visible-articles
98 | :articlesCount (count visible-articles)}]
99 | [true res]))
100 |
101 | (defn articles [auth-user limit offset author tag favorited]
102 | (let [limit (or limit 20)
103 | offset (or offset 0)
104 | articles (store/articles limit offset author tag favorited)
105 | visible-articles (mapv #(article->visible-article % auth-user) articles)
106 | res {:articles visible-articles
107 | :articlesCount (count visible-articles)}]
108 | [true res]))
109 |
--------------------------------------------------------------------------------
/components/user/test/clojure/realworld/user/core_test.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.user.core-test
2 | (:require [clj-time.core :as t]
3 | [clojure.java.jdbc :as jdbc]
4 | [clojure.realworld.database.interface :as database]
5 | [clojure.realworld.user.core :as core]
6 | [clojure.realworld.user.spec :as spec]
7 | [clojure.spec.alpha :as s]
8 | [clojure.spec.gen.alpha :as gen]
9 | [clojure.test :refer :all]))
10 |
11 | (defn- test-db
12 | ([] {:classname "org.sqlite.JDBC"
13 | :subprotocol "sqlite"
14 | :subname "test.db"})
15 | ([_] (test-db)))
16 |
17 | (defn prepare-for-tests [f]
18 | (with-redefs [database/db test-db]
19 | (let [db (test-db)]
20 | (database/generate-db db)
21 | (f)
22 | (database/drop-db db))))
23 |
24 | (use-fixtures :each prepare-for-tests)
25 |
26 | (deftest login--user-not-found--return-negative-result
27 | (let [[ok? res] (core/login! {:email "test@test.com" :password "password"})]
28 | (is (false? ok?))
29 | (is (= {:errors {:email ["Invalid email."]}} res))))
30 |
31 | (deftest login--invalid-password--return-negative-result
32 | (let [_ (jdbc/insert! (test-db) :user {:email "test@test.com"
33 | :password (core/encrypt-password "password")})
34 | [ok? res] (core/login! {:email "test@test.com" :password "invalid-password"})]
35 | (is (false? ok?))
36 | (is (= {:errors {:password ["Invalid password."]}} res))))
37 |
38 | (deftest login--valid-input--return-positive-result
39 | (let [_ (jdbc/insert! (test-db) :user {:email "test@test.com"
40 | :username "username"
41 | :password (core/encrypt-password "password")})
42 | [ok? res] (core/login! {:email "test@test.com" :password "password"})]
43 | (is (true? ok?))
44 | (is (true? (s/valid? spec/visible-user res)))))
45 |
46 | (deftest register!--user-exists-with-given-email--return-negative-result
47 | (let [_ (jdbc/insert! (test-db) :user {:email "test@test.com"})
48 | [ok? res] (core/register! {:email "test@test.com"})]
49 | (is (false? ok?))
50 | (is (= {:errors {:email ["A user exists with given email."]}} res))))
51 |
52 | (deftest register!--user-exists-with-given-username--return-negative-result
53 | (let [_ (jdbc/insert! (test-db) :user {:username "username"})
54 | [ok? res] (core/register! {:email "test@test.com" :username "username"})]
55 | (is (false? ok?))
56 | (is (= {:errors {:username ["A user exists with given username."]}} res))))
57 |
58 | (deftest register!--valid-input--return-positive-result
59 | (let [input (gen/generate (s/gen spec/register))
60 | [ok? res] (core/register! input)]
61 | (is (true? ok?))
62 | (is (s/valid? spec/visible-user res))
63 | (is (not (nil? (-> res :user :token))))))
64 |
65 | (deftest user-by-token--user-not-found--return-negative-result
66 | (let [[ok? res] (core/user-by-token "token")]
67 | (is (false? ok?))
68 | (is (= {:errors {:token ["Cannot find a user with associated token."]}} res))))
69 |
70 | (deftest user-by-token--user-found--return-positive-result
71 | (let [email "test@test.com"
72 | username "username"
73 | token (#'clojure.realworld.user.core/generate-token email username)
74 | _ (jdbc/insert! (test-db) :user {:email email :username username})
75 | [ok? res] (core/user-by-token token)]
76 | (is (true? ok?))
77 | (is (s/valid? spec/visible-user res))))
78 |
79 | (deftest user-by-token--expired-token--return-negative-result
80 | (let [email "test@test.com"
81 | username "username"
82 | now (t/now)
83 | ten-days-ago (t/minus now (t/days 10))
84 | token (with-redefs [clj-time.core/now (fn [] ten-days-ago)]
85 | (#'clojure.realworld.user.core/generate-token email username))
86 | _ (jdbc/insert! (test-db) :user {:email email :username username})
87 | [ok? res] (core/user-by-token token)]
88 | (is (false? ok?))
89 | (is (= {:errors {:token ["Cannot find a user with associated token."]}} res))))
90 |
91 | (deftest update-user!--user-exists-with-given-email--return-negative-result
92 | (let [_ (jdbc/insert! (test-db) :user {:email "test1@test.com"})
93 | auth-user (jdbc/insert! (test-db) :user {:email "test2@test.com"})
94 | [ok? res] (core/update-user! auth-user {:email "test1@test.com"})]
95 | (is (false? ok?))
96 | (is (= {:errors {:email ["A user exists with given email."]}} res))))
97 |
98 | (deftest update-user!--user-exists-with-given-username--return-negative-result
99 | (let [_ (jdbc/insert! (test-db) :user {:username "username"})
100 | auth-user (jdbc/insert! (test-db) :user {:email "test2@test.com"})
101 | [ok? res] (core/update-user! auth-user {:email "test@test.com" :username "username"})]
102 | (is (false? ok?))
103 | (is (= {:errors {:username ["A user exists with given username."]}} res))))
104 |
105 | (deftest update-user!--valid-input--return-positive-result
106 | (let [initial-inputs (gen/sample (s/gen spec/register) 20)
107 | users (map #(-> (core/register! %) second :user) initial-inputs)
108 | inputs (gen/sample (s/gen spec/update-user) 20)
109 | results (map-indexed #(core/update-user! (nth users %1) %2) inputs)]
110 | (is (every? true? (map first results)))
111 | (is (every? #(s/valid? spec/visible-user (second %)) results))
112 | (is (= (map #(dissoc % :password) inputs)
113 | (map-indexed #(select-keys (-> %2 second :user)
114 | (keys (nth inputs %1)))
115 | results)))))
116 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.0
2 |
3 | jobs:
4 | check:
5 | docker:
6 | - image: cimg/clojure:1.11.1
7 | working_directory: ~/realworld-example
8 | steps:
9 | - checkout
10 | - run:
11 | name: Get rid of erroneous git config
12 | command: |
13 | rm -rf ~/.gitconfig
14 | - restore_cache:
15 | keys:
16 | - realworld-example-{{ checksum "deps.edn" }}-{{ checksum "projects/realworld-backend/deps.edn" }}
17 | - realworld-example-
18 | - run:
19 | name: Create polylith config if it does not exist
20 | command: mkdir -p ~/.polylith && echo "{}" > ~/.polylith/config.edn
21 | - run:
22 | name: Force clojure to resolve dependencies
23 | command: clojure -A:dev:test -P
24 | - run:
25 | name: Check Polylith workspace
26 | command: clojure -M:poly check
27 | - save_cache:
28 | key: realworld-example-{{ checksum "deps.edn" }}-{{ checksum "projects/realworld-backend/deps.edn" }}
29 | paths:
30 | - ~/.polylith
31 | - ~/.m2
32 | - ~/.gitlibs
33 | - ~/.clojure
34 | - ~/realworld-example/.cpcache
35 | - persist_to_workspace:
36 | root: .
37 | paths:
38 | - .
39 |
40 | info:
41 | docker:
42 | - image: cimg/clojure:1.11.1
43 | working_directory: ~/realworld-example
44 | steps:
45 | - attach_workspace:
46 | at: ~/realworld-example
47 | - restore_cache:
48 | keys:
49 | - realworld-example-{{ checksum "deps.edn" }}-{{ checksum "projects/realworld-backend/deps.edn" }}
50 | - run:
51 | name: Add global git user email
52 | command: git config --global user.email "circleci@polyfy.com"
53 | - run:
54 | name: Add global git user name
55 | command: git config --global user.name "CircleCI"
56 | - run:
57 | name: Run ws command for Polylith workspace
58 | command: clojure -M:poly ws
59 | - run:
60 | name: Run info command for Polylith workspace
61 | command: clojure -M:poly info
62 | - run:
63 | name: Run deps command for Polylith workspace
64 | command: clojure -M:poly deps
65 | - run:
66 | name: Run libs command for Polylith workspace
67 | command: clojure -M:poly libs
68 |
69 | test:
70 | docker:
71 | - image: cimg/clojure:1.11.1
72 | working_directory: ~/realworld-example
73 | steps:
74 | - attach_workspace:
75 | at: ~/realworld-example
76 | - restore_cache:
77 | keys:
78 | - realworld-example-{{ checksum "deps.edn" }}-{{ checksum "projects/realworld-backend/deps.edn" }}
79 | - run:
80 | name: Add global git user email
81 | command: git config --global user.email "circleci@polyfy.com"
82 | - run:
83 | name: Add global git user name
84 | command: git config --global user.name "CircleCI"
85 | - run:
86 | name: Run tests for Polylith workspace
87 | command: clojure -M:poly test :project
88 |
89 | api-test:
90 | docker:
91 | - image: cimg/clojure:1.11.1-node
92 | working_directory: ~/realworld-example
93 | steps:
94 | - attach_workspace:
95 | at: ~/realworld-example
96 | - restore_cache:
97 | keys:
98 | - realworld-example-{{ checksum "deps.edn" }}-{{ checksum "projects/realworld-backend/deps.edn" }}
99 | - run:
100 | name: Run realworld backend
101 | command: clojure -M:ring
102 | working_directory: projects/realworld-backend
103 | background: true
104 | - run:
105 | name: Wait for backend to initialize
106 | command: wget --retry-connrefused --waitretry=10 --read-timeout=20 --timeout=15 -t 30 http://localhost:6003/api/health
107 | - run:
108 | name: Run api tests
109 | command: APIURL=http://localhost:6003/api ./run-api-tests.sh
110 | working_directory: ~/realworld-example/api-tests
111 |
112 | build-uberjar:
113 | docker:
114 | - image: cimg/clojure:1.11.1
115 | working_directory: ~/realworld-example
116 | steps:
117 | - attach_workspace:
118 | at: ~/realworld-example
119 | - restore_cache:
120 | keys:
121 | - realworld-example-{{ checksum "deps.edn" }}-{{ checksum "projects/realworld-backend/deps.edn" }}
122 | - run:
123 | name: Build realworld-backed project
124 | command: clojure -T:build uberjar :project realworld-backend
125 | - run:
126 | name: Copy artifact to artifacts folder
127 | command: |
128 | mkdir -p artifacts
129 |
130 | cp projects/realworld-backend/target/realworld-backend.jar artifacts/.
131 | - store_artifacts:
132 | path: ./artifacts
133 |
134 | mark-as-stable:
135 | docker:
136 | - image: cimg/clojure:1.11.1
137 | working_directory: ~/realworld-example
138 | steps:
139 | - attach_workspace:
140 | at: ~/realworld-example
141 | - add_ssh_keys:
142 | fingerprints:
143 | - "3d:7a:60:40:26:8e:a8:4e:9c:22:fe:77:3f:84:fb:ec"
144 | - restore_cache:
145 | keys:
146 | - realworld-example-{{ checksum "deps.edn" }}-{{ checksum "projects/realworld-backend/deps.edn" }}
147 | - run:
148 | name: Add github.com to known hosts
149 | command: mkdir -p ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
150 | - run:
151 | name: Add global git user email
152 | command: git config --global user.email "circleci@polyfy.com"
153 | - run:
154 | name: Add global git user name
155 | command: git config --global user.name "CircleCI"
156 | - run:
157 | name: Add git tag to mark this stable point
158 | command: git tag -f -a "stable-$CIRCLE_BRANCH" -m "[skip ci] Added Stable Polylith tag"
159 | - run:
160 | name: Push the new tag
161 | command: git push origin $CIRCLE_BRANCH --tags --force
162 |
163 | workflows:
164 | version: 2
165 | validate-test-build:
166 | jobs:
167 | - check
168 | - info:
169 | requires:
170 | - check
171 | - test:
172 | requires:
173 | - check
174 | - api-test:
175 | requires:
176 | - test
177 | - build-uberjar:
178 | requires:
179 | - test
180 | - mark-as-stable:
181 | requires:
182 | - api-test
183 | - build-uberjar
184 | filters:
185 | branches:
186 | only:
187 | - master
188 |
--------------------------------------------------------------------------------
/components/article/src/clojure/realworld/article/store.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.article.store
2 | (:require [clojure.java.jdbc :as jdbc]
3 | [clojure.realworld.database.interface :as database]
4 | [clojure.string :as str]
5 | [honey.sql :as sql]))
6 |
7 | (defn find-by-slug [slug]
8 | (let [query {:select [:*]
9 | :from [:article]
10 | :where [:= :slug slug]}
11 | results (jdbc/query (database/db) (sql/format query) {:identifiers identity})]
12 | (first results)))
13 |
14 | (defn insert-article! [article-input]
15 | (jdbc/insert! (database/db) :article article-input {:entities identity}))
16 |
17 | (defn tags-with-names [tag-names]
18 | (let [query {:select [:*]
19 | :from [:tag]
20 | :where [:in :name tag-names]}
21 | results (jdbc/query (database/db) (sql/format query))]
22 | results))
23 |
24 | (defn add-tags-to-article! [article-id tag-names]
25 | (when-not (empty? tag-names)
26 | (let [tags (tags-with-names tag-names)
27 | inputs (mapv #(hash-map :articleId article-id
28 | :tagId (:id %))
29 | tags)]
30 | (jdbc/insert-multi! (database/db) :articleTags inputs))))
31 |
32 | (defn article-tags [article-id]
33 | (let [query {:select [:name]
34 | :from [:articleTags]
35 | :join [:tag [:= :tagId :tag.id]]
36 | :where [:= :articleId article-id]}
37 | results (jdbc/query (database/db) (sql/format query))]
38 | (mapv :name results)))
39 |
40 | (defn insert-tag! [name]
41 | (let [tag (first (tags-with-names [name]))]
42 | (when-not tag
43 | (jdbc/insert! (database/db) :tag {:name name}))))
44 |
45 | (defn update-tags! [tag-names]
46 | (when-not (empty? tag-names)
47 | (doseq [name tag-names]
48 | (insert-tag! name))))
49 |
50 | (defn favorited? [user-id article-id]
51 | (let [query {:select [:%count.*]
52 | :from [:favoriteArticles]
53 | :where [:and [:= :articleId article-id]
54 | [:= :userId user-id]]}
55 | result (jdbc/query (database/db) (sql/format query))]
56 | (< 0 (-> result first first val))))
57 |
58 | (defn favorites-count [article-id]
59 | (let [query {:select [:%count.*]
60 | :from [:favoriteArticles]
61 | :where [:= :articleId article-id]}
62 | result (jdbc/query (database/db) (sql/format query))]
63 | (-> result first first val)))
64 |
65 | (defn update-article! [id article-input]
66 | (let [query {:update :article
67 | :set article-input
68 | :where [:= :id id]}]
69 | (jdbc/execute! (database/db) (sql/format query))))
70 |
71 | (defn delete-article! [id]
72 | (let [query-1 {:delete-from :articleTags
73 | :where [:= :articleId id]}
74 | query-2 {:delete-from :favoriteArticles
75 | :where [:= :articleId id]}
76 | query-3 {:delete-from :article
77 | :where [:= :id id]}]
78 | (jdbc/with-db-transaction [trans-conn (database/db)]
79 | (jdbc/execute! trans-conn (sql/format query-1))
80 | (jdbc/execute! trans-conn (sql/format query-2))
81 | (jdbc/execute! trans-conn (sql/format query-3)))
82 | nil))
83 |
84 | (defn favorite! [user-id article-id]
85 | (when-not (favorited? user-id article-id)
86 | (jdbc/insert! (database/db) :favoriteArticles {:articleId article-id
87 | :userId user-id})))
88 |
89 | (defn unfavorite! [user-id article-id]
90 | (when (favorited? user-id article-id)
91 | (let [query {:delete-from :favoriteArticles
92 | :where [:and [:= :articleId article-id]
93 | [:= :userId user-id]]}]
94 | (jdbc/execute! (database/db) (sql/format query)))))
95 |
96 | (defn feed [user-id limit offset]
97 | (let [query {:select [:a.*]
98 | :from [[:article :a]]
99 | :join [[:userFollows :u] [:= :a.userId :u.followedUserId]]
100 | :where [:= :u.userId user-id]
101 | :order-by [[:a.updatedAt :desc] [:a.id :desc]]
102 | :limit limit
103 | :offset offset}
104 | results (jdbc/query (database/db) (sql/format query) {:identifiers identity})]
105 | results))
106 |
107 | (defn articles-by-tag [limit offset tag]
108 | (let [query {:select [:a.*]
109 | :from [[:article :a]]
110 | :join [[:articleTags :at] [:= :a.id :at.articleId]
111 | [:tag :t] [:= :at.tagId :t.id]]
112 | :where [:= :t.name tag]
113 | :order-by [[:updatedAt :desc] [:id :desc]]
114 | :limit limit
115 | :offset offset}
116 | results (jdbc/query (database/db) (sql/format query) {:identifiers identity})]
117 | results))
118 |
119 | (defn articles-by-author [limit offset author]
120 | (let [query {:select [:a.*]
121 | :from [[:article :a]]
122 | :join [[:user :u] [:= :a.userId :u.id]]
123 | :where [:= :u.username author]
124 | :order-by [[:updatedAt :desc] [:id :desc]]
125 | :limit limit
126 | :offset offset}
127 | results (jdbc/query (database/db) (sql/format query) {:identifiers identity})]
128 | results))
129 |
130 | (defn articles-by-favorited [limit offset favorited]
131 | (let [query {:select [:a.*]
132 | :from [[:article :a]]
133 | :join [[:favoriteArticles :fa] [:= :a.id :fa.articleId]
134 | [:user :u] [:= :fa.userId :u.id]]
135 | :where [:= :u.username favorited]
136 | :order-by [[:updatedAt :desc] [:id :desc]]
137 | :limit limit
138 | :offset offset}
139 | results (jdbc/query (database/db) (sql/format query) {:identifiers identity})]
140 | results))
141 |
142 | (defn all-articles [limit offset]
143 | (let [query {:select [:*]
144 | :from [:article]
145 | :order-by [[:updatedAt :desc] [:id :desc]]
146 | :limit limit
147 | :offset offset}
148 | results (jdbc/query (database/db) (sql/format query) {:identifiers identity})]
149 | results))
150 |
151 | (defn articles [limit offset author tag favorited]
152 | (if-not (str/blank? author)
153 | (articles-by-author limit offset author)
154 | (if-not (str/blank? tag)
155 | (articles-by-tag limit offset tag)
156 | (if-not (str/blank? favorited)
157 | (articles-by-favorited limit offset favorited)
158 | (all-articles limit offset)))))
159 |
--------------------------------------------------------------------------------
/how-to.md:
--------------------------------------------------------------------------------
1 | ## How to create this workspace from scratch
2 | Workspace structure follows the Polylith Architecture ideas. The Polylith tool makes it easy to create a workspace, add components, and test and validate the workspace. Here, you can find all the steps required to create this workspace from scratch.
3 |
4 | ###### Install Clojure
5 | If you do not have Clojure installed on your machine already, you can install it through [Homebrew](https://brew.sh/) on MacOS by running:
6 |
7 | ```sh
8 | brew install clojure/tools/clojure
9 | ```
10 |
11 | Refer to [this page on clojure.org](https://clojure.org/guides/getting_started) for more options on installing.
12 |
13 | ###### Install Polylith Tool
14 | Polylith provides a command line tool that you can install on your machine to enhance your development experience. You can install it through Homebrew on MacOS by running:
15 |
16 | ```sh
17 | brew install polyfy/polylith/poly
18 | ```
19 |
20 | For more installation options, please have a look at [Polylith tool documentation](https://polylith.gitbook.io/poly/install/install).
21 |
22 | ###### Create a workspace
23 | - `` poly create workspace name:realworld-app top-ns:clojure.realworld ``
24 | - This will create your workspace under a directory named `` realworld-app ``
25 | - Your code will end up in a package named `` clojure.realworld ``
26 | - Inside your workspace, you'll find the structure as in the image below.
27 | - If you look inside these directories, you'll see that bases, components, and projects are empty.
28 |
29 | 
30 |
31 | Once the workspace is created, navigate to the workspace directory: `` cd realworld-backend ``
32 |
33 | ###### Open workspace in IDE
34 | You can open the workspace with your favorite IDE. It will look like the following at this stage if you open it with [Intellij IDEA](https://www.jetbrains.com/idea/) with [Cursive](https://cursive-ide.com) plugin:
35 |
36 | 
37 |
38 | ###### Create components
39 | - `` poly create component name:article ``
40 | - `` poly create component name:comment ``
41 | - `` poly create component name:database ``
42 | - `` poly create component name:env ``
43 | - `` poly create component name:log ``
44 | - `` poly create component name:profile ``
45 | - `` poly create component name:spec ``
46 | - `` poly create component name:tag ``
47 | - `` poly create component name:user ``
48 |
49 | These command above will create components under `` components `` directory.
50 |
51 | 
52 |
53 | However, our components are not yet added to development project's `` deps.edn ``. In order to start working with them, you need to add them to `` deps.edn `` in the root directory as following:
54 |
55 | 
56 |
57 | notice that we added the components both under the `:dev` and `:test` alias. Once you do this and load dev and test aliases, you can start working with your components.
58 |
59 | ###### Create base
60 | - `` poly create base name:rest-api ``
61 |
62 | This command will create a base named `` rest-api `` under bases directory.
63 | Same as components, you should add the base to the `deps.edn` file in the workspace root. It will look like this:
64 |
65 | 
66 |
67 | ###### Add code to components and the base
68 | You can take the code from the [repository](https://github.com/furkan3ayraktar/clojure-polylith-realworld-example-app) to populate the components and the base.
69 | You should also add the necessary dependencies to the `` deps.edn `` file in the workspace root.
70 |
71 | Once your code is ready, you can move on to the next step to create a project.
72 |
73 | ###### Create a project
74 | - `` poly create project name:realworld-backend ``
75 |
76 | This command will create a new directory under `` projects/realworld-backend ``. If you look into that directory, you will see that there is only a single file, named `` deps.edn `` and it will look like this:
77 |
78 | 
79 |
80 | This is where you will place the configuration for your project. In Polylith, a project is a configuration which includes a single base, a set of components and library dependencies. Since our project is a very simple one with one single artifact, we'll include our only base and all of our components in this configuration. After adding those, it will look like this:
81 |
82 | 
83 |
84 | As you can notice, we also have some extra configuration necessarry for our specific project, such as, ring configuration and two special aliases (`` :aot `` and `` :uberjar ``) for creating aot compiled uberjar artifact.
85 |
86 | At this stage, you should have a copy of this repository.
87 |
88 | ###### Workspace info
89 | ```sh
90 | poly info
91 | ```
92 |
93 | This command will print out the information about the current workspace. You can find documentation about it in the [Polylith tool documentation](https://polylith.gitbook.io/poly/). It should print an output like this:
94 |
95 |
96 |
97 | Here the asterisk symbol points the changed components and bases since the last stable point.
98 |
99 | ###### Validating intergrity
100 | In order to validate the integratiy of the Polylith workspace, run the following command:
101 |
102 | ```sh
103 | poly check
104 | ```
105 |
106 | This command should output `` OK `` as message if everything is okay. Otherwise, it will print out errors and warnings found in the workspace.
107 |
108 | ###### Running tests
109 | ```sh
110 | poly test
111 | ```
112 |
113 | This command will run all the tests since the last stable point. Since this is a newly created workspace, last stable point will be since the beginning.
114 |
115 | ###### Adding a stable point
116 | Once you are ready with your changes and the check and test commands run without any issues, you can commit your changes to your git repository. After commiting, you can add a git tag with `` stable- `` prefix. This will tell Polylith to take that commit as the last stable point next time you run any Polylith commands.
117 |
118 | ```sh
119 | git tag -f -a "stable-master" -m "Stable point"
120 | ```
121 |
122 | After adding tag, you can run info command again and get an output similar to this:
123 |
124 | 
125 |
126 | #### Sample REPL run configuration for Intellij IDEA with Cursive
127 | Polylith works out-of-the-box with Intellij IDEA + Cursive setup. Here is how my REPL run configuration looks like:
128 |
129 | 
130 |
131 | The only thing that is different from default Cursive REPL configuration is, I selected Run with Deps option and added two aliases (``dev,test``) that comes from the Polylith workspace `` deps.edn ``.
132 |
--------------------------------------------------------------------------------
/bases/rest-api/src/clojure/realworld/rest_api/handler.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.rest-api.handler
2 | (:require [clojure.edn :as edn]
3 | [clojure.realworld.article.interface :as article]
4 | [clojure.realworld.article.interface.spec :as article-spec]
5 | [clojure.realworld.comment.interface :as comment-comp]
6 | [clojure.realworld.comment.interface.spec :as comment-spec]
7 | [clojure.realworld.spec.interface :as spec]
8 | [clojure.realworld.profile.interface :as profile]
9 | [clojure.realworld.tag.interface :as tag]
10 | [clojure.realworld.user.interface :as user]
11 | [clojure.realworld.user.interface.spec :as user-spec]
12 | [clojure.spec.alpha :as s]
13 | [clojure.realworld.env.interface :as env]))
14 |
15 | (defn- parse-query-param [param]
16 | (if (string? param)
17 | (try
18 | (edn/read-string param)
19 | (catch Exception _
20 | param))
21 | param))
22 |
23 | (defn- handle
24 | ([status body]
25 | {:status (or status 404)
26 | :body body})
27 | ([status]
28 | (handle status nil)))
29 |
30 | (defn options [_]
31 | (handle 200))
32 |
33 | (defn health [_]
34 | (handle 200 {:environment (env/env :environment)}))
35 |
36 | (defn other [_]
37 | (handle 404 {:errors {:other ["Route not found."]}}))
38 |
39 | (defn login [req]
40 | (let [user (-> req :params :user)]
41 | (if (s/valid? user-spec/login user)
42 | (let [[ok? res] (user/login! user)]
43 | (handle (if ok? 200 404) res))
44 | (handle 422 {:errors {:body ["Invalid request body."]}}))))
45 |
46 | (defn register [req]
47 | (let [user (-> req :params :user)]
48 | (if (s/valid? user-spec/register user)
49 | (let [[ok? res] (user/register! user)]
50 | (handle (if ok? 200 404) res))
51 | (handle 422 {:errors {:body ["Invalid request body."]}}))))
52 |
53 | (defn current-user [req]
54 | (let [auth-user (-> req :auth-user)]
55 | (handle 200 {:user auth-user})))
56 |
57 | (defn update-user [req]
58 | (let [auth-user (-> req :auth-user)
59 | user (-> req :params :user)]
60 | (if (s/valid? user-spec/update-user user)
61 | (let [[ok? res] (user/update-user! auth-user user)]
62 | (handle (if ok? 200 404) res))
63 | (handle 422 {:errors {:body ["Invalid request body."]}}))))
64 |
65 | (defn profile [req]
66 | (let [auth-user (-> req :auth-user)
67 | username (-> req :params :username)]
68 | (if (s/valid? spec/username? username)
69 | (let [[ok? res] (profile/fetch-profile auth-user username)]
70 | (handle (if ok? 200 404) res))
71 | (handle 422 {:errors {:username ["Invalid username."]}}))))
72 |
73 | (defn follow-profile [req]
74 | (let [auth-user (-> req :auth-user)
75 | username (-> req :params :username)]
76 | (if (s/valid? spec/username? username)
77 | (let [[ok? res] (profile/follow! auth-user username)]
78 | (handle (if ok? 200 404) res))
79 | (handle 422 {:errors {:username ["Invalid username."]}}))))
80 |
81 | (defn unfollow-profile [req]
82 | (let [auth-user (-> req :auth-user)
83 | username (-> req :params :username)]
84 | (if (s/valid? spec/username? username)
85 | (let [[ok? res] (profile/unfollow! auth-user username)]
86 | (handle (if ok? 200 404) res))
87 | (handle 422 {:errors {:username ["Invalid username."]}}))))
88 |
89 | (defn articles [req]
90 | (let [auth-user (-> req :auth-user)
91 | limit (parse-query-param (-> req :params :limit))
92 | offset (parse-query-param (-> req :params :offset))
93 | author (-> req :params :author)
94 | tag (-> req :params :tag)
95 | favorited (-> req :params :favorited)
96 | [ok? res] (article/articles auth-user
97 | (if (pos-int? limit) limit nil)
98 | (if (nat-int? offset) offset nil)
99 | (if (string? author) author nil)
100 | (if (string? tag) tag nil)
101 | (if (string? favorited) favorited nil))]
102 | (handle (if ok? 200 404) res)))
103 |
104 | (defn article [req]
105 | (let [auth-user (-> req :auth-user)
106 | slug (-> req :params :slug)]
107 | (if (s/valid? spec/slug? slug)
108 | (let [[ok? res] (article/article auth-user slug)]
109 | (handle (if ok? 200 404) res))
110 | (handle 422 {:errors {:slug ["Invalid slug."]}}))))
111 |
112 | (defn comments [req]
113 | (let [auth-user (-> req :auth-user)
114 | slug (-> req :params :slug)]
115 | (if (s/valid? spec/slug? slug)
116 | (let [[ok? res] (comment-comp/article-comments auth-user slug)]
117 | (handle (if ok? 200 404) res))
118 | (handle 422 {:errors {:slug ["Invalid slug."]}}))))
119 |
120 | (defn tags [_]
121 | (let [[ok? res] (tag/all-tags)]
122 | (handle (if ok? 200 404) res)))
123 |
124 | (defn feed [req]
125 | (let [auth-user (-> req :auth-user)
126 | limit (parse-query-param (-> req :params :limit))
127 | offset (parse-query-param (-> req :params :offset))
128 | [ok? res] (article/feed auth-user
129 | (if (pos-int? limit) limit nil)
130 | (if (nat-int? offset) offset nil))]
131 | (handle (if ok? 200 404) res)))
132 |
133 | (defn create-article [req]
134 | (let [auth-user (-> req :auth-user)
135 | article (-> req :params :article)]
136 | (if (s/valid? article-spec/create-article article)
137 | (let [[ok? res] (article/create-article! auth-user article)]
138 | (handle (if ok? 200 404) res))
139 | (handle 422 {:errors {:body ["Invalid request body."]}}))))
140 |
141 | (defn update-article [req]
142 | (let [auth-user (-> req :auth-user)
143 | slug (-> req :params :slug)
144 | article (-> req :params :article)]
145 | (if (and (s/valid? article-spec/update-article article)
146 | (s/valid? spec/slug? slug))
147 | (let [[ok? res] (article/update-article! auth-user slug article)]
148 | (handle (if ok? 200 404) res))
149 | (handle 422 {:errors {:body ["Invalid request body."]}}))))
150 |
151 | (defn delete-article [req]
152 | (let [auth-user (-> req :auth-user)
153 | slug (-> req :params :slug)]
154 | (if (s/valid? spec/slug? slug)
155 | (let [[ok? res] (article/delete-article! auth-user slug)]
156 | (handle (if ok? 200 404) res))
157 | (handle 422 {:errors {:slug ["Invalid slug."]}}))))
158 |
159 | (defn add-comment [req]
160 | (let [auth-user (-> req :auth-user)
161 | slug (-> req :params :slug)
162 | comment (-> req :params :comment)]
163 | (if (and (s/valid? spec/slug? slug)
164 | (s/valid? comment-spec/add-comment comment))
165 | (let [[ok? res] (comment-comp/add-comment! auth-user slug comment)]
166 | (handle (if ok? 200 404) res))
167 | (handle 422 {:errors {:body ["Invalid request body."]}}))))
168 |
169 | (defn delete-comment [req]
170 | (let [auth-user (-> req :auth-user)
171 | id (parse-query-param (-> req :params :id))]
172 | (if (s/valid? comment-spec/id id)
173 | (let [[ok? res] (comment-comp/delete-comment! auth-user id)]
174 | (handle (if ok? 200 404) res))
175 | (handle 422 {:errors {:id ["Invalid comment id."]}}))))
176 |
177 | (defn favorite-article [req]
178 | (let [auth-user (-> req :auth-user)
179 | slug (-> req :params :slug)]
180 | (if (s/valid? spec/slug? slug)
181 | (let [[ok? res] (article/favorite-article! auth-user slug)]
182 | (handle (if ok? 200 404) res))
183 | (handle 422 {:errors {:slug ["Invalid slug."]}}))))
184 |
185 | (defn unfavorite-article [req]
186 | (let [auth-user (-> req :auth-user)
187 | slug (-> req :params :slug)]
188 | (if (s/valid? spec/slug? slug)
189 | (let [[ok? res] (article/unfavorite-article! auth-user slug)]
190 | (handle (if ok? 200 404) res))
191 | (handle 422 {:errors {:slug ["Invalid slug."]}}))))
192 |
--------------------------------------------------------------------------------
/development/src/dev/hello_repl.clj:
--------------------------------------------------------------------------------
1 | (ns dev.hello-repl)
2 |
3 | ;; == Some VS Code knowledge required ==
4 | ;; This tutorial assumes you know a few things about
5 | ;; VS Code. Please check out this page if you are new
6 | ;; to the editor: https://code.visualstudio.com/docs
7 | ;;
8 | ;; == Keyboard Shortcuts Notation used in this tutorial ==
9 | ;; We use a notation for keyboard shortcuts, where
10 | ;; `+` means the keys are pressed at the same time
11 | ;; and ` ` separates any keyboard presses in the sequence.
12 | ;; `Ctrl+Alt+C Enter` means to press Ctrl, Alt, and C
13 | ;; all at the same time, then release the keys and
14 | ;; then press Enter. (The Alt key is named Option or
15 | ;; Opt, on some machines)
16 | ;;
17 | ;; Let's start by loading this file in the REPL.
18 | ;; Please press `Ctrl+Alt+C Enter`
19 |
20 | "Welcome to the Getting Started REPL! 💜"
21 |
22 | ;; Once you see a message in the output/REPL window ->
23 | ;; saying that this file is loaded, you can start by
24 | ;; placing the cursor anywhere on the line with the
25 | ;; string above and press: `Alt+Enter`
26 |
27 | ;; Did it? Great!
28 | ;; See that `=> "Welcome ...` at the end of the line?
29 | ;; That's the result of the evaluation you just
30 | ;; performed. You just used the Clojure REPL!
31 | ;; 🎉 Congratulations! 🎂
32 |
33 | (comment
34 | ;; You can evaluate the string below in the same way
35 | ;; Place the cursor anywhere on the line with the
36 | ;; string and press `Alt+Enter`.
37 |
38 | "Hello World!"
39 |
40 | ;; This works because this is a 'Rich Comment Form'
41 | ;; which is where we Clojurians often develop new
42 | ;; code. You'll sometimes see it abbreviated as RCF.
43 | ;; See also: https://calva.io/rich-comments/
44 |
45 | ;; Evaluate the following form too (you can
46 | ;; place the cursor anywhere on any of the two lines):
47 |
48 | (repeat 7
49 | "I am using the REPL! 💪")
50 |
51 | ;; Only `=> ("I am using the REPL! 💪"` is displayed
52 | ;; inline. You can see the full result, and also copy
53 | ;; it, if you hover the evaluated expression. Or press
54 | ;; `Ctrl+K Ctrl/Cmd+I`.
55 |
56 | ;; Let's get into the mood for real. 😂
57 | ;; Place the cursor on any of the five code lines below:
58 | ;; `Alt+Enter`, then `Cmd+K Cmd+I`.
59 |
60 | (map (fn [s]
61 | (if (re-find #" [REPL]$" s)
62 | (str "Give me " s "! ~•~ " (last s) "!")
63 | s))
64 | ["an R" "an E" "a P" "an L" "What do you get?" "REPL!"])
65 |
66 | ;; Clear the inline display with `Esc`. The inline
67 | ;; results are also cleared when you edit the file.
68 |
69 | ;; Which brings us to a VERY IMPORTANT THING:
70 | ;; By default, Calva will be a Guardian of the Parens.
71 | ;; This means that the backspace and delete buttons
72 | ;; will not delete balanced brackets. Please go ahead
73 | ;; and try to delete a bracket in the expression above.
74 | ;; See?
75 |
76 | ;; TO DELETE A BALANCED BRACKET:
77 | ;; press `alt/option+backspace` or `alt/option+delete`
78 |
79 | ;; You might notice that the output/REPL window ->
80 | ;; is also displaying the results. Depending on your
81 | ;; preferences you might want to close that window or move
82 | ;; it to the same editor group (unsplit) as the files you
83 | ;; edit. But don't do that just yet, get a feel how how
84 | ;; it works having it in a split pane first.
85 |
86 | ;; BTW. That output/REPL window ->
87 | ;; You can evaluate code from its prompt too.
88 | ;; But the cool peeps do not do that very often.
89 | ;; Because the REPL lives in the files with the application
90 | ;; code! And because Rich Comment Forms (RCF).
91 | ;; It is Interactive Programming, and it is 💪.
92 |
93 | :rcf) ; <- This is a convenient way to keep the closing
94 | ; paren of a Rich comment form from folding
95 | ; when the code is formatted.
96 |
97 |
98 | ;; About commands and shortcuts:
99 | ;; Please read https://calva.io/finding-commands/
100 | ;; (It's very short.)
101 | ;; When we refer to commands by their name, use
102 | ;; the VS Code Command Palette to search for them
103 | ;; if you don't know the keyboard shortcut.
104 | ;; All Calva commands are prefixed with ”Calva”.
105 |
106 | ;; == Evaluating definitions ==
107 | ;; Alt+Enter is the Calva default keyboard shortcut
108 | ;; to evaluate the current ”top level” forms. Top
109 | ;; level meaning the outermost ”container” of forms,
110 | ;; which is the file. This function definition is on
111 | ;; the top level. Please evaluate it!
112 |
113 | (defn greet
114 | "I'll greet you"
115 | [s]
116 | (str "Hello " s "!"))
117 |
118 | ;; Forms inside `(comment ...)` are also considered
119 | ;; to be top level. This makes it easy to experiment
120 | ;; with code.
121 |
122 | (comment
123 | (greet "World")
124 | :rcf)
125 |
126 | ;; Anything printed to stdout is not shown inline.
127 |
128 | (comment
129 | (println (greet "World"))
130 | :rcf)
131 |
132 | ;; You should see the result of the evaluation, nil,
133 | ;; inline, and ”Hello World!” followed by the result
134 | ;; printed to the output window.
135 |
136 | ;; Maybe you wonder what a ”form” is? Loosely defined
137 | ;; it is about the same as an S-expression:
138 | ;; https://en.wikipedia.org/wiki/S-expression
139 | ;; That is, either a ”word” or something enclosed in
140 | ;; brackets of some type, parens (), hard brackets [],
141 | ;; curlies {}, or quotes "". This whole thing is a
142 | ;; form:
143 |
144 | (str 23 (apply + [2 3]) (:foo {:foo "foo"}))
145 |
146 | ;; So is `str`, `23`, "foo", `(apply + [2 3])`,
147 | ;; `{:foo "foo"}`, `+`, `[2 3]`, `apply`, and also
148 | ;; `(:foo {:foo "foo"})`.
149 |
150 | ;; Calva has a concept of ”current form”, to let you
151 | ;; evaluate forms that are not at the top level. The
152 | ;; ”current form” is determined by where the cursor is.
153 | ;; Calva has a command that will let you easily
154 | ;; experiment with which form is considered current:
155 | ;; * Calva: Expand Selection
156 | ;; Using this command, starting from no selection, will
157 | ;; select the current form.
158 |
159 | ;; == Evaluating the Current Form ==
160 | ;; Ctrl+Enter evaluates the ”current” form
161 | ;; Try it with the cursor at different places in this
162 | ;; code snippet:
163 |
164 | (comment
165 | (str 23 (apply + [2 3]) (:foo {:foo "foo"}))
166 |
167 | ;; You might discover that Calva regards words in
168 | ;; strings as forms. Don't panic if `foo` causes
169 | ;; `foo` is undefined until you top-level eval it.
170 | ;; an evaluation error. It is not defined, since
171 | ;; it shouldn't be. You can define it, of course,
172 | ;; just for fun and learning: Top level eval the
173 | ;; following definition.
174 |
175 | (def foo
176 | [1 2 :three :four])
177 |
178 | ;; Then *evaluate current form* with the cursor
179 | ;; inside the string "foo" above.
180 | ;; Whatever you ask Calva to send to the REPL,
181 | ;; Calva will send to the REPL.
182 |
183 | :rcf)
184 |
185 | ;; == Rich Comments Support ==
186 | ;; Repeating an important concept: Forms inside
187 | ;; `(comment ...)` are also considered top level
188 | ;; by Calva. Alt+Enter at different places below
189 | ;; to get a feel for it.
190 |
191 | (comment
192 | "I ♥️ Clojure"
193 |
194 | (greet "World")
195 |
196 | foo
197 |
198 | (range 10)
199 |
200 | ;; https://calva.io/rich-comments/
201 | :rcf)
202 |
203 |
204 | ;; Also try the commands *Show Hover*,
205 | ;; *Show Definition Preview Hover*
206 | ;; *Go to Definition*
207 |
208 | (comment
209 | (println (greet "side effect"))
210 | (+ (* 2 2)
211 | 2)
212 |
213 | :rcf)
214 |
215 |
216 | ;; == You Control what is Evaluated ==
217 | ;; Please note that Calva never evaluates your code
218 | ;; unless you explicitly ask for it. So, you will
219 | ;; have to load files you open yourself. Make it a
220 | ;; habit to do this, because sometimes things don't
221 | ;; work when your file is not loaded.
222 |
223 | ;; Try it with this file: `Ctrl+Alt+C Enter`.
224 | ;; The result of loading a file is whatever is the
225 | ;; last top level form in the file.
226 |
227 | ;; == Editing Code ==
228 | ;; A note about editing Clojure in Calva:
229 | ;; If you edit and experiment with the examples you
230 | ;; will notice that Calva auto-indents your code.
231 | ;; You can re-indent, and format, code at will, using
232 | ;; the `Tab` key. It will format the current enclosing
233 | ;; form. Try it at the numbered places in this piece
234 | ;; of code, starting at `; 1`:
235 |
236 | (comment ; 3
237 | (defn- divisible
238 | "Is `n` divisible by `d`?"
239 | [n d]
240 | (zero? (mod n d)
241 | )
242 |
243 | )
244 |
245 | (defn fizz-buzz [n] ; 2
246 | (cond ; 1
247 | (divisible n (* 5 3)) "FizzBuzz"
248 | (divisible n 5) "Buzz"
249 | (divisible n 3) "Fizz"
250 | :else n))
251 | :rcf)
252 |
253 | ;; === Paredit `strict` mode is on ===
254 | ;; Calva supports structural editing (editing that
255 | ;; considers forms rather than lines) using a system
256 | ;; called Paredit. By default Paredit tries to protect
257 | ;; from accidentally deleting brackets and unbalancing
258 | ;; the structure of forms. To override the protection,
259 | ;; use `Alt+Backspace` or `Alt+delete`.
260 |
261 | (comment
262 | (defn strict-greet
263 | "Try to remove brackets and string quotes
264 | using Backspace or Delete. Try the same
265 | with the Alt key pressed."
266 | [name]
267 | (str "Strictly yours, " name "!"))
268 |
269 | (strict-greet "dear Paredit fan")
270 | :rcf)
271 |
272 | ;; (Restore with *Undo* if needed.)
273 | ;; See also:
274 | ;; https://calva.io/paredit
275 |
276 | ;;;;;;;;;;;;;;;;;;;;;; 🤘 🎸 🎉 ;;;;;;;;;;;;;;;;;;;;;
277 |
278 | ;; Done? Awesome. Please continue with the Polylith
279 | ;; `getting_started.clj`
280 |
281 | ;; Learn much more about Calva at https://calva.io
282 | ;; Also: There is a more comprehensive guide to Calva
283 | ;; as well as to Paredit and even Clojure, built in
284 |
285 |
286 | ;; This string is the last expression in this file
287 |
288 | "hello_repl.clj is loaded, and ready with some things for you to try."
289 |
290 | ;; It is what you'll see printed in the Output
291 | ;; window when you load the file.
292 |
293 | ;; This guide downloaded from:
294 | ;; https://github.com/BetterThanTomorrow/dram
295 | ;; Please consider contributing.
296 |
--------------------------------------------------------------------------------
/components/article/test/clojure/realworld/article/core_test.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.article.core-test
2 | (:require [clojure.java.jdbc :as jdbc]
3 | [clojure.realworld.database.interface :as database]
4 | [clojure.realworld.article.core :as core]
5 | [clojure.realworld.article.spec :as spec]
6 | [clojure.realworld.user.interface.spec :as user-spec]
7 | [clojure.spec.alpha :as s]
8 | [clojure.spec.gen.alpha :as gen]
9 | [clojure.test :refer [deftest is use-fixtures]]))
10 |
11 | (defn- test-db
12 | ([] {:classname "org.sqlite.JDBC"
13 | :subprotocol "sqlite"
14 | :subname "test.db"})
15 | ([_] (test-db)))
16 |
17 | (defn prepare-for-tests [f]
18 | (with-redefs [database/db test-db]
19 | (let [db (test-db)]
20 | (database/generate-db db)
21 | (f)
22 | (database/drop-db db))))
23 |
24 | (use-fixtures :each prepare-for-tests)
25 |
26 | (deftest article--article-not-found--return-negative-result
27 | (let [[ok? res] (core/article nil "wrong-slug")]
28 | (is (false? ok?))
29 | (is (= {:errors {:slug ["Cannot find an article with given slug."]}} res))))
30 |
31 | (deftest article--article-found--return-positive-result
32 | (let [input {:title "title" :slug "this-is-slug"}
33 | _ (jdbc/insert! (test-db) :article input)
34 | [ok? res] (core/article nil "this-is-slug")]
35 | (is (true? ok?))
36 | (is (= 1 (-> res :article :id)))
37 | (is (= "title" (-> res :article :title)))
38 | (is (= "this-is-slug" (-> res :article :slug)))))
39 |
40 | (deftest create-article--test
41 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
42 | _ (jdbc/insert! (test-db) :user auth-user)
43 | inputs (gen/sample (s/gen spec/create-article) 20)
44 | results (map #(core/create-article! auth-user %) inputs)]
45 | (is (every? true? (map first results)))
46 | (is (every? #(s/valid? spec/visible-article (second %)) results))))
47 |
48 | (deftest update-article--article-not-found--return-negative-response
49 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
50 | _ (jdbc/insert! (test-db) :user auth-user)
51 | input (gen/generate (s/gen spec/update-article))
52 | [ok? res] (core/update-article! auth-user "slug" input)]
53 | (is (false? ok?))
54 | (is (= {:errors {:slug ["Cannot find an article with given slug."]}} res))))
55 |
56 | (deftest update-article--article-is-not-owned-by-user--return-negative-response
57 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
58 | _ (jdbc/insert! (test-db) :user auth-user)
59 | initial (gen/generate (s/gen spec/create-article))
60 | [_ article] (core/create-article! auth-user initial)
61 | input (gen/generate (s/gen spec/update-article))
62 | [ok? res] (core/update-article! (assoc auth-user :id 2)
63 | (-> article :article :slug)
64 | input)]
65 | (is (false? ok?))
66 | (is (= {:errors {:authorization ["You need to be author of this article to update it."]}} res))))
67 |
68 | (deftest update-article--input-is-ok--update-article-and-return-positive-response
69 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
70 | _ (jdbc/insert! (test-db) :user auth-user)
71 | initial-inputs (gen/sample (s/gen spec/create-article) 20)
72 | create-res (map #(core/create-article! auth-user %) initial-inputs)
73 | inputs (gen/sample (s/gen spec/update-article) 20)
74 | update-res (map-indexed #(core/update-article! auth-user
75 | (-> (nth create-res %1) second :article :slug)
76 | %2)
77 | inputs)]
78 | (is (every? true? (map first update-res)))
79 | (is (every? #(s/valid? spec/visible-article (second %)) update-res))))
80 |
81 | (deftest delete-article--article-not-found--return-negative-response
82 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
83 | _ (jdbc/insert! (test-db) :user auth-user)
84 | [ok? res] (core/delete-article! auth-user "slug")]
85 | (is (false? ok?))
86 | (is (= {:errors {:slug ["Cannot find an article with given slug."]}} res))))
87 |
88 | (deftest delete-article--article-is-not-owned-by-user--return-negative-response
89 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
90 | _ (jdbc/insert! (test-db) :user auth-user)
91 | initial (gen/generate (s/gen spec/create-article))
92 | [_ article] (core/create-article! auth-user initial)
93 | [ok? res] (core/delete-article! (assoc auth-user :id 2)
94 | (-> article :article :slug))]
95 | (is (false? ok?))
96 | (is (= {:errors {:authorization ["You need to be author of this article to delete it."]}} res))))
97 |
98 | (deftest delete-article--input-is-ok--delete-article-and-return-positive-response
99 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
100 | _ (jdbc/insert! (test-db) :user auth-user)
101 | initial-inputs (gen/sample (s/gen spec/create-article) 20)
102 | create-res (map #(core/create-article! auth-user %) initial-inputs)
103 | update-res (map #(core/delete-article! auth-user (-> % second :article :slug)) create-res)]
104 | (is (every? #(= [true nil] %) update-res))))
105 |
106 | (deftest favorite-article!--profile-not-found--return-negative-result
107 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
108 | _ (jdbc/insert! (test-db) :user auth-user)
109 | [ok? res] (core/favorite-article! auth-user "slug")]
110 | (is (false? ok?))
111 | (is (= {:errors {:slug ["Cannot find an article with given slug."]}} res))))
112 |
113 | (deftest favorite-article!--profile-found--return-positive-result
114 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
115 | _ (jdbc/insert! (test-db) :user auth-user)
116 | [_ article] (core/create-article! auth-user (gen/generate (s/gen spec/create-article)))
117 | [ok? res] (core/favorite-article! auth-user (-> article :article :slug))]
118 | (is (true? ok?))
119 | (is (= 1 (-> res :article :favoritesCount)))
120 | (is (true? (-> res :article :favorited)))))
121 |
122 | (deftest unfavorite-article!--profile-not-found--return-negative-result
123 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
124 | _ (jdbc/insert! (test-db) :user auth-user)
125 | [ok? res] (core/unfavorite-article! auth-user "slug")]
126 | (is (false? ok?))
127 | (is (= {:errors {:slug ["Cannot find an article with given slug."]}} res))))
128 |
129 | (deftest unfavorite-article!--logged-in-and-profile-found--return-positive-result
130 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
131 | _ (jdbc/insert! (test-db) :user auth-user)
132 | [_ article] (core/create-article! auth-user (gen/generate (s/gen spec/create-article)))
133 | _ (core/favorite-article! auth-user (-> article :article :slug))
134 | [ok? res] (core/unfavorite-article! auth-user (-> article :article :slug))]
135 | (is (true? ok?))
136 | (is (= 0 (-> res :article :favoritesCount)))
137 | (is (false? (-> res :article :favorited)))))
138 |
139 | (deftest feed--no-articles-found--return-response-with-empty-vector
140 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
141 | _ (jdbc/insert! (test-db) :user auth-user)
142 | [ok? res] (core/feed auth-user 10 0)]
143 | (is (true? ok?))
144 | (is (= {:articles []
145 | :articlesCount 0}
146 | res))))
147 |
148 | (deftest feed--articles-found--return-response
149 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
150 | other-user (assoc (gen/generate (s/gen user-spec/user)) :id 2)
151 | _ (jdbc/insert-multi! (test-db) :user [auth-user other-user])
152 | _ (jdbc/insert! (test-db) :userFollows {:userId 1 :followedUserId 2})
153 | articles (gen/sample (s/gen spec/create-article) 20)
154 | _ (doseq [a articles]
155 | (core/create-article! other-user a))
156 | [ok? res] (core/feed auth-user 10 0)]
157 | (is (true? ok?))
158 | (is (= 10 (:articlesCount res)))
159 | (is (= 10 (-> res :articles count)))
160 | (is (= (map :title (take 10 (reverse articles)))
161 | (map :title (:articles res))))))
162 |
163 | (deftest feed--multiple-followed-users--return-response
164 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
165 | other-user (assoc (gen/generate (s/gen user-spec/user)) :id 2)
166 | other-user-2 (assoc (gen/generate (s/gen user-spec/user)) :id 3)
167 | _ (jdbc/insert-multi! (test-db) :user [auth-user other-user])
168 | _ (jdbc/insert-multi! (test-db) :userFollows [{:userId 1 :followedUserId 2}
169 | {:userId 1 :followedUserId 3}])
170 | articles (gen/sample (s/gen spec/create-article) 20)
171 | _ (doseq [a (take 10 articles)]
172 | (core/create-article! other-user a))
173 | _ (doseq [a (take 10 (drop 10 articles))]
174 | (core/create-article! other-user-2 a))
175 | [ok? res] (core/feed auth-user 10 0)]
176 | (is (true? ok?))
177 | (is (= 10 (:articlesCount res)))
178 | (is (= 10 (-> res :articles count)))
179 | (is (= (map :title (take 10 (reverse articles)))
180 | (map :title (:articles res))))))
181 |
182 | (deftest feed--no-limit-provided--return-response-with-limit-20
183 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
184 | other-user (assoc (gen/generate (s/gen user-spec/user)) :id 2)
185 | _ (jdbc/insert-multi! (test-db) :user [auth-user other-user])
186 | _ (jdbc/insert! (test-db) :userFollows {:userId 1 :followedUserId 2})
187 | articles (gen/sample (s/gen spec/create-article) 20)
188 | _ (doseq [a articles]
189 | (core/create-article! other-user a))
190 | [ok? res] (core/feed auth-user nil nil)]
191 | (is (true? ok?))
192 | (is (= 20 (:articlesCount res)))
193 | (is (= 20 (-> res :articles count)))
194 | (is (= (map :title (take 20 (reverse articles)))
195 | (map :title (:articles res))))))
196 |
197 | (deftest feed--offset-provided--return-response-with-limit-20
198 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
199 | other-user (assoc (gen/generate (s/gen user-spec/user)) :id 2)
200 | _ (jdbc/insert-multi! (test-db) :user [auth-user other-user])
201 | _ (jdbc/insert! (test-db) :userFollows {:userId 1 :followedUserId 2})
202 | articles (gen/sample (s/gen spec/create-article) 20)
203 | _ (doseq [a articles]
204 | (core/create-article! other-user a))
205 | [ok? res] (core/feed auth-user nil 5)]
206 | (is (true? ok?))
207 | (is (= 15 (:articlesCount res)))
208 | (is (= 15 (-> res :articles count)))
209 | (is (= (map :title (take 15 (drop 5 (reverse articles))))
210 | (map :title (:articles res))))))
211 |
212 | (deftest articles--no-articles-found--return-response-with-empty-vector
213 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
214 | _ (jdbc/insert! (test-db) :user auth-user)
215 | [ok? res] (core/articles auth-user 10 0 nil nil nil)]
216 | (is (true? ok?))
217 | (is (= {:articles []
218 | :articlesCount 0}
219 | res))))
220 |
221 | (deftest articles--articles-found--return-response
222 | (let [auth-user (assoc (gen/generate (s/gen user-spec/user)) :id 1)
223 | other-user (assoc (gen/generate (s/gen user-spec/user)) :id 2)
224 | _ (jdbc/insert-multi! (test-db) :user [auth-user other-user])
225 | articles (gen/sample (s/gen spec/create-article) 20)
226 | _ (doseq [a articles]
227 | (core/create-article! other-user a))
228 | [ok? res] (core/articles auth-user 10 0 nil nil nil)]
229 | (is (true? ok?))
230 | (is (= 10 (:articlesCount res)))
231 | (is (= 10 (-> res :articles count)))
232 | (is (= (map :title (take 10 (reverse articles)))
233 | (map :title (:articles res))))))
234 |
--------------------------------------------------------------------------------
/components/article/test/clojure/realworld/article/store_test.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.article.store-test
2 | (:require [clj-time.core :as t]
3 | [clojure.java.jdbc :as jdbc]
4 | [clojure.realworld.database.interface :as database]
5 | [clojure.realworld.article.store :as store]
6 | [clojure.test :refer [deftest is use-fixtures]]))
7 |
8 | (defn test-db
9 | ([] {:classname "org.sqlite.JDBC"
10 | :subprotocol "sqlite"
11 | :subname "test.db"})
12 | ([_] (test-db)))
13 |
14 | (defn prepare-for-tests [f]
15 | (with-redefs [database/db test-db]
16 | (let [db (test-db)]
17 | (database/generate-db db)
18 | (f)
19 | (database/drop-db db))))
20 |
21 | (use-fixtures :each prepare-for-tests)
22 |
23 | (deftest find-by-slug--test
24 | (let [_ (jdbc/insert! (test-db) :article {:slug "this-is-slug"})
25 | res1 (store/find-by-slug "this-is-slug")
26 | article {:body nil
27 | :createdAt nil
28 | :description nil
29 | :id 1
30 | :slug "this-is-slug"
31 | :title nil
32 | :updatedAt nil
33 | :userId nil}
34 | res2 (store/find-by-slug "wrong-slug")]
35 | (is (= article res1))
36 | (is (nil? res2))))
37 |
38 | (deftest insert-article!--test
39 | (let [now (t/now)
40 | article {:slug "slug"
41 | :title "title"
42 | :description "description"
43 | :body "body"
44 | :createdAt now
45 | :updatedAt now
46 | :userId 1}
47 | _ (store/insert-article! article)
48 | res (store/find-by-slug "slug")]
49 | (is (= (assoc article :id 1
50 | :createdAt (-> article :createdAt str)
51 | :updatedAt (-> article :updatedAt str))
52 | res))))
53 |
54 | (deftest tags-with-names--test
55 | (let [_ (jdbc/insert-multi! (test-db) :tag [{:name "tag1"}
56 | {:name "tag2"}
57 | {:name "tag3"}
58 | {:name "tag4"}
59 | {:name "tag5"}
60 | {:name "tag6"}])
61 | res (store/tags-with-names ["tag1" "tag2" "tag3"])]
62 | (is (= [{:id 1
63 | :name "tag1"}
64 | {:id 2
65 | :name "tag2"}
66 | {:id 3
67 | :name "tag3"}]
68 | res))))
69 |
70 | (deftest add-tags-to-article!--test
71 | (let [_ (jdbc/insert-multi! (test-db) :tag [{:name "tag1"}
72 | {:name "tag2"}
73 | {:name "tag3"}
74 | {:name "tag4"}
75 | {:name "tag5"}
76 | {:name "tag6"}])
77 | _ (store/add-tags-to-article! 1 ["tag1" "tag2" "tag3"])
78 | res (store/article-tags 1)]
79 | (is (= ["tag1" "tag2" "tag3"] res))))
80 |
81 | (deftest article-tags--test
82 | (let [_ (jdbc/insert-multi! (test-db) :tag [{:name "tag1"}
83 | {:name "tag2"}
84 | {:name "tag3"}
85 | {:name "tag4"}
86 | {:name "tag5"}
87 | {:name "tag6"}])
88 | _ (jdbc/insert-multi! (test-db) :articleTags [{:articleId 1 :tagId 1}
89 | {:articleId 1 :tagId 2}
90 | {:articleId 1 :tagId 3}
91 | {:articleId 2 :tagId 3}
92 | {:articleId 2 :tagId 4}
93 | {:articleId 2 :tagId 5}])
94 | res (store/article-tags 1)]
95 | (is (= ["tag1" "tag2" "tag3"] res))))
96 |
97 | (deftest insert-tag!--tag-exists--do-nothing
98 | (let [_ (jdbc/insert! (test-db) :tag {:name "tag1"})
99 | res (store/insert-tag! "tag1")]
100 | (is (nil? res))))
101 |
102 | (deftest insert-tag!--tag-does-not-exist--insert-tag
103 | (let [_ (store/insert-tag! "tag1")
104 | tags (store/tags-with-names ["tag1"])]
105 | (is (= [{:id 1
106 | :name "tag1"}]
107 | tags))))
108 |
109 | (deftest favorited?--not-favorited--return-false
110 | (let [favorited? (store/favorited? 1 1)]
111 | (is (false? favorited?))))
112 |
113 | (deftest favorited?--favorited--return-true
114 | (let [_ (jdbc/insert! (test-db) :favoriteArticles {:articleId 1 :userId 1})
115 | favorited? (store/favorited? 1 1)]
116 | (is (true? favorited?))))
117 |
118 | (deftest favorites-count--not-favorited--return-0
119 | (let [favorites-count (store/favorites-count 1)]
120 | (is (= 0 favorites-count))))
121 |
122 | (deftest favorites-count--favorited--return-favorites-count
123 | (let [_ (jdbc/insert-multi! (test-db) :favoriteArticles [{:articleId 1 :userId 1}
124 | {:articleId 1 :userId 2}
125 | {:articleId 1 :userId 3}])
126 | favorites-count (store/favorites-count 1)]
127 | (is (= 3 favorites-count))))
128 |
129 | (deftest update-article!--test
130 | (let [now (t/now)
131 | _ (store/insert-article! {:slug "slug"
132 | :title "title"
133 | :description "description"
134 | :body "body"
135 | :createdAt now
136 | :updatedAt now
137 | :userId 1})
138 | update-time (t/now)
139 | article {:slug "updated-slug"
140 | :title "updated-title"
141 | :description "description"
142 | :body "body"
143 | :updatedAt update-time}
144 | _ (store/update-article! 1 article)
145 | res (store/find-by-slug "updated-slug")]
146 | (is (= (assoc article :id 1
147 | :createdAt (str now)
148 | :updatedAt (str update-time)
149 | :userId 1)
150 | res))))
151 |
152 | (deftest delete-article!--test
153 | (let [now (t/now)
154 | _ (store/insert-article! {:slug "slug"
155 | :title "title"
156 | :description "description"
157 | :body "body"
158 | :createdAt now
159 | :updatedAt now
160 | :userId 1})
161 | _ (jdbc/insert-multi! (test-db) :tag [{:name "tag1"} {:name "tag2"} {:name "tag3"}])
162 | _ (jdbc/insert-multi! (test-db) :articleTags [{:tagId 1 :articleId 1}
163 | {:tagId 2 :articleId 1}
164 | {:tagId 3 :articleId 1}])
165 | _ (jdbc/insert-multi! (test-db) :favoriteArticles [{:userId 1 :articleId 1}
166 | {:userId 2 :articleId 1}
167 | {:userId 3 :articleId 1}])
168 | article-before (store/find-by-slug "slug")
169 | favorites-count-before (store/favorites-count 1)
170 | tags-before (store/article-tags 1)
171 | _ (store/delete-article! 1)
172 | article-after (store/find-by-slug "slug")
173 | favorites-count-after (store/favorites-count 1)
174 | tags-after (store/article-tags 1)]
175 | (is (not (nil? article-before)))
176 | (is (= 3 favorites-count-before))
177 | (is (= ["tag1" "tag2" "tag3"] tags-before))
178 | (is (nil? article-after))
179 | (is (= 0 favorites-count-after))
180 | (is (= [] tags-after))))
181 |
182 | (deftest favorite!--currently-not-favorited--insert-user-favorite
183 | (let [before-favorited? (store/favorited? 1 2)
184 | _ (store/favorite! 1 2)
185 | after-favorited? (store/favorited? 1 2)]
186 | (is (false? before-favorited?))
187 | (is (true? after-favorited?))))
188 |
189 | (deftest favorite!--currently-favorited--do-nothing
190 | (let [_ (store/favorite! 1 2)
191 | before-favorited? (store/favorited? 1 2)
192 | _ (store/favorite! 1 2)
193 | after-favorited? (store/favorited? 1 2)]
194 | (is (true? before-favorited?))
195 | (is (true? after-favorited?))))
196 |
197 | (deftest unfavorite!--currently-favorited--delete-user-favorite
198 | (let [_ (store/favorite! 1 2)
199 | before-favorited? (store/favorited? 1 2)
200 | _ (store/unfavorite! 1 2)
201 | after-favorited? (store/favorited? 1 2)]
202 | (is (true? before-favorited?))
203 | (is (false? after-favorited?))))
204 |
205 | (deftest unfavorite!--currently-not-favorited--do-nothing
206 | (let [before-favorited? (store/favorited? 1 2)
207 | _ (store/unfavorite! 1 2)
208 | after-favorited? (store/favorited? 1 2)]
209 | (is (false? before-favorited?))
210 | (is (false? after-favorited?))))
211 |
212 | (deftest feed--no-articles-found--return-empty-vector
213 | (let [res (store/feed 1 10 0)]
214 | (is (= [] res))))
215 |
216 | (deftest feed--some-articles-found--return-articles
217 | (let [_ (jdbc/insert-multi! (test-db) :article [{:slug "slug1" :userId 1}
218 | {:slug "slug2" :userId 1}
219 | {:slug "slug3" :userId 1}
220 | {:slug "slug4" :userId 2}
221 | {:slug "slug5" :userId 2}
222 | {:slug "slug6" :userId 3}])
223 | _ (jdbc/insert-multi! (test-db) :userFollows [{:userId 4 :followedUserId 1}
224 | {:userId 4 :followedUserId 3}])
225 | res (store/feed 4 10 0)]
226 | (is (= ["slug6" "slug3" "slug2" "slug1"]
227 | (mapv :slug res)))))
228 |
229 | (deftest feed--more-than-10-articles-found--return-10-articles
230 | (let [articles (mapv #(hash-map :slug (str "slug" %) :userId 1) (range 30))
231 | _ (jdbc/insert-multi! (test-db) :article articles)
232 | _ (jdbc/insert-multi! (test-db) :userFollows [{:userId 2 :followedUserId 1}])
233 | res1 (store/feed 2 10 0)
234 | res2 (store/feed 2 10 10)]
235 | (is (= (take 10 (reverse articles))
236 | (mapv #(select-keys % [:slug :userId]) res1)))
237 | (is (= (take 10 (drop 10 (reverse articles)))
238 | (mapv #(select-keys % [:slug :userId]) res2)))))
239 |
240 | (deftest articles-by-tag--some-articles-found--return-articles
241 | (let [_ (jdbc/insert-multi! (test-db) :article [{:slug "slug1"}
242 | {:slug "slug2"}
243 | {:slug "slug3"}
244 | {:slug "slug4"}
245 | {:slug "slug5"}
246 | {:slug "slug6"}])
247 | _ (jdbc/insert-multi! (test-db) :tag [{:name "tag1"}
248 | {:name "tag2"}])
249 | _ (jdbc/insert-multi! (test-db) :articleTags [{:articleId 1 :tagId 1}
250 | {:articleId 1 :tagId 2}
251 | {:articleId 2 :tagId 2}
252 | {:articleId 3 :tagId 2}
253 | {:articleId 4 :tagId 1}
254 | {:articleId 6 :tagId 1}])
255 | res1 (store/articles-by-tag 10 0 "tag1")
256 | res2 (store/articles-by-tag 10 0 "tag2")]
257 | (is (= ["slug6" "slug4" "slug1"]
258 | (mapv :slug res1)))
259 | (is (= ["slug3" "slug2" "slug1"]
260 | (mapv :slug res2)))))
261 |
262 | (deftest articles-by-author--some-articles-found--return-articles
263 | (let [_ (jdbc/insert-multi! (test-db) :user [{:username "username1"}
264 | {:username "username2"}])
265 | _ (jdbc/insert-multi! (test-db) :article [{:slug "slug1" :userId 1}
266 | {:slug "slug2" :userId 1}
267 | {:slug "slug3" :userId 2}
268 | {:slug "slug4" :userId 2}
269 | {:slug "slug5" :userId 1}
270 | {:slug "slug6" :userId 2}])
271 | res1 (store/articles-by-author 10 0 "username1")
272 | res2 (store/articles-by-author 10 0 "username2")
273 | res3 (store/articles-by-author 10 0 "username3")]
274 | (is (= ["slug5" "slug2" "slug1"]
275 | (mapv :slug res1)))
276 | (is (= ["slug6" "slug4" "slug3"]
277 | (mapv :slug res2)))
278 | (is (empty? res3))))
279 |
280 | (deftest articles-by-favorited--some-articles-found--return-articles
281 | (let [_ (jdbc/insert-multi! (test-db) :user [{:username "username1"}
282 | {:username "username2"}])
283 | _ (jdbc/insert-multi! (test-db) :article [{:slug "slug1" :userId 1}
284 | {:slug "slug2" :userId 1}
285 | {:slug "slug3" :userId 2}
286 | {:slug "slug4" :userId 2}
287 | {:slug "slug5" :userId 1}
288 | {:slug "slug6" :userId 2}])
289 | _ (jdbc/insert-multi! (test-db) :favoriteArticles [{:articleId 1 :userId 1}
290 | {:articleId 2 :userId 1}
291 | {:articleId 3 :userId 2}
292 | {:articleId 6 :userId 1}])
293 | res1 (store/articles-by-favorited 10 0 "username1")
294 | res2 (store/articles-by-favorited 10 0 "username2")
295 | res3 (store/articles-by-favorited 10 0 "username3")]
296 | (is (= ["slug6" "slug2" "slug1"]
297 | (mapv :slug res1)))
298 | (is (= ["slug3"]
299 | (mapv :slug res2)))
300 | (is (empty? res3))))
301 |
302 | (deftest articles--no-other-filters--return-articles
303 | (let [_ (jdbc/insert-multi! (test-db) :article [{:slug "slug1"}
304 | {:slug "slug2"}
305 | {:slug "slug3"}
306 | {:slug "slug4"}
307 | {:slug "slug5"}
308 | {:slug "slug6"}])
309 | res (store/articles 10 0 nil nil nil)]
310 | (is (= ["slug6" "slug5" "slug4" "slug3" "slug2" "slug1"]
311 | (mapv :slug res)))))
312 |
--------------------------------------------------------------------------------
/bases/rest-api/test/clojure/realworld/rest_api/handler_test.clj:
--------------------------------------------------------------------------------
1 | (ns clojure.realworld.rest-api.handler-test
2 | (:require [clojure.test :refer [deftest is use-fixtures]]
3 | [clojure.realworld.article.interface :as article]
4 | [clojure.realworld.article.interface.spec :as article-spec]
5 | [clojure.realworld.rest-api.handler :as handler]
6 | [clojure.realworld.comment.interface :as comment-comp]
7 | [clojure.realworld.comment.interface.spec :as comment-spec]
8 | [clojure.realworld.profile.interface :as profile]
9 | [clojure.realworld.spec.interface :as spec]
10 | [clojure.realworld.tag.interface :as tag]
11 | [clojure.realworld.user.interface :as user]
12 | [clojure.realworld.user.interface.spec :as user-spec]
13 | [clojure.spec.alpha :as s]
14 | [clojure.spec.gen.alpha :as gen]))
15 |
16 | (defn prepare-for-tests [f]
17 | (with-redefs [user/login! (fn [_] [true {}])
18 | user/register! (fn [_] [true {}])
19 | user/user-by-token (fn [_] [true {}])
20 | user/update-user! (fn [_ _] [true {}])
21 | profile/fetch-profile (fn [_ _] [true {}])
22 | profile/follow! (fn [_ _] [true {}])
23 | profile/unfollow! (fn [_ _] [true {}])
24 | article/article (fn [_ _] [true {}])
25 | article/create-article! (fn [_ _] [true {}])
26 | article/update-article! (fn [_ _ _] [true {}])
27 | article/delete-article! (fn [_ _] [true {}])
28 | article/favorite-article! (fn [_ _] [true {}])
29 | article/unfavorite-article! (fn [_ _] [true {}])
30 | article/feed (fn [_ limit offset] [true {:limit limit :offset offset}])
31 | article/articles (fn [_ limit offset author tag favorited]
32 | [true {:limit limit :offset offset
33 | :author author :tag tag :favorited favorited}])
34 | tag/all-tags (fn [] [true {:tags []}])
35 | comment-comp/article-comments (fn [_ _] [true {:comments []}])
36 | comment-comp/add-comment! (fn [_ _ _] [true {}])
37 | comment-comp/delete-comment! (fn [_ _] [true {}])]
38 | (f)))
39 |
40 | (use-fixtures :each prepare-for-tests)
41 |
42 | (deftest login--invalid-input--return-422
43 | (let [res (handler/login {})]
44 | (is (= {:status 422
45 | :body {:errors {:body ["Invalid request body."]}}}
46 | res))))
47 |
48 | (deftest login--valid-input--return-200
49 | (let [res (handler/login {:params {:user (gen/generate (s/gen user-spec/login))}})]
50 | (is (= {:status 200
51 | :body {}}
52 | res))))
53 |
54 | (deftest register--invalid-input--return-422
55 | (let [res (handler/register {})]
56 | (is (= {:status 422
57 | :body {:errors {:body ["Invalid request body."]}}}
58 | res))))
59 |
60 | (deftest register--valid-input--return-200
61 | (let [res (handler/register {:params {:user (gen/generate (s/gen user-spec/register))}})]
62 | (is (= {:status 200
63 | :body {}}
64 | res))))
65 |
66 | (deftest current-user--valid-input--return-200
67 | (let [auth-user (gen/generate (s/gen user-spec/user))
68 | res (handler/current-user {:auth-user auth-user})]
69 | (is (= {:status 200
70 | :body {:user auth-user}}
71 | res))))
72 |
73 | (deftest update-user--invalid-input--return-422
74 | (let [res (handler/update-user {})]
75 | (is (= {:status 422
76 | :body {:errors {:body ["Invalid request body."]}}}
77 | res))))
78 |
79 | (deftest update-user--valid-input--return-200
80 | (let [res (handler/update-user {:auth-user (gen/generate (s/gen user-spec/user))
81 | :params {:user (gen/generate (s/gen user-spec/update-user))}})]
82 | (is (= {:status 200
83 | :body {}}
84 | res))))
85 |
86 | (deftest profile--invalid-input--return-422
87 | (let [res (handler/profile {})]
88 | (is (= {:status 422
89 | :body {:errors {:username ["Invalid username."]}}}
90 | res))))
91 |
92 | (deftest profile--valid-input--return-200
93 | (let [res (handler/profile {:auth-user (gen/generate (s/gen user-spec/user))
94 | :params {:username (gen/generate (s/gen spec/username?))}})]
95 | (is (= {:status 200
96 | :body {}}
97 | res))))
98 |
99 | (deftest follow--invalid-input--return-422
100 | (let [res (handler/follow-profile {})]
101 | (is (= {:status 422
102 | :body {:errors {:username ["Invalid username."]}}}
103 | res))))
104 |
105 | (deftest follow--valid-input--return-200
106 | (let [res (handler/follow-profile {:auth-user (gen/generate (s/gen user-spec/user))
107 | :params {:username (gen/generate (s/gen spec/username?))}})]
108 | (is (= {:status 200
109 | :body {}}
110 | res))))
111 |
112 | (deftest unfollow--invalid-input--return-422
113 | (let [res (handler/unfollow-profile {})]
114 | (is (= {:status 422
115 | :body {:errors {:username ["Invalid username."]}}}
116 | res))))
117 |
118 | (deftest unfollow--valid-input--return-200
119 | (let [res (handler/unfollow-profile {:auth-user (gen/generate (s/gen user-spec/user))
120 | :params {:username (gen/generate (s/gen spec/username?))}})]
121 | (is (= {:status 200
122 | :body {}}
123 | res))))
124 |
125 | (deftest article--invalid-input--return-422
126 | (let [res (handler/article {})]
127 | (is (= {:status 422
128 | :body {:errors {:slug ["Invalid slug."]}}}
129 | res))))
130 |
131 | (deftest article--valid-input--return-200
132 | (let [res (handler/article {:params {:slug "this-is-slug"}})]
133 | (is (= {:status 200
134 | :body {}}
135 | res))))
136 |
137 | (deftest create-article--invalid-input--return-422
138 | (let [res (handler/create-article {})]
139 | (is (= {:status 422
140 | :body {:errors {:body ["Invalid request body."]}}}
141 | res))))
142 |
143 | (deftest create-article--valid-input--return-200
144 | (let [res (handler/create-article {:auth-user (gen/generate (s/gen user-spec/user))
145 | :params {:article (gen/generate (s/gen article-spec/create-article))}})]
146 | (is (= {:status 200
147 | :body {}}
148 | res))))
149 |
150 | (deftest update-article--invalid-body--return-422
151 | (let [res (handler/update-article {:params {:slug "this-is-slug"}})]
152 | (is (= {:status 422
153 | :body {:errors {:body ["Invalid request body."]}}}
154 | res))))
155 |
156 | (deftest update-article--invalid-slug--return-422
157 | (let [res (handler/update-article {:params {:article (gen/generate (s/gen article-spec/update-article))}})]
158 | (is (= {:status 422
159 | :body {:errors {:body ["Invalid request body."]}}}
160 | res))))
161 |
162 | (deftest update-article--valid-input--return-200
163 | (let [res (handler/update-article {:auth-user (gen/generate (s/gen user-spec/user))
164 | :params {:slug "this-is-slug"
165 | :article (gen/generate (s/gen article-spec/update-article))}})]
166 | (is (= {:status 200
167 | :body {}}
168 | res))))
169 |
170 | (deftest delete-article--invalid-input--return-422
171 | (let [res (handler/delete-article {})]
172 | (is (= {:status 422
173 | :body {:errors {:slug ["Invalid slug."]}}}
174 | res))))
175 |
176 | (deftest delete-article--valid-input--return-200
177 | (let [res (handler/delete-article {:auth-user (gen/generate (s/gen user-spec/user))
178 | :params {:slug "this-is-slug"}})]
179 | (is (= {:status 200
180 | :body {}}
181 | res))))
182 |
183 | (deftest favorite-article--invalid-input--return-422
184 | (let [res (handler/favorite-article {})]
185 | (is (= {:status 422
186 | :body {:errors {:slug ["Invalid slug."]}}}
187 | res))))
188 |
189 | (deftest favorite-article--valid-input--return-200
190 | (let [res (handler/favorite-article {:auth-user (gen/generate (s/gen user-spec/user))
191 | :params {:slug "this-is-slug"}})]
192 | (is (= {:status 200
193 | :body {}}
194 | res))))
195 |
196 | (deftest unfavorite-article--invalid-input--return-422
197 | (let [res (handler/unfavorite-article {})]
198 | (is (= {:status 422
199 | :body {:errors {:slug ["Invalid slug."]}}}
200 | res))))
201 |
202 | (deftest unfavorite-article--valid-input--return-200
203 | (let [res (handler/unfavorite-article {:auth-user (gen/generate (s/gen user-spec/user))
204 | :params {:slug "this-is-slug"}})]
205 | (is (= {:status 200
206 | :body {}}
207 | res))))
208 |
209 | (deftest tags--return-200
210 | (let [res (handler/tags {})]
211 | (is (= {:status 200
212 | :body {:tags []}}
213 | res))))
214 |
215 | (deftest comments--invalid-input--return-422
216 | (let [res (handler/comments {})]
217 | (is (= {:status 422
218 | :body {:errors {:slug ["Invalid slug."]}}}
219 | res))))
220 |
221 | (deftest comments--valid-input--return-200
222 | (let [res (handler/comments {:auth-user (gen/generate (s/gen user-spec/user))
223 | :params {:slug "this-is-slug"}})]
224 | (is (= {:status 200
225 | :body {:comments []}}
226 | res))))
227 |
228 | (deftest delete-comment--invalid-id-string--return-422
229 | (let [res (handler/delete-comment {:id "asd"})]
230 | (is (= {:status 422
231 | :body {:errors {:id ["Invalid comment id."]}}}
232 | res))))
233 |
234 | (deftest delete-comment--nil-id-string--return-422
235 | (let [res (handler/delete-comment {})]
236 | (is (= {:status 422
237 | :body {:errors {:id ["Invalid comment id."]}}}
238 | res))))
239 |
240 | (deftest delete-comment--valid-string-input--return-200
241 | (let [res (handler/delete-comment {:auth-user (gen/generate (s/gen user-spec/user))
242 | :params {:id "1"}})]
243 | (is (= {:status 200
244 | :body {}}
245 | res))))
246 |
247 | (deftest delete-comment--valid-int-input--return-200
248 | (let [res (handler/delete-comment {:auth-user (gen/generate (s/gen user-spec/user))
249 | :params {:id 1}})]
250 | (is (= {:status 200
251 | :body {}}
252 | res))))
253 |
254 | (deftest add-comment--invalid-slug--return-422
255 | (let [res (handler/add-comment {:auth-user (gen/generate (s/gen user-spec/user))
256 | :params {:comment (gen/generate (s/gen comment-spec/add-comment))}})]
257 | (is (= {:status 422
258 | :body {:errors {:body ["Invalid request body."]}}}
259 | res))))
260 |
261 | (deftest add-comment--invalid-comment--return-422
262 | (let [res (handler/add-comment {:auth-user (gen/generate (s/gen user-spec/user))
263 | :params {:slug "this-is-slug"}})]
264 | (is (= {:status 422
265 | :body {:errors {:body ["Invalid request body."]}}}
266 | res))))
267 |
268 | (deftest add-comment--valid-input--return-200
269 | (let [res (handler/add-comment {:auth-user (gen/generate (s/gen user-spec/user))
270 | :params {:slug "this-is-slug"
271 | :comment (gen/generate (s/gen comment-spec/add-comment))}})]
272 | (is (= {:status 200
273 | :body {}}
274 | res))))
275 |
276 | (deftest feed--invalid-limit--return-200
277 | (let [res (handler/feed {:auth-user (gen/generate (s/gen user-spec/user))
278 | :params {:limit "invalid-limit"
279 | :offset 0}})]
280 | (is (= {:status 200
281 | :body {:limit nil
282 | :offset 0}}
283 | res))))
284 |
285 | (deftest feed--invalid-offset--return-200
286 | (let [res (handler/feed {:auth-user (gen/generate (s/gen user-spec/user))
287 | :params {:offset "invalid-offset"
288 | :limit 10}})]
289 | (is (= {:status 200
290 | :body {:limit 10
291 | :offset nil}}
292 | res))))
293 |
294 | (deftest feed--string-offset--return-200
295 | (let [res (handler/feed {:auth-user (gen/generate (s/gen user-spec/user))
296 | :params {:offset "5"
297 | :limit 10}})]
298 | (is (= {:status 200
299 | :body {:limit 10
300 | :offset 5}}
301 | res))))
302 |
303 | (deftest feed--string-limit--return-200
304 | (let [res (handler/feed {:auth-user (gen/generate (s/gen user-spec/user))
305 | :params {:offset 5
306 | :limit "10"}})]
307 | (is (= {:status 200
308 | :body {:limit 10
309 | :offset 5}}
310 | res))))
311 |
312 | (deftest feed--valid-input--return-200
313 | (let [res (handler/feed {:auth-user (gen/generate (s/gen user-spec/user))
314 | :params {:offset 5
315 | :limit 10}})]
316 | (is (= {:status 200
317 | :body {:limit 10
318 | :offset 5}}
319 | res))))
320 |
321 | (deftest feed--no-limit-and-offset--return-200
322 | (let [res (handler/feed {:auth-user (gen/generate (s/gen user-spec/user))
323 | :params {}})]
324 | (is (= {:status 200
325 | :body {:limit nil
326 | :offset nil}}
327 | res))))
328 |
329 | (deftest articles--invalid-limit--return-200
330 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user))
331 | :params {:limit "invalid-limit"
332 | :offset 0}})]
333 | (is (= {:status 200
334 | :body {:limit nil
335 | :offset 0
336 | :tag nil
337 | :author nil
338 | :favorited nil}}
339 | res))))
340 |
341 | (deftest articles--invalid-offset--return-200
342 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user))
343 | :params {:offset "invalid-offset"
344 | :limit 10}})]
345 | (is (= {:status 200
346 | :body {:limit 10
347 | :offset nil
348 | :tag nil
349 | :author nil
350 | :favorited nil}}
351 | res))))
352 |
353 | (deftest articles--string-offset--return-200
354 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user))
355 | :params {:offset "5"
356 | :limit 10}})]
357 | (is (= {:status 200
358 | :body {:limit 10
359 | :offset 5
360 | :tag nil
361 | :author nil
362 | :favorited nil}}
363 | res))))
364 |
365 | (deftest articles--string-limit--return-200
366 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user))
367 | :params {:offset 5
368 | :limit "10"}})]
369 | (is (= {:status 200
370 | :body {:limit 10
371 | :offset 5
372 | :tag nil
373 | :author nil
374 | :favorited nil}}
375 | res))))
376 |
377 | (deftest articles--invalid-tag--return-200
378 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user))
379 | :params {:offset 5
380 | :limit 10
381 | :tag 10}})]
382 | (is (= {:status 200
383 | :body {:limit 10
384 | :offset 5
385 | :tag nil
386 | :author nil
387 | :favorited nil}}
388 | res))))
389 |
390 | (deftest articles--invalid-author--return-200
391 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user))
392 | :params {:offset 5
393 | :limit 10
394 | :author 10}})]
395 | (is (= {:status 200
396 | :body {:limit 10
397 | :offset 5
398 | :tag nil
399 | :author nil
400 | :favorited nil}}
401 | res))))
402 |
403 | (deftest articles--invalid-favorited--return-200
404 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user))
405 | :params {:offset 5
406 | :limit 10
407 | :favorited 10}})]
408 | (is (= {:status 200
409 | :body {:limit 10
410 | :offset 5
411 | :tag nil
412 | :author nil
413 | :favorited nil}}
414 | res))))
415 |
416 | (deftest articles--valid-filters--return-200
417 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user))
418 | :params {:offset 5
419 | :limit 10
420 | :author "author"
421 | :tag "tag"
422 | :favorited "favorited"}})]
423 | (is (= {:status 200
424 | :body {:limit 10
425 | :offset 5
426 | :tag "tag"
427 | :author "author"
428 | :favorited "favorited"}}
429 | res))))
430 |
431 | (deftest articles--valid-input--return-200
432 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user))
433 | :params {:offset 5
434 | :limit 10}})]
435 | (is (= {:status 200
436 | :body {:limit 10
437 | :offset 5
438 | :tag nil
439 | :author nil
440 | :favorited nil}}
441 | res))))
442 |
443 | (deftest articles--no-limit-and-offset--return-200
444 | (let [res (handler/articles {:auth-user (gen/generate (s/gen user-spec/user))
445 | :params {}})]
446 | (is (= {:status 200
447 | :body {:limit nil
448 | :offset nil
449 | :tag nil
450 | :author nil
451 | :favorited nil}}
452 | res))))
453 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # 
2 |
3 | A full-fledged [RealWorld](https://github.com/gothinkster/realworld) server (CRUD, auth, advanced patterns, etc) built with [Clojure](https://clojure.org), [Polylith](https://polylith.gitbook.io/), and [Ring](https://github.com/ring-clojure/ring), including CRUD operations, authentication, routing, pagination, and more.
4 |
5 | #### Build Status
6 | [](https://dl.circleci.com/status-badge/redirect/gh/furkan3ayraktar/clojure-polylith-realworld-example-app/tree/master)
7 |
8 | ## Two quick ways to try this project
9 |
10 | You can try both!
11 |
12 | 1. On your own machine, see below
13 | 2. In your browser using Gitpod, see [Hack on Real World Polylith in your Browser](gitpod.md)
14 |
15 | There are many other ways too. But especially if your experience with Clojure is somewhat limited, the Gitpod variant is definitely an easy and quick one. You will have the development environment up and running in less than two minutes, without installing anything at all.
16 |
17 | ## Start a REPL in VSCode / Calva
18 |
19 | 1. Fork & clone this repo
20 | 2. Open the project in VSCode
21 | * [Install the Calva extension](https://calva.io/getting-started/#install-vs-code-and-calva), if you don't have it installed alreaady.
22 | 3. Press F1 and select `Calva: Start a Project REPL and Connect (aka Jack-In)`
23 | >
24 |
25 | Calva will start the Polylith REPL, connect it to the VSCode, and start the RealWorld server at port 6003 for you 💫
26 |
27 | > Check [.vscode/settings.json](.vscode/settings.json) file to see what Calva does under the hood.
28 |
29 | ### Test it with a RealWorld Frontend
30 |
31 | A sweet way to put the server to some tests is to fork the [re-frame RealWorld frontend by Jacek Schae](https://github.com/jacekschae/conduit) and modify it to run against this server by editing the definition of `api-url` in `src/conduit/events.cljs` to be:
32 |
33 | ```clojure
34 | (def api-url "http://localhost:6003/api")
35 | ```
36 |
37 | Then start the frontend and open http://localhost:3000/ in a web browser.
38 |
39 | ## Put the `poly` command to your service
40 |
41 | 1. Install the [Polylith tool](https://cljdoc.org/d/polylith/clj-poly/CURRENT/doc/install)
42 | 2. Get ”at a glance" info about the project:
43 | ```sh
44 | $ poly info
45 | ```
46 | > For detailed information, see [Workspace Info](#workspace-info) section below.
47 |
48 | > All Polylith commands can be run by starting a poly shell session by running `poly` command without any arguments on your terminal.
49 |
50 | ## Start it in your Clojure REPL
51 |
52 | 1. Fork & clone this repo
53 | 1. Open the project in your favorite Clojure editor, start the project, and connect the REPL. From a development perspective it is a regular `deps.edn` project. Just make sure to include the `:dev` and `:test` aliases, and you should be good.
54 | 1. In the `dev.server` namespace, evaluate:
55 | ```clojure
56 | (start! 6003)
57 | ```
58 |
59 | Now the Polylith RealWorld backend is up on port 6003!
60 |
61 | ## Table of Contents
62 |
63 | - [Getting Started](#two-quick-ways-to-try-this-project)
64 | - [General Structure](#general-structure)
65 | - [Project](#project)
66 | - [Base](#base)
67 | - [Components](#components)
68 | - [Environment Variables](#environment-variables)
69 | - [Database](#database)
70 | - [Workspace Info](#workspace-info)
71 | - [Check Workspace Integrity](#check-workspace-integrity)
72 | - [Running Tests](#running-tests)
73 | - [Stable Points](#stable-points)
74 | - [Continuous Integration](#continuous-integration)
75 | - [How to create this workspace from scratch](#how-to-create-this-workspace-from-scratch)
76 | - [Steps required to use IntelliJ IDEA / Cursive](#steps-required-to-use-intellij-idea--cursive)
77 |
78 |
79 | ### General Structure
80 | This project is structured according to Polylith Architecture principles.
81 | If you are not familiar with Polylith Architecture, please refer to its [documentation](https://polylith.gitbook.io/polylith) for further and deeper understanding.
82 |
83 | The workspace is the root directory in a Polylith codebase, and is where we work with all our building blocks and projects.
84 | A workspace is usually version controlled in a monorepo, and contains all the building blocks,
85 | supplementary development sources, and projects. The subdirectories of the workspace looks like this:
86 | ```
87 | ▾ bases
88 | ▸ rest-api
89 | ▾ components
90 | ▸ article
91 | ▸ comment
92 | ▸ database
93 | ▸ env
94 | ▸ log
95 | ▸ profile
96 | ▸ spec
97 | ▸ tag
98 | ▸ user
99 | ▸ development
100 | ▾ projects
101 | ▸ realworld-backend
102 | ```
103 |
104 | Components are the main building blocks in Polylith. Bases are another kind of building blocks where the difference from components is that they expose a public API to the outside world. Both bases and components are encapsulated blocks of code that can be assembled together into services, libraries or tools. Components communicate to each other through their 'interfaces'.
105 | The base in each project, glue components together via their 'interfaces' and expose the business logic via a public API, in this project's case, a REST API.
106 |
107 | There is only one base and one project in this workspace to keep it simple. The project named 'realworld-backend' bundles the base, components and libraries together. The development project makes it delightful to develop from one single place. You can run a REPL within the development project, start the Ring server for debugging or refactor the components easily by using your favorite IDE.
108 |
109 | The Polylith tool also helps you run the tests incrementally. If you run the `` poly test `` command from the root directory, it will detect changes made since the last stable point in time, and only run tests for the recent changes. [Check out Polylith tool](https://cljdoc.org/d/polylith/clj-poly/CURRENT/doc/testing) for further information about incremental testing or simply write `` poly help `` to see available commands.
110 |
111 | ##### Project
112 | Projects in the Polylith architecture are configurations for deployable artifacts.
113 | There is only one project in this workspace, which is called `` realworld-backend ``.
114 | Projects are a way to define a base, a set of components and libraries to deliver within a bundle.
115 | Since we only need to deliver one bundle for realworld backend, we have only one project.
116 |
117 | If you look at the directory `` projects/realworld-backend ``, you will see a standard ``deps.edn`` file.
118 | The magic here is the project's `` deps.edn `` file which refers to the sources, resources and tests of actual components and bases.
119 | A project only has it's `` deps.edn `` file to define project specific configuration and external dependencies.
120 | All the code and resources in a project come from the components and the base, which creates the project.
121 |
122 | ##### Base
123 | Bases in Polylith architecture are the building blocks that expose a public API to the outside world and `` rest-api `` is the only base in our workspace.
124 | As hinted in its name, it exposes its functionality via a RESTful API.
125 | In order to achieve this, it uses Ring and [Compojure](https://github.com/weavejester/compojure).
126 | There are four namespaces under the `` src `` directory of `` bases/rest-api ``:
127 | - `` api.clj ``
128 | - `` handler.clj ``
129 | - `` main.clj ``
130 | - `` middleware.clj``
131 |
132 | The `` api.clj `` namespace contains route definitions for compojure and init function for Ring. The REST API looks like this:
133 |
134 | 
135 |
136 | These routes are defined with compojure with this piece of code:
137 | ```clojure
138 | (defroutes public-routes
139 | (OPTIONS "/**" [] h/options)
140 | (POST "/api/users/login" [] h/login)
141 | (POST "/api/users" [] h/register)
142 | (GET "/api/profiles/:username" [] h/profile)
143 | (GET "/api/articles" [] h/articles)
144 | (GET "/api/articles/:slug" [] h/article)
145 | (GET "/api/articles/:slug/comments" [] h/comments)
146 | (GET "/api/tag" [] h/tags))
147 |
148 | (defroutes private-routes
149 | (GET "/api/user" [] h/current-user)
150 | (PUT "/api/user" [] h/update-user)
151 | (POST "/api/profiles/:username/follow" [] h/follow-profile)
152 | (DELETE "/api/profiles/:username/follow" [] h/unfollow-profile)
153 | (GET "/api/articles/feed" [] h/feed)
154 | (POST "/api/articles" [] h/create-article)
155 | (PUT "/api/articles/:slug" [] h/update-article)
156 | (DELETE "/api/articles/:slug" [] h/delete-article)
157 | (POST "/api/articles/:slug/comments" [] h/add-comment)
158 | (DELETE "/api/articles/:slug/comments/:id" [] h/delete-comment)
159 | (POST "/api/articles/:slug/favorite" [] h/favorite-article)
160 | (DELETE "/api/articles/:slug/favorite" [] h/unfavorite-article))
161 | ```
162 |
163 | The `` middleware.clj `` namespace contains several useful middleware definitions for Ring,
164 | like adding CORS headers, wrapping exceptions and authorization. Middlewares in Ring are functions that are called before or after the execution of your handlers. For example, for authorization we can have a simple middleware like this:
165 | ```clojure
166 | (defn wrap-authorization [handler]
167 | (fn [req]
168 | (if (:auth-user req)
169 | (handler req)
170 | {:status 401
171 | :body {:errors {:authorization "Authorization required."}}})))
172 | ```
173 | This middleware will check every request that it wraps and return an authorization error if it can't find `` :auth-user `` in the request. Otherwise, it will execute the handler.
174 |
175 | The `` main.clj `` namespace contains a main function to expose the REST API via a [Jetty](https://www.eclipse.org/jetty/) server. If you look at the project configuration at `` projects/realworld-backend/deps.edn `` you'll notice that there are two aliases named `` :aot `` and `` :uberjar ``. With the help of those two aliases and `` main.clj ``, we can create an uberjar which is a single jar file that can be run directly on any machine that has Java runtime. Once the jar file is run, the main function defined under `` main.clj `` will be triggered and it will start the server.
176 |
177 | Finally, the `` handler.clj `` namespace is the place where we define our handlers. Since `` rest-api `` is the only place where our project exposes its functionality, its handler needs to call functions in different components via their `` interfaces ``. If you check out the `` :require `` statements on top of the namespace, you'll see this:
178 | ```clojure
179 | (ns clojure.realworld.rest-api.handler
180 | (:require [clojure.realworld.article.interface :as article]
181 | [clojure.realworld.comment.interface :as comment-comp]
182 | [clojure.realworld.spec.interface :as spec]
183 | [clojure.realworld.profile.interface :as profile]
184 | [clojure.realworld.tag.interface :as tag]
185 | [clojure.realworld.user.interface :as user]
186 | [clojure.spec.alpha :as s]))
187 | ```
188 | Following the rules of the Polylith architecture means that `` handler.clj `` doesn't depend on anything except the interfaces of different components. An example handler for profile request can be written like this:
189 | ```clojure
190 | (defn profile [req]
191 | (let [auth-user (-> req :auth-user)
192 | username (-> req :params :username)]
193 | (if (s/valid? spec/username? username)
194 | (let [[ok? res] (profile/profile auth-user username)]
195 | (handler (if ok? 200 404) res))
196 | (handler 422 {:errors {:username ["Invalid username."]}}))))
197 | ```
198 |
199 | ##### Components
200 | Components are the main building blocks in a Polylith architecture. In this workspace, there are nine different components.
201 | Let's take a deeper look at one of the interfaces, like `` profile ``.
202 | The interface of the `` profile `` component is split into two different files. One of them contains the function interfaces and the other one contains the exposed specs.
203 | ```clojure
204 | (ns clojure.realworld.profile.interface
205 | (:require [clojure.realworld.profile.core :as core]))
206 |
207 | (defn fetch-profile [auth-user username]
208 | (core/fetch-profile auth-user username))
209 |
210 | (defn follow! [auth-user username]
211 | (core/follow! auth-user username))
212 |
213 | (defn unfollow! [auth-user username]
214 | (core/unfollow! auth-user username))
215 | ```
216 |
217 | ```clojure
218 | (ns clojure.realworld.profile.interface.spec
219 | (:require [clojure.realworld.profile.spec :as spec]))
220 |
221 | (def profile spec/profile)
222 | ```
223 |
224 | As you can see, the interfaces are just passing through to the real implemantation encapsulated in the component.
225 |
226 | One example of using these interfaces can be found under `` handler.clj `` namespace of `` rest-api `` base.
227 | ```clojure
228 | (ns clojure.realworld.rest-api.handler
229 | (:require ;;...
230 | [clojure.realworld.profile.interface :as profile]
231 | ;;...))
232 |
233 | ;;...
234 |
235 | (defn follow-profile [req]
236 | (let [auth-user (-> req :auth-user)
237 | username (-> req :params :username)]
238 | (if (s/valid? spec/username? username)
239 | (let [[ok? res] (profile/follow! auth-user username)]
240 | (handler (if ok? 200 404) res))
241 | (handler 422 {:errors {:username ["Invalid username."]}}))))
242 |
243 | ;;...
244 | ```
245 |
246 | The function `` profile/follow! `` is called via the `` profile `` interface, which delegates
247 | to the `follow!` function that lives in the `core` namespace inside the `profile` component:
248 | ```clojure
249 | (defn follow! [auth-user username]
250 | (if-let [user (user/find-by-username-or-id username)]
251 | (do
252 | (store/follow! (:id auth-user) (:id user))
253 | [true (create-profile user true)])
254 | [false {:errors {:username ["Cannot find a profile with given username."]}}]))
255 | ```
256 | Here is another function call to the `` user `` component from `` profile `` component.
257 | This is how the `` user ``s interface looks like:
258 | ```clojure
259 | (ns clojure.realworld.user.interface
260 | (:require [clojure.realworld.user.core :as core]
261 | [clojure.realworld.user.store :as store]))
262 |
263 | (defn login! [login-input]
264 | (core/login! login-input))
265 |
266 | (defn register! [register-input]
267 | (core/register! register-input))
268 |
269 | (defn user-by-token [token]
270 | (core/user-by-token token))
271 |
272 | (defn update-user! [auth-user user-input]
273 | (core/update-user! auth-user user-input))
274 |
275 | (defn find-by-username-or-id [username-or-id]
276 | (store/find-by-username-or-id username-or-id))
277 | ```
278 | `` profile `` uses `` find-by-username-or-id `` function from `` user `` component. This is how different components talk to each other within the workspace.
279 | It's only possible to call component functions via their `` interface.clj ``.
280 |
281 | In the code example above, we can see that the interface functions redirect each function call to an actual implementation inside the component.
282 | By having an interface and an implementation of that interface, it is easy to compile/test/build (as well as develop) components in isolation.
283 | This separation gives it ability to detect/test/build only changed parts of the workspace.
284 | It also gives the developer a better development experience locally, with support for IDE refactoring via the development project.
285 | You can read more about interfaces and their benefits [here](https://cljdoc.org/d/polylith/clj-poly/CURRENT/doc/interface).
286 |
287 | `` article ``, `` comment ``, `` profile ``, `` tag ``, and `` user `` components define functionality to endpoints required for the RealWorld backend.
288 | The other components, `` database ``, `` env ``, `` spec `` and `` log ``, are created to encapsulate some other common code in the workspace.
289 | `` spec `` component contains some basic spec definitions that are used in different components.
290 |
291 | Similarly, the `` log `` component creates a wrapper around the logging library [timbre](https://github.com/ptaoussanis/timbre).
292 | This is included in the workspace to demonstrate how to create wrapper components around external libraries.
293 | This gives you an opportunity to declare your own interface for an external library and if you decide to use another external library,
294 | you can just switch to another component implementing the same interface without affecting other components.
295 |
296 | The `` database `` component is another type of common functionality component. It contains schema definitions for the sqlite database and functions to apply that schema. If you check Ring initializer function in `` api.clj `` namespace of `` rest-api `` base, you'll see this:
297 | ```clojure
298 | (defn init []
299 | (try
300 | (log/init)
301 | (let [db (database/db)]
302 | (if (database/valid-schema? db)
303 | (log/info "Database schema is valid.")
304 | (if (database/db-exists?)
305 | (log/warn "Please fix database schema and restart")
306 | (do
307 | (log/info "Generating database.")
308 | (database/generate-db db)
309 | (log/info "Database generated.")))))
310 | (log/info "Initialized server.")
311 | (catch Exception e
312 | (log/error e "Could not start server."))))
313 | ```
314 | Here, we use helper functions from the `` database `` component's `` interface.clj `` to check if an sqlite database exists in the current path and if it exists, to check the validity of the schema.
315 | The interface for the `` database `` component looks like this:
316 | ```clojure
317 | (ns clojure.realworld.database.interface
318 | (:require [clojure.realworld.database.core :as core]
319 | [clojure.realworld.database.schema :as schema]))
320 |
321 | (defn db
322 | ([path]
323 | (core/db path))
324 | ([]
325 | (core/db)))
326 |
327 | (defn db-exists? []
328 | (core/db-exists?))
329 |
330 | (defn generate-db [db]
331 | (schema/generate-db db))
332 |
333 | (defn drop-db [db]
334 | (schema/drop-db db))
335 |
336 | (defn valid-schema? [db]
337 | (schema/valid-schema? db))
338 | ```
339 |
340 | ### Environment Variables
341 | The following environment variables are used in the project.
342 | You can define these variables under the `env.edn` file for local development.
343 |
344 | + `` :allowed-origins ``
345 | + Comma separated string of origins. Used to whitelist origins for CORS.
346 | + `` :environment ``
347 | + Defines current environment. Currently used for logging. If set to LOCAL, logs printed to console.
348 | + `` :database ``
349 | + Defaults to database.db. If provided, it will be the name of the file that contains the SQLite database.
350 | + `` :secret ``
351 | + Secret for JWT token.
352 |
353 | ### Database
354 | The project uses an SQLite database to make it easy to run.
355 | It can easily be changed to another SQL database, by editing the database connection and changing to a real jdbc dependency.
356 | There is an existing database under the development project, ready to be used. If you want to start from scratch, you can delete `database.db and start the server again.
357 | It will generate a database with correct schema on start. The project also checks if the schema is valid or not, and prints out proper logs for each case.
358 |
359 | ### Workspace info
360 | Run the following command from the root directory to print out workspace information and changes since the last stable point in time:
361 | `` poly info ``
362 |
363 | This command will print an output like below. Here you can see that changed components are marked with a * symbol.
364 | Refer to the [Polylith tool documentation](https://cljdoc.org/d/polylith/clj-poly/CURRENT/doc/reference/commands#info) for more detailed information about this command and other commands that Polylith provides.
365 |
366 |
367 |
368 | ### Check workspace integrity
369 | In order to guarantee workspace integrity, which means all components refer to each other through their interfaces.
370 | The Polylith tool provides you with the `` poly check `` command that will check the entire workspace and print out errors and/or warnings, if any.
371 |
372 | ### Running tests
373 | Run the following command from the root directory:
374 | `` poly test ``
375 |
376 | This command will run all the tests for changed components and other components that are affected by the current changes.
377 | You can read more about the test command [here](https://cljdoc.org/d/polylith/clj-poly/CURRENT/doc/reference/commands#test) and [here](https://cljdoc.org/d/polylith/clj-poly/CURRENT/doc/testing).
378 |
379 | ### Stable points in time
380 | Once you check the integrity of your workspace and see that all tests are green, you can commit your changes to your git repository and add (or move if there is one already) a git tag that starts with ``stable-`` prefix.
381 | The Polylith tool with use this point in time to to calculate what changes has been made.
382 | You can easily add this logic to your continuous integration pipeline as a way to automate it.
383 | Read more about stable points [here](https://cljdoc.org/d/polylith/clj-poly/CURRENT/doc/tagging) where you can find
384 | an example of how to implement the stable logic with the CI in the section below.
385 |
386 | ### Continuous integration
387 | This repository has a [CircleCI](https://circleci.com) configuration to demonstrate how to use the Polylith tool to incrementally run tests and build artifacts.
388 | The CircleCI configuration file is located at `` .circleci/config.yml ``.
389 |
390 | The CircleCI workflow for this project consists of six steps to demonstrate different commands from the Polylith tool.
391 | You can achieve the same result with fewer steps once you have learned the commands. The current steps are:
392 |
393 | - check
394 | - This job runs the check command from Polylith as follows: ```clojure -M:poly check```. If there are any errors in the Polylith workspace, it returns with a non-zero exit code and the CircleCI workflow stops at this stage.
395 | If there are any warnings printed by Polylith, it will be visible in the job's output.
396 | - info
397 | - Prints useful information about the current state of the workspace. This job runs the following commands, one after another:
398 | - ```clojure -M:poly ws```
399 | - Prints the current workspace as data in [edn format](https://github.com/edn-format/edn).
400 | - ```clojure -M:poly info```
401 | - Prints workspace information.
402 | - ```clojure -M:poly deps```
403 | - Prints the dependency information
404 | - ```clojure -M:poly libs```
405 | - Prints all libraries that are used in the workspace.
406 | - After this job is done, all this information will be available in the jobs output for debugging purposes if needed. You can read more about available commands [here](https://cljdoc.org/d/polylith/clj-poly/CURRENT/doc/reference/commands).
407 | - test
408 | - This job runs all the tests for all the bricks and projects that are directly or indirectly changed since the last stable point in time.
409 | Polylith supports incremental testing out of the box by using stable point marks in the git history.
410 | It runs the following command: ```clojure -M:poly test :project```.
411 | If any of the tests fail, it will exit with a non-zero exit code and the CircleCI workflow stops at this stage.
412 | Information about the passed/failed tests will be printed in the job's output.
413 | - api-test
414 | - Runs end-to-end API tests using a [Postman](https://www.postman.com) collection defined under the `` api-tests `` directory.
415 | Before running the tests, start the backend service by executing the `` clojure -M:ring `` statement under `` projects/realworld-backend `` directory.
416 | - build-uberjar
417 | - This job creates an aot compiled uberjar for the realworld-backend project. Created artifact can be found in the artifacts section of this job's output.
418 | - mark-as-stable
419 | - This job only runs for the commits made to the master branch.
420 | It adds (or moves if there is already one) the `stable-master` tag to the repository.
421 | At this point in the workflow, it is proven that the Polylith workspace is valid and that all the tests have passed.
422 | It is safe to mark this commit as stable. It does that by running the following commands one after another:
423 | - ```git tag -f -a "stable-$CIRCLE_BRANCH" -m "[skip ci] Added Stable Polylith tag"```
424 | - Creates or moves the tag
425 | - ```git push origin $CIRCLE_BRANCH --tags --force```
426 | - Pushed tag back to the git repository
427 |
428 | ### How to create this workspace from scratch
429 | You can find necessary steps to create this workspace with Polylith plugin [here](how-to.md).
430 |
431 | ### Steps required to use IntelliJ IDEA / Cursive
432 | You can find necessary steps to make this project work in [Intellij IDEA](https://www.jetbrains.com/idea/) / [Cursive](https://cursive-ide.com) plugin [here](https://cursive-ide.com/userguide/polylith.html).
433 |
434 | ### Note about deps.edn vs Leiningen
435 |
436 | > This version uses [tools.deps](https://github.com/clojure/tools.deps). There is also an older version of this project that uses [Leiningen](https://leiningen.org/) on the [leiningen branch](https://github.com/furkan3ayraktar/clojure-polylith-realworld-example-app/tree/leiningen).
437 |
438 | ## License
439 |
440 | Distributed under the [The MIT License](https://opensource.org/licenses/MIT), the same as [RealWorld](https://github.com/gothinkster/realworld) project.
441 |
--------------------------------------------------------------------------------