# State Management Standards for Clojure
This document outlines the standards and best practices for managing state in Clojure applications. It's intended to guide developers in creating maintainable, performant, and robust systems. These guidelines reflect current Clojure best practices and are applicable to modern Clojure development.
## 1. Principles of State Management in Clojure
Clojure, as a functional programming language, emphasizes immutability. However, real-world applications require managing state. The key is to manage state in a controlled and predictable manner.
* **Immutability as the Default:** Favor immutable data structures whenever possible. This simplifies reasoning about code, avoids side effects, and enables concurrency.
* **Explicit State Management:** Make state transitions explicit and avoid implicit modification. This improves auditability and reduces the risk of unintended consequences.
* **Controlled Concurrency:** Clojure provides powerful concurrency primitives (atoms, refs, agents, vars). Choose the appropriate primitive based on the specific concurrency requirements.
* **Reactivity and Dataflow:** Consider using libraries like "integrant", Component, "re-frame", "Fulcro", or plain clojure.spec/watch for managing application lifecycle and data flow, especially in larger applications with complex dependencies and reactivity requirements.
## 2. Vars: Global Mutable State (Use with Caution!)
Vars are the simplest mechanism for managing state in Clojure, providing thread-local mutable storage. However, overuse can lead to global state issues.
### 2.1. Standards for Using Vars
* **Do This:** Use vars for configuration values that rarely change or for thread-local storage.
* **Don't Do This:** Use vars for application state that is frequently modified or shared between threads unless thread isolation is explicitly managed.
### 2.2. Why This Matters
Global mutable state (vars) can make code harder to reason about, especially in concurrent environments. Uncontrolled mutation can lead to race conditions and unpredictable behavior.
### 2.3. Code Examples
"""clojure
(ns my-app.config
(:defonce +config+ (atom {}))) ; Use defonce to initialize only once
(defn set-config! [key value]
(swap! +config+ assoc key value))
(defn get-config [key]
(@+config+ key))
;; Example Usage:
(set-config! :database-url "jdbc:...")
(println (get-config :database-url))
"""
### 2.4 Anti-patterns
* **Over-reliance on Vars:** Using vars for core application logic instead of local bindings and immutable data flow.
* **Uncontrolled Mutation:** Modifying vars without consideration for concurrency or data consistency.
## 3. Atoms: Mutable References
Atoms provide a mechanism for managing mutable state with atomic updates.
### 3.1. Standards for Using Atoms
* **Do This:** Use atoms for managing shared mutable state that requires atomic updates. Use "swap!" and "compare-and-set!" for modifying the atom's value safely.
* **Don't Do This:** Perform long-running or potentially blocking operations within "swap!" as this can degrade performance.
### 3.2. Why This Matters
Atoms ensure that updates to the state are atomic, preventing race conditions and data corruption in concurrent environments.
### 3.3. Code Examples
"""clojure
(def counter (atom 0))
(defn increment! []
(swap! counter inc))
(defn get-count []
@counter)
;; Example Usage:
(future (dotimes [_ 1000] (increment!)))
(future (dotimes [_ 1000] (increment!)))
(Thread/sleep 100) ; give futures some time to complete
(println (get-count)) ; should be close to 2000
"""
### 3.4. Anti-patterns
* **Direct Dereferencing without Atomic Updates:** Directly dereferencing the atom ("@atom-name") and then modifying and resetting it, leading to possible race conditions. Always use "swap!" or "compare-and-set!".
* **Using Atoms for Immutable Data:** Using atoms to hold immutable data structures needlessly.
* **Over-contention:** When multiple threads are constantly trying to update the same atom, it can lead to contention and performance issues.
## 4. Refs and Transactions
Refs provide transactional state management, ensuring that a series of operations are performed atomically.
### 4.1. Standards for Using Refs
* **Do This:** Use refs for managing shared state where consistency across multiple, related values is crucial. Update refs within "dosync" blocks to ensure transactional semantics.
* **Don't Do This:** Perform I/O operations or other side effects within "dosync" blocks as these can violate the atomicity and isolation guarantees.
### 4.2. Why This Matters
Refs guarantee ACID (Atomicity, Consistency, Isolation, Durability) properties for state updates, crucial for maintaining data integrity in complex systems.
### 4.3. Code Examples
"""clojure
(def account1 (ref 100))
(def account2 (ref 0))
(defn transfer! [amount]
(dosync
(if (>= @account1 amount)
(do
(alter account1 #(- % amount))
(alter account2 #(+ % amount)))
(throw (Exception. "Insufficient funds")))))
;; Example Usage:
(try
(transfer! 50)
(println "Transfer successful")
(catch Exception e
(println "Transfer failed:" (.getMessage e))))
(println "Account 1:" @account1) ; 50
(println "Account 2:" @account2) ; 50
"""
### 4.4. Anti-patterns
* **Performing I/O in Transactions:** Performing potentially failing I/O operations inside "dosync" blocks.
* **Long-running Transactions:** Holding transactions open for extended periods, leading to contention and reduced concurrency.
## 5. Agents: Asynchronous State Updates
Agents provide a mechanism for managing asynchronous state updates.
### 5.1. Standards for Using Agents
* **Do This:** Use agents for performing asynchronous, potentially long-running operations that update state.
* **Don't Do This:** Rely on agents for immediate, synchronous updates. Agents are designed for asynchronous processing.
### 5.2. Why This Matters
Agents allow decoupling state updates from the main thread, improving responsiveness and preventing blocking.
### 5.3. Code Examples
"""clojure
(def logger (agent []))
(defn log-message [message]
(send logger conj message)) ;; Use send to ensure proper agent queueing
(defn get-logs []
@logger)
;;Example Usage:
(log-message "Starting process...")
(log-message "Processing data...")
(log-message "Process completed.")
(await-for 100 logger) ; Wait for agent to process all messages
(println (get-logs))
"""
### 5.4. Anti-patterns
* **Synchronous Agent Use:** Using "send-off" when "send" is required, resulting in unintended asynchronous dispatch, or vice versa.
* **Ignoring Agent Errors:** Failing to handle exceptions thrown during agent actions; agents halt on exceptions by default. Use "set-error-handler!" on each agent, even if the handler just logs the error.
## 6. Component and Integrant: Managing Application Lifecycle and State Dependencies
Component and Integrant are libraries that provide a structured approach to managing application lifecycle and dependencies.
### 6.1. Standards for Using Component/Integrant
* **Do This:** Use Component or Integrant for applications with more than a few basic services/dependencies and especially when application shutdown is an important consideration. Define components as records implementing "Lifecycle" protocol (for Component) or use the component keys and init/halt functions as the core of Integrant.
* **Don't Do This:** Build complex and difficult-to-test systems manually managing startup/shutdown state.
### 6.2. Why This Matters
These libraries offer a declarative way to define application components, their dependencies, and their lifecycle (start/stop). This promotes modularity, testability, and maintainability. Integrant works with plain data and functions making it a great fit for Clojure.
### 6.3. Code Examples (Integrant)
"""clojure
(ns my-app.system
(:require [integrant.core :as ig]))
(defmethod ig/init-key :db/database [_ config]
(println "Initializing database with config:" config)
{:connection (atom {})}) ; Replace with actual DB initialization
(defmethod ig/halt-key :db/database [_ db]
(println "Closing database connection")
;; Close DB connection here
)
(defmethod ig/init-key :web/server [_ {:keys [db port]}]
(println "Starting web server on port" port)
{:server (atom {:db db :port port})}) ; Simulate a web server
(defmethod ig/halt-key :web/server [_ server]
(println "Stopping web server")
;; Stop web server here
)
(def system-config
{:db/database {:url "jdbc:..."}
:web/server {:db (ig/ref :db/database) :port 8080}})
;; Example usage:
(def system (ig/init system-config))
;; ... application runs ...
(ig/halt! system)
"""
### 6.4. Anti-patterns
* **Ignoring Component Lifecycle:** Failing to properly start and stop components, leading to resource leaks or unexpected behavior.
* **Tight Coupling:** Creating tight dependencies between components, reducing modularity and testability.
## 7. Managing Reactivity with "re-frame" and Fulcro
For UI-intensive applications, libraries like "re-frame" and Fulcro provide a structured way to manage application state and reactivity.
### 7.1. Standards for Using "re-frame" and Fulcro
* **Do This:** Adopt "re-frame"'s event-driven architecture or Fulcro's data-driven (normalized) approach for managing application state in complex UIs. Use subscriptions for deriving UI state from the central data store.
* **Don't Do This:** Mutate application state directly within UI components, bypassing the "re-frame" event handling or Fulcro mutations.
### 7.2. Why This Matters
These libraries provide a predictable and efficient mechanism for updating the UI in response to application state changes.
### 7.3. Code Example ("re-frame")
"""clojure
(ns my-app.re-frame
(:require [re-frame.core :as rf]))
;;Define an event handler
(rf/reg-event-fx
:increment
(fn [{:keys [db]} [_]]
{:db (update db :counter inc)}))
;; Define a subscription
(rf/reg-sub
:counter
(fn [db [_]]
(:counter db)))
;; Usage in a component (Om Next example)
(defn my-component []
(let [counter (rf/subscribe [:counter])]
(fn []
(dom/div
(dom/h1 (str "Counter: " @counter))
(dom/button {:on-click #(rf/dispatch [:increment])} "Increment")))))
"""
### 7.4. Anti-patterns
* **Direct State Mutation:** Modifying the "re-frame" or Fulcro application database directly instead of using events or mutations.
* **Complex Subscriptions:** Creating overly complex subscriptions that perform heavy computations, impacting UI performance.
## 8. Metadata and Watches
Clojure provides a mechanism for attaching metadata to data structures and setting up watches to observe changes to vars, atoms, refs, and agents.
### 8.1. Standards for metadata and watches
* **Do This:** Use metadata to attach auxiliary information to data structures without modifying their core functionality. Use watches to observe changes in state variables during development and debugging or react to state changes in production.
* **Don't Do This:** Overuse watches in production as they can negatively affect performance.
### 8.2. Why This Matters
Metadata allows associating additional information with data without altering its structure or semantics. Watches provides notifications when the value of a stateful value changes.
### 8.3. Code Examples
"""clojure
;; Using metadata
(def my-vector (with-meta [1 2 3] {:description "A simple vector"}))
(println (meta my-vector)) ;; Prints: {:description "A simple vector"}
;; Using watches
(def my-atom (atom 0))
(add-watch my-atom :my-watch
(fn [key atom old-state new-state]
(println "Atom" key "changed from" old-state "to" new-state)))
(swap! my-atom inc) ;; Output: Atom :my-watch changed from 0 to 1
(remove-watch my-atom :my-watch)
"""
### 8.4. Anti-patterns
* **Over-reliance on Watches for Core Logic:** Do not implement business logic inside of watch functions as they are for debugging.
* **Ignoring Watch Performance:** Using watches in performance-critical sections of code without considering their impact.
* **Abusing Metadata:** Attaching critical application data or logic to metadata, making it less discoverable and harder to maintain.
## 9. General Recommendations
* **Choose the Right Tool:** Select the appropriate state management mechanism based on the specific requirements of your application.
* **Keep State Local:** Minimize the scope of mutable state to reduce complexity and improve testability.
* **Embrace Immutability:** Favor immutable data structures whenever possible to simplify reasoning about code and prevent side effects.
* **Test Thoroughly:** Write comprehensive tests to ensure that state transitions are correct and that concurrent access is handled safely.
* **Monitor Performance:** Use performance monitoring tools to identify and address any bottlenecks related to state management.
* **Avoid Legacy Code Patterns:** Clojure evolves, avoid techniques that are out of date or have known drawbacks.
* **Always document the reason behind state selection**: Explain briefly why a specific approach for managing state has been selected in a particular context.
By following these standards, you can create Clojure applications that are maintainable, performant, and robust. Remember that this document should be adapted according to particular use cases and team conventions.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Testing Methodologies Standards for Clojure This document outlines the standards for testing methodologies in Clojure projects. These standards promote maintainability, reliability, and code quality. They are designed to be used in conjunction with AI coding assistants such as GitHub Copilot and Cursor to promote adherence to these guidelines. ## 1. General Principles ### 1.1. Prioritize Testing * **Do This:** Treat testing as a first-class citizen in the development process. Write tests concurrently with, or even before, writing the function or module implementation itself. * **Don't Do This:** Neglect writing tests or consider it an "afterthought." * **Why:** Starting with tests (TDD – Test-Driven Development) fosters better design, reduces debugging time, and improves confidence in the code's correctness. ### 1.2. Test Pyramid * **Do This:** Structure tests in a pyramid shape: many unit tests, fewer integration tests, and even fewer end-to-end tests. * **Don't Do This:** Rely heavily on slow and brittle end-to-end tests at the expense of focused unit tests. * **Why:** Unit tests are fast, cheap to maintain, and provide quick feedback. Integration tests verify interactions between components, while end-to-end tests validate the entire system but are slower and more complex. ### 1.3. Test Isolation * **Do This:** Ensure tests are independent. Each test should set up its own context and tear it down after execution. * **Don't Do This:** Share mutable state between tests or rely on test execution order. * **Why:** Isolated tests ensure that failures are reproducible and easier to diagnose. They also prevent tests from interfering with each other. ### 1.4. Use Meaningful Assertions * **Do This:** Write assertions that clearly express the expected behavior. Use descriptive messages to communicate the purpose and context of the assertion. * **Don't Do This:** Use opaque or generic assertions that provide little insight into the underlying problem when a test fails. * **Why:** Clear assertions make it easier to understand what the test is verifying and aid in debugging when a test fails. ## 2. Unit Testing ### 2.1. Focus * **Do This:** Unit tests should verify the behavior of a single, isolated unit of code (usually a function or a small set of functions). * **Don't Do This:** Write unit tests that test multiple units of code or depend on external systems. * **Why:** Isolating units of code allows for precise bug identification; issues can be narrowed down and addressed at the foundational module level. ### 2.2. Mocking and Stubbing * **Do This:** Use mocking or stubbing to isolate the unit under test from its dependencies. Libraries like "clojure.test.check.clojure-test" and "mockfn" can be helpful. * **Don't Do This:** Directly test dependencies that are not part of the unit being tested. * **Why:** Isolating dependencies prevents failures in those dependencies from masking failures in the unit under test. It also leads to faster and more reliable tests. #### 2.2.1 Example with "mockfn" """clojure (ns my-project.core-test (:require [clojure.test :refer :all] [my-project.core :as core] [mockfn.clojure-test :refer [with-mock]])) (deftest test-calculate-total (testing "Calculate total with mocked discount" (with-mock [core/apply-discount (fn [price discount] 5.0)] ; Mock apply-discount function (let [items [{:price 10} {:price 20}] total (core/calculate-total items 0.2)] (is (= 100 total) "Total should be correct after applying mocked discount")))) (testing "Calculate total without mocks" (let [items [{:price 10} {:price 20}] total (core/calculate-total items 0.2)] (is (= 24 total) "Total should be correct without mocks")))) """ ### 2.3. Property-Based Testing * **Do This:** Use property-based testing, with libraries like "clojure.test.check", to automatically generate test data and verify that your functions satisfy certain properties. * **Don't Do This:** Rely solely on example-based testing (i.e., providing fixed inputs and outputs). * **Why:** Property-based testing can uncover edge cases and corner cases that are easily missed with example-based testing. #### 2.3.1 Example with "clojure.test.check" """clojure (ns my-project.core-test (:require [clojure.test :refer :all] [clojure.test.check.clojure-test :refer [defspec]] [clojure.test.check.properties :as prop] [clojure.test.check.generators :as gen] [my-project.core :as core])) (defspec square-non-negative 1000 ; Number of iterations (prop/for-all [x gen/nat] ; Generate natural numbers (>= (core/square x) 0))) ; Property: Square is non-negative """ In this example, "defspec" defines a property-based test named "square-non-negative". It uses the "gen/nat" generator to create random non-negative integers and verifies that the "square" function always returns a non-negative result. This tests a general property of "square", ensuring all non-negative results are >= 0. ### 2.4. Test Naming * **Do This:** Use descriptive names for tests that clearly indicate the scenario being tested. * **Don't Do This:** Use vague or ambiguous test names. * **Why:** Descriptive test names make it easier to understand the purpose of the test and to quickly identify failing tests. ### 2.5. Edge Cases * **Do This:** Always consider edge cases, boundary conditions, and invalid inputs when writing unit tests. * **Don't Do This:** Only test happy paths. * **Why:** Testing edge cases increases the robustness and reliability of your code. ## 3. Integration Testing ### 3.1. Focus * **Do This:** Integration tests should verify the interactions between two or more units of code. * **Don't Do This:** Use integration tests to test individual units in isolation (use unit tests for that). * **Why:** Integration tests ensure that components work together correctly and that data flows smoothly between them. ### 3.2. Database Interactions * **Do This:** Use a dedicated testing database for integration tests involving databases. * **Don't Do This:** Run integration tests against a production database. * **Why:** Using a separate testing database prevents tests from corrupting production data and ensures that tests are repeatable. ### 3.3. External APIs * **Do This:** Use mock services or stubs for external APIs when integration testing. Libraries like "clojure.test.check.clojure-test" alongside "mockfn" can be used. * **Don't Do This:** Directly call external APIs during integration tests, especially for APIs that might have rate limits or costs. * **Why:** Mocking external APIs provides control over the responses and eliminates dependencies on external systems, providing faster and more reliable tests. #### 3.3.1 Mocking External APIs with Component This is an example of mocking external APIs during integrations tests: """clojure (ns my-project.integration-test (:require [clojure.test :refer :all] [com.stuartsierra.component :as component] [my-project.system :as system] [my-project.api :as api])) (defn mock-api-component [value] (reify api/API (fetch-data [_ _] value))) (defn create-test-system [mock-api-value] (component/system-map :config (system/create-config) :api (mock-api-component mock-api-value) ; Use the mock API :service (system/create-service))) (deftest test-integration-with-mocked-api (let [test-system (component/start (create-test-system {:example "data"})) ; Provide desired return value service (:service test-system)] (try (is (= {:example "data"} (api/fetch-data (:api test-system) :some-param)) "Data fetched should match mock") (finally (component/stop test-system))))) """ ### 3.4. State Management * **Do This:** Carefully manage state during integration tests. Ensure that state is reset before each test. * **Don't Do This:** Allow state to accumulate between tests. * **Why:** Inconsistent state can lead to unpredictable test results and make it difficult to isolate bugs. ## 4. End-to-End (E2E) Testing ### 4.1. Focus * **Do This:** End-to-end tests should verify the entire system from the user's perspective. * **Don't Do This:** Use E2E tests to verify individual components or interactions (use unit and integration tests for that). * **Why:** E2E tests ensure that the system as a whole functions correctly and delivers the expected user experience. ### 4.2. Test Environment * **Do This:** Use a dedicated testing environment for E2E tests that closely resembles the production environment. * **Don't Do This:** Run E2E tests against a development environment or a shared testing environment. * **Why:** A dedicated testing environment provides a more realistic and reliable testing experience. ### 4.3. Automation * **Do This:** Automate E2E tests to ensure they can be run frequently and consistently. Use libraries like "selenium" or "playwright," wrapped as necessary. * **Don't Do This:** Manually run E2E tests. * **Why:** Automated E2E tests reduce the risk of human error and provide faster feedback on system-level issues. ### 4.4. Data Setup * **Do This:** Carefully manage test data for E2E tests. Ensure that data is set up and cleaned up before and after each test. * **Don't Do This:** Rely on existing data in the testing environment or leave data behind after tests are run. * **Why:** Consistent data management improves the reliability and repeatability of E2E tests. ### 4.5. Performance * **Do This:** Be mindful of the performance impact of E2E tests. Optimize tests to minimize execution time. Libraries like "perforate" or "criterium" can be used. * **Don't Do This:** Write E2E tests that are slow or inefficient. * **Why:** Fast E2E tests provide quicker feedback and reduce the overall testing time. ## 5. Tools and Libraries ### 5.1. "clojure.test" * Use the built-in "clojure.test" library for basic testing functionality. """clojure (ns my-project.core-test (:require [clojure.test :refer :all] [my-project.core :as core])) (deftest test-add (testing "Adding two positive numbers" (is (= 3 (core/add 1 2)))) (testing "Adding a positive and a negative number" (is (= -1 (core/add 1 -2))))) """ ### 5.2. "clojure.test.check" * Use "clojure.test.check" for property-based testing. """clojure (ns my-project.core-test (:require [clojure.test :refer :all] [clojure.test.check.clojure-test :refer [defspec]] [clojure.test.check.properties :as prop] [clojure.test.check.generators :as gen] [my-project.core :as core])) (defspec add-commutative 1000 (prop/for-all [x gen/nat y gen/nat] (= (core/add x y) (core/add y x)))) """ ### 5.3. "mockfn" * Use "mockfn" for mocking function calls during unit tests. """clojure (ns my-project.core-test (:require [clojure.test :refer :all] [my-project.core :as core] [mockfn.clojure-test :refer [with-mock]])) (deftest test-calculate-total (testing "Calculate total with mocked discount" (with-mock [core/apply-discount (fn [price discount] 5.0)] (let [items [{:price 10} {:price 20}] total (core/calculate-total items 0.2)] (is (= 100 total)))))) """ ### 5.4. "component" Testing * When working with components, tests often need to start and stop systems, mocking dependencies for isolation. """clojure (ns my-project.component-test (:require [clojure.test :refer :all] [com.stuartsierra.component :as component] [my-project.system :as system])) (deftest test-system-startup (let [test-system (component/start (system/new-system))] (try (is (not (nil? (:database test-system))) "Database should be started") (is (not (nil? (:web-server test-system))) "Web server should be started") (finally (component/stop test-system))))) """ ### 5.5 "stateful-testing" * For situations requiring in memory mocking instead of calls to external dependencies """clojure (ns my-project.state-test (:require [clojure.test :refer :all] [my-project.state :as state])) (deftest test-add-to-state (testing "Adding an item to the state" (state/reset-state) ; Reset the state before the test (state/add-item "item1") (is (= #{"item1"} @state/current-state) "State should contain the added item")) (testing "Adding another item to the state" (state/add-item "item2") (is (= #{"item1" "item2"} @state/current-state) "State should contain both items"))) """ ## 6. Test-Driven Development (TDD) ### 6.1. Red-Green-Refactor * **Do This:** Follow the Red-Green-Refactor cycle of TDD: * **Red:** Write a failing test. * **Green:** Write the minimum amount of code to make the test pass. * **Refactor:** Improve the code while keeping the test passing. * **Don't Do This:** Write code before writing tests or skip the refactoring step. * **Why:** TDD leads to better-designed code, reduces the risk of over-engineering, and increases confidence in the code's correctness. ### 6.2. Test Coverage * **Do This:** Aim for high test coverage (e.g., 80% or higher), but don't treat it as the sole metric of code quality. Libraries exist to determine line by line test coverage. * **Don't Do This:** Focus solely on achieving a certain test coverage percentage without considering the quality of the tests. * **Why:** High test coverage reduces the risk of regressions and makes it easier to maintain the code. However, meaningful tests count more than lines of code. ## 7. Reporting and CI/CD ### 7.1. Test Reporting * **Do This:** Use a test runner that provides detailed test reports with information about failures, errors, and code coverage. Use a variety of reporting formats, to allow multiple automated tools to process the results. * **Don't Do This:** Rely on manual inspection of test output. * **Why:** Automated test reporting makes it easier to track test results and identify potential issues. ### 7.2. Continuous Integration (CI) * **Do This:** Integrate tests into a CI/CD pipeline so that they are run automatically whenever code is changed. * **Don't Do This:** Run tests manually as part of the deployment process. * **Why:** CI/CD pipelines provide early feedback on code quality and ensure that changes are thoroughly tested before being deployed. ## 8. Code Examples ### 8.1. Asynchronous Testing When your Clojure code deals with asynchronous operations (e.g., using "core.async" or futures), testing requires special care to handle timing issues. """clojure (ns my-project.async-test (:require [clojure.test :refer :all] [clojure.core.async :as async :refer [>!! <!! timeout]] [my-project.async-operations :as ops])) (deftest test-async-operation (testing "Async operation completes successfully" (let [result-chan (async/chan) _ (ops/perform-async-task result-chan)] (let [result (<!! (async/timeout 1000) result-chan)] ; Timeout after 1 second (is (= :success result) "Async operation should return :success"))))) """ ### 8.2. Testing Exception Handling Ensuring that your code handles exceptions gracefully is critical. """clojure (ns my-project.exception-test (:require [clojure.test :refer :all] [my-project.error-handling :as eh])) (deftest test-divide-by-zero (testing "Divide by zero throws exception" (is (thrown? ArithmeticException (eh/divide 10 0))))) (deftest test-recover-from-exception (testing "Recovering from an exception using try-catch" (is (= :recovered (eh/recoverable-operation))))) """ ## 9. Conclusion Adhering to these testing standards will lead to more maintainable, reliable, and robust Clojure applications. Use these guidelines in tandem with AI coding assistants to promote consistent and high-quality testing practices within your development team. Regularly review and update these standards to align with the evolving Clojure ecosystem and your project's specific needs.
# Component Design Standards for Clojure This document outlines the component design standards for Clojure, focusing on creating reusable, maintainable, and performant components. It is intended to guide developers and inform AI coding assistants to produce idiomatic and high-quality Clojure code. It is specifically tailored to component design patterns in Clojure, and should be viewed as a single rule in a larger set of standards. ## 1. General Principles ### 1.1. Definition of a Component In Clojure, a component is a self-contained unit of functionality that encapsulates data and behavior. Components should be designed to be composable, independent, and easily testable. They should have clear, well-defined interfaces and responsibilities. ### 1.2. Key Principles of Component Design * **Single Responsibility Principle (SRP):** Each component should have one, and only one, reason to change. * **Open/Closed Principle (OCP):** Components should be open for extension but closed for modification. * **Liskov Substitution Principle (LSP):** Subtypes must be substitutable for their base types without altering the correctness of the program. While less directly applicable in Clojure with its focus on data and protocols, this principle translates to ensuring that functions accepting one type of data can also reliably handle variations or extensions of that data. * **Interface Segregation Principle (ISP):** Clients should not be forced to depend on methods they do not use. In Clojure, this suggests favoring small, focused protocols over large, monolithic ones. * **Dependency Inversion Principle (DIP):** High-level modules should not depend on low-level modules. Both should depend on abstractions. ### 1.3. Why These Principles Matter These principles are crucial for: * **Maintainability:** Easier to understand, modify, and debug components. * **Reusability:** Components can be used in different parts of the application or in different applications altogether. * **Testability:** Independent components are easier to test in isolation. * **Scalability:** Well-designed components facilitate easier scaling and distribution of the application. ## 2. Component Composition Approaches ### 2.1. Function Composition Clojure's functional nature makes function composition a natural way to build components. **Do This:** * Use "comp" to combine functions into a new function. * Leverage transducers for efficient data transformations. **Don't Do This:** * Create overly complex, deeply nested function compositions that are hard to read. * Depend excessively on mutable state within composed functions. **Example:** """clojure (defn add-one [x] (+ x 1)) (defn square [x] (* x x)) ; Compose functions to square a number and then add one (def square-and-add-one (comp add-one square)) (println (square-and-add-one 5)) ; Output: 26 ; Using transducers for efficient composition (def increment-and-square (comp (map inc) (map #(* % %)))) (println (into [] increment-and-square [1 2 3])) ; Output: [4 9 16] """ **Explanation:** Function composition allows combining simple functions into more complex operations. Transducers enhance this by providing a way to compose transformations on collections without creating intermediate collections. ### 2.2. Data-Driven Composition Components can be designed to operate on data structures. **Do This:** * Use data structures (maps, vectors, sets) as the primary means of communication between components. * Define functions that operate on these data structures to perform specific tasks. * Use schema libraries like "clojure.spec.alpha" or "malli" to define the structure and validity of your data. **Don't Do This:** * Pass large, mutable stateful objects between components. * Rely on side effects to communicate between components. **Example:** """clojure (require '[clojure.spec.alpha :as s]) ; Define a spec for a user (s/def ::user (s/keys :req-un [::id ::name ::email])) (s/def ::id uuid?) (s/def ::name string?) (s/def ::email string?) ; A function to validate a user (defn validate-user [user] (s/valid? ::user user)) ; A function to format a user's name (defn format-user-name [user] (str "User: " (:name user))) (def user-data {:id (random-uuid) :name "Alice" :email "alice@example.com"}) (println (validate-user user-data)) ; Output: true (println (format-user-name user-data)) ; Output: User: Alice """ **Explanation:** This approach emphasizes data structures as the central point of interaction. Using schemas further solidifies the interfaces between components, making them more robust and easier to reason about. ### 2.3. Protocol-Based Composition Protocols define interfaces that components can implement. This allows for polymorphism and extensibility. **Do This:** * Define protocols that represent the capabilities of a component. * Implement these protocols for different data types or data structures. * Use protocols to abstract away implementation details. **Don't Do This:** * Create overly large protocols with many methods. * Violate the Liskov Substitution Principle by implementing protocols inconsistently. **Example:** """clojure (defprotocol Printable (to-string [this] "Converts the object to a string representation.")) (extend-protocol Printable java.lang.String (to-string [this] this) ; Strings are already printable clojure.lang.PersistentVector (to-string [this] (str "[" (clojure.string/join ", " (map to-string this)) "]")) java.lang.Integer (to-string [this] (str this))) (println (to-string "Hello")) ; Output: Hello (println (to-string [1 2 "World"])) ; Output: [1, 2, World] """ **Explanation:** Protocols enable polymorphism. Different data types can implement the same protocol, providing a consistent interface. "extend-protocol" provides a flexible way to add protocol implementations to existing types. ### 2.4. Component Libraries and Frameworks Several libraries and frameworks facilitate component-based development in Clojure. * **Component:** A library for managing component lifecycles (start, stop). * **Integrant:** A configuration-based system for building applications from components controlled by a data structure. * **Mount:** A simpler alternative to Component for managing application state. * **System:** A newer library aiming to combine the best aspects of Component and Integrant. **Do This:** * Choose a component library appropriate for your project's complexity. * Use the library's lifecycle management features to manage component dependencies and state. * Employ configuration-based systems like Integrant to externalize component configuration. **Don't Do This:** * Manually manage component lifecycles without a library. * Hardcode component dependencies within the component itself. **Example (using Integrant):** """clojure (ns my-app (:require [integrant.core :as ig])) ; Define a database component (defmethod ig/init-key ::db [_ {:keys [url]}] (println "Connecting to DB at" url) {:connection url}) ; In a real app, establish an actual DB connection (defmethod ig/halt-key ::db [_ db] (println "Disconnecting from DB at" (:connection db))) ; Close the connection ; Define an HTTP server component (defmethod ig/init-key ::server [_ {:keys [port db]}] (println "Starting server on port" port "using DB" db) {:port port :db db}) ; In a real app, start an HTTP server (defmethod ig/halt-key ::server [_ server] (println "Stopping server on port" (:port server))) ; Stop the server ; Define a configuration (def config {::db {:url "jdbc://localhost:5432/mydb"} ::server {:port 8080 :db (ig/ref ::db)}}) ; Start the system (def system (ig/init config)) ; Stop the system when done (ig/halt! system) """ **Explanation:** Integrant allows defining components (like "::db" and "::server") and their dependencies in a configuration map. "ig/init" starts the system, resolving dependencies using references (e.g., "(ig/ref ::db)"). "ig/halt!" shuts down the system in the reverse order. The "ig/init-key" and "ig/halt-key" multimethods define the initialization and termination logic for each component. This removes boilerplate from your component implementation and centralizes configuration. ## 3. Component Communication ### 3.1. Asynchronous Messaging For decoupled components, consider asynchronous messaging using libraries like "core.async" or message queues (e.g., RabbitMQ, Kafka). **Do This:** * Use channels ("core.async") or message queues for non-blocking communication. * Define clear message formats using data structures and schemas. **Don't Do This:** * Overuse asynchronous messaging when synchronous calls are sufficient. * Create complex message routing logic within components. **Example (using core.async):** """clojure (require '[clojure.core.async :as async]) ; Create a channel (def message-channel (async/chan)) ; Component 1 (producer) (defn send-message [message] (async/>!! message-channel message)) ; Blocking send ; Component 2 (consumer) (defn receive-message [] (async/<!! message-channel)) ; Blocking receive ; Example Usage (future (send-message "Hello from Component 1")) (println (receive-message)) ; Output: Hello from Component 1 """ **Explanation:** "core.async" provides channels for asynchronous communication. The "send-message" function sends a message to the channel, and "receive-message" retrieves it. The "future" macro runs the sender in a separate thread. Non-blocking versions of these operations are available (">!", "<!"). ### 3.2. Publish/Subscribe (Pub/Sub) For components that need to react to events, consider using a publish/subscribe pattern. Libraries like "clojure.tools.namespace.repl" use this pattern internally. **Do This:** * Use a dedicated pub/sub library (e.g., implementing a simple one with atoms and callbacks). * Define clear event types and data structures. **Don't Do This:** * Create tightly coupled event listeners. * Overuse global event buses. **Example (Simple Pub/Sub implementation):** """clojure (def event-bus (atom {})) (defn subscribe [event-type callback] (swap! event-bus update event-type (fn [callbacks] (conj (or callbacks []) callback)))) (defn publish [event-type event-data] (doseq [callback (get @event-bus event-type)] (callback event-data))) ; Example Usage (subscribe :user-created (fn [user] (println "User created:" user))) (subscribe :user-created (fn [user] (println "Sending welcome email to:" (:email user)))) (publish :user-created {:name "Bob" :email "bob@example.com"}) """ **Explanation:** This demonstrates a basic pub/sub system managed with an atom. "subscribe" registers a callback for a specific event type. "publish" triggers all callbacks associated with that event type, passing event data to each. ### 3.3. Services and APIs Expose component functionality as services or APIs using libraries like Ring/Compojure or shadow-cljs for frontend components. **Do This:** * Define clear API contracts using schemas or Swagger/OpenAPI. * Implement proper authentication and authorization. **Don't Do This:** * Expose internal component implementation details in the API. * Neglect security considerations. ## 4. Testing ### 4.1. Unit Testing Test individual components in isolation. **Do This:** * Use "clojure.test" or a testing framework like Midje. * Mock dependencies when necessary. * Write clear and concise test cases. **Don't Do This:** * Write tests that are tightly coupled to implementation details. * Neglect edge cases and error handling. **Example:** """clojure (ns my-app.core-test (:require [clojure.test :refer :all] [my-app.core :refer :all])) (deftest test-add-one (testing "Should add one to a number" (is (= (add-one 5) 6)) (is (= (add-one -1) 0)))) """ ### 4.2. Integration Testing Test how components interact with each other. **Do This:** * Test the entire system or a significant portion. * Use realistic test data. * Verify that components communicate correctly. **Don't Do This:** * Skip integration testing. * Assume that components work together correctly without testing. ### 4.3. Component Lifecycle Testing If using a component lifecycle library like "component" or "integrant", verify the start and stop behavior of components. **Do This:** * Write tests that start and stop components. * Verify that resources are acquired and released correctly. * Ensure that dependencies are started in the correct order. ## 5. Error Handling ### 5.1. Exceptions Use exceptions to signal exceptional conditions. **Do This:** * Throw exceptions when errors occur. * Catch exceptions at appropriate boundaries. * Provide informative error messages. **Don't Do This:** * Ignore exceptions. * Use exceptions for normal control flow. ### 5.2. Error Values Return error values (e.g., "nil", "false", or a tagged union) to indicate errors. **Do This:** * Use error values when exceptions are not appropriate. * Check for error values and handle them appropriately. **Don't Do This:** * Ignore error values. * Assume that all operations succeed. ### 5.3. Logging Log errors and other important events. **Do This:** * Use a logging library like "clojure.tools.logging". * Log at appropriate levels (e.g., "error", "warn", "info", "debug"). * Include relevant context in log messages. **Don't Do This:** * Log too much information. * Log sensitive information. ## 6. Modern Clojure Features ### 6.1. clojure.spec.alpha and Malli Use "clojure.spec.alpha" or "malli" extensively for data validation, generation, and documentation. **Do This:** * Define specs/schemas for all data structures used in your application. * Use "s/valid?" or "malli.core/validate" to validate data. * Use "s/gen" or "malli.core/generate" for property-based testing. **Don't Do This:** * Neglect data validation. * Assume that all data is valid. ### 6.2. Datafy/Nav Consider using "datafy" and "nav" (Clojure 1.10+) to provide a consistent way to inspect and navigate data structures. This can improve debugging and introspection. **Do This:** * Implement "datafy" and "nav" for custom data types. * Use the "datafy" function in debugging tools. ## 7. Anti-Patterns ### 7.1. God Object Avoid creating large, monolithic components that do too much. ### 7.2. Tight Coupling Minimize dependencies between components. ### 7.3. Premature Optimization Don't optimize components before they are needed. ### 7.4. Reinventing the Wheel Use existing libraries and frameworks when possible. ## 8. Performance Optimization ### 8.1. Immutability Leverage Clojure's immutable data structures for performance and concurrency benefits. ### 8.2. Laziness Use lazy sequences efficiently to avoid unnecessary computation. ### 8.3. Concurrency Use Clojure's concurrency features (e.g., atoms, refs, agents, core.async) to improve performance. Be extremely cautious and deliberate using these as they are easy to misuse. ### 8.4. Profiling Use profiling tools to identify performance bottlenecks. ## 9. Security Best Practices ### 9.1. Input Validation Validate all user input to prevent injection attacks. ### 9.2. Authentication and Authorization Implement proper authentication and authorization to protect sensitive data. ### 9.3. Dependency Management Keep dependencies up to date to patch security vulnerabilities. By adhering to these component design standards, Clojure developers can create applications that are maintainable, reusable, testable, and performant. This document provides a framework for building high-quality Clojure code.
# API Integration Standards for Clojure This document outlines the coding standards for API integration in Clojure projects. It aims to provide a comprehensive guide for developers to build robust, maintainable, and secure integrations with external services. These standards will help improve code quality, reduce errors, and facilitate collaboration. ## 1. Architectural Considerations for API Integration ### 1.1. Microservices and API Gateways * **Do This:** Embrace a microservices architecture where each service is responsible for a specific domain or functionality. Use an API gateway (e.g., using "ring-swagger" or "reitit") to handle routing, authentication, and rate limiting. The API Gateway acts as a single entry point for all client requests, decoupling backend services from the client applications. * **Don't Do This:** Avoid monolithic designs where all API integrations are crammed into a single service causing tight coupling, and making it difficult to scale or modify independently. * **Why:** Microservices allow independent scaling and deployment. API gateways centralize cross-cutting concerns like authentication and rate limiting enhancing security and usability. """clojure ;; Example: Basic API Gateway routing with Reitit (require '[reitit.ring :as ring]) (require '[reitit.coercion.spec :as rcs]) (require '[reitit.ring.coercion :as rrc]) (require '[reitit.ring.middleware.muuntaja :as rrmm]) (require '[muuntaja.core :as m]) (require '[reitit.ring.middleware.exception :as exception]) (require '[ring.adapter.jetty :as jetty]) (defn hello-handler [request] {:status 200 :body {:message "Hello, World!"}}) (def routes [["/hello" {:get {:handler hello-handler}}]]) (def app (ring/ring-handler (ring/router routes {:data {:coercion rcs/coercion :muuntaja m/instance :middleware [rrmm/format-middleware rrc/coercion-middleware exception/exception-middleware]}}) (ring/create-default-handler {:not-found (fn [_] {:status 404, :body "Not found"})}))) (defn start-server [] (jetty/run-jetty app {:port 3000 :join? false})) """ ### 1.2. Asynchronous Communication * **Do This:** Utilize asynchronous messaging queues (e.g., Kafka, RabbitMQ, Amazon SQS) for non-critical API interactions to decouple services and improve resilience. Employ "core.async" for managing concurrency within Clojure services. Handle failures and retries gracefully. * **Don't Do This:** Rely solely on synchronous HTTP requests, especially for long-running operations, this can lead to performance bottlenecks and decreased system responsiveness. * **Why:** Asynchronous communication promotes loose coupling, improves scalability, and enhances system resilience by allowing services to operate independently. """clojure ;; Example: Using Manifold for asynchronous HTTP requests (require '[manifold.stream :as s] '[manifold.deferred :as d] '[aleph.http :as http]) (defn fetch-data [url] (-> (http/get url {:accept "application/json"}) (d/chain :body) ; Extract the body from the response (d/catch (fn [e] (println "Error fetching data from" url ":" (.getMessage e)) nil)))) (defn process-data [deferred-data] (d/chain deferred-data (fn [data] (if data (do (println "Processing data:" (count data) "bytes") (try (let [parsed-data (cheshire.core/parse-string data true)] ; Properly parse as JSON (println "Parsed Data:" (keys parsed-data)) parsed-data) (catch Exception e (println "Error parsing JSON:" (.getMessage e)) nil))) (println "No data to process"))))) (defn main [] (let [url "https://jsonplaceholder.typicode.com/todos/1" ; Use a stable and reliable API endpoint data-deferred (fetch-data url)] (process-data data-deferred))) (comment (main)) """ ### 1.3. Rate Limiting and Throttling * **Do This:** Implement rate limiting and throttling mechanisms to protect your APIs and backend services from overuse or abuse. Use libraries like "clj-throttle" or integrate with cloud provider rate limiting services. Clearly define the throttling limits and communicate these to API clients. * **Don't Do This:** Neglect rate limiting, leaving your services vulnerable to denial-of-service attacks and resource exhaustion. * **Why:** Rate limiting ensures fair usage of your APIs and protects your infrastructure from overload or malicious attacks. """clojure ;; Example: Basic rate limiting using "clj-throttle" (require '[clj-throttle.core :as throttle]) (def api-calls (atom 0)) (def rate-limiter (throttle/throttle 5 60000)) ; 5 calls per minute (60000ms) (defn handle-api-request [] (if (throttle/allowed? rate-limiter) (do (swap! api-calls inc) (println "API Request processed. Call count:" @api-calls)) (do (println "Rate limit exceeded. Try again later.") {:status 429 :body "Too Many Requests"}))) (comment (dotimes [_ 10] (handle-api-request)) ) """ ## 2. Data Serialization and Deserialization ### 2.1. JSON Handling * **Do This:** Employ "cheshire" or "data.json" for JSON serialization/deserialization. Configure these libraries for optimal performance. Consider using ":decode-key-fn" with cheshire to automatically convert keys to keywords. Specify encoding to UTF-8 when producing or consuming JSON. * **Don't Do This:** Roll your own JSON parsing logic or rely on inefficient or outdated libraries. Fail to handle potential parsing exceptions. * **Why:** "cheshire" and "data.json" are efficient, well-maintained libraries optimized for Clojure data structures. Proper error handling is crucial for robustness. """clojure ;; Example: Using Cheshire for JSON processing (require '[cheshire.core :as json]) (def data {:name "John Doe" :age 30 :city "New York"}) ;; Serialize data to JSON (def json-string (json/encode data)) (println json-string) ; Output: {"name":"John Doe","age":30,"city":"New York"} ;; Deserialize JSON string to Clojure data structure (def parsed-data (json/decode json-string true)) ; "true" converts keys to keywords (println parsed-data) ; Output: {:name "John Doe", :age 30, :city "New York"} ;; Example with :decode-key-fn (def parsed-data-keyword (json/decode json-string keyword)) (println parsed-data-keyword) ; Output: {:name "John Doe", :age 30, :city "New York"} ;;Proper error handling (try (json/decode "invalid json" true) (catch Exception e (println "Error decoding JSON:" (.getMessage e)))) """ ### 2.2. Data Coercion and Validation * **Do This:** Use "spec" or "malli" for data validation and coercion before sending or after receiving data from external APIs. Define clear schemas for API requests and responses to enforce consistency and catch errors early. * **Don't Do This:** Trust external data without validation, which could lead to runtime errors, security vulnerabilities, or data corruption. * **Why:** Validation ensures that data conforms to expected formats and constraints, reducing the risk of unexpected errors and security vulnerabilities, and ensures data consistency across systems. """clojure ;; Example: Using "spec" for data validation (require '[clojure.spec.alpha :as s]) ;; Define a spec for a user (s/def ::name string?) (s/def ::age (s/and int? #(>= % 0))) (s/def ::user (s/keys :req-un [::name ::age])) (def valid-user {:name "Alice" :age 25}) (def invalid-user {:name "Bob" :age -5}) (println "Valid user:" (s/valid? ::user valid-user)) ; Output: Valid user: true (println "Invalid user:" (s/valid? ::user invalid-user)) ; Output: Invalid user: false (println (s/explain-data ::user invalid-user)) """ ### 2.3. Handling Different Data Formats * **Do This:** Use libraries like "muuntaja" to handle various content types (JSON, XML, edn, etc.) in a consistent way. Specify content negotiation strategies when interacting with APIs that support multiple formats. * **Don't Do This:** Assume all APIs use the same data format. Hardcode format-specific parsing logic without considering content negotiation. * **Why:** "muuntaja" simplifies content negotiation and serialization/deserialization across different data formats, promoting flexibility and interoperability. """clojure ;; Example using muuntaja (require '[muuntaja.core :as m]) ;; Create a Muuntaja instance with support for JSON and EDN (def muuntaja (m/create)) (def data {:message "Hello, Muuntaja!"}) ;; Encode data to JSON (def json-bytes (m/encode muuntaja "application/json" data)) (println (String. json-bytes "UTF-8")) ; Output: {"message":"Hello, Muuntaja!"} ;; Decode JSON bytes to Clojure data (def decoded-data (m/decode muuntaja "application/json" json-bytes)) (println decoded-data) ; Output: {:message "Hello, Muuntaja!"} """ ## 3. HTTP Client Configuration ### 3.1. Choosing an HTTP Client * **Do This:** Prefer "clj-http", "hato", or "aleph" for making HTTP requests. Use "clj-http" for simple use-cases. Use "hato" for newer projects, and "aleph" for asynchronous, high-performance applications. Choose a client based on the project's needs and performance requirements. * **Don't Do This:** Use Java's built-in "java.net.URL" directly, as it lacks advanced features and is less convenient for modern API interactions. Overlook dependency vulnerabilities in HTTP client libraries. * **Why:** Dedicated HTTP client libraries offer features like connection pooling, request timeouts, and robust error handling, enhancing performance and reliability. ### 3.2. Request and Response Handling * **Do This:** Set appropriate request timeouts to prevent indefinite blocking. Handle different HTTP status codes gracefully (e.g., 2xx, 4xx, 5xx). Implement retry logic for transient errors (e.g., network glitches). Log both request and response details for debugging. * **Don't Do This:** Ignore HTTP status codes or fail to handle potential network errors. Implement naive retry logic that could worsen the situation (e.g., retry immediately and endlessly). * **Why:** Proper error handling and retry mechanisms improve the resilience of your API integrations. Detailed logging aids in diagnosing issues. """clojure ;;Example using "hato" (require '[hato.client :as client]) (defn make-api-call [url] (try (let [{:keys [status body] :as response} (client/get url {:accept "application/json" :socket-timeout 5000 :connect-timeout 5000})] (println "API Response:" response) (case status 200 (cheshire.core/parse-string body true) 404 {:error "Resource not found"} (throw (ex-info "API Error" {:status status :body body})))) ; Improved exception handling (catch Exception e (println "Error making API call:" (.getMessage (ex-data e))) {:error (.getMessage (ex-data e))}) (finally (client/close {})))) (defn retry-api-call [url retries] (loop [attempts 0] (if (> attempts retries) {:error "Max retries exceeded"} (let [result (make-api-call url)] (if (:error result) (do (println "Retrying in 2 seconds...") (Thread/sleep 2000) (recur (inc attempts))) result))))) (comment (def data (retry-api-call "https://jsonplaceholder.typicode.com/todos/1" 3)) (println data)) """ ### 3.3. Connection Pooling * **Do This:** Ensure your HTTP client library utilizes connection pooling to reuse connections and reduce latency, especially for frequent API calls. Configure connection pool size appropriately based on anticipated load. For "hato", this is managed automatically. With "clj-http" make sure you're reusing the same client instance across calls. * **Don't Do This:** Create a new HTTP client instance for every API call, which wastes resources and increases latency. * **Why:** Connection pooling improves performance by reducing the overhead associated with establishing new connections for each API interaction. ## 4. Authentication and Authorization ### 4.1. Secure Storage of Credentials * **Do This:** Store API keys, tokens, and other sensitive credentials securely using environment variables, encrypted configuration files (e.g., using "environ"), or a dedicated secret management service (e.g., HashiCorp Vault, AWS Secrets Manager). *Never* commit secrets directly to version control. * **Don't Do This:** Hardcode credentials in your code or store them in plain text configuration files. Expose credentials in logs or error messages. * **Why:** Secure storage of credentials is crucial to prevent unauthorized access to your APIs and backend systems. """clojure ;; Example using "environ" to fetch API key from environment variable (require '[environ.core :refer [env]]) (def api-key (env :my-api-key)) (if api-key (println "API Key:" api-key) (println "API Key not found in environment variables")) """ ### 4.2. Authentication Methods * **Do This:** Use appropriate authentication methods like OAuth 2.0, JWT, or API key authentication. Implement proper token validation. Support TLS/SSL (HTTPS) for all API communication to encrypt data in transit. * **Don't Do This:** Rely on weak or outdated authentication methods (e.g., Basic Auth without SSL). Skimp on certificate validation, which could expose your application to man-in-the-middle attacks. Use "http" instead of "https" for sensitive data transmission. * **Why:** Strong authentication and encryption are essential to protect against unauthorized access and data breaches. ### 4.3. Authorization * **Do This:** Enforce proper authorization checks to ensure that users only have access to the resources and operations they are permitted to access. Implement role-based access control (RBAC) or attribute-based access control (ABAC) if necessary. * **Don't Do This:** Grant excessive permissions or skip authorization checks, which could lead to privilege escalation attacks. * **Why:** Authorization prevents unauthorized users from accessing sensitive data or performing restricted operations. ## 5. Error Handling and Monitoring ### 5.1. Centralized Error Handling * **Do This:** Implement a centralized error handling mechanism using "try"/"catch"/"finally" blocks or a dedicated error logging library (e.g., "taoensso.timbre"). Log detailed error messages including relevant context for debugging. * **Don't Do This:** Swallow exceptions silently or propagate raw exceptions to the client without proper sanitization. * **Why:** Centralized error handling simplifies debugging and maintenance by providing a consistent way to manage and log errors. """clojure ;; Example: Centralized error handling with try/catch (require '[taoensso.timbre :as log]) (defn safe-api-call [url] (try (make-api-call url) (catch Exception e (log/error "API call failed:" (.getMessage e) (ex-data e)) {:error "An unexpected error occurred"}))) """ ### 5.2. Logging and Monitoring * **Do This:** Log all significant API interactions including requests, responses, and errors. Use structured logging formats (e.g., JSON) for easy analysis. Integrate with monitoring tools (e.g., Datadog, Prometheus) to track API performance, error rates, and other key metrics. * **Don't Do This:** Log sensitive data (e.g., passwords, API keys). Neglect monitoring, which makes it difficult to detect and diagnose issues. * **Why:** Logging and monitoring provide valuable insights into API usage, performance, and error patterns, enabling proactive issue detection and performance optimization. ### 5.3. Circuit Breaker Pattern * **Do This:** Implement the circuit breaker pattern using libraries like "hystrix-clj" or "failsafe" to prevent cascading failures when an external service becomes unavailable. The circuit breaker should automatically open when the error rate exceeds a certain threshold and close when the service recovers. * **Don't Do This:** Allow repeated calls to failing services which can worsen the system overload, Do not hardcode values. Use configuration. * **Why:** The circuit breaker pattern improves system resilience by isolating failing services and preventing them from bringing down the entire application. ## 6. Documentation and Testing ### 6.1. API Documentation * **Do This:** Document all APIs using tools like "ring-swagger" or "reitit-swagger". Include clear descriptions of API endpoints, request parameters, response formats, and error codes. Store your API spec as part of your code for versioning. * **Don't Do This:** Neglect API documentation, which makes it difficult for developers to understand and use your APIs. Keep API documentation separate from the codebase. * **Why:** API documentation is essential for enabling developers to effectively use your APIs. ### 6.2. Testing * **Do This:** Write unit tests and integration tests to verify the correctness and reliability of your API integrations. Use mocking libraries (e.g., "clojure.test.check.clojure-test") to isolate your code during testing. * **Don't Do This:** Skip testing or rely solely on manual testing, which can lead to undetected bugs. Write flaky tests making code review difficult. * **Why:** Thorough testing ensures that your API integrations function correctly and are resistant to errors. By following these coding standards, Clojure developers can create robust, maintainable, and secure API integrations that meet the needs of modern applications. This guide should promote code quality, collaboration, and reduce project risk.
# Security Best Practices Standards for Clojure This document outlines security best practices for Clojure development, designed to help developers write secure, maintainable, and performant code. It is intended to be used as a reference guide and a tool to configure AI coding assistants. This will provide a shared understanding of security principles and promote consistent application across projects. We will focus on the latest versions of Clojure and its ecosystem, addressing common vulnerabilities and secure coding patterns specific to the language. ## 1. General Security Principles ### 1.1 Principle of Least Privilege **Standard:** Grant only the minimum necessary permissions to users and applications. * **Do This:** Define fine-grained access control policies. * **Don't Do This:** Grant broad or administrative privileges unnecessarily. **Why:** Reduces the potential damage from compromised accounts or applications. **Example:** """clojure ;; Define roles and permissions (def roles {:admin #{:read :write :delete} :editor #{:read :write} :viewer #{:read}}) ;; Check permissions (defn has-permission? [user role permission] (contains? (get roles role #{}) permission)) ;; Example usage (if (has-permission? user :editor :write) (update-data data) (throw (ex-info "Insufficient permissions" {:user user :permission :write}))) """ ### 1.2 Defense in Depth **Standard:** Implement multiple layers of security controls. * **Do This:** Combine firewalls, intrusion detection systems, strong authentication mechanisms, and data encryption. * **Don't Do This:** Rely on a single security measure. **Why:** Ensures that if one layer fails, others remain in place to protect the system. **Example:** Layering security in a web application: 1. **Web Application Firewall (WAF):** Protects against common web attacks like SQL injection and XSS. 2. **Authentication:** Verify user identity with strong passwords and multi-factor authentication. 3. **Authorization:** Control access to specific resources based on user roles and permissions. 4. **Data Encryption:** Encrypt sensitive data at rest and in transit. 5. **Regular Security Audits:** Identify and address potential vulnerabilities. ### 1.3 Secure Configuration **Standard:** Securely configure all systems and applications. * **Do This:** Harden servers, disable unnecessary services, and use secure protocols like HTTPS. * **Don't Do This:** Use default passwords or insecure configurations. **Why:** Prevents attackers from exploiting misconfigurations to gain unauthorized access. **Example:** Securing a web server: * Disable default accounts and change default passwords. * Enable HTTPS and enforce TLS 1.3 or higher. * Configure firewalls to restrict access to necessary ports. * Regularly update software and apply security patches. * Use a configuration management tool like Ansible or Chef to automate secure configurations. ### 1.4 Input Validation and Sanitization **Standard:** Validate and sanitize all user input. * **Do This:** Use whitelists to define allowed input, encode special characters, and escape output. * **Don't Do This:** Trust user input without validation or sanitization. **Why:** Prevents injection attacks, such as SQL injection and cross-site scripting (XSS). This is especially true because of the dynamic nature of Clojure and the ease of creating dynamically executed forms. **Example:** """clojure (require '[clojure.string :as str]) (defn sanitize-string [s] (if (string? s) (-> s (str/replace #"<" "<") (str/replace #">" ">") (str/replace #"&" "&") (str/replace #"\"" """) (str/replace #"'" "'")) "")) (defn validate-email [email] (if (re-matches #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" email) email nil)) (defn process-data [params] (let [email (validate-email (:email params)) name (sanitize-string (:name params))] (if (and email name) {:email email :name name} (throw (ex-info "Invalid input" {:params params})))) ) """ ### 1.5 Error Handling and Logging **Standard:** Implement proper error handling and logging mechanisms. * **Do This:** Log errors securely, handle exceptions gracefully, and avoid exposing sensitive information in error messages. * **Don't Do This:** Expose stack traces or internal details to users. **Why:** Provides insights into system behavior, helps identify and fix vulnerabilities, and prevents information leakage. **Example:** """clojure (require '[clojure.tools.logging :as log]) (defn process-data [data] (try ;; Process data here (log/info "Data processed successfully") :success (catch Exception e (log/error e "Error processing data") (throw (ex-info "Failed to process data" {:cause e})))) ) """ ## 2. Clojure-Specific Security Considerations ### 2.1 Code Injection **Standard:** Avoid using "eval" or similar functions that execute arbitrary code. * **Do This:** Use data structures and functions to manipulate code instead of generating and executing code. Use "read-string" with caution. * **Don't Do This:** Dynamically generate and execute code based on user input. **Why:** "eval" poses a significant security risk, as it allows attackers to execute arbitrary code on the server. Clojure macros provide a much safer and testable alternative in many cases. **Example:** Instead of using "eval", use data structures and functions: Bad: """clojure ;; DANGEROUS: Avoid this (defn process-expression [expression] (eval (read-string expression))) """ Good: """clojure ;; Safer approach using functions and data structures (defn add [x y] (+ x y)) (defn subtract [x y] (- x y)) (def operations {:add add :subtract subtract}) (defn safe-process-expression [operation x y] (if-let [op-fn (get operations operation)] (op-fn x y) (throw (ex-info "Invalid operation" {:operation operation})))) (safe-process-expression :add 5 3) ; Returns 8 """ ### 2.2 Denial of Service (DoS) **Standard:** Implement measures to prevent DoS attacks. * **Do This:** Limit request rates, use connection pooling, and set timeouts. * **Don't Do This:** Allow unlimited requests or resource consumption. **Why:** Protects the system from being overwhelmed by malicious traffic. **Example:** Using rate limiting with "ring-rate-limit": """clojure (require '[ring.middleware.rate-limit :refer [wrap-rate-limit]]) (def app (-> your-handler (wrap-rate-limit {:limit 1000 :window 60}))) ; 1000 requests per minute """ ### 2.3 Dependency Management **Standard:** Manage dependencies securely and keep them up-to-date. * **Do This:** Use a dependency management tool like "Leiningen" or "deps.edn". Regularly update dependencies to patch security vulnerabilities. * **Don't Do This:** Use outdated or untrusted dependencies. **Why:** Prevents vulnerabilities in third-party libraries from being exploited. **Example:** Managing dependencies with "deps.edn": """clojure {:deps {org.clojure/clojure {:mvn/version "1.11.1"} ring/ring-core {:mvn/version "1.9.6"} ;;CHECK DEPENDENCY VULNERABILITIES REGULARLY AND UPDATE }} """ Use tools like "lein ancient" or "clojure -A:deps tree" to check for outdated dependencies. Also, use tools like "owasp-dependency-check" to review libraries for known vulnerabilities. ### 2.4 Database Security **Standard:** Protect against SQL injection and other database vulnerabilities. * **Do This:** Use parameterized queries or ORM libraries to escape user input. Implement proper access control policies and encrypt sensitive data. * **Don't Do This:** Construct SQL queries directly from user input. **Why:** Prevents unauthorized access to and manipulation of data. **Example:** Using "clojure.java.jdbc" with parameterized queries: """clojure (require '[clojure.java.jdbc :as jdbc]) (def db-spec {:classname "org.postgresql.Driver" :subprotocol "postgresql" :subname "//localhost:5432/mydb" :user "myuser" :password "mypassword"}) (defn get-user [user-id] (jdbc/query db-spec ["SELECT * FROM users WHERE id = ?" user-id])) ;;Example: (get-user 123) """ ### 2.5 Session Management **Standard:** Implement secure session management practices. * **Do This:** Use strong session IDs, set appropriate session timeouts, and protect session data from tampering. Enforce HTTPS for session cookies. * **Don't Do This:** Use predictable session IDs or store sensitive data in session cookies. **Why:** Prevents session hijacking and unauthorized access to user accounts. **Example:** Using "ring.middleware.session" with secure options: """clojure (require '[ring.middleware.session :refer [wrap-session]]) (require '[ring.middleware.session.cookie :refer [cookie-store]]) (def app (-> your-handler (wrap-session {:store (cookie-store {:key "your-secret-key" :cookie-attrs {:http-only true, :secure true}})}))) """ ### 2.6 Cross-Site Scripting (XSS) **Standard:** Protect against XSS attacks. * **Do This:** Sanitize user input, encode output, and use Content Security Policy (CSP) headers. * **Don't Do This:** Trust user input without proper encoding. **Why:** Prevents attackers from injecting malicious scripts into web pages. **Example:** Using "hiccup" with auto-escaping and setting CSP headers: """clojure (require '[hiccup.core :as h]) (require '[ring.util.response :as response]) (defn display-data [data] (response/header (response/response (h/html [:div (h/escape-html data)])) "Content-Security-Policy" "default-src 'self'")) """ Utilize libraries like "clj-csp" to manage CSP headers more effectively. ### 2.7 Authentication and Authorization **Standard:** Implement strong authentication and authorization mechanisms. * **Do This:** Use strong passwords, multi-factor authentication, and role-based access control (RBAC). * **Don't Do This:** Store passwords in plain text or rely on weak authentication methods. **Why:** Ensures that only authorized users can access sensitive resources. **Example:** Using "buddy-auth" for authentication and authorization: """clojure (require '[buddy.auth.backends :as backends]) (require '[buddy.auth.middleware :as middleware]) (require '[buddy.auth.accessrules :as accessrules]) ;; Authentication backend (def backend (backends/jws {:secret "your-secret"})) ;; Access rules (def rules (accessrules/restrictive [[:admin] (accessrules/roles #{:admin})])) (def app (-> your-handler (middleware/wrap-authentication backend) (middleware/wrap-authorization rules))) """ ### 2.8 Secrets Management **Standard:** Securely manage sensitive information like API keys, passwords, and database credentials. * **Do This:** Store secrets in a secure vault (e.g., HashiCorp Vault, AWS Secrets Manager) and access them programmatically. Avoid storing secrets in code or configuration files. * **Don't Do This:** Hardcode secrets or store them in plaintext. **Why:** Prevents unauthorized access to sensitive information. **Example:** Using environment variables and a library to load secrets, rather than embedding them directly in the code. """clojure (defn get-secret [key] (System/getenv key)) (def db-password (get-secret "DB_PASSWORD")) (def db-spec {:classname "org.postgresql.Driver" :subprotocol "postgresql" :subname "//localhost:5432/mydb" :user "myuser" :password db-password}) """ For production environments, consider using libraries that integrate directly with secret management tools like Vault. ### 2.9 Data Encryption **Standard:** Encrypt sensitive data at rest and in transit. * **Do This:** Use strong encryption algorithms and protocols (e.g., AES-256, TLS 1.3). * **Don't Do This:** Use weak or outdated encryption methods. **Why:** Protects data from unauthorized access and disclosure. **Example:** Encrypting data using "friend" library: """clojure (require '[friend.crypto :as crypto]) (defn encrypt-data [data secret-key] (crypto/encrypt data secret-key)) (defn decrypt-data [encrypted-data secret-key] (crypto/decrypt encrypted-data secret-key)) (def secret-key "your-secret-key") (def data "sensitive data") (def encrypted-data (encrypt-data data secret-key)) (def decrypted-data (decrypt-data encrypted-data secret-key)) """ Ensure you handle the key securely, following secrets management best practices. ## 3. Security Tooling and Libraries ### 3.1 Static Analysis **Tools:** * **Klocwork:** Commercial static analysis tool that can identify security vulnerabilities and coding defects. * **Semgrep:** Open-source static analysis tool that supports custom rules for identifying security issues. * **clj-kondo:** Linter for Clojure code. While not explicitly a security tool, it can help in enforcing coding standards that reduce the risk of vulnerabilities. ### 3.2 Dependency Scanning **Tools:** * **OWASP Dependency-Check:** Detects known vulnerabilities in project dependencies. * **Snyk:** Commercial tool for identifying and remediating vulnerabilities in open-source dependencies. * "lein ancient": A Leiningen plugin to identify outdated dependencies. ### 3.3 Security Auditing * **Regular Security Audits:** Conduct regular security assessments and penetration testing to identify and address potential vulnerabilities. * **Code Reviews:** Ensure that code changes are reviewed by multiple developers to identify security issues. ## 4. Cloud-Specific Security If deploying to cloud environments, these security considerations are relevant: ### 4.1 Identity and Access Management (IAM) **Standard:** Utilize cloud provider's IAM services to manage access to resources. * **Do This:** Assign roles to users and services based on the principle of least privilege. * **Don't Do This:** Use root or administrative accounts for day-to-day operations. ### 4.2 Network Security **Standard:** Implement network security controls to isolate resources and restrict traffic. * **Do This:** Use Virtual Private Clouds (VPCs), security groups, and network ACLs to control network access. * **Don't Do This:** Expose resources directly to the internet without proper security controls. ### 4.3 Data Encryption **Standard:** Encrypt data at rest and in transit using cloud provider's encryption services. * **Do This:** Use KMS (Key Management Service) to manage encryption keys securely. * **Don't Do This:** Store encryption keys in code or configuration files. This document provides a foundation for building secure Clojure applications. By adhering to these standards and continuously improving security practices, development teams can minimize the risk of vulnerabilities and protect sensitive data. Continuous learning and adaptation to new threats and security technologies are crucial for maintaining a strong security posture.
# Tooling and Ecosystem Standards for Clojure This document outlines coding standards specifically related to tooling and ecosystem practices for Clojure development. Following these guidelines will promote consistency, maintainability, and efficiency within Clojure projects. The aim is to equip developers and AI coding assistants with context for generating optimal Clojure code adhering to the latest best practices. ## 1. Project Setup and Dependencies ### 1.1. Project Structure and "deps.edn" **Standard:** Use "deps.edn" for dependency management and project configuration. Adopt a consistent project structure. **Do This:** * Structure your project following established conventions (e.g., "src/", "test/", "resources/"). * Use "deps.edn" to declare dependencies and aliases. * Keep "deps.edn" well-formatted and organized. * Define namespaces consistent with the project structure. For instance, a file under "src/my_app/core.clj" should have the namespace "my-app.core". **Don't Do This:** * Use "project.clj" (Leiningen) for new projects. "deps.edn" is the standard and recommended approach. * Mix dependency management approaches (e.g., using "deps.edn" for some dependencies and manual additions for others). * Create deeply nested or unstructured project layouts. **Why:** "deps.edn" is Clojure's built-in dependency management tool; it is simpler, configuration-driven, and avoids the heavy plugin ecosystem associated with Leiningen. A consistent structure enables maintainability & tooling support. **Example "deps.edn":** """clojure {:paths ["src"] :deps {org.clojure/clojure {:mvn/version "1.11.2"} org.clojure/core.async {:mvn/version "1.6.681"} ;; Add other dependencies here } :aliases {:test {:extra-paths ["test"] :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}} :main-opts ["-m" "clojure.test runner"]} :repl {:extra-deps {nrepl/nrepl {:mvn/version "1.0.0"} cider/cider-nrepl {:mvn/version "0.36.0"} ;; other REPL tools such as refactor-nrepl refactor-nrepl/refactor-nrepl {:mvn/version "3.4.0"}} :main-opts ["-m" "nrepl.cmdline" "--middleware" "[cider.nrepl/cider-middleware]"]}}} """ ### 1.2. Dependency Versioning **Standard:** Explicitly declare dependency versions. **Do This:** * Always specify concrete versions for all dependencies in "deps.edn". * Regularly review and update dependency versions. **Don't Do This:** * Omit dependency versions or rely on "latest.release" (or similar dynamic versioning). **Why:** Specifying versions ensures reproducible builds and reduces the risk of unexpected behavior changes due to transitive dependency updates. **Example:** """clojure {:deps {org.clojure/data.json {:mvn/version "2.4.0"}}} """ ### 1.3. Developing against SNAPSHOT dependencies **Standard:** Use SNAPSHOT dependencies when working on libraries that depend on each other. **Why:** Speeds up the workflow developing multiple Clojure libraries simultaneously, allowing for incremental changes without deploying each library to a Maven repository on every change. **Example:** Assume you are working on "library-a" and "library-b". "library-b" depends on "library-a". In "library-b/deps.edn": """clojure {:deps {com.example/library-a {:mvn/version "1.0.0-SNAPSHOT"}}} """ Before working on "library-b", install "library-a" locally with "clojure -T:build install". Make iterative changes to "library-a" and reinstall again. The SNAPSHOT dependency will use the latest locally installed version. ## 2. REPL-Driven Development ### 2.1. REPL Workflow **Standard:** Embrace REPL-driven development as the primary workflow. **Do This:** * Use a REPL (CIDER, REPL-y, etc.) for interactive development. * Load code incrementally into the REPL and test changes immediately. * Use "cider-jack-in" or similar commands to start a REPL connected to your editor. * Use tools like "clojure.tools.namespace" to reload code. * Use "clojure.repl/doc", "clojure.repl/source", and "clojure.repl/dir" in the REPL to explore code **Don't Do This:** * Rely solely on static analysis or compilation cycles without leveraging the REPL. * Restart the entire application after making small code changes. **Why:** REPL-driven development dramatically increases development speed and facilitates experimentation. It empowers immediate feedback & iterative design refinement. **Example:** """clojure ;; In the REPL (require '[clojure.tools.namespace.repl :as repl]) (repl/refresh) ; Reload changed namespaces (my-app.core/my-function 1 2 3) ; Execute the function """ ### 2.2. Hot Code Reloading **Standard:** Use hot code reloading, using the REPL, to update running applications with new code changes without restarting the entire system. **Do This:** * Require "clojure.tools.namespace.repl" in your development environment or use a library that provides reloading such as "integrant". * When changing source code, call "(repl/refresh)" to reload changed namespaces and ensure the changes are applied to your REPL environment. **Don't Do This:** * Manually "require" or "refer" namespaces after making changes, as this can lead to inconsistencies. * Avoid reloading when making changes because you are uncertain about the implications. Understanding how namespace reloading works is crucial for productive Clojure development. **Why:** Hot code reloading vastly improves developer turnaround, allowing for quick iterative design and problem-solving. **Example:** """clojure ;; In the REPL, after modifying src/my_app/core.clj: (require 'clojure.tools.namespace.repl :reload) (clojure.tools.namespace.repl/refresh) ;; Now test the modified function (my_app.core/my-modified-function) """ ### 2.3 REPL history and persistence **Standard:** Store REPL history and persist the REPL session between restarts. **Do This:** * Configure your REPL environment (CIDER, etc.) to save history to a file. * Use a REPL persistence library such as "reply" which saves the state of the REPL to a database. * Use "portal" as a REPL enhancement for inspecting data structures. **Don't Do This:** * Start from scratch every time you open a REPL connection. **Why:** Persisting REPL history enables you to recall previous commands and experiments, saving time and effort. Keeping session persistence means you don't need to redefine functions on every restart. ## 3. Linting and Static Analysis ### 3.1. Using "clj-kondo" **Standard:** Use "clj-kondo" for linting and static analysis. **Do This:** * Integrate "clj-kondo" into your development workflow (editor integration or command-line tool). * Configure "clj-kondo" to enforce project-specific coding standards. * Address "clj-kondo" warnings and errors promptly. * Leverage ".clj-kondo/config.edn" for custom linting rules. * Use the ":lint-as" feature for advanced type hinting or behavior modification. **Don't Do This:** * Ignore "clj-kondo" warnings or treat them as inconsequential. * Disable relevant linting rules without a valid justification. * Fail to share a common ".clj-kondo/config.edn" across the development team. **Why:** "clj-kondo" helps identify code smells, potential errors, and style inconsistencies early in the development process. This promotes code quality and reduces debugging efforts. Static analysis catches a multitude of issues before runtime. **Example ".clj-kondo/config.edn":** """clojure {:linters {:unresolved-symbol {:level :warning} ; treat unresolved symbols as warnings :unresolved-namespace {:level :error} ; treat unresolved namespaces as errors :unused-binding {:level :off} ; disable unused binding warnings :deprecated-vars {:level :warning}} :config-paths ["/path/to/shared/config"] ; Shared configuration :lint-as {my-project.macros/defservice clojure.core/defn} ; Treat defservice like defn for linting purposes } """ ### 3.2. Custom Linting **Standard:** Develop custom linting configurations for project-specific checks using clj-kondo. **Do This:** * Create a ".clj-kondo/config.edn" file at the root of your project. * Define custom ":hooks" within the configuration to analyze code structure and semantics specific to your libraries or domain logic. * Use the "clj-kondo --lint" command to run the linter and identify violations. **Don't Do This:** * Rely solely on default linting rules without tailoring them to your project's specific needs. * Introduce overly complex or performance-intensive hooks that significantly slow down the linting process. * Neglect to document the purpose and behavior of your custom linting rules. **Why:** Custom linting allows teams to enforce project-specific coding standards, catch domain-specific errors earlier, and maintain a consistent codebase. **Example:** """clojure ;; .clj-kondo/config.edn {:hooks [[:find-require-refer {:lint-ns 'my-project.core :require 'my-project.deprecated-lib :message "Referring to deprecated library" :level :error}]]} """ This example creates a custom hook, "find-require-refer", that checks if the namespace "my-project.core" refers to "my-project.deprecated-lib". If it does, "clj-kondo" will report an error. ## 4. Testing ### 4.1. Test Frameworks **Standard:** Use "clojure.test" as the primary testing framework, supplemented by data-driven testing libraries like "clojure.test.check". **Do This:** * Write unit tests using "clojure.test"'s "deftest" and "is" macros. * Organize tests into namespaces mirroring the source code structure. * Use "clojure.test.check" for property-based (generative) testing. * Run tests frequently and automatically (e.g., with "kaocha"). **Don't Do This:** * Avoid writing tests or rely solely on manual testing. * Write overly complex or brittle tests. * Fail to cover edge cases or boundary conditions in your tests. **Why:** Automated testing is essential for code quality, reliability, and maintainability. "clojure.test" is the built-in standard, and "clojure.test.check" adds a powerful dimension of generative testing. **Example "clojure.test":** """clojure (ns my-app.core-test (:require [clojure.test :refer [deftest is testing]] [my-app.core :as core])) (deftest addition-test (testing "Should add two numbers correctly" (is (= 3 (core/add 1 2))))) """ **Example "clojure.test.check":** """clojure (ns my-app.core-test (:require [clojure.test :refer [deftest]] [clojure.test.check.clojure-test :refer [defspec]] [clojure.test.check.properties :as prop] [clojure.test.check.generators :as gen] [my-app.core :as core])) (defspec add-associative 100 (prop/for-all [a gen/int b gen/int c gen/int] (= (core/add a (core/add b c)) (core/add (core/add a b) c)))) """ ### 4.2. Test Runners **Standard:** Utilize a dedicated test runner like Kaocha to manage and execute tests efficiently. **Do This:** * Configure Kaocha with a "tests.edn" file in your project root. * Specify test namespaces, source paths, and other test-related configurations. * Use the "kaocha" command to run tests from the command line or integrate it into your build process. * Customize Kaocha's behavior with plugins, such as code coverage or JUnit XML reporting. **Don't Do This:** * Run tests directly from the REPL without using a test runner for larger projects. * Neglect to configure Kaocha for your specific project needs, such as excluding certain tests or setting up custom test environments. **Why:** Kaocha provides a convenient way to discover, execute, and manage tests in Clojure projects, offering features like parallel test execution, code coverage reporting, and JUnit XML output. **Example "tests.edn":** """clojure {:source-paths ["src"] :test-paths ["test"] :dependencies [[org.clojure/clojure "1.11.2"] [org.clojure/test.check "1.1.1"]] :namespace-prefixes ["my-app"] :plugins [[kaocha.plugin/spec-discover]]} """ ### 4.3 Mocking and Stubbing **Standard:** Use appropriate mocking libraries when unit testing functions with external dependencies; use sparingly, favoring integration tests over excessive mocking. **Do This:** * If mocking is required (i.e. for external DB connection), use a library like "clojure.test.check.clojure-test/with-redefs" to temporarily redefine functions during testing. * Focus on testing the core logic of your functions, and avoid mocking unnecessarily. * Consider using integration tests to verify the interaction between different components of your system. **Don't Do This:** * Over-mock your code, which leads to brittle tests that don't reflect real-world usage. * Mock functions that are part of your own application's core logic. Why: Mocking allows you to isolate units of code for testing without relying on external dependencies, but over-mocking can make tests less reliable and more difficult to maintain. It is key to strike a balance between isolating units of code and verifying interactions between external dependencies. **Example:** """clojure (ns my-app.payment-processor-test (:require [clojure.test :refer [deftest is with-redefs]] [my-app.payment-processor :as processor])) (deftest process-payment-test (with-redefs [processor/charge-credit-card (fn [amount card-number] {:status :success :transaction-id "fake-transaction-id"})] (let [result (processor/process-payment 100 "1234-5678-9012-3456")] (is (= :success (:status result))) (is (= "fake-transaction-id" (:transaction-id result)))))) """ ## 5. Logging ### 5.1. Logging Libraries **Standard:** Use a dedicated logging library, preferably "taoensso.timbre". **Do This:** * Configure "taoensso.timbre" with appropriate log levels and output destinations. * Use macros like "(timbre/info "Message")" for logging. * Include relevant context information in log messages (e.g., user ID, request ID). **Don't Do This:** * Use "println" or "prn" for logging in production code. * Log sensitive information (e.g., passwords, credit card numbers). * Rely on default logging configurations without customization. **Why:** Dedicated logging libraries offer features like log levels, formatting, and configurable output, which are essential for debugging and monitoring applications. **Example:** """clojure (ns my-app.core (:require [taoensso.timbre :as log])) (log/set-config! {:level :info}) ; Set log level to info (defn my-function [x] (log/debug "my-function called with x:" x) (log/info "Processing value:" x) (if (> x 10) (log/warn "Value x is greater than 10") (log/trace "Value x is less than or equal to 10")) (+ x 1)) """ ### 5.2 Structured Logging **Standard:** Favor structured logging over unstructured logging, using logging features tailored for structured data. **Do This:** * Use logging libraries that support structured logging (e.g., "taoensso.timbre" with its "with-context" and metadata capabilities). * Log data as key-value pairs rather than just raw text strings. * Use consistent keys and value types for structured log data. **Don't Do This:** * Rely solely on string concatenation to create log messages, as it makes parsing and analysis difficult. Why: Structured logging makes it easier to analyze logs programmatically, filter based on specific data points, and derive insights. **Example:** """clojure (ns my-app.payment-processor (:require [taoensso.timbre :as log])) (defn process-payment [user-id amount card-number] (log/info "Processing payment" :user-id user-id :amount amount) ;; ... payment processing logic ... (log/info "Payment processed successfully" :user-id user-id :amount amount :transaction-id "12345")) """ ## 6. Build Automation and Deployment ### 6.1. Build Tools **Standard:** Use "clojure.tools.build", Clojure's official build tool. **Do This:** * Define a "build.clj" file in your project root. * Define tasks for compiling, testing, and packaging your application. * Automate builds using command-line tools or CI/CD pipelines. **Don't Do This:** * Use "Leiningen" or other build tools for new projects. * Manually build and deploy applications without automation. **Why:** Build automation ensures reproducible builds, simplifies deployment, and promotes consistency across environments. **Example "build.clj":** """clojure (ns build (:require [clojure.tools.build.api :as b])) (def lib 'my-app/core) (def version (format "1.0.%s" (b/git-count-revs nil))) (def class-dir "target/classes") (def basis (b/create-basis {:project "deps.edn"})) (def jar-file (format "target/%s-%s.jar" (name lib) version)) (defn clean [_] (b/delete {:path "target"})) (defn compile [_] (b/compile-clj {:basis basis :src-dirs ["src"] :class-dir class-dir})) (defn jar [_] (b/create-jar {:basis basis :class-dir class-dir :jar-file jar-file})) (defn deploy [_] ;; Implement deployment logic here (println "Deploying" jar-file)) """ ### 6.2. Containerization **Standard:** Use Docker for containerizing Clojure applications. **Do This:** * Create a "Dockerfile" that defines the application environment. * Use a minimal base image (e.g., Alpine Linux with OpenJDK). * Copy the compiled JAR file into the container. * Define the entry point for running the application. * Use multi-stage builds to reduce the size of the Docker image. **Don't Do This:** * Build Docker images manually without a "Dockerfile." * Include unnecessary files or dependencies in the Docker image. * Expose sensitive information in the Docker image. **Why:** Containerization provides a consistent and isolated environment for running Clojure applications, simplifying deployment and ensuring portability. **Example "Dockerfile":** """dockerfile FROM eclipse-temurin:17-jre-alpine as builder WORKDIR /app COPY deps.edn . COPY src ./src RUN clojure -T:build compile RUN clojure -T:build jar FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY --from=builder /app/target/my-app-1.0.0.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"] """ ## 7. Documentation ### 7.1. Code Documentation **Standard:** Document Clojure code thoroughly using docstrings. **Do This:** * Add docstrings to all functions, namespaces, and macros. * Describe the purpose, arguments, and return value of each function. * Use clear and concise language. * Include examples of how to use the function. **Don't Do This:** * Omit docstrings or write incomplete documentation. * Write overly verbose or confusing docstrings. **Why:** Documentation is essential for code maintainability, readability, and collaboration. Docstrings provide a quick reference for developers using your code. **Example:** """clojure (ns my-app.core "This namespace contains the core functions of the application.") (defn add "Adds two numbers together. Args: x: The first number. y: The second number. Returns: The sum of x and y. Example: (add 1 2) => 3" [x y] (+ x y)) """ ### 7.2. API Documentation **Standard:** Generate API documentation using tools like "codox". **Do This:** * Configure "codox" to generate documentation from docstrings. * Publish API documentation online for easy access. * Tailor "codox" configurations (via comments) to suit the specific formatting of your API * Include detailed explanations of API endpoints and data structures. **Don't Do This:** * Rely solely on code comments without generating formal documentation. * Leave APIs uncommented. **Why:** API documentation provides developers with a comprehensive guide to using your application's APIs, reducing the need for manual code inspection. **Example configuration comment for "codox"** """clojure (ns my-library.functions "A collection of useful functions. ;; :codox/metadata {:doc/format :markdown} ") (defn my-function "Does something useful. ;; :codox/visibility :private ;; :codox/tags [:experimental] " [x] x) """ ## 8. Security Best Practices ### 8.1 Dependency Vulnerability Scanning **Standard:** Implement automated dependency vulnerability scanning as part of your build process. **Do This:** * Evaluate and integrate a vulnerability scanning tool (e.g., OWASP Dependency-Check, Snyk, or similar) into your CI/CD pipeline, either via CLI or a CI/CD platform plugin. * Configure the tool to scan your "deps.edn" file for known vulnerabilities in third-party dependencies. * Regularly update the vulnerability database used by the scanner. * Configure the tool to fail the build if vulnerabilities are found with a severity level above a predefined threshold. **Don't Do This:** * Neglect to scan dependencies for vulnerabilities, as this can introduce significant security risks. * Ignore vulnerability reports or postpone addressing them, as this increases the potential for exploitation. * Rely solely on manual dependency updates without automated vulnerability scanning. **Why:** Automated dependency vulnerability scanning helps identify and mitigate security risks arising from vulnerable third-party libraries, reducing the attack surface of your application. ### 8.2 Avoid direct use of "eval" **Standard:** Avoid using "eval" or similar functions that execute arbitrary code from strings. **Do This:** * Find alternatives to "eval" such as data-driven architectures and function dispatch. * If "eval" is unavoidable, sanitize the input string to prevent code injection attacks. * Restrict the scope of "eval" to a minimum. **Don't Do This:** * Use "eval" directly on user-supplied strings. * Use "eval" without understanding its security implications. **Why:** Executing arbitrary code from strings can lead to severe security vulnerabilities, allowing attackers to inject malicious code into your application. **Example:** Instead of using eval: """clojure (defn execute-command [command] (eval (read-string command))) ; Avoid this! """ Use a data-driven approach: """clojure (def commands {"add" + "subtract" -}) (defn execute-command [command & args] (if-let [f (get commands command)] (apply f args) (throw (Exception. (str "Unknown command: " command))))) """ ## 9. Performance Optimization Tools ### 9.1 Profiling tools **Standard:** Employ profiling tools to identify performance bottlenecks in Clojure code. **Do This:** * Integrate a profiling library (e.g., "clj-async-profiler" or "criterium") into your development workflow. * Use the profiler to measure the execution time of different code sections and identify performance bottlenecks. * Analyze the profiling results to pinpoint areas where optimization is needed. * Repeat profiling after applying optimizations to verify their effectiveness. **Don't Do This:** * Rely solely on intuition or guesswork to identify performance bottlenecks. * Neglect to profile code before and after applying optimizations to measure their impact. * Profile in a non-representative environment for the workload characteristics. **Why:** Profiling provides valuable insights into the performance characteristics of Clojure code, enabling developers to identify and address bottlenecks effectively. **Example Usage of "criterium"** """clojure (ns my-app.performance-test (:require [criterium.core :as crit] [my-app.core :as core])) (defn slow-function [n] (reduce + (range n))) (defn fast-function [n] (apply + (range n))) (comment (crit/quick-bench (slow-function 10000)) (crit/quick-bench (fast-function 10000))) """ This concludes the Tooling and Ecosystem standards for Clojure. Adhering to these guidelines will improve the consistency, maintainability, and security of Clojure projects.