Skip to content

Commit 2d38b43

Browse files
authored
fix: явные мапперы dto в entity и строгий парсинг (#38)
## Что сделано - Добавлен пакет `mapper`: `UserMapper` (firstname/lastname -> firstName/lastName), `ResourceMapper`, `FileMapper`; приватные `toEntity` в `AuthServiceImpl`, `UsersServiceImpl`, `ResourceServiceImpl` и ручная конвертация в `FileServiceImpl` заменены на мапперы. - `UserDto` приведен к контракту бэкенда 1:1: поля `firstName`/`lastName` переименованы в `firstname`/`lastname` (как в `RegisterRequest`/`UpdateUserRequest`). - Из `ApiClient` убраны `// FIXME` и отключение `FAIL_ON_UNKNOWN_PROPERTIES` — парсинг снова строгий, дрейф контракта ловится fail fast. - Добавлены unit-тесты: маппинг всех полей, firstname/lastname -> firstName/lastName, null-список файлов -> пустой список, разбор бэкенд-JSON 1:1 и падение на неизвестных полях. ## Проверки - `mvn -B verify` зеленый: 61 тест, 0 ошибок; spotless и checkstyle без нарушений. - Интеграционные тесты против живого бэкенда пропущены: в worktree нет `.env`, живого бэкенда нет. Closes #19
1 parent 0e45081 commit 2d38b43

14 files changed

Lines changed: 246 additions & 45 deletions

File tree

‎pom.xml‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
<encoding>UTF-8</encoding>
1919
<project.build.sourceEncoding>${encoding}</project.build.sourceEncoding>
2020
<project.reporting.outputEncoding>${encoding}</project.reporting.outputEncoding>
21+
<mapstruct.version>1.6.3</mapstruct.version>
22+
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
2123
<excludedGroups>integration</excludedGroups>
2224
</properties>
2325

@@ -95,6 +97,11 @@
9597
<version>1.18.48</version>
9698
<scope>provided</scope>
9799
</dependency>
100+
<dependency>
101+
<groupId>org.mapstruct</groupId>
102+
<artifactId>mapstruct</artifactId>
103+
<version>${mapstruct.version}</version>
104+
</dependency>
98105
<dependency>
99106
<groupId>com.dlsc.formsfx</groupId>
100107
<artifactId>formsfx-core</artifactId>
@@ -165,6 +172,16 @@
165172
<artifactId>lombok</artifactId>
166173
<version>1.18.48</version>
167174
</path>
175+
<path>
176+
<groupId>org.projectlombok</groupId>
177+
<artifactId>lombok-mapstruct-binding</artifactId>
178+
<version>${lombok-mapstruct-binding.version}</version>
179+
</path>
180+
<path>
181+
<groupId>org.mapstruct</groupId>
182+
<artifactId>mapstruct-processor</artifactId>
183+
<version>${mapstruct.version}</version>
184+
</path>
168185
</annotationProcessorPaths>
169186
<compilerArgs>
170187
<arg>-proc:full</arg>

‎src/main/java/com/DesktopApplicationClientJava/Api/ApiClient.java‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import com.DesktopApplicationClientJava.Api.Dto.response.ResourceDto;
1515
import com.DesktopApplicationClientJava.Api.Dto.response.UserDto;
1616
import com.DesktopApplicationClientJava.session.Session;
17-
import com.fasterxml.jackson.databind.DeserializationFeature;
1817
import com.fasterxml.jackson.databind.ObjectMapper;
1918
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
2019
import java.io.ByteArrayOutputStream;
@@ -50,9 +49,6 @@ public ApiClient(Session session) {
5049
.build();
5150
this.objectMapper = new ObjectMapper();
5251
this.objectMapper.registerModule(new JavaTimeModule());
53-
// FIXME: temporary — backend returns firstname/lastname instead of firstName/lastName.
54-
// Remove once backend uses camelCase; then unknown properties should fail fast again.
55-
this.objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
5652
}
5753

5854
// ======================== AUTH ========================

‎src/main/java/com/DesktopApplicationClientJava/Api/Dto/response/UserDto.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ public record UserDto(
88
UUID uuid,
99
String email,
1010
Role role,
11-
String firstName,
12-
String lastName,
11+
String firstname,
12+
String lastname,
1313
Boolean enabled,
1414
LocalDateTime createdAt,
1515
LocalDateTime updatedAt) {}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.DesktopApplicationClientJava.mapper;
2+
3+
import com.DesktopApplicationClientJava.Api.Dto.response.FileDto;
4+
import com.DesktopApplicationClientJava.entities.FileInfo;
5+
import org.mapstruct.Mapper;
6+
import org.mapstruct.factory.Mappers;
7+
8+
@Mapper
9+
public interface FileMapper {
10+
11+
FileMapper INSTANCE = Mappers.getMapper(FileMapper.class);
12+
13+
FileInfo toEntity(FileDto dto);
14+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.DesktopApplicationClientJava.mapper;
2+
3+
import com.DesktopApplicationClientJava.Api.Dto.response.ResourceDto;
4+
import com.DesktopApplicationClientJava.entities.Resource;
5+
import org.mapstruct.Mapper;
6+
import org.mapstruct.NullValueMappingStrategy;
7+
import org.mapstruct.factory.Mappers;
8+
9+
@Mapper(
10+
uses = FileMapper.class,
11+
// Бэкенд присылает null вместо пустого списка файлов.
12+
nullValueIterableMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT)
13+
public interface ResourceMapper {
14+
15+
ResourceMapper INSTANCE = Mappers.getMapper(ResourceMapper.class);
16+
17+
Resource toEntity(ResourceDto dto);
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.DesktopApplicationClientJava.mapper;
2+
3+
import com.DesktopApplicationClientJava.Api.Dto.response.UserDto;
4+
import com.DesktopApplicationClientJava.entities.User;
5+
import org.mapstruct.Mapper;
6+
import org.mapstruct.Mapping;
7+
import org.mapstruct.factory.Mappers;
8+
9+
@Mapper
10+
public interface UserMapper {
11+
12+
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
13+
14+
// Имена в контракте бэкенда в нижнем регистре, в сущности — camelCase.
15+
@Mapping(target = "firstName", source = "firstname")
16+
@Mapping(target = "lastName", source = "lastname")
17+
User toEntity(UserDto dto);
18+
}

‎src/main/java/com/DesktopApplicationClientJava/services/impl/AuthServiceImpl.java‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import com.DesktopApplicationClientJava.Api.ApiClient;
44
import com.DesktopApplicationClientJava.Api.ApiException;
55
import com.DesktopApplicationClientJava.Api.Dto.request.Auth.LoginRequest;
6-
import com.DesktopApplicationClientJava.Api.Dto.response.UserDto;
76
import com.DesktopApplicationClientJava.entities.Role;
87
import com.DesktopApplicationClientJava.entities.User;
8+
import com.DesktopApplicationClientJava.mapper.UserMapper;
99
import com.DesktopApplicationClientJava.services.AuthService;
1010
import com.DesktopApplicationClientJava.services.ServiceException;
1111
import com.DesktopApplicationClientJava.session.Session;
@@ -25,8 +25,7 @@ public AuthServiceImpl(ApiClient apiClient, Session session) {
2525
public User login(String email, String password) {
2626
try {
2727
apiClient.login(new LoginRequest(email, password));
28-
UserDto me = apiClient.getMe();
29-
User user = toEntity(me);
28+
User user = UserMapper.INSTANCE.toEntity(apiClient.getMe());
3029
session.setCurrentUser(user);
3130
return user;
3231
} catch (ApiException e) {
@@ -70,11 +69,6 @@ public boolean isModerator() {
7069
return hasRole(Role.MODER);
7170
}
7271

73-
private User toEntity(UserDto dto) {
74-
return new User(
75-
dto.uuid(), dto.email(), dto.role(), dto.firstName(), dto.lastName(), dto.enabled());
76-
}
77-
7872
private static int level(Role role) {
7973
return switch (role) {
8074
case USER -> 0;

‎src/main/java/com/DesktopApplicationClientJava/services/impl/FileServiceImpl.java‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import com.DesktopApplicationClientJava.Api.ApiClient;
44
import com.DesktopApplicationClientJava.Api.ApiException;
5-
import com.DesktopApplicationClientJava.Api.Dto.response.FileDto;
65
import com.DesktopApplicationClientJava.entities.FileInfo;
6+
import com.DesktopApplicationClientJava.mapper.FileMapper;
77
import com.DesktopApplicationClientJava.services.FileService;
88
import com.DesktopApplicationClientJava.services.ServiceException;
99
import java.io.IOException;
@@ -20,8 +20,7 @@ public FileServiceImpl(ApiClient apiClient) {
2020

2121
public FileInfo uploadFile(UUID resourceId, Path file) {
2222
try {
23-
FileDto dto = apiClient.uploadFile(resourceId, file);
24-
return new FileInfo(dto.uuid(), dto.name(), dto.contentType(), dto.size());
23+
return FileMapper.INSTANCE.toEntity(apiClient.uploadFile(resourceId, file));
2524
} catch (ApiException e) {
2625
throw new ServiceException(e.getMessage(), e.getStatus());
2726
} catch (IOException | InterruptedException e) {

‎src/main/java/com/DesktopApplicationClientJava/services/impl/ResourceServiceImpl.java‎

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55
import com.DesktopApplicationClientJava.Api.Dto.request.Resource.CreateResourceRequest;
66
import com.DesktopApplicationClientJava.Api.Dto.request.Resource.ResourceFilterRequest;
77
import com.DesktopApplicationClientJava.Api.Dto.request.Resource.UpdateResourceRequest;
8-
import com.DesktopApplicationClientJava.Api.Dto.response.ResourceDto;
9-
import com.DesktopApplicationClientJava.entities.FileInfo;
108
import com.DesktopApplicationClientJava.entities.Resource;
9+
import com.DesktopApplicationClientJava.mapper.ResourceMapper;
1110
import com.DesktopApplicationClientJava.services.ResourceService;
1211
import com.DesktopApplicationClientJava.services.ServiceException;
1312
import com.DesktopApplicationClientJava.services.filter.ResourceFilter;
@@ -29,7 +28,7 @@ public List<Resource> search(ResourceFilter filter, long offset, long count) {
2928
new ResourceFilterRequest(filter.getTitle(), filter.getDescription(), null, null);
3029
try {
3130
return apiClient.findResourcesByFilters(req, offset, count, "createdAt", "desc").stream()
32-
.map(this::toEntity)
31+
.map(ResourceMapper.INSTANCE::toEntity)
3332
.toList();
3433
} catch (ApiException e) {
3534
throw new ServiceException(e.getMessage(), e.getStatus());
@@ -41,7 +40,8 @@ public List<Resource> search(ResourceFilter filter, long offset, long count) {
4140
@Override
4241
public Resource create(String title, String description) {
4342
try {
44-
return toEntity(apiClient.createResource(new CreateResourceRequest(title, description)));
43+
return ResourceMapper.INSTANCE.toEntity(
44+
apiClient.createResource(new CreateResourceRequest(title, description)));
4545
} catch (ApiException e) {
4646
throw new ServiceException(e.getMessage(), e.getStatus());
4747
} catch (IOException | InterruptedException e) {
@@ -52,7 +52,7 @@ public Resource create(String title, String description) {
5252
@Override
5353
public Resource getById(UUID resourceId) {
5454
try {
55-
return toEntity(apiClient.findResourceById(resourceId));
55+
return ResourceMapper.INSTANCE.toEntity(apiClient.findResourceById(resourceId));
5656
} catch (ApiException e) {
5757
throw new ServiceException(e.getMessage(), e.getStatus());
5858
} catch (IOException | InterruptedException e) {
@@ -67,7 +67,7 @@ public List<Resource> search(
6767
new ResourceFilterRequest(filter.getTitle(), filter.getDescription(), null, null);
6868
try {
6969
return apiClient.findResourcesByFilters(req, offset, count, sortBy, sortDir).stream()
70-
.map(this::toEntity)
70+
.map(ResourceMapper.INSTANCE::toEntity)
7171
.toList();
7272
} catch (ApiException e) {
7373
throw new ServiceException(e.getMessage(), e.getStatus());
@@ -80,7 +80,7 @@ public List<Resource> search(
8080
public Resource update(UUID resourceId, String title, String description) {
8181
UpdateResourceRequest req = new UpdateResourceRequest(title, description);
8282
try {
83-
return toEntity(apiClient.updateResource(resourceId, req));
83+
return ResourceMapper.INSTANCE.toEntity(apiClient.updateResource(resourceId, req));
8484
} catch (ApiException e) {
8585
throw new ServiceException(e.getMessage(), e.getStatus());
8686
} catch (IOException | InterruptedException e) {
@@ -98,15 +98,4 @@ public void delete(UUID resourceId) {
9898
throw new ServiceException("Ошибка соединения с сервером");
9999
}
100100
}
101-
102-
private Resource toEntity(ResourceDto dto) {
103-
List<FileInfo> files =
104-
dto.files() == null
105-
? List.of()
106-
: dto.files().stream()
107-
.map(f -> new FileInfo(f.uuid(), f.name(), f.contentType(), f.size()))
108-
.toList();
109-
return new Resource(
110-
dto.uuid(), dto.title(), dto.description(), dto.createdAt(), dto.updatedAt(), files);
111-
}
112101
}

‎src/main/java/com/DesktopApplicationClientJava/services/impl/UsersServiceImpl.java‎

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import com.DesktopApplicationClientJava.Api.Dto.request.Admin.RegisterRequest;
66
import com.DesktopApplicationClientJava.Api.Dto.request.Admin.UpdateUserRequest;
77
import com.DesktopApplicationClientJava.Api.Dto.request.Admin.UserFilterRequest;
8-
import com.DesktopApplicationClientJava.Api.Dto.response.UserDto;
98
import com.DesktopApplicationClientJava.entities.User;
9+
import com.DesktopApplicationClientJava.mapper.UserMapper;
1010
import com.DesktopApplicationClientJava.services.ServiceException;
1111
import com.DesktopApplicationClientJava.services.UsersService;
1212
import com.DesktopApplicationClientJava.services.data.RegisterData;
@@ -37,7 +37,7 @@ public List<User> search(UserFilter filter, long offset, long count) {
3737

3838
try {
3939
return apiClient.getUsersByFilters(req, offset, count, "createdAt", "desc").stream()
40-
.map(this::toEntity)
40+
.map(UserMapper.INSTANCE::toEntity)
4141
.toList();
4242
} catch (ApiException e) {
4343
throw new ServiceException(e.getMessage(), e.getStatus());
@@ -49,7 +49,7 @@ public List<User> search(UserFilter filter, long offset, long count) {
4949
@Override
5050
public User getById(UUID uuid) {
5151
try {
52-
return toEntity(apiClient.getUserById(uuid));
52+
return UserMapper.INSTANCE.toEntity(apiClient.getUserById(uuid));
5353
} catch (ApiException e) {
5454
throw new ServiceException(e.getMessage(), e.getStatus());
5555
} catch (IOException | InterruptedException e) {
@@ -67,7 +67,7 @@ public User create(RegisterData data) {
6767
data.getFirstName(),
6868
data.getLastName());
6969
try {
70-
return toEntity(apiClient.createUser(req));
70+
return UserMapper.INSTANCE.toEntity(apiClient.createUser(req));
7171
} catch (ApiException e) {
7272
throw new ServiceException(e.getMessage(), e.getStatus());
7373
} catch (IOException | InterruptedException e) {
@@ -86,7 +86,7 @@ public User update(UUID uuid, UpdateUserData data) {
8686
data.getLastName());
8787

8888
try {
89-
return toEntity(apiClient.updateUser(uuid, req));
89+
return UserMapper.INSTANCE.toEntity(apiClient.updateUser(uuid, req));
9090
} catch (ApiException e) {
9191
throw new ServiceException(e.getMessage(), e.getStatus());
9292
} catch (IOException | InterruptedException e) {
@@ -104,9 +104,4 @@ public void delete(UUID uuid) {
104104
throw new ServiceException("Ошибка соединения с сервером");
105105
}
106106
}
107-
108-
private User toEntity(UserDto dto) {
109-
return new User(
110-
dto.uuid(), dto.email(), dto.role(), dto.firstName(), dto.lastName(), dto.enabled());
111-
}
112107
}

0 commit comments

Comments
 (0)