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 000000000..ce1e58673 --- /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 = "item 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 000000000..3799bed1a --- /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/GetPatronResponse.java b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/GetPatronResponse.java new file mode 100644 index 000000000..0abf0c1cc --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/GetPatronResponse.java @@ -0,0 +1,14 @@ +package com.codedifferently.lesson16.web; + +import java.util.List; +import lombok.Builder; +import lombok.Data; +import lombok.Singular; + +@Data +@Builder +public class GetPatronResponse { + @Singular private List patronResponses; +} + + 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 000000000..73bc429c6 --- /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 patronResponses; +} 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 d4bfb7bf7..3bc9ce5fb 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 @@ -6,9 +6,13 @@ import com.codedifferently.lesson16.library.search.SearchCriteria; import java.io.IOException; import java.util.List; +import java.util.Optional; import java.util.Set; import org.springframework.web.bind.annotation.CrossOrigin; 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; @RestController @@ -29,4 +33,28 @@ public GetMediaItemsResponse getItems() { var response = GetMediaItemsResponse.builder().items(responseItems).build(); return response; } + + @PostMapping("/items") + public CreateMediaItemResponse createItem(@RequestBody CreateMediaItemRequest request) { + MediaItem item = MediaItemRequest.asMediaItem(request.getItem()); + library.addMediaItem(item, librarian); + return CreateMediaItemResponse.builder().item(MediaItemResponse.from(item)).build(); + } + + @GetMapping("/items/{id}") + public ResponseEntity getItem(@PathVariable UUID id) { + Optional item = + library.search(SearchCriteria.builder().id(id.toString()).build()).stream().findFirst(); + + if (item.isEmpty()) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(MediaItemResponse.from(item.get())); + } + + @DeleteMapping("/items/{id}") + public ResponseEntity deleteItem(@PathVariable("id") UUID id) { + library.removeMediaItem(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 000000000..ebd9e8c55 --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronRequest.java @@ -0,0 +1,3 @@ +package com.codedifferently.lesson16.web; + +public class PatronRequest {} 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 000000000..9aa2ba601 --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronResponse.java @@ -0,0 +1,22 @@ +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; + + public static PatronResponse from(LibraryGuest guest) { + return PatronResponse.builder() + .id(guest.getId()) + .name(guest.getName()) + .email(guest.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 000000000..0e105f7d6 --- /dev/null +++ b/lesson_16/api/api_app/src/main/java/com/codedifferently/lesson16/web/PatronsController.java @@ -0,0 +1,58 @@ +package com.codedifferently.lesson16.web; + +import com.codedifferently.lesson16.library.Librarian; +import com.codedifferently.lesson16.library.Library; +import com.codedifferently.lesson16.library.LibraryGuest; +import com.codedifferently.lesson16.library.Patron; +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.*; + +@RestController +public class PatronsController { + private final Library library; + private final Librarian librarian; + + public PatronsController(Library library) throws IOException { + this.library = library; + this.librarian = library.getLibrarians().stream().findFirst().orElseThrow(); + } + + @GetMapping("/patrons") + public ResponseEntity getPatrons() { + Set guests = library.getPatrons(); + List patrons = guests.stream().map(PatronResponse::from).toList(); + var response = GetPatronsResponse.builder().patrons(patrons).build(); + return ResponseEntity.ok(response); + } + + @PostMapping("/patrons") + public ResponseEntity addPatron(@RequestBody PatronRequest patronRequest) { + Patron patron = PatronRequest.toPatron(patronRequest); + library.addLibraryGuest(patron); + return ResponseEntity.status(HttpStatus.CREATED).body(GetPatronsResponse.from(patron)); + } + + @GetMapping("/patrons/{id}") + public ResponseEntity getPatron(@PathVariable UUID id) { + Patron patron = library.getPatrons(id); + if (patron == null) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok(PatronResponse.from(patron)); + } + + @DeleteMapping("/patrons/{id}") + public ResponseEntity deletePatron(@PathVariable UUID id) { + Patron patron = library.getPatrons(id); + if (patron == null) { + return ResponseEntity.notFound().build(); + } + library.removeLibraryGuest(patron); + return ResponseEntity.noContent().build(); + } +} 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 000000000..02cc594cf --- /dev/null +++ b/lesson_16/api/api_app/src/test/java/com/codedifferently/lesson16/web/PatronsControllerTest.java @@ -0,0 +1,101 @@ +package com.codedifferently.lesson16.web; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +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.Patron; +import com.codedifferently.lesson16.library.search.PatronSearchCriteria; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +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; + +@SpringBootTest +@ContextConfiguration(classes = Lesson16.class) +class PatronsControllerTest { + + private static MockMvc mockMvc; + @Autowired private Library library; + + @BeforeEach + void setUp() {} + + @Test + void testController_getsAllPatrons() throws Exception { + mockMvc + .perform(get("/patrons").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.patrons").isArray()); + } + + @Test + void testController_getsAPatron() throws Exception { + Patron patron = new Patron("John Doe"); + library.addLibraryGuest(patron); + + mockMvc + .perform(get("/patrons/{id}", patron.getId()).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + void testController_returnsNotFoundOnGetPatron() throws Exception { + mockMvc + .perform(get("/patrons/{id}", "00000").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 testController_addsPatron() throws Exception { + String json = "{\"name\": \"John Doe\"}"; + + mockMvc + .perform(post("/patrons").contentType(MediaType.APPLICATION_JSON).content(json)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.patron.id").isString()); + + Set patrons = + library.searchPatrons(PatronSearchCriteria.builder().name("John Doe").build()); + assertThat(patrons).hasSize(1); + } + + @Test + void testController_returnsNotFoundOnDeletePatron() throws Exception { + mockMvc + .perform(delete("/patrons/{id}", "00000").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + void testController_deletesPatron() throws Exception { + Patron patron = new Patron("John Doe"); + library.addLibraryGuest(patron); + + mockMvc + .perform(delete("/patrons/{id}", patron.getId()).contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + + Set patrons = + library.searchPatrons(PatronSearchCriteria.builder().id(patron.getId().toString()).build()); + assertThat(patrons).hasSize(0); + } +}