diff --git a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/CreatePatronRequest.java b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/CreatePatronRequest.java new file mode 100644 index 00000000..db557148 --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/CreatePatronRequest.java @@ -0,0 +1,17 @@ +package com.codedifferently.lesson16.web; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class CreatePatronRequest { + @NotNull(message = "patron is required") @Valid + private PatronRequest patron; +} diff --git a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/CreatePatronResponse.java b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/CreatePatronResponse.java new file mode 100644 index 00000000..3799bed1 --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/CreatePatronResponse.java @@ -0,0 +1,10 @@ +package com.codedifferently.lesson16.web; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class CreatePatronResponse { + private PatronResponse patron; +} diff --git a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/GetPatronsResponse.java b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/GetPatronsResponse.java new file mode 100644 index 00000000..846639a5 --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/GetPatronsResponse.java @@ -0,0 +1,12 @@ +package com.codedifferently.lesson16.web; + +import java.util.List; +import lombok.Builder; +import lombok.Data; +import lombok.Singular; + +@Data +@Builder +public class GetPatronsResponse { + @Singular private List patrons; +} diff --git a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/MediaItemsController.java b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/MediaItemsController.java index d4bfb7bf..8859cdb7 100644 --- a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/MediaItemsController.java +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/MediaItemsController.java @@ -4,12 +4,21 @@ import com.codedifferently.lesson16.library.Library; import com.codedifferently.lesson16.library.MediaItem; import com.codedifferently.lesson16.library.search.SearchCriteria; +import jakarta.validation.Valid; import java.io.IOException; import java.util.List; import java.util.Set; +import java.util.UUID; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; @RestController @CrossOrigin @@ -29,4 +38,35 @@ public GetMediaItemsResponse getItems() { var response = GetMediaItemsResponse.builder().items(responseItems).build(); return response; } + + @PostMapping("/items") + public CreateMediaItemResponse postItem(@Valid @RequestBody CreateMediaItemRequest request) { + MediaItem item = MediaItemRequest.asMediaItem(request.getItem()); + library.addMediaItem(item, librarian); + var response = CreateMediaItemResponse.builder().item(MediaItemResponse.from(item)).build(); + return response; + } + + @GetMapping("/items/{id}") + public GetMediaItemsResponse getItem(@PathVariable String id) { + SearchCriteria criteria = SearchCriteria.builder().id(id).build(); + Set items = library.search(criteria); + + if (items.isEmpty()) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Media item not found"); + } + + List responseItems = items.stream().map(MediaItemResponse::from).toList(); + return GetMediaItemsResponse.builder().items(responseItems).build(); + } + + @DeleteMapping("/items/{id}") + public ResponseEntity deleteItem(@PathVariable String id) { + if (!library.hasMediaItem(UUID.fromString(id))) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Media item not found"); + } + + library.removeMediaItem(UUID.fromString(id), librarian); + return ResponseEntity.noContent().build(); + } } diff --git a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronRequest.java b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronRequest.java new file mode 100644 index 00000000..ee5a36e6 --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronRequest.java @@ -0,0 +1,30 @@ +package com.codedifferently.lesson16.web; + +import com.codedifferently.lesson16.library.Patron; +import jakarta.validation.constraints.NotBlank; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PatronRequest { + private UUID id; + + @NotBlank(message = "name is required") + private String name; + + @NotBlank(message = "email is required") + private String email; + + public static Patron asPatron(PatronRequest request) { + String name = request.getName(); + String email = request.getEmail(); + + return new Patron(name, email); + } +} diff --git a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronResponse.java b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronResponse.java new file mode 100644 index 00000000..10d96c79 --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronResponse.java @@ -0,0 +1,29 @@ +package com.codedifferently.lesson16.web; + +import com.codedifferently.lesson16.library.LibraryGuest; +import java.util.UUID; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class PatronResponse { + + private UUID id; + private String name; + private String email; + + /** + * Converts a Patron object to a PatronResponse object. + * + * @param patron The Patron object to convert. + * @return The converted PatronResponse object. + */ + public static PatronResponse from(LibraryGuest patron) { + return PatronResponse.builder() + .id(patron.getId()) + .name(patron.getName()) + .email(patron.getEmail()) + .build(); + } +} diff --git a/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronsController.java b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronsController.java new file mode 100644 index 00000000..f2516988 --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronsController.java @@ -0,0 +1,65 @@ +package com.codedifferently.lesson16.web; + +import com.codedifferently.lesson16.library.Library; +import com.codedifferently.lesson16.library.LibraryGuest; +import com.codedifferently.lesson16.library.Patron; +import jakarta.validation.Valid; +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +@RestController +public class PatronsController { + private final Library library; + + public PatronsController(Library library) throws IOException { + this.library = library; + } + + // Mohamed helped me sort out this method! + @GetMapping("/patrons") + public GetPatronsResponse getPatrons() { + Set patrons = library.getPatrons(); + List responsePatrons = patrons.stream().map(PatronResponse::from).toList(); + var response = GetPatronsResponse.builder().patrons(responsePatrons).build(); + return response; + } + + @PostMapping("/patrons") + public CreatePatronResponse postPatron(@Valid @RequestBody CreatePatronRequest request) { + Patron patron = PatronRequest.asPatron(request.getPatron()); + library.addLibraryGuest(patron); + var response = CreatePatronResponse.builder().patron(PatronResponse.from(patron)).build(); + return response; + } + + @GetMapping("/patrons/{id}") + public GetPatronsResponse getPatronById(@PathVariable UUID id) { + for (LibraryGuest patron : library.getPatrons()) { + if (patron.getId().equals(id)) { + return GetPatronsResponse.builder().patron(PatronResponse.from(patron)).build(); + } + } + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Patron not found"); + } + + @DeleteMapping("/patrons/{id}") + public ResponseEntity deleteItem(@PathVariable String id) { + if (!library.hasLibraryGuest(UUID.fromString(id))) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Patron not found"); + } + + library.removeLibraryGuest(UUID.fromString(id)); + return ResponseEntity.noContent().build(); + } +} diff --git a/lesson_16/api/api_app/src/test/java/com/codedifferently/lesson16/web/MediaItemsControllerTest.java b/lesson_16/api/api_app/src/test/java/com/codedifferently/lesson16/web/MediaItemsControllerTest.java index fdb1ea7d..3baf159b 100644 --- a/lesson_16/api/api_app/src/test/java/com/codedifferently/lesson16/web/MediaItemsControllerTest.java +++ b/lesson_16/api/api_app/src/test/java/com/codedifferently/lesson16/web/MediaItemsControllerTest.java @@ -62,11 +62,11 @@ void testController_returnsNotFoundOnGetItem() throws Exception { } @Test - void testController_reportsBadRequestOnAddItem() throws Exception { + void testController_reportsBadRequestOnAddPatron() throws Exception { String json = "{}"; mockMvc - .perform(post("/items").contentType(MediaType.APPLICATION_JSON).content(json)) + .perform(post("/patrons").contentType(MediaType.APPLICATION_JSON).content(json)) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.errors").isArray()) .andExpect(jsonPath("$.errors.length()").value(1)); diff --git a/lesson_16/api/api_app/src/test/java/com/codedifferently/lesson16/web/PatronsControllerTest.java b/lesson_16/api/api_app/src/test/java/com/codedifferently/lesson16/web/PatronsControllerTest.java new file mode 100644 index 00000000..b550f427 --- /dev/null +++ b/lesson_16/api/api_app/src/test/java/com/codedifferently/lesson16/web/PatronsControllerTest.java @@ -0,0 +1,133 @@ +package com.codedifferently.lesson16.web; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.codedifferently.lesson16.Lesson16; +import com.codedifferently.lesson16.library.Library; +import com.codedifferently.lesson16.library.LibraryGuest; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@SpringBootTest +@ContextConfiguration(classes = Lesson16.class) +class PatronsControllerTest { + private static MockMvc mockMvc; + @Autowired private Library library; + + @BeforeAll + static void setUp(WebApplicationContext wac) { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); + } + + @Test + void testController_getsAllPatrons() throws Exception { + mockMvc + .perform(get("/patrons").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.patrons").isArray()) + .andExpect(jsonPath("$.patrons.length()").value(5)); + } + + // Got help from Mohamed on this one. Learned that .stream() is super useful. + @Test + void testGetPatronById() throws Exception { + + List patron = library.getPatrons().stream().toList(); + UUID ids = patron.get(3).getId(); + + mockMvc + .perform(get("/patrons/" + ids.toString()).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + void testController_returnsNotFoundOnGetPatron() throws Exception { + mockMvc + .perform( + get("/patrons/00000000-0000-0000-0000-000000000000") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + void testController_reportsBadRequestOnAddPatron() throws Exception { + String json = "{}"; + + mockMvc + .perform(post("/patrons").contentType(MediaType.APPLICATION_JSON).content(json)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.errors").isArray()) + .andExpect(jsonPath("$.errors.length()").value(1)); + } + + @Test + void testPostPatron() throws Exception { + String json = + """ + { + "patron": { + "name": "Rich Hawkins", + "email": "rich@email.com" + } + } + """; + + mockMvc + .perform(post("/patrons").contentType(MediaType.APPLICATION_JSON).content(json)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.patron.name").value("Rich Hawkins")); + } + + @Test + void testController_returnsNotFoundOnDeletePatron() throws Exception { + mockMvc + .perform( + delete("/patrons/00000000-0000-0000-0000-000000000000") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + // Mo' also helped on this...didn't think of making a helper function. Kudos to him! + @Test + void testController_deletesPatron() throws Exception { + Library lib = library; + List pat = library.getPatrons().stream().toList(); + UUID ids = getGuestId(pat); + + mockMvc + .perform(delete("/patrons/" + ids.toString()).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + int i = 0; + pat = library.getPatrons().stream().toList(); + for (LibraryGuest guest : pat) { + if (guest.getId() == ids) { + i++; + } + } + library = lib; + assertThat(i).isEqualTo(0); + } + + UUID getGuestId(List list) { + for (LibraryGuest guest : list) { + if (guest.getCheckedOutMediaItems().isEmpty()) { + return guest.getId(); + } + } + return list.get(4).getId(); + } +}