feat: basic Recipe class and RecipeController with GET, POST, PUT, DELETE

This commit is contained in:
Oskar Rasieński 2025-11-25 17:46:15 +01:00
commit 3aa530ffde
7 changed files with 899 additions and 0 deletions

View file

@ -0,0 +1,216 @@
/*
* 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 +
'}';
}
}

View file

@ -0,0 +1,121 @@
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!!!
}
}