|
| 1 | +package info.jab.ms.controller; |
| 2 | + |
| 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.ExampleObject; |
| 7 | +import io.swagger.v3.oas.annotations.media.Schema; |
| 8 | +import io.swagger.v3.oas.annotations.responses.ApiResponse; |
| 9 | +import io.swagger.v3.oas.annotations.responses.ApiResponses; |
| 10 | +import io.swagger.v3.oas.annotations.tags.Tag; |
| 11 | +import org.springframework.http.ResponseEntity; |
| 12 | +import org.springframework.web.bind.annotation.GetMapping; |
| 13 | +import org.springframework.web.bind.annotation.RequestParam; |
| 14 | + |
| 15 | +/** |
| 16 | + * FilmControllerApi - API Contract for Film Query Operations |
| 17 | + * |
| 18 | + * This interface defines the REST API contract for querying films from the Sakila database. |
| 19 | + * It contains all the OpenAPI/Swagger documentation and defines the API endpoints for |
| 20 | + * retrieving films that start with specific letters. |
| 21 | + * |
| 22 | + * API Endpoints: |
| 23 | + * - GET /api/v1/films - Retrieve all films or filter by starting letter |
| 24 | + * - GET /api/v1/films?startsWith=A - Retrieve films starting with letter "A" |
| 25 | + * |
| 26 | + * This interface separates the API contract and documentation from the business logic |
| 27 | + * implementation, following the principle of separation of concerns. |
| 28 | + */ |
| 29 | +@Tag(name = "Films", description = "Film query operations") |
| 30 | +public interface FilmControllerApi { |
| 31 | + |
| 32 | + /** |
| 33 | + * Retrieves films from the Sakila database, optionally filtered by starting letter. |
| 34 | + * |
| 35 | + * @param startsWith Optional parameter to filter films by starting letter (single character A-Z) |
| 36 | + * @param request HttpServletRequest for building error responses |
| 37 | + * @return JSON response containing films array, count, and filter information or error response |
| 38 | + */ |
| 39 | + @Operation( |
| 40 | + summary = "Query films by starting letter", |
| 41 | + description = """ |
| 42 | + ### Query Films from Sakila Database |
| 43 | +
|
| 44 | + Retrieves films from the PostgreSQL Sakila database that start with the specified letter. |
| 45 | + The search is case-insensitive and returns complete film information including ID and title. |
| 46 | +
|
| 47 | + ### Query Behavior |
| 48 | + - **Without parameter**: Returns all films in the database (1000 films) |
| 49 | + - **With startsWith parameter**: Returns films starting with the specified letter |
| 50 | + - **Case handling**: Search is case-insensitive (both 'A' and 'a' work identically) |
| 51 | + - **Ordering**: Results are ordered by film ID for consistent pagination |
| 52 | +
|
| 53 | + ### Performance Characteristics |
| 54 | + - **Response time**: Guaranteed under 2 seconds for all queries |
| 55 | + - **Database optimization**: Uses indexed queries for optimal performance |
| 56 | + - **Result caching**: Frequently accessed results may be cached |
| 57 | + - **Connection pooling**: Efficient database connection management |
| 58 | +
|
| 59 | + ### Expected Result Counts |
| 60 | + Based on the complete Sakila database: |
| 61 | + - **Letter "A"**: 46 films (e.g., "ACADEMY DINOSAUR", "AIRPORT POLLOCK") |
| 62 | + - **Letter "B"**: 37 films (e.g., "BADMAN DAWN", "BATMAN BEGINS") |
| 63 | + - **Letter "C"**: 57 films (e.g., "CABIN FLASH", "CALIFORNIA BIRDS") |
| 64 | + - **Letter "D"**: 41 films |
| 65 | + - **Letter "E"**: 20 films |
| 66 | + - **Letter "F"**: 58 films |
| 67 | + - **Letter "G"**: 42 films |
| 68 | + - **Letter "H"**: 49 films |
| 69 | +
|
| 70 | + ### Usage Examples |
| 71 | + ``` |
| 72 | + GET /api/v1/films?startsWith=A # Returns 46 films starting with 'A' |
| 73 | + GET /api/v1/films?startsWith=a # Same result (case-insensitive) |
| 74 | + GET /api/v1/films # Returns all films |
| 75 | + ``` |
| 76 | + """, |
| 77 | + operationId = "getFilmsByStartingLetter", |
| 78 | + tags = {"Films"} |
| 79 | + ) |
| 80 | + @ApiResponses(value = { |
| 81 | + @ApiResponse( |
| 82 | + responseCode = "200", |
| 83 | + description = """ |
| 84 | + Successfully retrieved films matching the query criteria. |
| 85 | + Response includes films array, total count, and applied filter parameters. |
| 86 | + """, |
| 87 | + content = @Content( |
| 88 | + mediaType = "application/json", |
| 89 | + schema = @Schema(implementation = info.jab.ms.controller.FilmDTO.class), |
| 90 | + examples = { |
| 91 | + @ExampleObject( |
| 92 | + name = "Films starting with A", |
| 93 | + summary = "46 films starting with letter 'A' (most common scenario)", |
| 94 | + description = "Returns all films from the Sakila database that start with letter 'A'. This is the most frequently requested query with 46 matching films.", |
| 95 | + value = """ |
| 96 | + { |
| 97 | + "films": [ |
| 98 | + { |
| 99 | + "film_id": 1, |
| 100 | + "title": "ACADEMY DINOSAUR" |
| 101 | + }, |
| 102 | + { |
| 103 | + "film_id": 8, |
| 104 | + "title": "AIRPORT POLLOCK" |
| 105 | + }, |
| 106 | + { |
| 107 | + "film_id": 10, |
| 108 | + "title": "ALADDIN CALENDAR" |
| 109 | + }, |
| 110 | + { |
| 111 | + "film_id": 13, |
| 112 | + "title": "ALI FOREVER" |
| 113 | + }, |
| 114 | + { |
| 115 | + "film_id": 15, |
| 116 | + "title": "ALIEN CENTER" |
| 117 | + } |
| 118 | + ], |
| 119 | + "count": 46, |
| 120 | + "filter": { |
| 121 | + "startsWith": "A" |
| 122 | + } |
| 123 | + } |
| 124 | + """ |
| 125 | + ), |
| 126 | + @ExampleObject( |
| 127 | + name = "Films starting with B", |
| 128 | + summary = "37 films starting with letter 'B'", |
| 129 | + description = "Example response showing films starting with 'B'. Note the case-insensitive filter behavior.", |
| 130 | + value = """ |
| 131 | + { |
| 132 | + "films": [ |
| 133 | + { |
| 134 | + "film_id": 16, |
| 135 | + "title": "ALLEY EVOLUTION" |
| 136 | + }, |
| 137 | + { |
| 138 | + "film_id": 23, |
| 139 | + "title": "ANACONDA CONFESSIONS" |
| 140 | + }, |
| 141 | + { |
| 142 | + "film_id": 25, |
| 143 | + "title": "ANGELS LIFE" |
| 144 | + } |
| 145 | + ], |
| 146 | + "count": 37, |
| 147 | + "filter": { |
| 148 | + "startsWith": "B" |
| 149 | + } |
| 150 | + } |
| 151 | + """ |
| 152 | + ), |
| 153 | + @ExampleObject( |
| 154 | + name = "All films (no filter)", |
| 155 | + summary = "All films when no startsWith parameter is provided", |
| 156 | + description = "Returns all films from the database when no filter is applied. Shows the complete dataset structure.", |
| 157 | + value = """ |
| 158 | + { |
| 159 | + "films": [ |
| 160 | + { |
| 161 | + "film_id": 1, |
| 162 | + "title": "ACADEMY DINOSAUR" |
| 163 | + }, |
| 164 | + { |
| 165 | + "film_id": 2, |
| 166 | + "title": "ACE GOLDFINGER" |
| 167 | + }, |
| 168 | + { |
| 169 | + "film_id": 3, |
| 170 | + "title": "ADAPTATION HOLES" |
| 171 | + } |
| 172 | + ], |
| 173 | + "count": 1000, |
| 174 | + "filter": {} |
| 175 | + } |
| 176 | + """ |
| 177 | + ), |
| 178 | + @ExampleObject( |
| 179 | + name = "Empty result set", |
| 180 | + summary = "No films found for a specific letter", |
| 181 | + description = "Example response when no films match the filter criteria. Some letters might have no films in the database.", |
| 182 | + value = """ |
| 183 | + { |
| 184 | + "films": [], |
| 185 | + "count": 0, |
| 186 | + "filter": { |
| 187 | + "startsWith": "Q" |
| 188 | + } |
| 189 | + } |
| 190 | + """ |
| 191 | + ), |
| 192 | + @ExampleObject( |
| 193 | + name = "Case-insensitive filter", |
| 194 | + summary = "Lowercase parameter produces same results", |
| 195 | + description = "Demonstrates case-insensitive behavior - lowercase 'a' returns the same results as uppercase 'A'.", |
| 196 | + value = """ |
| 197 | + { |
| 198 | + "films": [ |
| 199 | + { |
| 200 | + "film_id": 1, |
| 201 | + "title": "ACADEMY DINOSAUR" |
| 202 | + }, |
| 203 | + { |
| 204 | + "film_id": 8, |
| 205 | + "title": "AIRPORT POLLOCK" |
| 206 | + } |
| 207 | + ], |
| 208 | + "count": 46, |
| 209 | + "filter": { |
| 210 | + "startsWith": "a" |
| 211 | + } |
| 212 | + } |
| 213 | + """ |
| 214 | + ) |
| 215 | + } |
| 216 | + ) |
| 217 | + ), |
| 218 | + @ApiResponse( |
| 219 | + responseCode = "400", |
| 220 | + description = """ |
| 221 | + Bad Request - Invalid query parameter provided. |
| 222 | + The startsWith parameter must be a single letter (A-Z or a-z). |
| 223 | + Returns empty response body. |
| 224 | + """ |
| 225 | + ), |
| 226 | + @ApiResponse( |
| 227 | + responseCode = "500", |
| 228 | + description = """ |
| 229 | + Internal Server Error - An unexpected error occurred while processing the request. |
| 230 | + This could be due to database connectivity issues or other system problems. |
| 231 | + """, |
| 232 | + content = @Content( |
| 233 | + mediaType = "application/json", |
| 234 | + schema = @Schema(implementation = org.springframework.http.ProblemDetail.class), |
| 235 | + examples = { |
| 236 | + @ExampleObject( |
| 237 | + name = "Database connection error", |
| 238 | + summary = "Database connectivity issues", |
| 239 | + description = "Response when the service cannot connect to the PostgreSQL database", |
| 240 | + value = """ |
| 241 | + { |
| 242 | + "title": "Database Connection Error", |
| 243 | + "status": 500, |
| 244 | + "detail": "Unable to connect to the database. Please try again later.", |
| 245 | + "instance": "/api/v1/films", |
| 246 | + "timestamp": "2024-01-15T10:30:00Z" |
| 247 | + } |
| 248 | + """ |
| 249 | + ), |
| 250 | + @ExampleObject( |
| 251 | + name = "Query timeout error", |
| 252 | + summary = "Database query timeout", |
| 253 | + description = "Response when a database query takes longer than the configured timeout", |
| 254 | + value = """ |
| 255 | + { |
| 256 | + "title": "Query Timeout", |
| 257 | + "status": 500, |
| 258 | + "detail": "The database query timed out. Please try again with a smaller dataset.", |
| 259 | + "instance": "/api/v1/films", |
| 260 | + "timestamp": "2024-01-15T10:30:00Z" |
| 261 | + } |
| 262 | + """ |
| 263 | + ), |
| 264 | + @ExampleObject( |
| 265 | + name = "Generic server error", |
| 266 | + summary = "General internal server error", |
| 267 | + description = "Response for unexpected server errors not covered by specific error types", |
| 268 | + value = """ |
| 269 | + { |
| 270 | + "title": "Internal Server Error", |
| 271 | + "status": 500, |
| 272 | + "detail": "An unexpected error occurred while processing your request", |
| 273 | + "instance": "/api/v1/films", |
| 274 | + "timestamp": "2024-01-15T10:30:00Z" |
| 275 | + } |
| 276 | + """ |
| 277 | + ) |
| 278 | + } |
| 279 | + ) |
| 280 | + ) |
| 281 | + }) |
| 282 | + @GetMapping("/films") |
| 283 | + ResponseEntity<FilmDTO> getFilms( |
| 284 | + @Parameter( |
| 285 | + name = "startsWith", |
| 286 | + description = """ |
| 287 | + Filter films by their starting letter. |
| 288 | +
|
| 289 | + **Requirements:** |
| 290 | + - Must be a single letter (A-Z or a-z) |
| 291 | + - Case-insensitive (both 'A' and 'a' return the same results) |
| 292 | + - Cannot be empty, numeric, or special characters |
| 293 | + - Cannot be multiple characters |
| 294 | +
|
| 295 | + **Examples:** |
| 296 | + - `A` or `a` → Returns 46 films starting with 'A' |
| 297 | + - `B` or `b` → Returns 37 films starting with 'B' |
| 298 | + - `Z` or `z` → Returns films starting with 'Z' |
| 299 | +
|
| 300 | + **Invalid values:** |
| 301 | + - `AB` (multiple characters) |
| 302 | + - `1` (numeric) |
| 303 | + - `@` (special character) |
| 304 | + - ` ` (empty/whitespace) |
| 305 | + """, |
| 306 | + example = "A", |
| 307 | + required = false, |
| 308 | + schema = @Schema( |
| 309 | + type = "string", |
| 310 | + pattern = "^[A-Za-z]$", |
| 311 | + minLength = 1, |
| 312 | + maxLength = 1, |
| 313 | + description = "Single letter (A-Z, case-insensitive)" |
| 314 | + ) |
| 315 | + ) |
| 316 | + @RequestParam(required = false) String startsWith); |
| 317 | +} |
0 commit comments