Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ jobs:

- name: Run tests
run: lein test

- name: Run architectural fitness checks
run: ./bin/check-architecture.sh

- name: Build uberjar
run: lein uberjar
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Walue - Portfolio Evaluation Service

[![CI/CD Pipeline](https://github.com/yourusername/walue/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/yourusername/walue/actions/workflows/ci-cd.yml)
[![Architectural Fitness](https://img.shields.io/badge/Architectural%20Fitness-Checked-brightgreen.svg)](https://github.com/yourusername/walue/blob/main/src/walue/infra/fitness.clj)

A Clojure web service that implements a portfolio evaluation system using Domain-Driven Design (DDD) and Hexagonal Architecture.

## Overview
Expand Down Expand Up @@ -42,6 +45,33 @@ The project follows Hexagonal Architecture (also known as Ports and Adapters) an
- **Adapters**: Implement the interfaces defined by the ports, connecting the domain to external systems (HTTP, databases, etc.).
- **Infrastructure**: Provides technical capabilities like logging and metrics.

### Architectural Fitness Functions

This project includes automated architectural fitness functions that enforce the following rules:

1. **Layer Dependency Rules**:
- Domain can only depend on Clojure stdlib
- Port can only depend on Domain
- Adapter can depend on Domain and Port
- Infrastructure should be self-contained
- Core can depend on all layers

2. **Domain Purity**: Ensures domain code has no external dependencies beyond Clojure stdlib

3. **Circular Dependency Prevention**: Detects and prevents circular dependencies between components

4. **Interface Isolation**: Ensures all ports define protocols (interfaces)

5. **Adapter Implementation**: Verifies that adapters implement at least one port

These checks run automatically in the CI/CD pipeline and locally via:

```bash
./bin/check-architecture.sh
```

The fitness functions prevent architectural degradation over time as new changes are introduced.

## API

### Evaluate Portfolio
Expand Down
5 changes: 5 additions & 0 deletions bin/check-architecture.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash
set -e

echo "Running architectural fitness checks..."
lein run -m walue.infra.fitness
1 change: 1 addition & 0 deletions project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
:description "Portfolio evaluation service using Domain-Driven Design and Hexagonal Architecture"
:dependencies [[org.clojure/clojure "1.11.1"]
[org.clojure/data.json "2.4.0"]
[org.clojure/tools.namespace "1.4.4"]
[ring/ring-core "1.9.5"]
[ring/ring-jetty-adapter "1.9.5"]
[ring/ring-json "0.5.1"]]
Expand Down
33 changes: 17 additions & 16 deletions src/walue/adapter/http_adapter.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,37 @@
(:require [clojure.data.json :as json]
[ring.middleware.params :refer [wrap-params]]
[ring.middleware.json :refer [wrap-json-body wrap-json-response]]
[walue.infra.logging :as logging]
[walue.port.evaluation-port :as port]))
[walue.port.evaluation-port :as evaluation-port]
[walue.port.logging-port :as logging-port]))

(defn- handle-evaluate-portfolio [request evaluation-service]
(defn- handle-evaluate-portfolio [request evaluation-service logging-service]
(try
(let [body (:body request)
portfolio (or (get body "portfolio") (get body :portfolio))
criteria (or (get body "criterios") (get body :criterios))]
(if (and portfolio criteria)
(let [result (port/evaluate-portfolio evaluation-service portfolio criteria)]
(let [result (evaluation-port/evaluate-portfolio evaluation-service portfolio criteria)]
{:status 200
:body result})
{:status 400
:body {:error "Invalid request. Portfolio and criteria are required."}}))
(catch Exception e
(logging/error "Error evaluating portfolio:" (.getMessage e))
(logging-port/log-error logging-service (str "Error evaluating portfolio: " (.getMessage e)))
{:status 500
:body {:error "An error occurred while processing your request."}})))

(defn- handle-health-check [_]
{:status 200
:body {:status "UP"}})

(defn make-handler [evaluation-service]
(defn make-handler [evaluation-service logging-service]
(fn [request]
(let [uri (:uri request)
method (:request-method request)]
(logging/info "Request received:" method uri)
(logging-port/log-info logging-service (str "Request received: " method " " uri))
(cond
(and (= uri "/api/evaluate") (= method :post))
(handle-evaluate-portfolio request evaluation-service)
(handle-evaluate-portfolio request evaluation-service logging-service)

(and (= uri "/health") (= method :get))
(handle-health-check request)
Expand All @@ -41,27 +41,28 @@
{:status 404
:body {:error "Not found"}}))))

(defn wrap-logging [handler]
(defn wrap-logging [handler logging-service]
(fn [request]
(let [start (System/currentTimeMillis)
response (handler request)
duration (- (System/currentTimeMillis) start)]
(logging/info "Request completed in" duration "ms with status" (:status response))
(logging-port/log-info logging-service
(str "Request completed in " duration " ms with status " (:status response)))
response)))

(defn wrap-exception [handler]
(defn wrap-exception [handler logging-service]
(fn [request]
(try
(handler request)
(catch Exception e
(logging/error "Unhandled exception:" (.getMessage e))
(logging-port/log-error logging-service (str "Unhandled exception: " (.getMessage e)))
{:status 500
:body {:error "Internal server error"}}))))

(defn create-app [evaluation-service]
(-> (make-handler evaluation-service)
(wrap-logging)
(wrap-exception)
(defn create-app [evaluation-service logging-service]
(-> (make-handler evaluation-service logging-service)
(wrap-logging logging-service)
(wrap-exception logging-service)
(wrap-json-body {:keywords? false})
(wrap-json-response)
(wrap-params)))
22 changes: 14 additions & 8 deletions src/walue/core.clj
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
(ns walue.core
(:require [ring.adapter.jetty :as jetty]
[walue.adapter.http-adapter :as http]
[walue.port.evaluation-port :as port]
[walue.port.evaluation-port :as evaluation-port]
[walue.port.logging-port :as logging-port]
[walue.infra.logging :as logging]
[walue.infra.metrics :as metrics])
(:gen-class))

(def server-atom (atom nil))

(defn start-server [port]
(let [evaluation-service (port/->EvaluationService)
app (http/create-app evaluation-service)
(let [evaluation-service (evaluation-port/->EvaluationService)
logging-service (logging-port/->LoggingService)
app (http/create-app evaluation-service logging-service)
metrics (metrics/init-metrics)
server (jetty/run-jetty app {:port port :join? false})]
(reset! server-atom server)
(logging/info "Server started on port" port)
(logging-port/log-info logging-service (str "Server started on port " port))
server))

(defn stop-server []
(when-let [server @server-atom]
(.stop server)
(reset! server-atom nil)
(logging/info "Server stopped")))
(let [logging-service (logging-port/->LoggingService)]
(logging-port/log-info logging-service "Server stopped"))))

(defn- get-env-var [name default]
(if-let [value (System/getenv name)]
Expand All @@ -33,7 +36,9 @@
(try
(Integer/parseInt port-str)
(catch NumberFormatException _
(logging/warn "Invalid PORT environment variable value:" port-str "- using default 8080")
(let [logging-service (logging-port/->LoggingService)]
(logging-port/log-warn logging-service
(str "Invalid PORT environment variable value: " port-str " - using default 8080")))
8080))))

(defn- get-log-level []
Expand All @@ -51,9 +56,10 @@
(catch NumberFormatException _
(get-port)))
(get-port))
log-level (get-log-level)]
log-level (get-log-level)
logging-service (logging-port/->LoggingService)]
(logging/set-log-level! log-level)
(logging/info "Starting server with log level:" log-level)
(logging-port/log-info logging-service (str "Starting server with log level: " log-level))
(start-server port)
(.addShutdownHook (Runtime/getRuntime)
(Thread. ^Runnable stop-server))))
Loading