persistent_code_challenge

Signed-off-by: rlalwanigit <connect@rohitlalwani.com>
This commit is contained in:
rlalwanigit 2025-07-07 19:17:54 +05:30
parent 30aab0ae76
commit 90a20943bf
21 changed files with 695 additions and 4 deletions

View file

@ -31,6 +31,57 @@ Or you can run it from Maven directly using the Spring Boot Maven plugin. If you
> NOTE: If you prefer to use Gradle, you can build the app using `./gradlew build` and look for the jar file in `build/libs`.
## 🔧 New Features Added
### ✅ PetAttribute Module
- Added a new model `PetAttribute` linked to `PetType` to capture details like temperament or breed.
- REST endpoints:
- `POST /api/pettypes/{petTypeId}/attributes` Add attribute
- `GET /api/pettypes/{petTypeId}/attributes` Get attributes by pet type
### ⚡ gRPC Integration
- Introduced gRPC support using the proto definition `pet-attribute.proto`.
- Sample proto file:
```proto
syntax = "proto3";
option java_multiple_files = true;
option java_package = "org.springframework.samples.petclinic.grpc";
option java_outer_classname = "PetAttributeProto";
service PetAttributeService {
rpc GetAttributes(PetAttributeRequest) returns (PetAttributeList);
rpc AddAttribute(NewPetAttribute) returns (PetAttributeResponse);
}
### ⚙️ gRPC Java Classes
- Auto-generated under:
target/generated-sources/grpc
### 🔗 Spring HATEOAS Support
Hypermedia links are now included in `PetAttribute` responses.
**Example Response:**
```json
{
"temperament": "Energetic",
"_links": {
"self": {
"href": "http://localhost:8080/api/pettypes/1/attributes/3"
},
"petType": {
"href": "http://localhost:8080/api/pettypes/1"
}
}
}
```
## Building a Container
There is no `Dockerfile` in this project. You can build a container image (if you have a docker daemon) using the Spring Boot build plugin:

57
pom.xml
View file

@ -36,7 +36,7 @@
<maven-checkstyle.version>3.6.0</maven-checkstyle.version>
<nohttp-checkstyle.version>0.0.11</nohttp-checkstyle.version>
<spring-format.version>0.0.46</spring-format.version>
<os.detected.classifier>windows-x86_64</os.detected.classifier>
</properties>
<dependencies>
@ -146,10 +146,65 @@
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/net.devh/grpc-server-spring-boot-starter -->
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>3.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.2</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
<protocArtifact>com.google.protobuf:protoc:3.21.12:exe:${os.detected.classifier}</protocArtifact>
</configuration>
<executions>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>compile-grpc</id>
<goals>
<goal>compile-custom</goal>
</goals>
<configuration>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.58.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>

View file

@ -15,10 +15,15 @@
*/
package org.springframework.samples.petclinic.owner;
import jakarta.persistence.CascadeType;
import jakarta.persistence.OneToMany;
import org.springframework.samples.petclinic.model.NamedEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import org.springframework.samples.petclinic.pet.model.PetAttribute;
import java.util.Set;
/**
* @author Juergen Hoeller Can be Cat, Dog, Hamster...
@ -27,4 +32,7 @@ import jakarta.persistence.Table;
@Table(name = "types")
public class PetType extends NamedEntity {
@OneToMany(mappedBy = "petType", cascade = CascadeType.ALL)
private Set<PetAttribute> attributes;
}

View file

@ -0,0 +1,66 @@
package org.springframework.samples.petclinic.pet.controller;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.samples.petclinic.owner.PetType;
import org.springframework.samples.petclinic.pet.model.PetAttribute;
import org.springframework.samples.petclinic.pet.service.PetAttributeService;
import org.springframework.samples.petclinic.pet.service.PetTypeService;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
/**
* @author Rohit Lalwani
*/
@RestController
@RequestMapping("/api/petTypes/{typeId}/attributes")
public class PetAttributeController {
private final PetTypeService petTypeService;
private final PetAttributeService petAttributeService;
private final PetAttributeModelAssembler assembler;
public PetAttributeController(PetTypeService petTypeService, PetAttributeService attrService,
PetAttributeModelAssembler assembler) {
this.petTypeService = petTypeService;
this.petAttributeService = attrService;
this.assembler = assembler;
}
@PostMapping
public ResponseEntity<EntityModel<PetAttribute>> createAttribute(@PathVariable Integer typeId,
@RequestBody PetAttribute attr) {
PetType type = petTypeService.findPetTypeById(typeId);
if (type == null)
return ResponseEntity.notFound().build();
attr.setPetType(type);
PetAttribute saved = petAttributeService.save(attr);
return ResponseEntity.status(HttpStatus.CREATED).body(assembler.toModel(saved));
}
@GetMapping
public ResponseEntity<CollectionModel<EntityModel<PetAttribute>>> getAttributes(@PathVariable Integer typeId) {
List<PetAttribute> attrs = petAttributeService.findByPetTypeId(typeId);
List<EntityModel<PetAttribute>> attrModels = attrs.stream().map(assembler::toModel).toList();
return ResponseEntity.ok(CollectionModel.of(attrModels,
linkTo(methodOn(PetAttributeController.class).getAttributes(typeId)).withSelfRel()));
}
}

View file

@ -0,0 +1,27 @@
package org.springframework.samples.petclinic.pet.controller;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.RepresentationModelAssembler;
import org.springframework.samples.petclinic.pet.model.PetAttribute;
import org.springframework.stereotype.Component;
/**
* @author Rohit Lalwani
*/
@Component
public class PetAttributeModelAssembler
implements RepresentationModelAssembler<PetAttribute, EntityModel<PetAttribute>> {
@Override
public EntityModel<PetAttribute> toModel(PetAttribute attr) {
Integer typeId = attr.getPetType().getId();
return EntityModel.of(attr,
linkTo(methodOn(PetAttributeController.class).getAttributes(typeId)).withRel("allAttributes"),
linkTo(methodOn(PetAttributeController.class).createAttribute(typeId, attr))
.withRel("createAttribute"));
}
}

View file

@ -0,0 +1,76 @@
package org.springframework.samples.petclinic.pet.grpc;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;
import org.springframework.samples.petclinic.grpc.NewPetAttribute;
import org.springframework.samples.petclinic.grpc.PetAttributeList;
import org.springframework.samples.petclinic.grpc.PetAttributeRequest;
import org.springframework.samples.petclinic.grpc.PetAttributeResponse;
import org.springframework.samples.petclinic.grpc.PetAttributeServiceGrpc;
import org.springframework.samples.petclinic.owner.PetType;
import org.springframework.samples.petclinic.pet.model.PetAttribute;
import org.springframework.samples.petclinic.pet.service.PetAttributeService;
import org.springframework.samples.petclinic.pet.service.PetTypeService;
import java.util.List;
/**
* @author Rohit Lalwani
*/
@GrpcService
public class PetAttributeGrpcService extends PetAttributeServiceGrpc.PetAttributeServiceImplBase {
private final PetAttributeService petAttributeService;
private final PetTypeService petTypeService;
public PetAttributeGrpcService(PetAttributeService petAttributeService, PetTypeService petTypeService) {
this.petAttributeService = petAttributeService;
this.petTypeService = petTypeService;
}
public void GetAttributes(PetAttributeRequest request, StreamObserver<PetAttributeList> responseObserver) {
List<PetAttribute> attributes = petAttributeService.findByPetTypeId(request.getTypeId());
PetAttributeList.Builder listBuilder = PetAttributeList.newBuilder();
for (PetAttribute attr : attributes) {
PetAttributeResponse response = PetAttributeResponse.newBuilder()
.setId(attr.getId())
.setTemperament(attr.getTemperament())
.setWeight(attr.getWeight())
.setLength(attr.getLength())
.build();
listBuilder.addAttributes(response);
}
responseObserver.onNext(listBuilder.build());
responseObserver.onCompleted();
}
public void AddAttribute(NewPetAttribute request, StreamObserver<PetAttributeResponse> responseObserver) {
PetType petType = petTypeService.findPetTypeById(request.getTypeId());
if (petType == null) {
responseObserver.onError(new IllegalArgumentException("Pet type not found"));
return;
}
PetAttribute attr = new PetAttribute();
attr.setTemperament(request.getTemperament());
attr.setWeight(request.getWeight());
attr.setLength(request.getLength());
attr.setPetType(petType);
PetAttribute saved = petAttributeService.save(attr);
PetAttributeResponse response = PetAttributeResponse.newBuilder()
.setId(saved.getId())
.setTemperament(saved.getTemperament())
.setWeight(saved.getWeight())
.setLength(saved.getLength())
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}

View file

@ -0,0 +1,78 @@
package org.springframework.samples.petclinic.pet.model;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.validation.constraints.NotNull;
import org.springframework.samples.petclinic.owner.PetType;
import java.io.Serializable;
/**
* @author Rohit Lalwani
*/
@Entity
public class PetAttribute implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@NotNull
private String temperament;
@NotNull
private Double weight;
@NotNull
private Double length;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "type_id")
private PetType petType;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getTemperament() {
return temperament;
}
public void setTemperament(String temperament) {
this.temperament = temperament;
}
public Double getWeight() {
return weight;
}
public void setWeight(Double weight) {
this.weight = weight;
}
public Double getLength() {
return length;
}
public void setLength(Double length) {
this.length = length;
}
public PetType getPetType() {
return petType;
}
public void setPetType(PetType petType) {
this.petType = petType;
}
}

View file

@ -0,0 +1,17 @@
package org.springframework.samples.petclinic.pet.repository;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.repository.CrudRepository;
import org.springframework.samples.petclinic.pet.model.PetAttribute;
import java.util.List;
/**
* @author Rohit Lalwani
*/
public interface PetAttributeRepository extends CrudRepository<PetAttribute, Integer> {
@EntityGraph(attributePaths = "petType")
List<PetAttribute> findByPetTypeId(Integer typeId);
}

View file

@ -0,0 +1,29 @@
package org.springframework.samples.petclinic.pet.service;
import org.springframework.samples.petclinic.pet.model.PetAttribute;
import org.springframework.samples.petclinic.pet.repository.PetAttributeRepository;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author Rohit Lalwani
*/
@Service
public class PetAttributeService {
private final PetAttributeRepository petAttributeRepository;
public PetAttributeService(PetAttributeRepository repo) {
this.petAttributeRepository = repo;
}
public PetAttribute save(PetAttribute attr) {
return petAttributeRepository.save(attr);
}
public List<PetAttribute> findByPetTypeId(Integer petTypeId) {
return petAttributeRepository.findByPetTypeId(petTypeId);
}
}

View file

@ -0,0 +1,26 @@
package org.springframework.samples.petclinic.pet.service;
import org.springframework.samples.petclinic.owner.PetType;
import org.springframework.samples.petclinic.owner.PetTypeRepository;
import org.springframework.stereotype.Service;
import java.util.Optional;
/**
* @author Rohit Lalwani
*/
@Service
public class PetTypeService {
private final PetTypeRepository petTypeRepository;
public PetTypeService(PetTypeRepository petTypeRepository) {
this.petTypeRepository = petTypeRepository;
}
public PetType findPetTypeById(Integer id) {
Optional<PetType> petType = petTypeRepository.findById(id);
return petType.orElse(null);
}
}

View file

@ -0,0 +1,32 @@
syntax = "proto3";
option java_multiple_files = true;
option java_package = "org.springframework.samples.petclinic.grpc";
option java_outer_classname = "PetAttributeProto";
message PetAttributeRequest {
int32 typeId = 1;
}
message NewPetAttribute {
int32 typeId = 1;
string temperament = 2;
double weight = 3;
double length = 4;
}
message PetAttributeResponse {
int32 id = 1;
string temperament = 2;
double weight = 3;
double length = 4;
}
message PetAttributeList {
repeated PetAttributeResponse attributes = 1;
}
service PetAttributeService {
rpc GetAttributes (PetAttributeRequest) returns (PetAttributeList);
rpc AddAttribute (NewPetAttribute) returns (PetAttributeResponse);
}

View file

@ -23,3 +23,5 @@ logging.level.org.springframework=INFO
# Maximum time static resources should be cached
spring.web.resources.cache.cachecontrol.max-age=12h
grpc.server.port=9090

View file

@ -62,3 +62,12 @@ CREATE TABLE visits (
);
ALTER TABLE visits ADD CONSTRAINT fk_visits_pets FOREIGN KEY (pet_id) REFERENCES pets (id);
CREATE INDEX visits_pet_id ON visits (pet_id);
CREATE TABLE IF NOT EXISTS pet_attribute (
id INT PRIMARY KEY AUTO_INCREMENT,
temperament VARCHAR(50) NOT NULL,
weight DOUBLE NOT NULL,
length DOUBLE NOT NULL,
type_id INT,
FOREIGN KEY (type_id) REFERENCES types(id)
);

View file

@ -62,3 +62,12 @@ CREATE TABLE visits (
);
ALTER TABLE visits ADD CONSTRAINT fk_visits_pets FOREIGN KEY (pet_id) REFERENCES pets (id);
CREATE INDEX visits_pet_id ON visits (pet_id);
CREATE TABLE IF NOT EXISTS pet_attribute (
id INT PRIMARY KEY AUTO_INCREMENT,
temperament VARCHAR(50) NOT NULL,
weight DOUBLE NOT NULL,
length DOUBLE NOT NULL,
type_id INT,
FOREIGN KEY (type_id) REFERENCES types(id)
);

View file

@ -53,3 +53,12 @@ CREATE TABLE IF NOT EXISTS visits (
description VARCHAR(255),
FOREIGN KEY (pet_id) REFERENCES pets(id)
) engine=InnoDB;
CREATE TABLE IF NOT EXISTS pet_attribute (
id INT PRIMARY KEY AUTO_INCREMENT,
temperament VARCHAR(50) NOT NULL,
weight DOUBLE NOT NULL,
length DOUBLE NOT NULL,
type_id INT,
FOREIGN KEY (type_id) REFERENCES types(id)
) engine=InnoDB;

View file

@ -50,3 +50,12 @@ CREATE TABLE IF NOT EXISTS visits (
description TEXT
);
CREATE INDEX ON visits (pet_id);
CREATE TABLE IF NOT EXISTS pet_attribute (
id INT PRIMARY KEY AUTO_INCREMENT,
temperament VARCHAR(50) NOT NULL,
weight DOUBLE NOT NULL,
length DOUBLE NOT NULL,
type_id INT,
FOREIGN KEY (type_id) REFERENCES types(id)
);

View file

@ -0,0 +1,89 @@
package org.springframework.samples.petclinic.pet.controller;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.http.ResponseEntity;
import org.springframework.samples.petclinic.owner.PetType;
import org.springframework.samples.petclinic.pet.model.PetAttribute;
import org.springframework.samples.petclinic.pet.service.PetAttributeService;
import org.springframework.samples.petclinic.pet.service.PetTypeService;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PetAttributeControllerTest {
@Mock
private PetTypeService petTypeService;
@Mock
private PetAttributeService petAttributeService;
@Mock
private PetAttributeModelAssembler assembler;
@InjectMocks
private PetAttributeController controller;
@Test
void testCreateAttribute_returnsCreated() {
PetType type = new PetType();
PetAttribute attr = new PetAttribute();
attr.setTemperament("Energetic");
when(petTypeService.findPetTypeById(1)).thenReturn(type);
when(petAttributeService.save(attr)).thenReturn(attr);
when(assembler.toModel(attr)).thenReturn(EntityModel.of(attr));
ResponseEntity<EntityModel<PetAttribute>> response = controller.createAttribute(1, attr);
assertEquals(201, response.getStatusCode().value());
assertNotNull(response.getBody());
assertEquals("Energetic", Objects.requireNonNull(response.getBody().getContent()).getTemperament());
verify(petAttributeService).save(attr);
}
@Test
void testCreateAttribute_petTypeNotFound() {
PetAttribute attr = new PetAttribute();
when(petTypeService.findPetTypeById(999)).thenReturn(null);
ResponseEntity<EntityModel<PetAttribute>> response = controller.createAttribute(999, attr);
assertEquals(404, response.getStatusCode().value());
assertNull(response.getBody());
}
@Test
void testGetAttributes_returnsList() {
PetAttribute attr = new PetAttribute();
attr.setTemperament("Curious");
List<PetAttribute> attributes = Collections.singletonList(attr);
EntityModel<PetAttribute> model = EntityModel.of(attr);
when(petAttributeService.findByPetTypeId(1)).thenReturn(attributes);
when(assembler.toModel(attr)).thenReturn(model);
ResponseEntity<CollectionModel<EntityModel<PetAttribute>>> response = controller.getAttributes(1);
assertEquals(200, response.getStatusCode().value());
assertNotNull(response.getBody());
assertEquals(1, response.getBody().getContent().size());
}
}

View file

@ -0,0 +1,53 @@
package org.springframework.samples.petclinic.pet.service;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.samples.petclinic.pet.model.PetAttribute;
import org.springframework.samples.petclinic.pet.repository.PetAttributeRepository;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PetAttributeServiceTest {
@Mock
private PetAttributeRepository repository;
@InjectMocks
private PetAttributeService service;
@Test
void testSave() {
PetAttribute attr = new PetAttribute();
attr.setTemperament("Calm");
attr.setWeight(10.0);
attr.setLength(20.0);
when(repository.save(attr)).thenReturn(attr);
PetAttribute saved = service.save(attr);
assertEquals("Calm", saved.getTemperament());
verify(repository, times(1)).save(attr);
}
@Test
void testFindByPetTypeId() {
PetAttribute attr = new PetAttribute();
attr.setTemperament("Playful");
when(repository.findByPetTypeId(1)).thenReturn(Arrays.asList(attr));
List<PetAttribute> result = service.findByPetTypeId(1);
assertEquals(1, result.size());
verify(repository, times(1)).findByPetTypeId(1);
}
}

View file

@ -0,0 +1,46 @@
package org.springframework.samples.petclinic.pet.service;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.samples.petclinic.owner.PetType;
import org.springframework.samples.petclinic.owner.PetTypeRepository;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PetTypeServiceTest {
@Mock
private PetTypeRepository petTypeRepository;
@InjectMocks
private PetTypeService petTypeService;
@Test
void testFindPetTypeById_found() {
PetType type = new PetType();
type.setId(1);
when(petTypeRepository.findById(1)).thenReturn(Optional.of(type));
PetType found = petTypeService.findPetTypeById(1);
assertNotNull(found);
assertEquals(found.getId(), Optional.of(1).get());
}
@Test
void testFindPetTypeById_notFound() {
when(petTypeRepository.findById(2)).thenReturn(Optional.empty());
PetType found = petTypeService.findPetTypeById(2);
assertNull(found);
}
}

View file

@ -27,7 +27,7 @@ import java.util.Collection;
*
* @author Juergen Hoeller
* @author Sam Brannen
* @see org.springframework.samples.petclinic.model.BaseEntity
* @see BaseEntity
* @since 29.10.2003
*/
public abstract class EntityUtils {

View file

@ -46,8 +46,8 @@ import org.springframework.http.ResponseEntity;
* @author Alex Lutz
*/
// NOT Waiting https://github.com/spring-projects/spring-boot/issues/5574
@SpringBootTest(webEnvironment = RANDOM_PORT,
properties = { "server.error.include-message=ALWAYS", "management.endpoints.enabled-by-default=false" })
@SpringBootTest(webEnvironment = RANDOM_PORT, properties = { "server.error.include-message=ALWAYS",
"management.endpoints.enabled-by-default=false", "grpc.server.port=0", "grpc.server.address=127.0.0.1" })
class CrashControllerIntegrationTests {
@Value(value = "${local.server.port}")