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
113 changes: 102 additions & 11 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,95 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.18.36</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>

<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>

<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>

<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.7</version>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>

</dependencies>

<build>
Expand All @@ -57,21 +139,30 @@
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<goals>
<goal>report</goal>
</goals>
<phase>test</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/com/daniilkhanukov/spring/myworks/api/TodoApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.daniilkhanukov.spring.myworks.api;


import com.daniilkhanukov.spring.myworks.dto.TodoDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Tag(name = "todo", description = "TODO API")
public interface TodoApi {

@Operation(summary = "List todos with pagination and optional completed filter")
@GetMapping("/api/todos")
List<TodoDto> list(@RequestParam(required = false) Boolean completed,
@RequestParam(defaultValue = "20") int limit,
@RequestParam(defaultValue = "0") int offset);

@Operation(summary = "Get todo")
@GetMapping("/api/todos/{id}")
TodoDto get(@PathVariable Long id);

@Operation(summary = "Create todo")
@PostMapping("/api/todos")
@ResponseStatus(code = HttpStatus.CREATED)
TodoDto create(@Valid @RequestBody TodoDto dto, HttpServletResponse response);

@Operation(summary = "Update todo")
@PutMapping("/api/todos/{id}")
TodoDto update(@PathVariable Long id, @Valid @RequestBody TodoDto dto);

@Operation(summary = "Delete todo")
@DeleteMapping("/api/todos/{id}")
@ResponseStatus(code = HttpStatus.NO_CONTENT)
void delete(@PathVariable Long id);

@Operation(summary = "Toggle completed")
@PatchMapping("/api/todos/{id}/toggle")
TodoDto toggle(@PathVariable Long id);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.daniilkhanukov.spring.myworks.config;

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableCaching
public class CacheConfig {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.daniilkhanukov.spring.myworks.controller;

import com.daniilkhanukov.spring.myworks.api.TodoApi;
import com.daniilkhanukov.spring.myworks.dto.TodoDto;
import com.daniilkhanukov.spring.myworks.entity.Todo;
import com.daniilkhanukov.spring.myworks.service.TodoService;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
public class TodoController implements TodoApi {

private final TodoService service;

public TodoController(TodoService service) {
this.service = service;
}

@Override
public List<TodoDto> list(Boolean completed, int limit, int offset) {
return service.getAll(completed, limit, offset);
}

@Override
public TodoDto get(Long id) {
return service.getById(id);
}

@Override
public TodoDto create(TodoDto dto, HttpServletResponse response) {
TodoDto created = service.create(dto);
// установить Location header без ResponseEntity
response.setHeader("Location", "/api/todos/" + created.getId());
return created;
}

@Override
public TodoDto update(Long id, TodoDto dto) {
return service.update(id, dto);
}

@Override
public void delete(Long id) {
service.delete(id);
}


@Override
public TodoDto toggle(Long id) {
return service.toggle(id);
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/daniilkhanukov/spring/myworks/dto/TodoDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.daniilkhanukov.spring.myworks.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDateTime;

@Getter
@Setter
@NoArgsConstructor
public class TodoDto {
private Long id;

@NotBlank(message = "Title is required")
@Size(max = 255, message = "Title must be at most 255 characters")
private String title;

private String description;
private Boolean completed;
private LocalDateTime createdAt;
}
18 changes: 18 additions & 0 deletions src/main/java/com/daniilkhanukov/spring/myworks/entity/Todo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.daniilkhanukov.spring.myworks.entity;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDateTime;

@Getter
@Setter
@NoArgsConstructor
public class Todo {
private Long id;
private String title;
private String description;
private boolean completed;
private LocalDateTime createdAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.daniilkhanukov.spring.myworks.exception;

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiError {

private String error;
private Object details;

public ApiError(String error, Object details) {
this.error = error;
this.details = details;
}

public ApiError(String error) {
this.error = error;
}

public String getError() {
return error;
}

public Object getDetails() {
return details;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.daniilkhanukov.spring.myworks.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
@ResponseBody
public ApiError handleNotFound(NotFoundException ex) {
return new ApiError("Not Found", ex.getMessage());
}

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseBody
public ApiError handleValidation(MethodArgumentNotValidException ex) {
Map<String,String> errors = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
return new ApiError("Validation Failed", errors);
}

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public ApiError handleAll(Exception ex) {
return new ApiError("Internal Error", ex.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.daniilkhanukov.spring.myworks.exception;

public class NotFoundException extends RuntimeException {
public NotFoundException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.daniilkhanukov.spring.myworks.mapper;

import com.daniilkhanukov.spring.myworks.dto.TodoDto;
import com.daniilkhanukov.spring.myworks.entity.Todo;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public interface TodoMapper {
TodoDto toDto(Todo todo);
Todo toEntity(TodoDto dto);
}
Loading