Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ implement ingredient registration #78

Merged
merged 18 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
package net.pengcook.ingredient.domain;

import static lombok.AccessLevel.PRIVATE;
import static lombok.AccessLevel.PROTECTED;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor(access = PROTECTED)
@AllArgsConstructor(access = PRIVATE)
@Getter
public class Ingredient {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;

private String name;

public Ingredient(String name) {
this(0L, name);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AllArgumentConstructor 사용후 this(0L, ...) 으로 초기화 해도 좋을것 같아요.
Category 도메인 참고 해보시죠

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package net.pengcook.ingredient.domain;

import static lombok.AccessLevel.PRIVATE;
import static lombok.AccessLevel.PROTECTED;

import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
Expand All @@ -8,9 +11,15 @@
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.pengcook.recipe.domain.Recipe;

@Entity
@NoArgsConstructor(access = PROTECTED)
@AllArgsConstructor(access = PRIVATE)
@Getter
public class IngredientRecipe {

@Id
Expand All @@ -27,4 +36,8 @@ public class IngredientRecipe {

@Enumerated(EnumType.STRING)
private Requirement requirement;

public IngredientRecipe(Ingredient ingredient, Recipe recipe, Requirement requirement) {
this(0L, ingredient, recipe, requirement);
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
package net.pengcook.ingredient.domain;

import static lombok.AccessLevel.PRIVATE;
import static lombok.AccessLevel.PROTECTED;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor(access = PROTECTED)
@AllArgsConstructor(access = PRIVATE)
@Getter
public class IngredientSubstitution {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
Expand All @@ -20,4 +30,8 @@ public class IngredientSubstitution {
@ManyToOne
@JoinColumn(name = "ingredient_id")
private Ingredient ingredient;

public IngredientSubstitution(IngredientRecipe ingredientRecipe, Ingredient ingredient) {
this(0L, ingredientRecipe, ingredient);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package net.pengcook.ingredient.dto;

import java.util.List;
import net.pengcook.ingredient.domain.Requirement;
import net.pengcook.ingredient.exception.InvalidNameException;

public record IngredientCreateRequest(
String name,
Requirement requirement,
List<String> substitutions
) {

public IngredientCreateRequest {
validateName(name);
if (requirement == Requirement.ALTERNATIVE) {
validateSubstitutions(substitutions);
}
}

private void validateName(String name) {
if (name == null || name.isBlank()) {
throw new InvalidNameException("name cannot be null or empty");
}
}

private void validateSubstitutions(List<String> substitutions) {
if (substitutions == null) {
throw new InvalidNameException("substitutions cannot be null");
}
for (String substitution : substitutions) {
validateName(substitution);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.pengcook.ingredient.exception;

import org.springframework.http.HttpStatus;

public class InvalidNameException extends IngredientException {

public InvalidNameException(String message) {
super(HttpStatus.BAD_REQUEST, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.pengcook.ingredient.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import net.pengcook.ingredient.domain.IngredientRecipe;

@Repository
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 어노테이션 없어도 된대요
JpaRepository면 Spring Data JPA가 알아서 구현체를 주입해준다고 하네요!

public interface IngredientRecipeRepository extends JpaRepository<IngredientRecipe, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package net.pengcook.ingredient.repository;

import java.util.Optional;
import net.pengcook.ingredient.domain.Ingredient;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface IngredientRepository extends JpaRepository<Ingredient, Long> {

Optional<Ingredient> findByName(String name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.pengcook.ingredient.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import net.pengcook.ingredient.domain.IngredientSubstitution;

@Repository
public interface IngredientSubstitutionRepository extends JpaRepository<IngredientSubstitution, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package net.pengcook.ingredient.service;

import org.springframework.stereotype.Service;

import net.pengcook.ingredient.domain.Ingredient;
import net.pengcook.ingredient.domain.IngredientRecipe;
import net.pengcook.ingredient.domain.Requirement;
import net.pengcook.ingredient.repository.IngredientRecipeRepository;
import net.pengcook.recipe.domain.Recipe;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class IngredientRecipeService {

private final IngredientRecipeRepository ingredientRecipeRepository;

public IngredientRecipe save(Ingredient ingredient, Recipe recipe, Requirement requirement) {
IngredientRecipe ingredientRecipe = new IngredientRecipe(ingredient, recipe, requirement);
return ingredientRecipeRepository.save(ingredientRecipe);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package net.pengcook.ingredient.service;


import jakarta.transaction.Transactional;
import java.util.HashSet;
import java.util.List;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.pengcook.ingredient.domain.Ingredient;
import net.pengcook.ingredient.domain.IngredientRecipe;
import net.pengcook.ingredient.domain.Requirement;
import net.pengcook.ingredient.dto.IngredientCreateRequest;
import net.pengcook.ingredient.exception.InvalidNameException;
import net.pengcook.ingredient.repository.IngredientRepository;
import net.pengcook.recipe.domain.Recipe;
import org.springframework.stereotype.Service;

@Service
@Transactional
@RequiredArgsConstructor
@Getter
public class IngredientService {

private final IngredientRepository ingredientRepository;
private final IngredientRecipeService ingredientRecipeService;
private final IngredientSubstitutionService ingredientSubstitutionService;

public void register(List<IngredientCreateRequest> requests, Recipe recipe) {
validateDuplicateNames(getIngredientNames(requests));
for (IngredientCreateRequest request : requests) {
registerOne(request, recipe);
}
}

private void registerOne(IngredientCreateRequest request, Recipe recipe) {
Ingredient ingredient = registerOrGetIngredient(request.name());
IngredientRecipe ingredientRecipe = registerIngredientRecipe(recipe, request, ingredient);

if (request.requirement() == Requirement.ALTERNATIVE) {
registerSubstitution(request, ingredientRecipe);
}
}

private Ingredient registerOrGetIngredient(String ingredientName) {
String name = ingredientName.toLowerCase();

return ingredientRepository.findByName(name)
.orElseGet(() -> ingredientRepository.save(new Ingredient(name)));
}

private IngredientRecipe registerIngredientRecipe(
Recipe recipe,
IngredientCreateRequest request,
Ingredient ingredient
) {
return ingredientRecipeService.save(ingredient, recipe, request.requirement());
}

private void registerSubstitution(IngredientCreateRequest request, IngredientRecipe ingredientRecipe) {
List<String> substitutionNames = request.substitutions();
validateDuplicateNames(substitutionNames);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

registerSubstitution 메서드와 registerIngredientSubstitution 이 메서드를 private 보다 IngredientRecipeService와 IngredientSubstitutionService의 public 메서드로 해보는것은 어떨까요?

IngredientSubstitution을 저장하는것은 IngredientSubstitutionService의 책임인데 이걸 IngredientService가 대신 해주고 있는것 같아요.

또한 Private 메서드라 테스트가 어려워요.

for (String substitutionName : substitutionNames) {
Ingredient substitution = registerOrGetIngredient(substitutionName);
registerIngredientSubstitution(ingredientRecipe, substitution);
}
}

private void registerIngredientSubstitution(IngredientRecipe ingredientRecipe, Ingredient substitution) {
ingredientSubstitutionService.save(ingredientRecipe, substitution);
}
Comment on lines +69 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 메서드가... 꼭 필요한가요....???
정말 몰라서 물어보는 겁니다!!!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

가독성을 위해 생성하였습니다.
꼭 필요하진 않습니다!🥲


private List<String> getIngredientNames(List<IngredientCreateRequest> requests) {
return requests.stream()
.map(IngredientCreateRequest::name)
.map(String::toLowerCase)
.toList();
}

private void validateDuplicateNames(List<String> names) {
if (hasDuplicateName(names)) {
throw new InvalidNameException("ingredient name duplicated");
}
}

private boolean hasDuplicateName(List<String> names) {
HashSet<String> nonDuplicate = new HashSet<>(names);
return (names.size() != nonDuplicate.size());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package net.pengcook.ingredient.service;

import org.springframework.stereotype.Service;

import net.pengcook.ingredient.domain.Ingredient;
import net.pengcook.ingredient.domain.IngredientRecipe;
import net.pengcook.ingredient.domain.IngredientSubstitution;
import net.pengcook.ingredient.repository.IngredientSubstitutionRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class IngredientSubstitutionService {

private final IngredientSubstitutionRepository ingredientSubstitutionRepository;

public void save(IngredientRecipe ingredientRecipe, Ingredient substitution) {
IngredientSubstitution ingredientSubstitution = new IngredientSubstitution(ingredientRecipe, substitution);
ingredientSubstitutionRepository.save(ingredientSubstitution);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package net.pengcook.ingredient.dto;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import net.pengcook.ingredient.domain.Requirement;
import net.pengcook.ingredient.exception.InvalidNameException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;

class IngredientCreateRequestTest {

static Stream<Arguments> substitutions() {
List<String> nullSubstitution = new ArrayList<>();
nullSubstitution.add(null);

return Stream.of(
Arguments.arguments(List.of("", "wheat")),
Arguments.arguments(List.of(" ", "wheat")),
Arguments.arguments(nullSubstitution)
);
}

@ParameterizedTest
@CsvSource(value = {
"''",
"' '",
})
@DisplayName("재료 이름에 공백 또는 null은 허용하지 않는다.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 테스트 코드 네이밍

  • DisplayName은 꼭 사용해요.
  • DisplayName
    • 성공
      • {행위}가 된다.
    • 실패
      • {상황}에 {행위}를 하면 예외가 발생한다.

재료 이름에 공백 또는 null이 입력되면 예외가 발생한다.
어떨지요

void validateName(String name) {
assertThatThrownBy(() -> new IngredientCreateRequest(name, Requirement.ALTERNATIVE, List.of("wheat")))
.isInstanceOf(InvalidNameException.class);
}

@ParameterizedTest
@MethodSource("substitutions")
@DisplayName("ALTERNATIVE 상태일 때, 대체 재료 이름에 공백 또는 null은 허용하지 않는다.")
void validateSubstitutionName(List<String> substitutions) {
assertThatThrownBy(() -> new IngredientCreateRequest("rice", Requirement.ALTERNATIVE, substitutions))
.isInstanceOf(InvalidNameException.class);
}

@Test
@DisplayName("ALTERNATIVE 상태일 때, 대체 재료 리스트에 null은 허용하지 않는다.")
void validateSubstitutionIsNull() {
assertThatThrownBy(() -> new IngredientCreateRequest("rice", Requirement.ALTERNATIVE, null))
.isInstanceOf(InvalidNameException.class);
}
}
Loading
Loading