Skip to content

Commit 5a6e896

Browse files
committed
PI-2374 Migrate OpenAPI yaml to annotations (WIP)
1 parent 55636fd commit 5a6e896

36 files changed

+768
-55
lines changed

Makefile

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ authenticate-docker:
33

44
build-dev:
55
docker-compose pull hmpps-auth
6-
docker-compose build --no-cache
6+
docker-compose build
77

88
build:
99
docker build -t hmpps-integration-api .
1010

11+
serve-dependencies:
12+
docker-compose up hmpps-auth prism local-stack-aws -d
13+
1114
serve: build-dev
1215
docker-compose up -d
1316

README.md

+7-6
Original file line numberDiff line numberDiff line change
@@ -88,20 +88,21 @@ minutes.
8888

8989
## Usage
9090

91-
### Running the application
91+
### Running the application locally
9292

9393
To run the application using IntelliJ:
9494

95-
1. Select the `HmppsIntegrationApi` run configuration file.
96-
2. Click the run button.
95+
1. Start dependencies using `make serve-dependencies`
96+
2. Select the `HmppsIntegrationApi` run configuration file.
97+
3. Click the run button.
9798

98-
To run the application using the command line:
99+
Or, to run the application using the command line:
99100

100101
```bash
101-
./gradlew bootRun
102+
SPRING_PROFILES_ACTIVE=local ./gradlew bootRun
102103
```
103104

104-
Then visit [http://localhost:8080](http://localhost:8080).
105+
Then visit [http://localhost:8080/swagger-ui/index.html](http://localhost:8080/swagger-ui/index.html).
105106

106107
#### With dependent services
107108

build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ dependencies {
2020
exclude("org.springframework.security", "spring-security-crypto")
2121
exclude("org.springframework.security", "spring-security-web")
2222
}
23+
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0")
2324
testImplementation("io.kotest:kotest-assertions-json-jvm:5.8.0")
2425
testImplementation("io.kotest:kotest-runner-junit5-jvm:5.8.0")
2526
testImplementation("io.kotest:kotest-assertions-core-jvm:5.8.0")

docs/adr/0011-openapi-revision.md

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# 0011 - Revert decision 0003 and auto-generate OpenAPI specification
2+
3+
2024-08-14
4+
5+
## Status
6+
7+
Proposed
8+
9+
## Context
10+
11+
In [#0003](./0003-manually-manage-openapi-file.md), the team decided to write OpenAPI specifications by hand. This went
12+
against the way other HMPPS usually do this, but had the benefit of being able to write documentation for endpoints
13+
before they have been built.
14+
15+
...
16+
17+
## Decision
18+
19+
...

openapi.yml

+2
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ paths:
306306
startDate: "2020-05-10"
307307
statuteCode: RR85
308308
pagination:
309+
summary: Example response with pagination
310+
value:
309311
$ref: "#/components/schemas/Pagination"
310312
"404":
311313
description: Failed to find offences a person with the provided HMPPS ID.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.config
2+
3+
import io.swagger.v3.oas.annotations.OpenAPIDefinition
4+
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn
5+
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
6+
import io.swagger.v3.oas.annotations.info.Info
7+
import io.swagger.v3.oas.annotations.info.License
8+
import io.swagger.v3.oas.annotations.security.SecurityRequirement
9+
import io.swagger.v3.oas.annotations.security.SecurityScheme
10+
import io.swagger.v3.oas.annotations.servers.Server
11+
import io.swagger.v3.oas.models.OpenAPI
12+
import io.swagger.v3.oas.models.media.Schema
13+
import io.swagger.v3.oas.models.parameters.Parameter
14+
import org.springdoc.core.customizers.GlobalOpenApiCustomizer
15+
import org.springdoc.core.customizers.OpenApiCustomizer
16+
import org.springframework.context.annotation.Bean
17+
import org.springframework.context.annotation.Configuration
18+
import java.math.BigDecimal
19+
20+
@OpenAPIDefinition(
21+
info =
22+
Info(
23+
title = "HMPPS Integration API",
24+
description = "A long-lived API that exposes data from HMPPS systems such as the National Offender Management Information System (NOMIS), nDelius (probation system) and Offender Assessment System (OASys), providing a single point of entry for consumers.",
25+
license =
26+
License(
27+
name = "MIT",
28+
url = "https://github.com/ministryofjustice/hmpps-integration-api/blob/main/LICENSE",
29+
),
30+
version = "1.0",
31+
),
32+
servers = [
33+
Server(url = "https://hmpps-integration-api-dev.apps.live.cloud-platform.service.justice.gov.uk", description = "Development server"),
34+
Server(url = "https://hmpps-integration-api-preprod.apps.live.cloud-platform.service.justice.gov.uk", description = "Pre-production server, containing live data"),
35+
Server(url = "https://hmpps-integration-api-prod.apps.live.cloud-platform.service.justice.gov.uk", description = "Production"),
36+
],
37+
security = [
38+
SecurityRequirement(name = "mutual-tls"),
39+
SecurityRequirement(name = "api-key"),
40+
],
41+
)
42+
@SecurityScheme(
43+
name = "mutual-tls",
44+
type = SecuritySchemeType.MUTUALTLS,
45+
)
46+
@SecurityScheme(
47+
name = "api-key",
48+
type = SecuritySchemeType.APIKEY,
49+
`in` = SecuritySchemeIn.HEADER,
50+
paramName = "x-api-key",
51+
)
52+
@Configuration
53+
class OpenAPIConfig {
54+
companion object {
55+
const val HMPPS_ID = "#components/parameters/hmppsId"
56+
const val PAGE = "#components/parameters/page"
57+
const val PER_PAGE = "#components/parameters/perPage"
58+
}
59+
60+
@Bean
61+
fun openApiCustomizer(): OpenApiCustomizer =
62+
object : GlobalOpenApiCustomizer {
63+
override fun customise(openApi: OpenAPI) {
64+
openApi.components
65+
.addParameters("hmppsId", Parameter().name("hmppsId").description("A URL-encoded HMPPS identifier").example("2008%2F0545166T").schema(Schema<String>().type("string")).`in`("path").required(true))
66+
.addParameters("page", Parameter().name("page").description("The page number (starting from 1)").schema(Schema<Int>().type("number").minimum(BigDecimal.ONE)._default(1)).`in`("query").required(false))
67+
.addParameters("perPage", Parameter().name("perPage").description("The maximum number of results for a page").schema(Schema<Int>().type("number").minimum(BigDecimal.ONE)._default(10)).`in`("query").required(false))
68+
.addSchemas("BadRequest", Schema<ErrorResponse>().example(ErrorResponse(400, userMessage = "Validation failure: No query parameters specified.", developerMessage = "No query parameters specified.")))
69+
.addSchemas(
70+
"PersonNotFound",
71+
Schema<ErrorResponse>()
72+
.description("Failed to find a person with the provided HMPPS ID.")
73+
.example(ErrorResponse(404, userMessage = "404 Not found error: Could not find person with HMPPS id: 2003/0011991D.", developerMessage = "Could not find person with HMPPS id: 2003/0011991D.")),
74+
)
75+
.addSchemas(
76+
"InternalServerError",
77+
Schema<ErrorResponse>()
78+
.description("An upstream service was not responding, so we cannot verify the accuracy of any data we did get.")
79+
.example(ErrorResponse(500, userMessage = "Internal Server Error", developerMessage = "Unable to complete request as an upstream service is not responding.")),
80+
)
81+
}
82+
}
83+
}
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,45 @@
11
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person
22

3+
import io.swagger.v3.oas.annotations.Operation
4+
import io.swagger.v3.oas.annotations.Parameter
5+
import io.swagger.v3.oas.annotations.media.Content
6+
import io.swagger.v3.oas.annotations.media.Schema
7+
import io.swagger.v3.oas.annotations.responses.ApiResponse
8+
import io.swagger.v3.oas.annotations.tags.Tag
39
import org.springframework.beans.factory.annotation.Autowired
410
import org.springframework.web.bind.annotation.GetMapping
511
import org.springframework.web.bind.annotation.PathVariable
612
import org.springframework.web.bind.annotation.RequestMapping
713
import org.springframework.web.bind.annotation.RestController
14+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.config.OpenAPIConfig.Companion.HMPPS_ID
815
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException
916
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.decodeUrlCharacters
1017
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Address
18+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Data
1119
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApi
1220
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.UpstreamApiError
1321
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.GetAddressesForPersonService
1422
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.services.internal.AuditService
1523

1624
@RestController
1725
@RequestMapping("/v1/persons")
26+
@Tag(name = "persons")
1827
class AddressController(
1928
@Autowired val auditService: AuditService,
2029
@Autowired val getAddressesForPersonService: GetAddressesForPersonService,
2130
) {
2231
@GetMapping("{encodedHmppsId}/addresses")
32+
@Operation(
33+
summary = "Returns addresses associated with a person, ordered by startDate.",
34+
responses = [
35+
ApiResponse(responseCode = "200", description = "Successfully found a person with the provided HMPPS ID."),
36+
ApiResponse(responseCode = "404", content = [Content(schema = Schema(ref = "#/components/schemas/PersonNotFound"))]),
37+
ApiResponse(responseCode = "500", content = [Content(schema = Schema(ref = "#/components/schemas/InternalServerError"))]),
38+
],
39+
)
2340
fun getPersonAddresses(
24-
@PathVariable encodedHmppsId: String,
25-
): Map<String, List<Address>> {
41+
@Parameter(ref = HMPPS_ID) @PathVariable encodedHmppsId: String,
42+
): Data<List<Address>> {
2643
val hmppsId = encodedHmppsId.decodeUrlCharacters()
2744
val response = getAddressesForPersonService.execute(hmppsId)
2845

@@ -33,6 +50,6 @@ class AddressController(
3350
throw EntityNotFoundException("Could not find person with id: $hmppsId")
3451
}
3552
auditService.createEvent("GET_PERSON_ADDRESS", mapOf("hmppsId" to hmppsId))
36-
return mapOf("data" to response.data)
53+
return Data(response.data)
3754
}
3855
}

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/AdjudicationsController.kt

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person
22

3+
import io.swagger.v3.oas.annotations.Operation
4+
import io.swagger.v3.oas.annotations.Parameter
5+
import io.swagger.v3.oas.annotations.media.Content
6+
import io.swagger.v3.oas.annotations.media.Schema
7+
import io.swagger.v3.oas.annotations.responses.ApiResponse
8+
import io.swagger.v3.oas.annotations.tags.Tag
39
import org.springframework.beans.factory.annotation.Autowired
410
import org.springframework.web.bind.annotation.GetMapping
511
import org.springframework.web.bind.annotation.PathVariable
612
import org.springframework.web.bind.annotation.RequestMapping
713
import org.springframework.web.bind.annotation.RequestParam
814
import org.springframework.web.bind.annotation.RestController
15+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.config.OpenAPIConfig.Companion.HMPPS_ID
16+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.config.OpenAPIConfig.Companion.PAGE
17+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.config.OpenAPIConfig.Companion.PER_PAGE
918
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException
1019
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.decodeUrlCharacters
1120
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Adjudication
@@ -17,15 +26,24 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.util.paginateWith
1726

1827
@RestController
1928
@RequestMapping("/v1/persons")
29+
@Tag(name = "default")
2030
class AdjudicationsController(
2131
@Autowired val auditService: AuditService,
2232
@Autowired val getAdjudicationsForPersonService: GetAdjudicationsForPersonService,
2333
) {
2434
@GetMapping("{encodedHmppsId}/reported-adjudications")
35+
@Operation(
36+
summary = "Returns adjudications associated with a person, sorted by dateTimeOfIncident (newest first).",
37+
responses = [
38+
ApiResponse(responseCode = "200", description = "OK"),
39+
ApiResponse(responseCode = "404", description = "Failed to find adjudications for the person with the provided hmppsId.", content = [Content(schema = Schema(ref = "#/components/schemas/PersonNotFound"))]),
40+
ApiResponse(responseCode = "500", content = [Content(schema = Schema(ref = "#/components/schemas/InternalServerError"))]),
41+
],
42+
)
2543
fun getPersonAdjudications(
26-
@PathVariable encodedHmppsId: String,
27-
@RequestParam(required = false, defaultValue = "1", name = "page") page: Int,
28-
@RequestParam(required = false, defaultValue = "8", name = "perPage") perPage: Int,
44+
@Parameter(ref = HMPPS_ID) @PathVariable encodedHmppsId: String,
45+
@Parameter(ref = PAGE) @RequestParam(required = false, defaultValue = "1", name = "page") page: Int,
46+
@Parameter(ref = PER_PAGE) @RequestParam(required = false, defaultValue = "8", name = "perPage") perPage: Int,
2947
): PaginatedResponse<Adjudication> {
3048
val hmppsId = encodedHmppsId.decodeUrlCharacters()
3149
val response = getAdjudicationsForPersonService.execute(hmppsId)

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/AlertsController.kt

+32-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person
22

3+
import io.swagger.v3.oas.annotations.Operation
4+
import io.swagger.v3.oas.annotations.Parameter
5+
import io.swagger.v3.oas.annotations.media.Content
6+
import io.swagger.v3.oas.annotations.media.Schema
7+
import io.swagger.v3.oas.annotations.responses.ApiResponse
8+
import io.swagger.v3.oas.annotations.tags.Tag
39
import org.springframework.beans.factory.annotation.Autowired
410
import org.springframework.web.bind.annotation.GetMapping
511
import org.springframework.web.bind.annotation.PathVariable
612
import org.springframework.web.bind.annotation.RequestMapping
713
import org.springframework.web.bind.annotation.RequestParam
814
import org.springframework.web.bind.annotation.RestController
15+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.config.OpenAPIConfig.Companion.HMPPS_ID
16+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.config.OpenAPIConfig.Companion.PAGE
17+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.config.OpenAPIConfig.Companion.PER_PAGE
918
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException
1019
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.decodeUrlCharacters
1120
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.hmpps.Alert
@@ -17,15 +26,24 @@ import uk.gov.justice.digital.hmpps.hmppsintegrationapi.util.paginateWith
1726

1827
@RestController
1928
@RequestMapping("/v1/persons")
29+
@Tag(name = "persons")
2030
class AlertsController(
2131
@Autowired val getAlertsForPersonService: GetAlertsForPersonService,
2232
@Autowired val auditService: AuditService,
2333
) {
2434
@GetMapping("{encodedHmppsId}/alerts")
35+
@Operation(
36+
summary = "Returns alerts associated with a person, sorted by dateCreated (newest first).",
37+
responses = [
38+
ApiResponse(responseCode = "200", description = "Successfully found alerts for a person with the provided HMPPS ID."),
39+
ApiResponse(responseCode = "404", content = [Content(schema = Schema(ref = "#/components/schemas/PersonNotFound"))]),
40+
ApiResponse(responseCode = "500", content = [Content(schema = Schema(ref = "#/components/schemas/InternalServerError"))]),
41+
],
42+
)
2543
fun getPersonAlerts(
26-
@PathVariable encodedHmppsId: String,
27-
@RequestParam(required = false, defaultValue = "1", name = "page") page: Int,
28-
@RequestParam(required = false, defaultValue = "10", name = "perPage") perPage: Int,
44+
@Parameter(ref = HMPPS_ID) @PathVariable encodedHmppsId: String,
45+
@Parameter(ref = PAGE) @RequestParam(required = false, defaultValue = "1", name = "page") page: Int,
46+
@Parameter(ref = PER_PAGE) @RequestParam(required = false, defaultValue = "10", name = "perPage") perPage: Int,
2947
): PaginatedResponse<Alert> {
3048
val hmppsId = encodedHmppsId.decodeUrlCharacters()
3149
val response = getAlertsForPersonService.execute(hmppsId)
@@ -38,10 +56,18 @@ class AlertsController(
3856
}
3957

4058
@GetMapping("{encodedHmppsId}/alerts/pnd")
59+
@Operation(
60+
summary = "Returns alerts associated with a person, sorted by dateCreated (newest first).",
61+
responses = [
62+
ApiResponse(responseCode = "200", description = "Successfully found alerts for a person with the provided HMPPS ID."),
63+
ApiResponse(responseCode = "404", content = [Content(schema = Schema(ref = "#/components/schemas/PersonNotFound"))]),
64+
ApiResponse(responseCode = "500", content = [Content(schema = Schema(ref = "#/components/schemas/InternalServerError"))]),
65+
],
66+
)
4167
fun getPersonAlertsPND(
42-
@PathVariable encodedHmppsId: String,
43-
@RequestParam(required = false, defaultValue = "1", name = "page") page: Int,
44-
@RequestParam(required = false, defaultValue = "10", name = "perPage") perPage: Int,
68+
@Parameter(ref = HMPPS_ID) @PathVariable encodedHmppsId: String,
69+
@Parameter(ref = PAGE) @RequestParam(required = false, defaultValue = "1", name = "page") page: Int,
70+
@Parameter(ref = PER_PAGE) @RequestParam(required = false, defaultValue = "10", name = "perPage") perPage: Int,
4571
): PaginatedResponse<Alert> {
4672
val hmppsId = encodedHmppsId.decodeUrlCharacters()
4773
val response = getAlertsForPersonService.getAlertsForPnd(hmppsId)

src/main/kotlin/uk/gov/justice/digital/hmpps/hmppsintegrationapi/controllers/v1/person/CaseNotesController.kt

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
package uk.gov.justice.digital.hmpps.hmppsintegrationapi.controllers.v1.person
22

3+
import io.swagger.v3.oas.annotations.Operation
4+
import io.swagger.v3.oas.annotations.Parameter
5+
import io.swagger.v3.oas.annotations.media.Content
6+
import io.swagger.v3.oas.annotations.media.Schema
7+
import io.swagger.v3.oas.annotations.responses.ApiResponse
8+
import io.swagger.v3.oas.annotations.tags.Tag
39
import org.springframework.beans.factory.annotation.Autowired
410
import org.springframework.format.annotation.DateTimeFormat
511
import org.springframework.web.bind.annotation.GetMapping
612
import org.springframework.web.bind.annotation.PathVariable
713
import org.springframework.web.bind.annotation.RequestMapping
814
import org.springframework.web.bind.annotation.RequestParam
915
import org.springframework.web.bind.annotation.RestController
16+
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.config.OpenAPIConfig.Companion.HMPPS_ID
1017
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.exception.EntityNotFoundException
1118
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.extensions.decodeUrlCharacters
1219
import uk.gov.justice.digital.hmpps.hmppsintegrationapi.models.filters.CaseNoteFilter
@@ -21,19 +28,31 @@ import java.time.LocalDateTime
2128

2229
@RestController
2330
@RequestMapping("/v1/persons")
31+
@Tag(name = "default")
2432
class CaseNotesController(
2533
@Autowired val getCaseNoteForPersonService: GetCaseNotesForPersonService,
2634
@Autowired val auditService: AuditService,
2735
) {
2836
@GetMapping("{encodedHmppsId}/case-notes")
37+
@Operation(
38+
summary = "Returns case notes associated with a person.",
39+
responses = [
40+
ApiResponse(responseCode = "200", description = "Successfully found case notes for a person with the provided HMPPS ID."),
41+
ApiResponse(responseCode = "404", content = [Content(schema = Schema(ref = "#/components/schemas/PersonNotFound"))]),
42+
ApiResponse(responseCode = "500", content = [Content(schema = Schema(ref = "#/components/schemas/InternalServerError"))]),
43+
],
44+
)
2945
fun getCaseNotesForPerson(
30-
@PathVariable encodedHmppsId: String,
46+
@Parameter(ref = HMPPS_ID) @PathVariable encodedHmppsId: String,
47+
@Parameter(description = "Filter case notes from this date")
3148
@RequestParam(required = false, name = "startDate")
3249
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
3350
startDate: LocalDateTime?,
51+
@Parameter(description = "Filter case notes up to this date")
3452
@RequestParam(required = false, name = "endDate")
3553
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
3654
endDate: LocalDateTime?,
55+
@Parameter(description = "Filter by the location. example MDI", example = "MDI")
3756
@RequestParam(required = false, name = "locationId") locationId: String?,
3857
@RequestParam(required = false, defaultValue = "1", name = "page") page: Int,
3958
@RequestParam(required = false, defaultValue = "10", name = "perPage") perPage: Int,

0 commit comments

Comments
 (0)