add openapi documentation

This commit is contained in:
2025-11-27 06:02:57 +02:00
parent 795d01b737
commit 5590e0a134
9 changed files with 379 additions and 15 deletions

View File

@@ -18,6 +18,9 @@ dependencies {
implementation(libs.spring.boot.starter.hateoas)
developmentOnly(libs.spring.boot.devtools)
// openapi docs
implementation(libs.springdoc.openapi.starter.webmvc.ui)
// xml
implementation(libs.jackson.dataformat.xml)

View File

@@ -2,6 +2,7 @@
junit = "6.0.1"
spring-boot-plugin = "4.0.0"
jspecify = "1.0.0"
springdoc = "3.0.0"
[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot-plugin" }
@@ -13,6 +14,9 @@ spring-boot-starter-web-test = { group = "org.springframework.boot", name = "spr
spring-boot-starter-hateoas = { group = "org.springframework.boot", name = "spring-boot-starter-hateoas" }
spring-boot-devtools = { group = "org.springframework.boot", name = "spring-boot-devtools" }
# openapi
springdoc-openapi-starter-webmvc-ui = { group = "org.springdoc", name = "springdoc-openapi-starter-webmvc-ui", version.ref = "springdoc" }
# xml
jackson-dataformat-xml = { group = "tools.jackson.dataformat", name = "jackson-dataformat-xml" }

View File

@@ -0,0 +1,10 @@
package ua.com.dxrkness.client;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class ApplicationClient {
}

View File

@@ -1,5 +1,10 @@
package ua.com.dxrkness.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.jspecify.annotations.NullMarked;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
@@ -15,6 +20,7 @@ import java.util.List;
produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE},
consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}
)
@Tag(name = "Freight", description = "Freights management")
@NullMarked
public class FreightController {
private final FreightService service;
@@ -23,31 +29,123 @@ public class FreightController {
this.service = service;
}
@Operation(
summary = "Retrieve all freights",
description = "Gets a list of all freight records in the system"
)
@ApiResponse(
responseCode = "200",
description = "Successfully retrieved list of freights (may be empty!)",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Freight.class)
)
)
@GetMapping(consumes = MediaType.ALL_VALUE)
public List<Freight> getAll() {
return service.getAll();
}
@Operation(
summary = "Get freight by ID",
description = "Finds a freight record by its id"
)
@ApiResponse(
responseCode = "200",
description = "Successfully retrieved freight",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Freight.class)
)
)
@ApiResponse(
responseCode = "404",
description = "Freight not found"
)
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
public ResponseEntity<Freight> getById(@PathVariable("id") long id) {
return ResponseEntity.ofNullable(service.getById(id));
}
@Operation(
summary = "Create a new freight",
description = "Adds a new freight"
)
@ApiResponse(
responseCode = "200",
description = "Freight successfully created",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Freight.class)
)
)
@ApiResponse(
responseCode = "400",
description = "Invalid input"
)
@PostMapping
public ResponseEntity<Freight> add(@RequestBody Freight newFreight) {
return ResponseEntity.ok(service.add(newFreight));
}
@Operation(
summary = "Update a freight (full)",
description = "Updates all fields of an existing freight record"
)
@ApiResponse(
responseCode = "200",
description = "Freight successfully updated",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Freight.class)
)
)
@ApiResponse(
responseCode = "400",
description = "Invalid input"
)
@PutMapping("/{id}")
public ResponseEntity<Freight> update(@PathVariable("id") long id, @RequestBody Freight newFreight) {
return ResponseEntity.ok(service.update(id, newFreight));
}
@Operation(
summary = "Update a freight (partial)",
description = "Updates specific fields of an existing freight record"
)
@ApiResponse(
responseCode = "200",
description = "Freight successfully updated",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Freight.class)
)
)
@ApiResponse(
responseCode = "400",
description = "Invalid input"
)
@PatchMapping("/{id}")
public ResponseEntity<Freight> updatePatch(@PathVariable("id") long id, @RequestBody Freight newFreight) {
return ResponseEntity.ok(service.update(id, newFreight));
}
@Operation(
summary = "Delete a freight",
description = "Removes a freight"
)
@ApiResponse(
responseCode = "200",
description = "Freight successfully deleted",
content = @Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = Freight.class)
)
)
@ApiResponse(
responseCode = "404",
description = "Freight not found"
)
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
public ResponseEntity<Freight> delete(@PathVariable("id") long id) {
return ResponseEntity.ofNullable(service.delete(id));

View File

@@ -1,5 +1,11 @@
package ua.com.dxrkness.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
@@ -16,6 +22,7 @@ import java.util.List;
produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE},
consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}
)
@Tag(name = "Route", description = "Routes management")
@NullMarked
public class RouteController {
private final RouteService service;
@@ -24,9 +31,23 @@ public class RouteController {
this.service = service;
}
@Operation(
summary = "Get routes",
description = "Get all routes or filter by freight ID/vehicle ID. May result in empty list!"
)
@ApiResponse(
responseCode = "200",
description = "Successfully retrieved routes",
content = @Content(
schema = @Schema(implementation = Route.class)
)
)
@GetMapping(consumes = MediaType.ALL_VALUE)
public List<Route> getAll(@RequestParam(name = "freightId", required = false) @Nullable Long freightId,
@RequestParam(name = "vehicleId", required = false) @Nullable Long vehicleId) {
public List<Route> getAll(
@Parameter(description = "Filter routes by freight ID")
@RequestParam(name = "freightId", required = false) @Nullable Long freightId,
@Parameter(description = "Filter routes by vehicle ID")
@RequestParam(name = "vehicleId", required = false) @Nullable Long vehicleId) {
if (vehicleId != null) {
return service.getByVehicleId(vehicleId);
}
@@ -40,28 +61,134 @@ public class RouteController {
return service.getAll();
}
@Operation(
summary = "Create new route",
description = "Add new route"
)
@ApiResponse(
responseCode = "200",
description = "Route successfully created",
content = @Content(
schema = @Schema(implementation = Route.class)
)
)
@ApiResponse(
responseCode = "400",
description = "Invalid route data provided"
)
@PostMapping
public ResponseEntity<Route> add(@RequestBody Route newRoute) {
public ResponseEntity<Route> add(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Route object to be created",
required = true,
content = @Content(
schema = @Schema(implementation = Route.class)
)
)
@RequestBody Route newRoute) {
return ResponseEntity.ok(service.add(newRoute));
}
@Operation(
summary = "Update a route (full)",
description = "Update all fields of an existing route by ID"
)
@ApiResponse(
responseCode = "200",
description = "Route successfully updated",
content = @Content(
schema = @Schema(implementation = Route.class)
)
)
@ApiResponse(
responseCode = "400",
description = "Invalid route data provided"
)
@PutMapping("/{id}")
public ResponseEntity<Route> update(@PathVariable("id") long id, @RequestBody Route newRoute) {
public ResponseEntity<Route> update(
@Parameter(description = "ID of the route to update", required = true)
@PathVariable("id") long id,
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Updated route object",
required = true,
content = @Content(
schema = @Schema(implementation = Route.class)
)
)
@RequestBody Route newRoute) {
return ResponseEntity.ok(service.update(id, newRoute));
}
@Operation(
summary = "Update a route (partial)",
description = "Update specific fields of an existing route by ID"
)
@ApiResponse(
responseCode = "200",
description = "Route successfully updated",
content = @Content(
schema = @Schema(implementation = Route.class)
)
)
@ApiResponse(
responseCode = "400",
description = "Invalid route data provided"
)
@PatchMapping("/{id}")
public ResponseEntity<Route> updatePatch(@PathVariable("id") long id, @RequestBody Route newRoute) {
public ResponseEntity<Route> updatePatch(
@Parameter(description = "ID of the route to update", required = true)
@PathVariable("id") long id,
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Route object with fields to update",
required = true,
content = @Content(
schema = @Schema(implementation = Route.class)
)
)
@RequestBody Route newRoute) {
return ResponseEntity.ok(service.update(id, newRoute));
}
@Operation(
summary = "Retrieve a route by ID",
description = "Get a single route by its ID"
)
@ApiResponse(
responseCode = "200",
description = "Route found",
content = @Content(
schema = @Schema(implementation = Route.class)
)
)
@ApiResponse(
responseCode = "404",
description = "Route not found"
)
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
public ResponseEntity<Route> getById(@PathVariable("id") long id) {
public ResponseEntity<Route> getById(
@Parameter(description = "ID of the route to retrieve", required = true)
@PathVariable("id") long id) {
return ResponseEntity.ofNullable(service.getById(id));
}
@Operation(
summary = "Delete a route",
description = "Delete a route by ID"
)
@ApiResponse(
responseCode = "200",
description = "Route successfully deleted",
content = @Content(schema = @Schema(implementation = Route.class)
)
)
@ApiResponse(
responseCode = "404",
description = "Route not found"
)
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
public ResponseEntity<Route> delete(@PathVariable("id") long id) {
public ResponseEntity<Route> delete(
@Parameter(description = "ID of the route to delete", required = true)
@PathVariable("id") long id) {
return ResponseEntity.ofNullable(service.delete(id));
}
}

View File

@@ -1,5 +1,11 @@
package ua.com.dxrkness.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.jspecify.annotations.NullMarked;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
@@ -15,6 +21,7 @@ import java.util.List;
produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE},
consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}
)
@Tag(name = "Vehicle", description = "Vehicles management")
@NullMarked
public class VehicleController {
private final VehicleService service;
@@ -23,33 +30,98 @@ public class VehicleController {
this.service = service;
}
@Operation(
summary = "Retrieve all vehicles",
description = "Get a list of all vehicles"
)
@ApiResponse(responseCode = "200", description = "Successfully retrieved list of vehicles",
content = @Content(schema = @Schema(implementation = Vehicle.class)))
@GetMapping(consumes = MediaType.ALL_VALUE)
public List<Vehicle> getAll() {
return service.getAll();
}
@Operation(
summary = "Retrieve a vehicle by ID",
description = "Get a single vehicle by its ID"
)
@ApiResponse(responseCode = "200", description = "Vehicle found",
content = @Content(schema = @Schema(implementation = Vehicle.class)))
@ApiResponse(responseCode = "404", description = "Vehicle not found")
@GetMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
public ResponseEntity<Vehicle> getById(@PathVariable("id") long id) {
public ResponseEntity<Vehicle> getById(
@Parameter(description = "ID of the vehicle to retrieve", required = true)
@PathVariable("id") long id) {
return ResponseEntity.ofNullable(service.getById(id));
}
@Operation(
summary = "Create a new vehicle",
description = "Add a new vehicle to the system"
)
@ApiResponse(responseCode = "200", description = "Vehicle successfully created",
content = @Content(schema = @Schema(implementation = Vehicle.class)))
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
@PostMapping
public ResponseEntity<Vehicle> add(@RequestBody Vehicle newVehicle) {
public ResponseEntity<Vehicle> add(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Vehicle object to be created",
required = true,
content = @Content(schema = @Schema(implementation = Vehicle.class)))
@RequestBody Vehicle newVehicle) {
return ResponseEntity.ok(service.add(newVehicle));
}
@Operation(
summary = "Update a vehicle (full)",
description = "Update all fields of an existing vehicle by ID"
)
@ApiResponse(responseCode = "200", description = "Vehicle successfully updated",
content = @Content(schema = @Schema(implementation = Vehicle.class)))
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
@PutMapping("/{id}")
public ResponseEntity<Vehicle> update(@PathVariable("id") long id, @RequestBody Vehicle newVehicle) {
public ResponseEntity<Vehicle> update(
@Parameter(description = "ID of the vehicle to update", required = true)
@PathVariable("id") long id,
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Updated vehicle object",
required = true,
content = @Content(schema = @Schema(implementation = Vehicle.class)))
@RequestBody Vehicle newVehicle) {
return ResponseEntity.ok(service.update(id, newVehicle));
}
@Operation(
summary = "Update a vehicle (partial)",
description = "Update specific fields of an existing vehicle by ID"
)
@ApiResponse(responseCode = "200", description = "Vehicle successfully updated",
content = @Content(schema = @Schema(implementation = Vehicle.class)))
@ApiResponse(responseCode = "400", description = "Invalid vehicle data provided")
@PatchMapping("/{id}")
public ResponseEntity<Vehicle> updatePatch(@PathVariable("id") long id, @RequestBody Vehicle newVehicle) {
public ResponseEntity<Vehicle> updatePatch(
@Parameter(description = "ID of the vehicle to update", required = true)
@PathVariable("id") long id,
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "Vehicle object with fields to update",
required = true,
content = @Content(schema = @Schema(implementation = Vehicle.class)))
@RequestBody Vehicle newVehicle) {
return ResponseEntity.ok(service.update(id, newVehicle));
}
@Operation(
summary = "Delete a vehicle",
description = "Delete a vehicle by its ID"
)
@ApiResponse(responseCode = "200", description = "Vehicle successfully deleted",
content = @Content(schema = @Schema(implementation = Vehicle.class)))
@ApiResponse(responseCode = "404", description = "Vehicle not found")
@DeleteMapping(value = "/{id}", consumes = MediaType.ALL_VALUE)
public ResponseEntity<Vehicle> delete(@PathVariable("id") long id) {
public ResponseEntity<Vehicle> delete(
@Parameter(description = "ID of the vehicle to delete", required = true)
@PathVariable("id") long id) {
return ResponseEntity.ofNullable(service.delete(id));
}
}

View File

@@ -1,4 +1,20 @@
package ua.com.dxrkness.model;
public record Freight(long id) implements Identifiable {
import tools.jackson.databind.PropertyNamingStrategies;
import tools.jackson.databind.annotation.JsonNaming;
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record Freight(long id,
String brand,
String model,
String licensePlate,
int year,
int capacityKg,
Status status) implements Identifiable {
public enum Status {
PLANNED,
IN_PROGRESS,
COMPLETED,
CANCELLED
}
}

View File

@@ -1,6 +1,23 @@
package ua.com.dxrkness.model;
import tools.jackson.databind.PropertyNamingStrategies;
import tools.jackson.databind.annotation.JsonNaming;
import java.util.List;
public record Route(long id, Vehicle vehicle, List<Freight> freights) implements Identifiable {
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record Route(long id,
long vehicleId,
List<Long> freightId,
String startLocation,
String endLocation,
Double distanceKm,
Double estimatedDurationHours,
Status status) implements Identifiable {
public enum Status {
PLANNED,
IN_PROGRESS,
COMPLETED,
CANCELLED
}
}

View File

@@ -1,4 +1,21 @@
package ua.com.dxrkness.model;
public record Vehicle(long id) implements Identifiable {
import tools.jackson.databind.PropertyNamingStrategies;
import tools.jackson.databind.annotation.JsonNaming;
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record Vehicle(long id,
String name,
String description,
int weightKg,
Dimensions dimensions,
Status status) implements Identifiable {
public record Dimensions(int widthCm,
int heightCm,
int lengthCm) {
}
public enum Status {
PENDING, IN_TRANSIT, DELIVERED
}
}