Revert "Merge branch 'backend' into 'main'"
This reverts merge request !2
This commit is contained in:
parent
79fa5d5f8b
commit
07d3fe304d
7 changed files with 0 additions and 899 deletions
|
|
@ -1,216 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2021 Delft University of Technology
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package commons;
|
|
||||||
|
|
||||||
import jakarta.persistence.GenerationType;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.CollectionTable;
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.JoinColumn;
|
|
||||||
import jakarta.persistence.OrderColumn;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
|
||||||
import jakarta.persistence.ElementCollection;
|
|
||||||
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
// TABLE named recipes
|
|
||||||
@Entity
|
|
||||||
@Table(name = "recipes")
|
|
||||||
public class Recipe {
|
|
||||||
|
|
||||||
// PRIMARY Key, unique id for recipe, generated automatically.
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
|
||||||
@Column(name = "id", nullable = false, unique = true)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
// Recipe name
|
|
||||||
@Column(name = "name", nullable = false, unique = true)
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates another table named recipe_ingredients which stores:
|
|
||||||
* recipe_ingredients(recipe_id -> recipes(id), ingredient).
|
|
||||||
* <p>
|
|
||||||
* Example recipe_ingredients table:
|
|
||||||
* <pre>
|
|
||||||
* | recipe_id | ingredient |
|
|
||||||
* |---------------------|------------|
|
|
||||||
* | 0 (Chocolate Cake) | Egg |
|
|
||||||
* | 0 (Chocolate Cake) | Egg |
|
|
||||||
* | 0 (Chocolate Cake) | Flour |
|
|
||||||
* | 1 (Steak) | 40g salt |
|
|
||||||
* | 1 (Steak) | 40g pepper |
|
|
||||||
* | 1 (Steak) | Meat |
|
|
||||||
* |----------------------------------|
|
|
||||||
* </pre>
|
|
||||||
* TODO: Replace String with Embeddable Ingredient Class
|
|
||||||
*/
|
|
||||||
@ElementCollection
|
|
||||||
@CollectionTable(name = "recipe_ingredients", joinColumns = @JoinColumn(name = "recipe_id"))
|
|
||||||
@Column(name = "ingredient")
|
|
||||||
private List<String> ingredients = new ArrayList<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates another table named recipe_preparation which stores:
|
|
||||||
* recipe_preparation(recipe_id -> recipes(id), preparation_step, step_order).
|
|
||||||
* <p>
|
|
||||||
* Example recipe_preparation table:
|
|
||||||
* <pre>
|
|
||||||
* | recipe_id | preparation_step | step_order |
|
|
||||||
* |---------------------|----------------------|------------|
|
|
||||||
* | 0 (Chocolate Cake) | Preheat oven | 1 |
|
|
||||||
* | 0 (Chocolate Cake) | Mix eggs and sugar | 2 |
|
|
||||||
* | 0 (Chocolate Cake) | Add flour gradually | 3 |
|
|
||||||
* | 1 (Steak) | Season meat | 1 |
|
|
||||||
* | 1 (Steak) | Heat pan | 2 |
|
|
||||||
* |---------------------------------------------------------|
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
@ElementCollection
|
|
||||||
@CollectionTable(name = "recipe_preparation", joinColumns = @JoinColumn(name = "recipe_id"))
|
|
||||||
@Column(name = "preparation_step")
|
|
||||||
@OrderColumn(name = "step_order")
|
|
||||||
private List<String> preparationSteps = new ArrayList<>();
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public Recipe() {
|
|
||||||
// for object mapper
|
|
||||||
}
|
|
||||||
|
|
||||||
public Recipe(Long id, String name) {
|
|
||||||
// Not used by JPA/Spring
|
|
||||||
this.id = id;
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Replace String with Embeddable Ingredient Class for ingredients
|
|
||||||
public Recipe(Long id, String name, List<String> ingredients, List<String> preparationSteps) {
|
|
||||||
// Not used by JPA/Spring
|
|
||||||
this.id = id;
|
|
||||||
this.name = name;
|
|
||||||
this.ingredients = ingredients;
|
|
||||||
this.preparationSteps = preparationSteps;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(Long id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setName(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an unmodifiable view of the ingredients list.
|
|
||||||
* <p>
|
|
||||||
* The returned list cannot be modified directly. To modify ingredients,
|
|
||||||
* create a copy using {@link List#copyOf(Collection)} or create your own list,
|
|
||||||
* populate it, and use {@link #setIngredients(List)} to update.
|
|
||||||
*
|
|
||||||
* @return An unmodifiable list of ingredients.
|
|
||||||
* @see #setIngredients(List)
|
|
||||||
*/
|
|
||||||
// TODO: Replace String with Embeddable Ingredient Class
|
|
||||||
public List<String> getIngredients() {
|
|
||||||
// Disallow modifying the returned list.
|
|
||||||
// You can still copy it with List.copyOf(...)
|
|
||||||
return Collections.unmodifiableList(ingredients);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: Replace String with Embeddable Ingredient Class
|
|
||||||
public void setIngredients(List<String> ingredients) {
|
|
||||||
this.ingredients = ingredients;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an unmodifiable view of the preparation steps list.
|
|
||||||
* <p>
|
|
||||||
* The returned list cannot be modified directly. To modify preparation steps,
|
|
||||||
* create a copy using {@link List#copyOf(Collection)} or create your own list,
|
|
||||||
* populate it, and use {@link #setPreparationSteps(List)} to update.
|
|
||||||
*
|
|
||||||
* @return An unmodifiable list of preparation steps in order.
|
|
||||||
* @see #setPreparationSteps(List)
|
|
||||||
*/
|
|
||||||
public List<String> getPreparationSteps() {
|
|
||||||
// Disallow modifying the returned list.
|
|
||||||
// You can still copy it with List.copyOf(...)
|
|
||||||
return Collections.unmodifiableList(preparationSteps);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPreparationSteps(List<String> preparationSteps) {
|
|
||||||
this.preparationSteps = preparationSteps;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
// Faster/Better equals than reflectionEquals
|
|
||||||
if (this == o) return true;
|
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
|
||||||
Recipe recipe = (Recipe) o;
|
|
||||||
return Objects.equals(id, recipe.id); // Check only for ID, as its unique!
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hash(id); // Use ID only, as its unique.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "Recipe{" +
|
|
||||||
"name='" + name + '\'' +
|
|
||||||
", ingredientsCount=" + ingredients.size() +
|
|
||||||
", preparationStepsCount=" + preparationSteps.size() +
|
|
||||||
"}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a more detailed string than {@link #toString()} of this recipe, including
|
|
||||||
* the full contents of all ingredients and preparation steps.
|
|
||||||
* <p>
|
|
||||||
* Intended only for debugging.
|
|
||||||
*
|
|
||||||
* @return A detailed string representation containing all recipe data.
|
|
||||||
* @see #toString()
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public String toDetailedString() {
|
|
||||||
return "Recipe{" +
|
|
||||||
"name='" + name + '\'' +
|
|
||||||
", ingredients=" + ingredients +
|
|
||||||
", preparationSteps=" + preparationSteps +
|
|
||||||
'}';
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
package commons;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
class RecipeTest {
|
|
||||||
|
|
||||||
Recipe recipe;
|
|
||||||
static final Long RECIPE_ID = 1L;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setupRecipe() {
|
|
||||||
this.recipe = new Recipe(RECIPE_ID, "Chocolate Cake");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getId() {
|
|
||||||
assertEquals(RECIPE_ID, recipe.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void setId() {
|
|
||||||
recipe.setId(RECIPE_ID + 1);
|
|
||||||
assertEquals(RECIPE_ID + 1, recipe.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getName() {
|
|
||||||
assertEquals("Chocolate Cake", recipe.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void setName() {
|
|
||||||
recipe.setName("Steak");
|
|
||||||
assertEquals("Steak", recipe.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getIngredientsAddThrow() {
|
|
||||||
// TODO: Change to actual Ingredient class later
|
|
||||||
assertThrows(UnsupportedOperationException.class, () -> recipe.getIngredients().add("Lasagna"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getIngredientsClearThrow() {
|
|
||||||
assertThrows(UnsupportedOperationException.class, () -> recipe.getIngredients().clear());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void setIngredients() {
|
|
||||||
// TODO: Change to actual Ingredient class later
|
|
||||||
List<String> ingredients = new ArrayList<>(List.of("Chocolate", "Flour", "Egg"));
|
|
||||||
recipe.setIngredients(ingredients);
|
|
||||||
|
|
||||||
assertEquals(recipe.getIngredients(), ingredients);
|
|
||||||
assertEquals(recipe.getIngredients().size(), ingredients.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getPreparationStepsAddThrow() {
|
|
||||||
assertThrows(UnsupportedOperationException.class, () -> recipe.getPreparationSteps().add("Preheat Oven"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getPreparationStepsClearThrow() {
|
|
||||||
assertThrows(UnsupportedOperationException.class, () -> recipe.getPreparationSteps().clear());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void setPreparationSteps() {
|
|
||||||
List<String> steps = new ArrayList<>(List.of("Preheat oven", "Mix stuff", "decorate"));
|
|
||||||
recipe.setPreparationSteps(steps);
|
|
||||||
|
|
||||||
assertEquals(recipe.getPreparationSteps(), steps);
|
|
||||||
assertEquals(recipe.getPreparationSteps().size(), steps.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testEqualsSame() {
|
|
||||||
Recipe recipe2 = new Recipe(RECIPE_ID, "Chocolate Cake");
|
|
||||||
assertEquals(recipe, recipe2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testEqualsSameExceptId() {
|
|
||||||
Recipe recipe2 = new Recipe(RECIPE_ID + 1, "Chocolate Cake");
|
|
||||||
assertNotEquals(recipe, recipe2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testEqualsOnlySameId() {
|
|
||||||
Recipe recipe2 = new Recipe(RECIPE_ID, "Some random recipe");
|
|
||||||
assertEquals(recipe, recipe2); // Equals, we only look at ID!!!
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testHashCodeSame() {
|
|
||||||
Recipe recipe2 = new Recipe(RECIPE_ID, "Chocolate Cake");
|
|
||||||
|
|
||||||
assertEquals(recipe.hashCode(), recipe2.hashCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testHashCodeSameExceptId() {
|
|
||||||
Recipe recipe2 = new Recipe(RECIPE_ID + 1, "Chocolate Cake");
|
|
||||||
|
|
||||||
assertNotEquals(recipe.hashCode(), recipe2.hashCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testHashCodeOnlySameId() {
|
|
||||||
Recipe recipe2 = new Recipe(RECIPE_ID, "Some random recipe");
|
|
||||||
|
|
||||||
assertEquals(recipe.hashCode(), recipe2.hashCode()); // Same, only looks at ID!!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,243 +0,0 @@
|
||||||
## Recipe API
|
|
||||||
|
|
||||||
Base path: `/api`
|
|
||||||
|
|
||||||
This API allows clients to create, read, update, and delete `Recipe` resources.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Endpoints
|
|
||||||
|
|
||||||
### Get a Recipe by ID
|
|
||||||
|
|
||||||
**GET** `/api/recipe/{id}`
|
|
||||||
|
|
||||||
Retrieves a specific recipe by its unique identifier.
|
|
||||||
|
|
||||||
* **Path Parameters**
|
|
||||||
|
|
||||||
| Name | Type | Required | Description |
|
|
||||||
| ---- | ------ | -------- | ------------------------------ |
|
|
||||||
| `id` | `Long` | Yes | The ID of the recipe to fetch. |
|
|
||||||
|
|
||||||
* **Responses**
|
|
||||||
|
|
||||||
| Status Code | Description |
|
|
||||||
| --------------- | ------------------------------------- |
|
|
||||||
| `200 OK` | Returns the recipe with the given ID. |
|
|
||||||
| `404 Not Found` | No recipe exists with that ID. |
|
|
||||||
|
|
||||||
* **Example Request**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X GET "http://SERVER_ADDRESS/api/recipe/1" \
|
|
||||||
-H "Accept: application/json"
|
|
||||||
```
|
|
||||||
|
|
||||||
* **Example Response (200)**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "Pancakes",
|
|
||||||
"ingredients": ["Ingredient 1", "Ingredient 2"],
|
|
||||||
"preparationSteps": ["Step 1", "Step 2"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### List Recipes
|
|
||||||
|
|
||||||
**GET** `/api/recipes`
|
|
||||||
|
|
||||||
Retrieves a list of recipes. Supports optional pagination via a `limit` query parameter.
|
|
||||||
|
|
||||||
* **Query Parameters**
|
|
||||||
|
|
||||||
| Name | Type | Required | Description |
|
|
||||||
| ------- | --------- | -------- | ---------------------------------------------------------------------------- |
|
|
||||||
| `limit` | `Integer` | No | Maximum number of recipes to return. If not provided, returns *all* recipes. |
|
|
||||||
|
|
||||||
* **Responses**
|
|
||||||
|
|
||||||
| Status Code | Description |
|
|
||||||
| ----------- | --------------------------------------------- |
|
|
||||||
| `200 OK` | Returns a list of recipes (possibly limited). |
|
|
||||||
|
|
||||||
* **Example Request (no limit)**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X GET "http://SERVER_ADDRESS/api/recipes" \
|
|
||||||
-H "Accept: application/json"
|
|
||||||
```
|
|
||||||
|
|
||||||
* **Example Request (with limit)**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X GET "http://SERVER_ADDRESS/api/recipes?limit=10" \
|
|
||||||
-H "Accept: application/json"
|
|
||||||
```
|
|
||||||
|
|
||||||
* **Example Response (200)**
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 10,
|
|
||||||
"name": "Recipe 0",
|
|
||||||
"ingredients": ["Flour", "Milk"],
|
|
||||||
"preparationSteps": ["Do something", "Do something else"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 11,
|
|
||||||
"name": "Recipe 1",
|
|
||||||
"ingredients": ["Flour", "Milk"],
|
|
||||||
"preparationSteps": ["Do something", "Do something else"]
|
|
||||||
}
|
|
||||||
// etc. max {limit} items
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Create a New Recipe
|
|
||||||
|
|
||||||
**PUT** `/api/recipe/new`
|
|
||||||
|
|
||||||
Creates a new recipe. The recipe name must be unique in the repository.
|
|
||||||
|
|
||||||
* **Request Body**
|
|
||||||
|
|
||||||
A JSON object representing the recipe. Example:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Pancakes",
|
|
||||||
"ingredients": ["Flour", "Milk", "Eggs"],
|
|
||||||
"preparationSteps": ["Step 1", "Step 2"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* **Responses**
|
|
||||||
|
|
||||||
| Status Code | Description |
|
|
||||||
| ----------------- | -------------------------------------------------------------------------------------------- |
|
|
||||||
| `200 OK` | The recipe was successfully created. Returns the created recipe (including its assigned ID). |
|
|
||||||
| `400 Bad Request` | A recipe with the same name already exists. |
|
|
||||||
|
|
||||||
* **Example Request**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X PUT "https://SERVER_ADDRESS/api/recipe/new" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"name": "Pancakes",
|
|
||||||
"ingredients": ["Flour", "Milk", "Eggs"],
|
|
||||||
"preparationSteps": ["Step 1", "Step 2"]
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
* **Example Response (200)**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": 125,
|
|
||||||
"name": "Pancakes",
|
|
||||||
"ingredients": ["Flour", "Milk", "Eggs"],
|
|
||||||
"preparationSteps": ["Step 1", "Step 2"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Update an Existing Recipe
|
|
||||||
|
|
||||||
**POST** `/api/recipe/{id}`
|
|
||||||
|
|
||||||
Replaces or updates the recipe with the given ID.
|
|
||||||
|
|
||||||
* **Path Parameters**
|
|
||||||
|
|
||||||
| Name | Type | Required | Description |
|
|
||||||
| ---- | ------ | -------- | ------------------------------- |
|
|
||||||
| `id` | `Long` | Yes | The ID of the recipe to update. |
|
|
||||||
|
|
||||||
* **Request Body**
|
|
||||||
|
|
||||||
A JSON object containing the new recipe data. It is expected to include the ID (or the server-side save will override it), or at least map correctly to the stored entity.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": 123,
|
|
||||||
"name": "Better Pancakes",
|
|
||||||
"ingredients": ["Flour", "Almond milk", "Eggs"],
|
|
||||||
"preparationSteps": ["Step 10", "Step 11"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* **Responses**
|
|
||||||
|
|
||||||
| Status Code | Description |
|
|
||||||
| ----------------- | ---------------------------------------------------------------- |
|
|
||||||
| `200 OK` | The recipe was successfully updated. Returns the updated recipe. |
|
|
||||||
| `400 Bad Request` | No recipe exists with the given ID. |
|
|
||||||
|
|
||||||
* **Example Request**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST "https://your-domain.com/api/recipe/123" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{
|
|
||||||
"id": 123,
|
|
||||||
"name": "Better Pancakes",
|
|
||||||
"ingredients": ["Flour", "Almond milk", "Eggs"],
|
|
||||||
"preparationSteps": ["Step 10", "Step 11"]
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
* **Example Response (200)**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": 123,
|
|
||||||
"name": "Updated Pancakes",
|
|
||||||
"ingredients": ["Flour", "Almond milk", "Eggs"],
|
|
||||||
"preparationSteps": "Mix and fry differently."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Delete a Recipe
|
|
||||||
|
|
||||||
**DELETE** `/api/recipe/{id}`
|
|
||||||
|
|
||||||
Deletes the recipe with the given ID.
|
|
||||||
|
|
||||||
* **Path Parameters**
|
|
||||||
|
|
||||||
| Name | Type | Required | Description |
|
|
||||||
| ---- | ------ | -------- | ------------------------------- |
|
|
||||||
| `id` | `Long` | Yes | The ID of the recipe to delete. |
|
|
||||||
|
|
||||||
* **Responses**
|
|
||||||
|
|
||||||
| Status Code | Description |
|
|
||||||
| ----------------- | ---------------------------------------------------- |
|
|
||||||
| `200 OK` | The recipe was successfully deleted. Returns `true`. |
|
|
||||||
| `400 Bad Request` | No recipe exists with the given ID. |
|
|
||||||
|
|
||||||
* **Example Request**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X DELETE "https://your-domain.com/api/recipe/123"
|
|
||||||
```
|
|
||||||
|
|
||||||
* **Example Response (200)**
|
|
||||||
|
|
||||||
```json
|
|
||||||
true
|
|
||||||
```
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
package server.api;
|
|
||||||
|
|
||||||
import commons.Recipe;
|
|
||||||
|
|
||||||
import org.springframework.data.domain.Example;
|
|
||||||
import org.springframework.data.domain.PageRequest;
|
|
||||||
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.PutMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import server.database.RecipeRepository;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api")
|
|
||||||
public class RecipeController {
|
|
||||||
private final RecipeRepository recipeRepository; // JPA repository used in this controller
|
|
||||||
|
|
||||||
public RecipeController(RecipeRepository recipeRepository) {
|
|
||||||
this.recipeRepository = recipeRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapping for <code>GET /recipe/{id}</code>
|
|
||||||
* <p>
|
|
||||||
* Gets a specific recipe by its unique id.
|
|
||||||
* </p>
|
|
||||||
* @param id id of the recipe
|
|
||||||
* @return The recipe if it exists in the repository; otherwise returns 404 Not Found status
|
|
||||||
*/
|
|
||||||
@GetMapping("/recipe/{id}")
|
|
||||||
public ResponseEntity<Recipe> getRecipe(@PathVariable Long id) {
|
|
||||||
if (!recipeRepository.existsById(id)) {
|
|
||||||
return ResponseEntity.notFound().build();
|
|
||||||
}
|
|
||||||
return ResponseEntity.ok(recipeRepository.findById(id).get());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapping for <code>GET /recipes(?limit=)</code>
|
|
||||||
* <p>
|
|
||||||
* If the limit parameter is unspecified, return all recipes in the repository.
|
|
||||||
* @param limit Integer limit of items you want to get
|
|
||||||
* @return The list of recipes
|
|
||||||
*/
|
|
||||||
@GetMapping("/recipes")
|
|
||||||
public ResponseEntity<List<Recipe>> getRecipes(@RequestParam Optional<Integer> limit) {
|
|
||||||
if (limit.isPresent()) {
|
|
||||||
return ResponseEntity.ok(
|
|
||||||
recipeRepository.findAll(
|
|
||||||
PageRequest.of(0, limit.get())
|
|
||||||
).toList());
|
|
||||||
}
|
|
||||||
return ResponseEntity.ok(recipeRepository.findAll());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapping for <code>POST /recipe/{id}</code>
|
|
||||||
* @param id The recipe id to replace
|
|
||||||
* @param recipe The new recipe to be replaced from the original
|
|
||||||
* @return The changed recipe; returns 400 Bad Request if the recipe does not exist
|
|
||||||
*/
|
|
||||||
@PostMapping("/recipe/{id}")
|
|
||||||
public ResponseEntity<Recipe> updateRecipe(@PathVariable Long id, @RequestBody Recipe recipe) {
|
|
||||||
if (!recipeRepository.existsById(id)) {
|
|
||||||
return ResponseEntity.badRequest().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Send WS update to all subscribers with the updated recipe
|
|
||||||
|
|
||||||
return ResponseEntity.ok(recipeRepository.save(recipe));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapping for <code>PUT /recipe/new</code>
|
|
||||||
* <p>
|
|
||||||
* Inserts a new recipe into the repository
|
|
||||||
* </p>
|
|
||||||
* @param recipe The new recipe as a request body
|
|
||||||
* @return 200 OK with the recipe you added; or 400 Bad Request if the recipe already exists by name
|
|
||||||
*/
|
|
||||||
@PutMapping("/recipe/new")
|
|
||||||
public ResponseEntity<Recipe> createRecipe(@RequestBody Recipe recipe) {
|
|
||||||
|
|
||||||
// We initialize a new example recipe with the name of input recipe
|
|
||||||
// This is the only attribute we are concerned about making sure it's unique
|
|
||||||
Recipe example = new Recipe();
|
|
||||||
example.setName(recipe.getName());
|
|
||||||
|
|
||||||
/* Here we use very funny JPA magic repository.exists(Example<Recipe>)
|
|
||||||
We check if any recipe in the repository has the same name as the input
|
|
||||||
*/
|
|
||||||
if (recipeRepository.exists(Example.of(example))) {
|
|
||||||
return ResponseEntity.badRequest().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Send WS update to all subscribers with the new recipe
|
|
||||||
|
|
||||||
return ResponseEntity.ok(recipeRepository.save(recipe));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapping for <code>DELETE /recipe/{id}</code>
|
|
||||||
* <p>
|
|
||||||
* Deletes a recipe identified by its <code>id</code>.
|
|
||||||
* </p>
|
|
||||||
* @param id The id of the recipe to be deleted.
|
|
||||||
* @return 200 OK with true; or 400 Bad Request if the recipe doesn't exist.
|
|
||||||
*/
|
|
||||||
@DeleteMapping("/recipe/{id}")
|
|
||||||
public ResponseEntity<Boolean> deleteRecipe(@PathVariable Long id) {
|
|
||||||
if (!recipeRepository.existsById(id)) {
|
|
||||||
return ResponseEntity.badRequest().build();
|
|
||||||
}
|
|
||||||
recipeRepository.deleteById(id);
|
|
||||||
|
|
||||||
// TODO: Send WS update to propagate deletion
|
|
||||||
return ResponseEntity.ok(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2021 Delft University of Technology
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package server.database;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
import commons.Recipe;
|
|
||||||
|
|
||||||
public interface RecipeRepository extends JpaRepository<Recipe, Long> {}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
spring.datasource.driverClassName=org.h2.Driver
|
|
||||||
spring.datasource.username=sa
|
|
||||||
spring.datasource.password=
|
|
||||||
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
|
||||||
|
|
||||||
# use one of these alternatives...
|
|
||||||
# ... purely in-memory, wiped on restart, but great for testing
|
|
||||||
spring.datasource.url=jdbc:h2:mem:testdb
|
|
||||||
# ... persisted on disk (in project directory)
|
|
||||||
#spring.datasource.url=jdbc:h2:file:./h2-database
|
|
||||||
|
|
||||||
# enable DB view on http://localhost:8080/h2-console
|
|
||||||
spring.h2.console.enabled=true
|
|
||||||
|
|
||||||
# strategy for table (re-)generation
|
|
||||||
spring.jpa.hibernate.ddl-auto=update
|
|
||||||
# show auto-generated SQL commands
|
|
||||||
#spring.jpa.hibernate.show_sql=true
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
package server.api;
|
|
||||||
|
|
||||||
|
|
||||||
import commons.Recipe;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Tag;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.TestInfo;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
|
||||||
import server.database.RecipeRepository;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.LongStream;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
|
|
||||||
// Spring Boot unit testing magic
|
|
||||||
// Before each test the state of the repository is reset by
|
|
||||||
// rolling back transaction of changes from previous test
|
|
||||||
@DataJpaTest
|
|
||||||
|
|
||||||
// This test sources its application profile from
|
|
||||||
// resources/application-mock-data-test.properties
|
|
||||||
// This config uses an in-memory database
|
|
||||||
@ActiveProfiles("mock-data-test")
|
|
||||||
public class RecipeControllerTest {
|
|
||||||
private RecipeController controller;
|
|
||||||
private List<Recipe> recipes;
|
|
||||||
private final RecipeRepository recipeRepository;
|
|
||||||
private List<Long> recipeIds;
|
|
||||||
public static final int NUM_RECIPES = 10;
|
|
||||||
|
|
||||||
// Injects a test repository into the test class
|
|
||||||
@Autowired
|
|
||||||
public RecipeControllerTest(RecipeRepository recipeRepository) {
|
|
||||||
this.recipeRepository = recipeRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
public void setup(TestInfo info) {
|
|
||||||
recipes = LongStream
|
|
||||||
.range(0, NUM_RECIPES)
|
|
||||||
.mapToObj(x -> new Recipe(null, "Recipe " + x, List.of(), List.of()))
|
|
||||||
.toList();
|
|
||||||
controller = new RecipeController(recipeRepository);
|
|
||||||
Set<String> tags = info.getTags();
|
|
||||||
List<Long> ids = new ArrayList<>();
|
|
||||||
|
|
||||||
// Some tests want initial data to be created.
|
|
||||||
if (tags.contains("test-from-init-data")) {
|
|
||||||
ids = LongStream
|
|
||||||
.range(0, NUM_RECIPES)
|
|
||||||
.map(idx -> recipeRepository.save(recipes.get((int) idx)).getId())
|
|
||||||
.boxed().toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some tests need to know the stored IDs of objects
|
|
||||||
if (tags.contains("need-ids")) {
|
|
||||||
recipeIds = ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no tags specified, the repository is initialized as empty.
|
|
||||||
}
|
|
||||||
@Test
|
|
||||||
public void createOneRecipe() {
|
|
||||||
controller.createRecipe(recipes.getFirst());
|
|
||||||
|
|
||||||
// There is 1 recipe in the repository
|
|
||||||
assertEquals(1, recipeRepository.count());
|
|
||||||
}
|
|
||||||
@Test
|
|
||||||
public void createManyRecipes() {
|
|
||||||
recipes.forEach(recipe -> controller.createRecipe(recipe));
|
|
||||||
|
|
||||||
// There are the same number of recipes in the repository as the input list
|
|
||||||
assertEquals(recipes.size(), recipeRepository.count());
|
|
||||||
}
|
|
||||||
@Test
|
|
||||||
@Tag("test-from-init-data")
|
|
||||||
public void getManyRecipes() {
|
|
||||||
// The number of recipes returned is the same as the entire input list
|
|
||||||
assertEquals(recipes.size(), controller.getRecipes(Optional.empty()).getBody().size());
|
|
||||||
}
|
|
||||||
@Test
|
|
||||||
@Tag("test-from-init-data")
|
|
||||||
public void getSomeRecipes() {
|
|
||||||
final int LIMIT = 5;
|
|
||||||
// The number of recipes returned is the same as the entire input list
|
|
||||||
assertEquals(LIMIT, controller.getRecipes(Optional.of(LIMIT)).getBody().size());
|
|
||||||
}
|
|
||||||
@Test
|
|
||||||
@Tag("test-from-init-data")
|
|
||||||
@Tag("need-ids")
|
|
||||||
public void findOneRecipeExists() {
|
|
||||||
final int CHECK_INDEX = 3;
|
|
||||||
// The third item in the input list is the same as the third item retrieved from the database
|
|
||||||
assertEquals(
|
|
||||||
recipes.get(CHECK_INDEX),
|
|
||||||
controller.getRecipe(recipeIds.get(CHECK_INDEX)).getBody());
|
|
||||||
}
|
|
||||||
@Test
|
|
||||||
public void findOneRecipeNotExists() {
|
|
||||||
final int CHECK_INDEX = 3;
|
|
||||||
// There does not exist a recipe with ID=3 since there are no items in the repository.
|
|
||||||
assertEquals(
|
|
||||||
HttpStatus.NOT_FOUND,
|
|
||||||
controller.getRecipe((long) CHECK_INDEX).getStatusCode());
|
|
||||||
}
|
|
||||||
@Test
|
|
||||||
@Tag("test-from-init-data")
|
|
||||||
@Tag("need-ids")
|
|
||||||
public void deleteOneRecipeGood() {
|
|
||||||
final int DELETE_INDEX = 5;
|
|
||||||
|
|
||||||
// The object has been successfully deleted
|
|
||||||
assertEquals(HttpStatus.OK, controller.deleteRecipe(recipeIds.get(DELETE_INDEX)).getStatusCode());
|
|
||||||
}
|
|
||||||
@Test
|
|
||||||
@Tag("test-from-init-data")
|
|
||||||
@Tag("need-ids")
|
|
||||||
public void deleteOneRecipeCountGood() {
|
|
||||||
final int DELETE_INDEX = 5;
|
|
||||||
controller.deleteRecipe(recipeIds.get(DELETE_INDEX));
|
|
||||||
// The count of items decreased by 1 after the 5th item has been removed.
|
|
||||||
assertEquals(recipeIds.size() - 1, recipeRepository.count());
|
|
||||||
}
|
|
||||||
@Test
|
|
||||||
public void deleteOneRecipeFail() {
|
|
||||||
final Long DELETE_INDEX = 5L;
|
|
||||||
assertEquals(HttpStatus.BAD_REQUEST, controller.deleteRecipe(DELETE_INDEX).getStatusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Tag("test-from-init-data")
|
|
||||||
@Tag("need-ids")
|
|
||||||
public void updateOneRecipeHasNewData() {
|
|
||||||
final int UPDATE_INDEX = 5;
|
|
||||||
Recipe newRecipe = controller.getRecipe(recipeIds.get(UPDATE_INDEX)).getBody();
|
|
||||||
newRecipe.setName("New recipe");
|
|
||||||
controller.updateRecipe(newRecipe.getId(), newRecipe);
|
|
||||||
assertEquals("New recipe", recipeRepository.getReferenceById(recipeIds.get(UPDATE_INDEX)).getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue