Skip to content
Open
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
2 changes: 2 additions & 0 deletions .env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SERVER_PORT=5000
SPRING_PROFILES_ACTIVE=local
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf
33 changes: 33 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
HELP.md
target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/

### VS Code ###
.vscode/
4 changes: 4 additions & 0 deletions .sdkmanrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=21.0.7-tem
maven=3.9.6
25 changes: 25 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
FROM eclipse-temurin:21.0.7_6-jre-alpine

ARG PROJECT_VERSION="0.0.1-SNAPSHOT"
ENV PROJECT_VERSION="$PROJECT_VERSION"
ENV MAIN_DIR="/app"

RUN addgroup -S backprod && adduser -S backprod -G backprod && \
mkdir -p "/app" && \
chown -R backprod:backprod "/app" && \
chgrp -R backprod $JAVA_HOME && \
chmod ugo+rwx -R /var/log && \
mkdir /var/log/backprod && \
chmod ugo+rwx -R /var/log/backprod && \
chmod -R g+rw $JAVA_HOME

ADD bootstrap/target/bootstrap-${PROJECT_VERSION}.jar /app/app.jar

WORKDIR $MAIN_DIR

ADD start.sh /app/start.sh
RUN chmod +x /app/start.sh

USER backprod

ENTRYPOINT [ "sh", "/app/start.sh" ]
132 changes: 132 additions & 0 deletions adapters/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>dev.molaya.tests</groupId>
<artifactId>backprods</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>adapters</artifactId>
<name>Adapters Module</name>

<properties>
<openapi.out.client>dev.molaya.tests.adapters.out.client.gen.openapi</openapi.out.client>
<openapi.in.rest>dev.molaya.tests.adapters.in.rest.gen.openapi</openapi.in.rest>
<springdoc-openapi-starter-webmvc-ui.version>2.8.6</springdoc-openapi-starter-webmvc-ui.version>
<openapi-generator-maven-plugin.apiNameSuffix>GenApi</openapi-generator-maven-plugin.apiNameSuffix>
<jacoco-maven-plugin.excludes>**/auth/**,**/gen/**</jacoco-maven-plugin.excludes>
</properties>

<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>application</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc-openapi-starter-webmvc-ui.version}</version>
</dependency>
<!-- open api -->
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>${openapi-generator-maven-plugin.version}</version>
<executions>
<!-- open api client -->
<execution>
<id>client-interface-generator</id>
<goals><goal>generate</goal></goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/existingApis.yaml</inputSpec>
<generatorName>java</generatorName>
<library>webclient</library>
<apiPackage>${openapi.out.client}</apiPackage>
<modelPackage>${openapi.out.client}.dto</modelPackage>
<apiNameSuffix>${openapi-generator-maven-plugin.apiNameSuffix}</apiNameSuffix>
<generateApiTests>false</generateApiTests>
<generateModelTests>false</generateModelTests>
<configOptions>
<useJakartaEe>true</useJakartaEe>
<reactive>true</reactive>
</configOptions>
</configuration>
</execution>
<!-- end open api client -->
<!-- open api rest -->
<execution>
<id>rest-interface-generator</id>
<goals><goal>generate</goal></goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/similarProducts.yaml</inputSpec>
<generatorName>spring</generatorName>
<apiPackage>${openapi.in.rest}</apiPackage>
<modelPackage>${openapi.in.rest}.dto</modelPackage>
<generateApiTests>false</generateApiTests>
<apiNameSuffix>${openapi-generator-maven-plugin.apiNameSuffix}</apiNameSuffix>
<configOptions>
<reactive>true</reactive>
<interfaceOnly>true</interfaceOnly>
<skipDefaultInterface>true</skipDefaultInterface>
<useJakartaEe>true</useJakartaEe>
<restControllerStyle>true</restControllerStyle>
</configOptions>
</configuration>
</execution>
<!-- end open api rest -->
</executions>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package dev.molaya.tests.adapters.in.rest;

import dev.molaya.tests.adapters.in.rest.gen.openapi.dto.ProductDetail;
import dev.molaya.tests.application.in.dto.GetSimilarProductsInput;
import dev.molaya.tests.domain.Product;
import dev.molaya.tests.domain.exceptions.BadParametersException;
import java.util.Optional;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.springframework.http.ResponseEntity;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Mapper
public interface RestSimilarProductsMapper {

@Mapping(target = "availability", source = "available")
ProductDetail toRestProductDetail(Product product);

default Mono<ResponseEntity<Flux<ProductDetail>>> wrapAsOkResponse(Flux<ProductDetail> dto) {
return Mono.just(ResponseEntity.ok(dto));
}

default GetSimilarProductsInput toGetSimilarProductsInput(String productId) {
return Optional.ofNullable(productId)
.map(GetSimilarProductsInput::new)
.orElseThrow(() -> new BadParametersException("Product id should not be null"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package dev.molaya.tests.adapters.in.rest;

import dev.molaya.tests.adapters.in.rest.gen.openapi.ProductGenApi;
import dev.molaya.tests.adapters.in.rest.gen.openapi.dto.ProductDetail;
import dev.molaya.tests.application.in.GetSimilarProductsUseCase;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Slf4j
@RestController
@RequiredArgsConstructor
public class SimilarProductsController implements ProductGenApi {
private final GetSimilarProductsUseCase useCase;
private final RestSimilarProductsMapper mapper;

@Override
@GetMapping(ProductGenApi.PATH_GET_PRODUCT_SIMILAR)
public Mono<ResponseEntity<Flux<ProductDetail>>> getProductSimilar(
@NotNull @Parameter(name = "productId", description = "", required = true, in = ParameterIn.PATH)
@PathVariable("productId")
String productId,
@Parameter(hidden = true) final ServerWebExchange exchange) {
log.info("Getting similar products for {}", productId);
final var input = mapper.toGetSimilarProductsInput(productId);
return useCase.getSimilarProducts(input)
.map(mapper::toRestProductDetail)
.as(mapper::wrapAsOkResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package dev.molaya.tests.adapters.in.rest.advice;

import dev.molaya.tests.adapters.in.rest.advice.dto.ErrorType;
import dev.molaya.tests.domain.exceptions.IntegrationException;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class SimilarProductsControllerAdvice {

@ExceptionHandler({Exception.class})
public ResponseEntity<ErrorType.ApiError> handleException(Exception ex) {
log.info("Unexpected error occurred: {} - trace: ", ex.getMessage(), ex);
return ErrorType.UNKNOWN.getErrorResponseEntity(ex.getMessage());
}

@ExceptionHandler({IntegrationException.class})
public ResponseEntity<ErrorType.ApiError> handleIntegrationException(IntegrationException exception) {
final var message = Optional.ofNullable(exception.getCause())
.map(Throwable::getMessage)
.orElse("not provided");
log.info(
"Integration error occurred: {} - internal exception [{}] - trace: ",
exception.getMessage(),
message,
exception);
return ErrorType.EXTERNAL_SERVICE.getErrorResponseEntity(exception.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package dev.molaya.tests.adapters.in.rest.advice.dto;

import lombok.Builder;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

@RequiredArgsConstructor
public enum ErrorType {
INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "MOLAR-400", "Invalid parameter provided"),
UNKNOWN(HttpStatus.INTERNAL_SERVER_ERROR, "MOLAR-500", "Unknown error occurred"),
EXTERNAL_SERVICE(HttpStatus.CONFLICT, "MOLAR-409", "Integration with external service failed");
private final HttpStatus httpStatus;
private final String code;
private final String message;

public ResponseEntity<@NonNull ApiError> getErrorResponseEntity(String customMessage) {
return ResponseEntity.status(httpStatus.value())
.body(ApiError.builder()
.code(code)
.message(message)
.detail(customMessage)
.build());
}

@Builder
public record ApiError(String code, String message, String detail) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package dev.molaya.tests.adapters.out.client;

import dev.molaya.tests.adapters.out.client.gen.openapi.DefaultGenApi;
import dev.molaya.tests.application.out.ProductRepositoryPort;
import dev.molaya.tests.application.out.dto.ProductCommand;
import dev.molaya.tests.domain.Product;
import jakarta.validation.constraints.NotNull;
import java.util.Collections;
import java.util.Set;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

@Slf4j
@Service
@RequiredArgsConstructor
public class ClientProductRepositoryAdapter implements ProductRepositoryPort {
private final DefaultGenApi defaultApi;
private final ClientSimilarProductsMapper mapper;

@Override
public Mono<@NonNull Product> getProductDetail(@NotNull ProductCommand productCommand) {
final var id = productCommand.id();
return Mono.defer(() -> defaultApi.getProductProductId(id).map(mapper::toDomainProduct))
.doOnSuccess(resp -> log.info("Fetched product detail: {}", resp))
.doOnError(e -> log.warn("Failed to fetch product detail for product: {}", id))
.onErrorResume(e -> Mono.empty());
}

@Override
public Mono<@NonNull Set<String>> getSimilarProductIds(@NotNull ProductCommand productCommand) {
final var id = productCommand.id();
return Mono.defer(() -> defaultApi.getProductSimilarids(id))
.doOnSuccess(resp -> log.info("Fetched similar product ids for product: {} {}", id, resp.size()))
.doOnError(e -> log.warn("Failed to fetch similar product ids for product: {}", id))
.onErrorResume(e -> Mono.just(Collections.emptySet()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dev.molaya.tests.adapters.out.client;

import dev.molaya.tests.adapters.out.client.gen.openapi.dto.ProductDetail;
import dev.molaya.tests.domain.Product;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper
public interface ClientSimilarProductsMapper {

@Mapping(target = "available", source = "availability")
Product toDomainProduct(ProductDetail source);
}
Loading