diff --git a/pom.xml b/pom.xml
index 2db888c..ac238ab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -51,11 +51,13 @@
h2
test
+
org.springframework.boot
spring-boot-starter-test
test
+
org.springframework.boot
spring-boot-starter-validation
diff --git a/src/main/java/ru/practicum/shareit/exception/DuplicateDataException.java b/src/main/java/ru/practicum/shareit/exception/DuplicateDataException.java
new file mode 100644
index 0000000..c453ae0
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/exception/DuplicateDataException.java
@@ -0,0 +1,7 @@
+package ru.practicum.shareit.exception;
+
+public class DuplicateDataException extends RuntimeException {
+ public DuplicateDataException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/ru/practicum/shareit/exception/ForbiddenAccessException.java b/src/main/java/ru/practicum/shareit/exception/ForbiddenAccessException.java
new file mode 100644
index 0000000..214dfa4
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/exception/ForbiddenAccessException.java
@@ -0,0 +1,7 @@
+package ru.practicum.shareit.exception;
+
+public class ForbiddenAccessException extends RuntimeException {
+ public ForbiddenAccessException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/ru/practicum/shareit/exception/NotFoundException.java b/src/main/java/ru/practicum/shareit/exception/NotFoundException.java
new file mode 100644
index 0000000..9dcff00
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/exception/NotFoundException.java
@@ -0,0 +1,13 @@
+package ru.practicum.shareit.exception;
+
+import java.util.function.Supplier;
+
+public class NotFoundException extends RuntimeException {
+ public NotFoundException(String message) {
+ super(message);
+ }
+
+ public static Supplier supplier(String message, Object... args) {
+ return () -> new NotFoundException(message.formatted(args));
+ }
+}
diff --git a/src/main/java/ru/practicum/shareit/exception/dto/ErrorResponse.java b/src/main/java/ru/practicum/shareit/exception/dto/ErrorResponse.java
new file mode 100644
index 0000000..4e2e596
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/exception/dto/ErrorResponse.java
@@ -0,0 +1,4 @@
+package ru.practicum.shareit.exception.dto;
+
+public record ErrorResponse(String name, String message) {
+}
diff --git a/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java b/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java
new file mode 100644
index 0000000..497330f
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/exception/dto/ValidationErrorResponse.java
@@ -0,0 +1,6 @@
+package ru.practicum.shareit.exception.dto;
+
+import java.util.List;
+
+public record ValidationErrorResponse(List violations) {
+}
diff --git a/src/main/java/ru/practicum/shareit/exception/dto/Violation.java b/src/main/java/ru/practicum/shareit/exception/dto/Violation.java
new file mode 100644
index 0000000..e01906e
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/exception/dto/Violation.java
@@ -0,0 +1,4 @@
+package ru.practicum.shareit.exception.dto;
+
+public record Violation(String fieldName, String message) {
+}
diff --git a/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java b/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java
new file mode 100644
index 0000000..8e95630
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/exception/handler/GlobalExceptionHandler.java
@@ -0,0 +1,81 @@
+package ru.practicum.shareit.exception.handler;
+
+import jakarta.validation.ConstraintViolationException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import ru.practicum.shareit.exception.DuplicateDataException;
+import ru.practicum.shareit.exception.ForbiddenAccessException;
+import ru.practicum.shareit.exception.NotFoundException;
+import ru.practicum.shareit.exception.dto.ErrorResponse;
+import ru.practicum.shareit.exception.dto.ValidationErrorResponse;
+import ru.practicum.shareit.exception.dto.Violation;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Slf4j
+@RestControllerAdvice
+class GlobalExceptionHandler {
+
+ @ExceptionHandler
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public ErrorResponse onException(Exception ex) {
+ log.error("Error occurred while processing request {}", ex.getMessage());
+ return new ErrorResponse("internal server error",
+ "An error occurred while processing request");
+ }
+
+ @ExceptionHandler(ConstraintViolationException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ValidationErrorResponse onConstraintValidationException(
+ ConstraintViolationException ex
+ ) {
+ final List violations = ex.getConstraintViolations().stream()
+ .map(
+ violation -> new Violation(
+ violation.getPropertyPath().toString(),
+ violation.getMessage()
+ )
+ )
+ .collect(Collectors.toList());
+ log.warn(violations.toString());
+ return new ValidationErrorResponse(violations);
+ }
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ValidationErrorResponse onMethodArgumentNotValidException(
+ MethodArgumentNotValidException ex
+ ) {
+ final List violations = ex.getBindingResult().getFieldErrors().stream()
+ .map(error -> new Violation(error.getField(), error.getDefaultMessage()))
+ .collect(Collectors.toList());
+ log.warn(violations.toString());
+ return new ValidationErrorResponse(violations);
+ }
+
+ @ExceptionHandler(NotFoundException.class)
+ @ResponseStatus(HttpStatus.NOT_FOUND)
+ public ErrorResponse onNotFoundException(NotFoundException ex) {
+ log.warn("Not found exception occurred while processing request {}", ex.getMessage());
+ return new ErrorResponse("not found", ex.getMessage());
+ }
+
+ @ExceptionHandler(DuplicateDataException.class)
+ @ResponseStatus(HttpStatus.CONFLICT)
+ public ErrorResponse onDuplicateDataException(DuplicateDataException ex) {
+ log.warn("Duplicate data exception occurred while processing request {}", ex.getMessage());
+ return new ErrorResponse("duplicate data", ex.getMessage());
+ }
+
+ @ExceptionHandler(ForbiddenAccessException.class)
+ @ResponseStatus(HttpStatus.FORBIDDEN)
+ public ErrorResponse onForbiddenAccessException(ForbiddenAccessException ex) {
+ log.warn("Forbidden access exception occurred while processing request {}", ex.getMessage());
+ return new ErrorResponse("forbidden", ex.getMessage());
+ }
+}
diff --git a/src/main/java/ru/practicum/shareit/item/ItemController.java b/src/main/java/ru/practicum/shareit/item/ItemController.java
index bb17668..36ff3be 100644
--- a/src/main/java/ru/practicum/shareit/item/ItemController.java
+++ b/src/main/java/ru/practicum/shareit/item/ItemController.java
@@ -1,12 +1,61 @@
package ru.practicum.shareit.item;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.shareit.item.dto.ItemDto;
+import ru.practicum.shareit.item.dto.NewItemDto;
+import ru.practicum.shareit.item.dto.UpdateItemDto;
-/**
- * TODO Sprint add-controllers.
- */
+import java.util.Collection;
+
+@Slf4j
+@Validated
@RestController
@RequestMapping("/items")
+@RequiredArgsConstructor
public class ItemController {
+ private static final String SHARER_USER_ID_HEADER = "X-Sharer-User-Id";
+ private final ItemService itemService;
+
+ @GetMapping("/{itemId}")
+ public ItemDto getItem(
+ @RequestHeader(SHARER_USER_ID_HEADER) long userId,
+ @PathVariable long itemId
+ ) {
+ return itemService.getItemOfUserById(userId, itemId);
+ }
+
+ @GetMapping
+ public Collection getItems(
+ @RequestHeader(SHARER_USER_ID_HEADER) long userId
+ ) {
+ return itemService.getAllItemsOfUser(userId);
+ }
+
+ @PostMapping
+ public ItemDto createItem(
+ @RequestHeader(SHARER_USER_ID_HEADER) long userId,
+ @RequestBody @Valid NewItemDto newItemDto
+ ) {
+ return itemService.saveItem(userId, newItemDto);
+ }
+
+ @PatchMapping("/{itemId}")
+ public ItemDto updateItem(
+ @RequestHeader(SHARER_USER_ID_HEADER) long userId,
+ @PathVariable long itemId,
+ @RequestBody @Valid UpdateItemDto updatedItem
+ ) {
+ return itemService.updateItem(userId, itemId, updatedItem);
+ }
+
+ @GetMapping("/search")
+ public Collection searchItems(
+ @RequestParam(name = "text") String query
+ ) {
+ return itemService.searchItems(query);
+ }
}
diff --git a/src/main/java/ru/practicum/shareit/item/ItemMapper.java b/src/main/java/ru/practicum/shareit/item/ItemMapper.java
new file mode 100644
index 0000000..93d3883
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/ItemMapper.java
@@ -0,0 +1,45 @@
+package ru.practicum.shareit.item;
+
+import lombok.experimental.UtilityClass;
+import ru.practicum.shareit.item.dto.ItemDto;
+import ru.practicum.shareit.item.dto.NewItemDto;
+import ru.practicum.shareit.item.dto.UpdateItemDto;
+import ru.practicum.shareit.item.model.Item;
+
+@UtilityClass
+public class ItemMapper {
+
+ public ItemDto toItemDto(Item item) {
+ return new ItemDto(
+ item.getId(),
+ item.getName(),
+ item.getDescription(),
+ item.isAvailable(),
+ item.getRequest() != null ? item.getRequest().getId() : null
+ );
+ }
+
+ public Item toItem(NewItemDto newItemDto) {
+ Item item = new Item();
+ item.setName(newItemDto.name());
+ item.setDescription(newItemDto.description());
+ item.setAvailable(newItemDto.available());
+ return item;
+ }
+
+ public Item updateItem(Item item, UpdateItemDto updateItemDto) {
+ if (updateItemDto.hasName()) {
+ item.setName(updateItemDto.name());
+ }
+
+ if (updateItemDto.hasDescription()) {
+ item.setDescription(updateItemDto.description());
+ }
+
+ if (updateItemDto.hasAvailable()) {
+ item.setAvailable(updateItemDto.available());
+ }
+
+ return item;
+ }
+}
diff --git a/src/main/java/ru/practicum/shareit/item/ItemService.java b/src/main/java/ru/practicum/shareit/item/ItemService.java
new file mode 100644
index 0000000..86db07f
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/ItemService.java
@@ -0,0 +1,19 @@
+package ru.practicum.shareit.item;
+
+import ru.practicum.shareit.item.dto.ItemDto;
+import ru.practicum.shareit.item.dto.NewItemDto;
+import ru.practicum.shareit.item.dto.UpdateItemDto;
+
+import java.util.Collection;
+
+public interface ItemService {
+ ItemDto getItemOfUserById(long userId, long itemId);
+
+ Collection getAllItemsOfUser(long userId);
+
+ ItemDto saveItem(long userId, NewItemDto newItem);
+
+ ItemDto updateItem(long userId, long itemId, UpdateItemDto updatedItem);
+
+ Collection searchItems(String query);
+}
diff --git a/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java
new file mode 100644
index 0000000..13609f2
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/ItemServiceImpl.java
@@ -0,0 +1,90 @@
+package ru.practicum.shareit.item;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import ru.practicum.shareit.exception.ForbiddenAccessException;
+import ru.practicum.shareit.exception.NotFoundException;
+import ru.practicum.shareit.item.dto.ItemDto;
+import ru.practicum.shareit.item.dto.NewItemDto;
+import ru.practicum.shareit.item.dto.UpdateItemDto;
+import ru.practicum.shareit.item.model.Item;
+import ru.practicum.shareit.item.repository.ItemRepository;
+import ru.practicum.shareit.user.model.User;
+import ru.practicum.shareit.user.repository.UserRepository;
+
+import java.util.Collection;
+import java.util.Collections;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ItemServiceImpl implements ItemService {
+ private final ItemRepository itemRepository;
+ private final UserRepository userRepository;
+
+ @Override
+ public ItemDto getItemOfUserById(long userId, long itemId) {
+ getUserOrThrow(userId);
+
+ return itemRepository.findById(itemId)
+ .map(ItemMapper::toItemDto)
+ .orElseThrow(NotFoundException.supplier("item with id %d not found", itemId));
+ }
+
+ @Override
+ public Collection getAllItemsOfUser(long userId) {
+ getUserOrThrow(userId);
+
+ return itemRepository.findAllByUserId(userId)
+ .stream()
+ .map(ItemMapper::toItemDto)
+ .toList();
+ }
+
+ @Override
+ public ItemDto saveItem(long userId, NewItemDto newItem) {
+ User owner = getUserOrThrow(userId);
+ Item item = ItemMapper.toItem(newItem);
+ item.setOwner(owner);
+ item = itemRepository.save(item);
+ return ItemMapper.toItemDto(item);
+ }
+
+ @Override
+ public ItemDto updateItem(long userId, long itemId, UpdateItemDto newItem) {
+ getUserOrThrow(userId);
+ Item item = getItemOrThrow(itemId);
+ if (item.getOwner().getId() != userId) {
+ throw new ForbiddenAccessException("Only owner of item can update it");
+ }
+ Item updatedItem = ItemMapper.updateItem(item, newItem);
+ itemRepository.save(updatedItem);
+ return ItemMapper.toItemDto(updatedItem);
+ }
+
+
+ @Override
+ public Collection searchItems(String query) {
+ if (query.isBlank()) {
+ return Collections.emptyList();
+ }
+
+ return itemRepository.searchItems(query)
+ .stream()
+ .map(ItemMapper::toItemDto)
+ .toList();
+ }
+
+ private Item getItemOrThrow(long itemId) {
+ return itemRepository.findById(itemId).orElseThrow(
+ NotFoundException.supplier("Item with id %d not found", itemId)
+ );
+ }
+
+ private User getUserOrThrow(long userId) {
+ return userRepository.findById(userId).orElseThrow(
+ NotFoundException.supplier("User with id %d not found", userId)
+ );
+ }
+}
diff --git a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java
index 9319d7d..30863dd 100644
--- a/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java
+++ b/src/main/java/ru/practicum/shareit/item/dto/ItemDto.java
@@ -1,7 +1,10 @@
package ru.practicum.shareit.item.dto;
-/**
- * TODO Sprint add-controllers.
- */
-public class ItemDto {
+public record ItemDto(
+ Long id,
+ String name,
+ String description,
+ boolean available,
+ Long requestId
+) {
}
diff --git a/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java b/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java
new file mode 100644
index 0000000..65648c3
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/dto/NewItemDto.java
@@ -0,0 +1,11 @@
+package ru.practicum.shareit.item.dto;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+
+public record NewItemDto(
+ @NotBlank String name,
+ @NotBlank String description,
+ @NotNull Boolean available
+) {
+}
diff --git a/src/main/java/ru/practicum/shareit/item/dto/UpdateItemDto.java b/src/main/java/ru/practicum/shareit/item/dto/UpdateItemDto.java
new file mode 100644
index 0000000..8332e19
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/dto/UpdateItemDto.java
@@ -0,0 +1,19 @@
+package ru.practicum.shareit.item.dto;
+
+public record UpdateItemDto(
+ String name,
+ String description,
+ Boolean available
+) {
+ public boolean hasName() {
+ return name != null && !name.isBlank();
+ }
+
+ public boolean hasDescription() {
+ return description != null && !description.isBlank();
+ }
+
+ public boolean hasAvailable() {
+ return available != null;
+ }
+}
diff --git a/src/main/java/ru/practicum/shareit/item/model/Item.java b/src/main/java/ru/practicum/shareit/item/model/Item.java
index 44eb73d..775a4cd 100644
--- a/src/main/java/ru/practicum/shareit/item/model/Item.java
+++ b/src/main/java/ru/practicum/shareit/item/model/Item.java
@@ -1,7 +1,23 @@
package ru.practicum.shareit.item.model;
-/**
- * TODO Sprint add-controllers.
- */
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.FieldDefaults;
+import ru.practicum.shareit.request.model.ItemRequest;
+import ru.practicum.shareit.user.model.User;
+
+@Data
+@FieldDefaults(level = AccessLevel.PRIVATE)
+@NoArgsConstructor
+@AllArgsConstructor
public class Item {
+ Long id;
+ String name;
+ String description;
+ boolean available;
+ User owner;
+ ItemRequest request;
+
}
diff --git a/src/main/java/ru/practicum/shareit/item/repository/InMemoryItemRepository.java b/src/main/java/ru/practicum/shareit/item/repository/InMemoryItemRepository.java
new file mode 100644
index 0000000..a74f783
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/repository/InMemoryItemRepository.java
@@ -0,0 +1,56 @@
+package ru.practicum.shareit.item.repository;
+
+import org.springframework.stereotype.Repository;
+import ru.practicum.shareit.item.model.Item;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+@Repository
+public class InMemoryItemRepository implements ItemRepository {
+ private final Map items = new HashMap<>();
+
+ @Override
+ public Optional- findById(long id) {
+ return Optional.ofNullable(items.get(id));
+ }
+
+ @Override
+ public Collection
- findAllByUserId(long userId) {
+ return items.values()
+ .stream()
+ .filter(item -> item.getOwner().getId() == userId)
+ .toList();
+ }
+
+ @Override
+ public Item save(Item item) {
+ Long id = generateNextId();
+ item.setId(id);
+
+ items.put(id, item);
+
+ return item;
+ }
+
+ @Override
+ public Collection
- searchItems(String query) {
+ String lowercaseQuery = query.trim().toLowerCase();
+
+ return items.values()
+ .stream()
+ .filter(Item::isAvailable)
+ .filter(item -> item.getName().toLowerCase().contains(lowercaseQuery)
+ || item.getDescription().toLowerCase().contains(lowercaseQuery))
+ .toList();
+ }
+
+ private Long generateNextId() {
+ Long nextId = items.keySet().stream()
+ .max(Long::compareTo)
+ .orElse(0L);
+ return ++nextId;
+ }
+}
diff --git a/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java b/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java
new file mode 100644
index 0000000..6426fb6
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/item/repository/ItemRepository.java
@@ -0,0 +1,16 @@
+package ru.practicum.shareit.item.repository;
+
+import ru.practicum.shareit.item.model.Item;
+
+import java.util.Collection;
+import java.util.Optional;
+
+public interface ItemRepository {
+ Optional
- findById(long id);
+
+ Collection
- findAllByUserId(long userId);
+
+ Item save(Item item);
+
+ Collection
- searchItems(String query);
+}
diff --git a/src/main/java/ru/practicum/shareit/request/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/ItemRequest.java
deleted file mode 100644
index 95d6f23..0000000
--- a/src/main/java/ru/practicum/shareit/request/ItemRequest.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package ru.practicum.shareit.request;
-
-/**
- * TODO Sprint add-item-requests.
- */
-public class ItemRequest {
-}
diff --git a/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java
new file mode 100644
index 0000000..6f4ab59
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/request/model/ItemRequest.java
@@ -0,0 +1,14 @@
+package ru.practicum.shareit.request.model;
+
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+
+/**
+ * TODO Sprint add-item-requests.
+ */
+@Data
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class ItemRequest {
+ Long id;
+}
diff --git a/src/main/java/ru/practicum/shareit/user/User.java b/src/main/java/ru/practicum/shareit/user/User.java
deleted file mode 100644
index ae6e7f3..0000000
--- a/src/main/java/ru/practicum/shareit/user/User.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package ru.practicum.shareit.user;
-
-/**
- * TODO Sprint add-controllers.
- */
-public class User {
-}
diff --git a/src/main/java/ru/practicum/shareit/user/UserController.java b/src/main/java/ru/practicum/shareit/user/UserController.java
index 03039b9..8516884 100644
--- a/src/main/java/ru/practicum/shareit/user/UserController.java
+++ b/src/main/java/ru/practicum/shareit/user/UserController.java
@@ -1,12 +1,58 @@
package ru.practicum.shareit.user;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import ru.practicum.shareit.user.dto.NewUserDto;
+import ru.practicum.shareit.user.dto.UpdateUserDto;
+import ru.practicum.shareit.user.dto.UserDto;
-/**
- * TODO Sprint add-controllers.
- */
+import java.util.Collection;
+
+@Slf4j
+@Validated
@RestController
-@RequestMapping(path = "/users")
+@RequestMapping("/users")
+@RequiredArgsConstructor
public class UserController {
+ private final UserService userService;
+
+ @GetMapping
+ public Collection getAllUsers() {
+ log.trace("get all users requested");
+ return userService.findAll();
+ }
+
+ @GetMapping("/{userId}")
+ public UserDto getUser(@PathVariable long userId) {
+ log.trace("get user requested with id: {}", userId);
+ return userService.findById(userId);
+ }
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.CREATED)
+ public UserDto createUser(@RequestBody @Valid NewUserDto newUserDto) {
+ log.trace("create user requested with body: {}", newUserDto);
+ return userService.save(newUserDto);
+ }
+
+ @PatchMapping("/{userId}")
+ public UserDto updateUser(
+ @RequestBody @Valid UpdateUserDto updateUserDto,
+ @PathVariable long userId
+ ) {
+ updateUserDto.setId(userId);
+ log.trace("update user requested with id: {} and body {}", userId, updateUserDto);
+ return userService.update(updateUserDto);
+ }
+
+ @DeleteMapping("/{userId}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void deleteUser(@PathVariable long userId) {
+ log.trace("delete user requested with id: {}", userId);
+ userService.delete(userId);
+ }
}
diff --git a/src/main/java/ru/practicum/shareit/user/UserMapper.java b/src/main/java/ru/practicum/shareit/user/UserMapper.java
new file mode 100644
index 0000000..f94c933
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/user/UserMapper.java
@@ -0,0 +1,37 @@
+package ru.practicum.shareit.user;
+
+import lombok.experimental.UtilityClass;
+import ru.practicum.shareit.user.dto.NewUserDto;
+import ru.practicum.shareit.user.dto.UpdateUserDto;
+import ru.practicum.shareit.user.dto.UserDto;
+import ru.practicum.shareit.user.model.User;
+
+@UtilityClass
+public class UserMapper {
+
+ public UserDto toUserDto(User user) {
+ return new UserDto(
+ user.getId(),
+ user.getName(),
+ user.getEmail());
+ }
+
+ public User toUser(NewUserDto newUser) {
+ User user = new User();
+ user.setName(newUser.name());
+ user.setEmail(newUser.email());
+ return user;
+ }
+
+ public User updateUser(User user, UpdateUserDto updatedUser) {
+ if (updatedUser.hasName()) {
+ user.setName(updatedUser.getName());
+ }
+
+ if (updatedUser.hasEmail()) {
+ user.setEmail(updatedUser.getEmail());
+ }
+
+ return user;
+ }
+}
diff --git a/src/main/java/ru/practicum/shareit/user/UserService.java b/src/main/java/ru/practicum/shareit/user/UserService.java
new file mode 100644
index 0000000..3bdf54a
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/user/UserService.java
@@ -0,0 +1,19 @@
+package ru.practicum.shareit.user;
+
+import ru.practicum.shareit.user.dto.NewUserDto;
+import ru.practicum.shareit.user.dto.UpdateUserDto;
+import ru.practicum.shareit.user.dto.UserDto;
+
+import java.util.Collection;
+
+public interface UserService {
+ Collection findAll();
+
+ UserDto findById(long id);
+
+ UserDto save(NewUserDto user);
+
+ UserDto update(UpdateUserDto user);
+
+ void delete(long id);
+}
diff --git a/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java b/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java
new file mode 100644
index 0000000..bb48c34
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/user/UserServiceImpl.java
@@ -0,0 +1,60 @@
+package ru.practicum.shareit.user;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import ru.practicum.shareit.exception.NotFoundException;
+import ru.practicum.shareit.user.dto.NewUserDto;
+import ru.practicum.shareit.user.dto.UpdateUserDto;
+import ru.practicum.shareit.user.dto.UserDto;
+import ru.practicum.shareit.user.model.User;
+import ru.practicum.shareit.user.repository.UserRepository;
+
+import java.util.Collection;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class UserServiceImpl implements UserService {
+ private final UserRepository userRepository;
+
+ @Override
+ public Collection findAll() {
+ return userRepository.findAll()
+ .stream()
+ .map(UserMapper::toUserDto)
+ .toList();
+ }
+
+ @Override
+ public UserDto findById(long id) {
+ return UserMapper.toUserDto(getUserOrThrow(id));
+ }
+
+ @Override
+ public UserDto save(NewUserDto newUser) {
+ User user = UserMapper.toUser(newUser);
+ user = userRepository.save(user);
+ return UserMapper.toUserDto(user);
+ }
+
+ @Override
+ public UserDto update(UpdateUserDto newUser) {
+ User user = getUserOrThrow(newUser.getId());
+ User updatedUser = UserMapper.updateUser(user, newUser);
+ userRepository.update(updatedUser);
+ return UserMapper.toUserDto(updatedUser);
+ }
+
+ @Override
+ public void delete(long id) {
+ getUserOrThrow(id);
+ userRepository.delete(id);
+ }
+
+ private User getUserOrThrow(long id) {
+ return userRepository.findById(id).orElseThrow(
+ NotFoundException.supplier("User with id %d not found", id)
+ );
+ }
+}
diff --git a/src/main/java/ru/practicum/shareit/user/dto/NewUserDto.java b/src/main/java/ru/practicum/shareit/user/dto/NewUserDto.java
new file mode 100644
index 0000000..3351658
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/user/dto/NewUserDto.java
@@ -0,0 +1,11 @@
+package ru.practicum.shareit.user.dto;
+
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+
+public record NewUserDto(
+ @NotBlank String name,
+ @NotNull @Email String email
+) {
+}
diff --git a/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java b/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java
new file mode 100644
index 0000000..5bbd748
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/user/dto/UpdateUserDto.java
@@ -0,0 +1,26 @@
+package ru.practicum.shareit.user.dto;
+
+import jakarta.validation.constraints.Email;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.experimental.FieldDefaults;
+
+@Data
+@AllArgsConstructor
+@FieldDefaults(level = AccessLevel.PRIVATE)
+public class UpdateUserDto {
+ Long id;
+ String name;
+
+ @Email
+ String email;
+
+ public boolean hasName() {
+ return name != null && !name.isBlank();
+ }
+
+ public boolean hasEmail() {
+ return email != null;
+ }
+}
diff --git a/src/main/java/ru/practicum/shareit/user/dto/UserDto.java b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java
new file mode 100644
index 0000000..96feaeb
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/user/dto/UserDto.java
@@ -0,0 +1,4 @@
+package ru.practicum.shareit.user.dto;
+
+public record UserDto(Long id, String name, String email) {
+}
diff --git a/src/main/java/ru/practicum/shareit/user/model/User.java b/src/main/java/ru/practicum/shareit/user/model/User.java
new file mode 100644
index 0000000..7cd8314
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/user/model/User.java
@@ -0,0 +1,17 @@
+package ru.practicum.shareit.user.model;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.FieldDefaults;
+
+@Data
+@FieldDefaults(level = AccessLevel.PRIVATE)
+@NoArgsConstructor
+@AllArgsConstructor
+public class User {
+ Long id;
+ String name;
+ String email;
+}
diff --git a/src/main/java/ru/practicum/shareit/user/repository/InMemoryUserRepository.java b/src/main/java/ru/practicum/shareit/user/repository/InMemoryUserRepository.java
new file mode 100644
index 0000000..7d7bd18
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/user/repository/InMemoryUserRepository.java
@@ -0,0 +1,74 @@
+package ru.practicum.shareit.user.repository;
+
+import org.springframework.stereotype.Repository;
+import ru.practicum.shareit.exception.DuplicateDataException;
+import ru.practicum.shareit.user.model.User;
+
+import java.util.*;
+
+@Repository
+public class InMemoryUserRepository implements UserRepository {
+ private final Map users = new HashMap<>();
+ private final Set userEmails = new HashSet<>();
+
+ @Override
+ public Collection findAll() {
+ return Collections.unmodifiableCollection(users.values());
+ }
+
+ @Override
+ public Optional findById(long id) {
+ User user = users.get(id);
+ if (user == null) {
+ return Optional.empty();
+ }
+
+ return Optional.of(new User(
+ user.getId(),
+ user.getName(),
+ user.getEmail()));
+ }
+
+ @Override
+ public User save(User user) {
+ Long id = generateNextId();
+ user.setId(id);
+
+ if (userEmails.contains(user.getEmail())) {
+ throw new DuplicateDataException("email %s already exists".formatted(user.getEmail()));
+ }
+
+ users.put(id, user);
+ userEmails.add(user.getEmail());
+ return user;
+ }
+
+ @Override
+ public void update(User user) {
+ User currentUser = users.get(user.getId());
+
+ if (!currentUser.getEmail().equals(user.getEmail())) {
+ if (userEmails.contains(user.getEmail())) {
+ throw new DuplicateDataException("email %s already exists".formatted(user.getEmail()));
+ }
+
+ userEmails.remove(currentUser.getEmail());
+ userEmails.add(user.getEmail());
+ }
+
+ users.put(user.getId(), user);
+ }
+
+ @Override
+ public void delete(long id) {
+ User removedUser = users.remove(id);
+ userEmails.remove(removedUser.getEmail());
+ }
+
+ private Long generateNextId() {
+ Long nextId = users.keySet().stream()
+ .max(Long::compareTo)
+ .orElse(0L);
+ return ++nextId;
+ }
+}
diff --git a/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java b/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java
new file mode 100644
index 0000000..be812dc
--- /dev/null
+++ b/src/main/java/ru/practicum/shareit/user/repository/UserRepository.java
@@ -0,0 +1,19 @@
+package ru.practicum.shareit.user.repository;
+
+import ru.practicum.shareit.user.model.User;
+
+import java.util.Collection;
+import java.util.Optional;
+
+public interface UserRepository {
+
+ Collection findAll();
+
+ Optional findById(long id);
+
+ User save(User user);
+
+ void update(User user);
+
+ void delete(long id);
+}