Commit 936fe414 authored by Colin DAMON's avatar Colin DAMON

Pagination in an hexagonal architecture

parents
Pipeline #37633 failed with stage
in 1 minute and 13 seconds
# Created by https://www.toptal.com/developers/gitignore/api/eclipse,intellij,visualstudiocode,java,maven
# Edit at https://www.toptal.com/developers/gitignore?templates=eclipse,intellij,visualstudiocode,java,maven
### Eclipse ###
.metadata
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.settings/
.loadpath
.recommenders
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# PyDev specific (Python IDE for Eclipse)
*.pydevproject
# CDT-specific (C/C++ Development Tooling)
.cproject
# CDT- autotools
.autotools
# Java annotation processor (APT)
.factorypath
# PDT-specific (PHP Development Tools)
.buildpath
# sbteclipse plugin
.target
# Tern plugin
.tern-project
# TeXlipse plugin
.texlipse
# STS (Spring Tool Suite)
.springBeans
# Code Recommenders
.recommenders/
# Annotation Processing
.apt_generated/
# Scala IDE specific (Scala & Java development for Eclipse)
.cache-main
.scala_dependencies
.worksheet
### Eclipse Patch ###
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath
# Annotation Processing
.apt_generated
.sts4-cache/
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea/
*.iml
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Intellij Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
.idea/sonarlint
### Java ###
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
### Maven ###
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
### Node ###
node_modules/
# End of https://www.toptal.com/developers/gitignore/api/eclipse,intellij,visualstudiocode,java,maven
variables:
MAVEN_OPTS: "-Dhttps.protocols=TLSv1.2 -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
stages:
- build
format-check:
stage: build
image: node:14.15.4-slim
script:
- npm i
- npm run prettier:java:check
only:
refs:
- master
- merge_requests
verify:
stage: build
image: maven:3.6.3-jdk-11
script:
- mvn checkstyle:check verify
- awk -F"," '{ branches += $4 + $5 + $6 + $7; covered += $5 + $7 } END { print covered, "/", branches, "branches covered"; print 100*covered/branches, "%covered" }' target/site/jacoco/jacoco.csv
artifacts:
reports:
junit:
- target/surefire-reports/**/TEST-*.xml
coverage: "/([^%]+) %covered/"
cache:
paths:
- .m2/repository
only:
refs:
- master
- merge_requests
module.exports = {
hooks: {
"pre-commit": "lint-staged"
}
};
.git
node_modules
package-lock.json
**/node_modules
**/package-lock.json
**/target
# Prettier configuration
overrides:
- files: "**/*.java"
options:
# js and ts rules
arrowParens: avoid
# jsx and tsx rules
jsxBracketSameLine: false
printWidth: 140
tabWidth: 2
useTabs: false
singleQuote: true
- files:
- "**/*.{yml,yaml}"
options:
singleQuote: false
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"https://www.puppycrawl.com/dtds/configuration_1_3.dtd">
<module name="Checker">
<property name="charset" value="UTF-8" />
<property name="fileExtensions" value="java" />
<module name="SuppressWithPlainTextCommentFilter" />
<module name="TreeWalker">
<module name="UnusedImports" />
<module name="EmptyBlock"/>
</module>
</module>
This diff is collapsed.
This diff is collapsed.
{
"name": "hexagonal-pagination",
"version": "0.0.0",
"description": "Simple example of pagination in an hexagonal architecture",
"private": true,
"license": "Apache2",
"cacheDirectories": [
"node_modules"
],
"devDependencies": {
"husky": "1.3.1",
"lint-staged": "8.1.4",
"onchange": "^7.0.2",
"prettier": "^1.19.1",
"prettier-plugin-java": "^0.6.0"
},
"engines": {
"node": ">= 12.16.1",
"npm": ">= 6.13.4"
},
"lint-staged": {
"**/*.{md,json,js,ts,tsx,css,scss,yml,yaml,java}": [
"prettier --write",
"git add"
]
},
"scripts": {
"prettier:java": "prettier --write \"**/*.java\" --plugin=./node_modules/prettier-plugin-java",
"prettier:java:watch": "onchange '**/*.java' -- prettier --write {{changed}} --plugin=./node_modules/prettier-plugin-java",
"prettier:java:check": "prettier --check \"**/*.java\" --plugin=./node_modules/prettier-plugin-java"
}
}
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ippon</groupId>
<artifactId>hexagonal-pagination</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>HexagonalPagination</name>
<developers>
<developer>
<email>cdamon@ippon.fr</email>
<name>Colin DAMON</name>
</developer>
</developers>
<properties>
<java.version>14</java.version>
<common-lang.version>3.11</common-lang.version>
<swagger-annotations.version>1.6.2</swagger-annotations.version>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<hibernate-validator.version>7.0.0.Final</hibernate-validator.version>
<junit.version>5.7.0</junit.version>
<assertj.version>3.18.1</assertj.version>
<mockito.version>3.7.0</mockito.version>
<reflections.version>0.9.12</reflections.version>
<surefire-plugin.version>3.0.0-M5</surefire-plugin.version>
<compiler-plugin.version>3.8.1</compiler-plugin.version>
<jacoco.version>0.8.5</jacoco.version>
<checkstyle-plugin.version>3.1.1</checkstyle-plugin.version>
<checkstyle.version>8.39</checkstyle.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${common-lang.version}</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger-annotations.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>${reflections.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${checkstyle-plugin.version}</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>${checkstyle.version}</version>
</dependency>
</dependencies>
<configuration>
<configLocation>checkstyle.xml</configLocation>
<excludes>.git/**/*,target/**/*,node_modules/**/*,node/**/*</excludes>
<sourceDirectories>./</sourceDirectories>
</configuration>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
# Pagination in the hexagone
This a simple example of a way to handle pagination in an hexagonal architecture. The only `secondary` tool is to convert from and to Spring Data pages but you can adapt to whatever you want.
package com.ippon;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR })
public @interface Generated {
}
package com.ippon.error.domain;
public final class Assert {
private Assert() {}
public static void notNull(String field, Object input) {
if (input == null) {
throw MissingMandatoryValueException.forNullValue(field);
}
}
public static void notBlank(String field, String input) {
notNull(field, input);
if (input.isBlank()) {
throw MissingMandatoryValueException.forBlankValue(field);
}
}
public static LongAsserter field(String fieldName, long value) {
return new LongAsserter(fieldName, value);
}
public static class LongAsserter {
private final String fieldName;
private final long value;
private LongAsserter(String fieldName, long value) {
this.fieldName = fieldName;
this.value = value;
}
public LongAsserter min(long minValue) {
if (value < minValue) {
throw ValueUnderMinException.builder().field(fieldName).value(value).minValue(minValue).build();
}
return this;
}
public LongAsserter max(long maxValue) {
if (value > maxValue) {
throw ValueOverMaxException.builder().field(fieldName).value(value).maxValue(maxValue).build();
}
return this;
}
}
}
package com.ippon.error.domain;
public enum ErrorStatus {
BAD_REQUEST,
UNAUTHORIZED,
FORBIDDEN,
NOT_FOUND,
CONFLICT,
INTERNAL_SERVER_ERROR
}
package com.ippon.error.domain;
public class MissingMandatoryValueException extends MyAppException {
protected MissingMandatoryValueException(MyAppExceptionBuilder builder) {
super(builder);
}
private static MyAppExceptionBuilder builder(MyAppMessage myAppMessage, String fieldName, String message) {
return MyAppException.builder(myAppMessage).status(ErrorStatus.INTERNAL_SERVER_ERROR).argument("field", fieldName).message(message);
}
private static String defaultMessage(String fieldName) {
return "The field \"" + fieldName + "\" is mandatory and wasn't set";
}
public static MissingMandatoryValueException forNullValue(String fieldName) {
return new MissingMandatoryValueException(
builder(StandardMessage.SERVER_MANDATORY_NULL, fieldName, defaultMessage(fieldName) + " (null)")
);
}
public static MissingMandatoryValueException forBlankValue(String fieldName) {
return new MissingMandatoryValueException(
builder(StandardMessage.SERVER_MANDATORY_BLANK, fieldName, defaultMessage(fieldName) + " (blank)")
);
}
}
package com.ippon.error.domain;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Parent exception used in your application. Those exceptions will be resolved as human readable errors.
*
* <p>
* You can use this implementation directly:
* </p>
*
* <p>
* <code>
* <pre>
* MyAppException.builder(StandardMessages.USER_MANDATORY)
* .argument("key", "value")
* .argument("other", "test")
* .status(ErrorsHttpStatus.BAD_REQUEST)
* .message("Error message")
* .cause(new RuntimeException())
* .build();
* </pre>
* </code>
* </p>
*
* <p>
* Or make extension exceptions:
* </p>
*
* <p>
* <code>
* <pre>
* public class MissingMandatoryValueException extends MyAppException {
*
* public MissingMandatoryValueException(MyAppMessage myAppMessage, String fieldName) {
* this(builder(myAppMessage, fieldName, defaultMessage(fieldName)));
* }
*
* protected MissingMandatoryValueException(MyAppExceptionBuilder builder) {
* super(builder);
* }
*
* private static MyAppExceptionBuilder builder(MyAppMessage myAppMessage, String fieldName, String message) {
* return MyAppException.builder(myAppMessage)
* .status(ErrorsHttpStatus.INTERNAL_SERVER_ERROR)
* .argument("field", fieldName)
* .message(message);
* }
*
* private static String defaultMessage(String fieldName) {
* return "The field \"" + fieldName + "\" is mandatory and wasn't set";
* }
* }
* </pre>
* </code>
* </p>
*/
public class MyAppException extends RuntimeException {
private final Map<String, String> arguments;
private final ErrorStatus status;
private final MyAppMessage myAppMessage;
protected MyAppException(MyAppExceptionBuilder builder) {
super(getMessage(builder), getCause(builder));
arguments = getArguments(builder);
status = getStatus(builder);
myAppMessage = getMyAppMessage(builder);
}
private static String getMessage(MyAppExceptionBuilder builder) {
if (builder == null) {