Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven

- name: Compile
run: ./mvnw compile -B

- name: Run tests
run: ./mvnw test -B
Binary file added .mvn/wrapper/maven-wrapper.jar
Binary file not shown.
18 changes: 18 additions & 0 deletions .mvn/wrapper/maven-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.0/maven-wrapper-3.3.0.jar
20 changes: 20 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<description>Environment manager</description>
<properties>
<java.version>21</java.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
</properties>
<dependencies>
<dependency>
Expand Down Expand Up @@ -49,6 +50,15 @@
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
Expand Down Expand Up @@ -92,6 +102,16 @@
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
Expand Down
51 changes: 33 additions & 18 deletions src/main/java/preponderous/viron/controllers/DebugController.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
package preponderous.viron.controllers;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import jakarta.validation.constraints.NotBlank;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import preponderous.viron.dto.EntityDto;
import preponderous.viron.dto.EnvironmentDto;
import preponderous.viron.exceptions.InvalidRequestException;
import preponderous.viron.mappers.EntityMapper;
import preponderous.viron.mappers.EnvironmentMapper;
import preponderous.viron.models.Entity;
import preponderous.viron.models.Environment;
import preponderous.viron.models.Grid;
Expand All @@ -24,30 +25,41 @@
@RestController
@RequestMapping("/api/v1/debug")
@Slf4j
@Validated
public class DebugController {
private final EntityService entityService;
private final EnvironmentService environmentService;
private final GridService gridService;
private final LocationService locationService;
private final EntityMapper entityMapper;
private final EnvironmentMapper environmentMapper;

List<String> entityNamePool = new ArrayList<>(Arrays.asList("Tom", "Jerry", "Spike", "Tyke", "Nibbles", "Butch", "Tuffy", "Lightning", "Mammy", "Quacker", "Toodles", "Droopy", "Screwy", "Meathead", "George", "Dripple", "McWolf"));
private final List<String> entityNamePool = List.of("Tom", "Jerry", "Spike", "Tyke", "Nibbles", "Butch", "Tuffy", "Lightning", "Mammy", "Quacker", "Toodles", "Droopy", "Screwy", "Meathead", "George", "Dripple", "McWolf");

public DebugController(EntityService entityService, EnvironmentService environmentService, GridService gridService, LocationService locationService) {
public DebugController(EntityService entityService, EnvironmentService environmentService,
GridService gridService, LocationService locationService,
EntityMapper entityMapper, EnvironmentMapper environmentMapper) {
this.entityService = entityService;
this.environmentService = environmentService;
this.gridService = gridService;
this.locationService = locationService;
this.entityMapper = entityMapper;
this.environmentMapper = environmentMapper;
}

/**
* Creates a sample environment with a single 10x10 grid and places ten entities in random, valid locations within the grid.
* It ensures the entities are properly created and assigned to valid locations in the grid.
*/
@PostMapping("/create-sample-data")
public ResponseEntity<Environment> createSampleData() {
@ResponseStatus(HttpStatus.CREATED)
public EnvironmentDto createSampleData() {
// create an environment with one 10x10 grid
Environment environment = environmentService.createEnvironment("Sample Environment", 1, 10);
List<Grid> grids = gridService.getGridsInEnvironment(environment.getEnvironmentId());
if (grids.isEmpty()) {
throw new InvalidRequestException("No grids found in environment: " + environment.getEnvironmentId());
}
Grid grid = grids.getFirst();
List<Location> locations = locationService.getLocationsInGrid(grid.getGridId());

Expand All @@ -66,12 +78,12 @@ public ResponseEntity<Environment> createSampleData() {
}
}
if (location == null) {
return ResponseEntity.badRequest().body(null); // exit if no valid location found
throw new InvalidRequestException("No valid location found at coordinates (" + x + ", " + y + ")");
}
locationService.addEntityToLocation(entity.getEntityId(), location.getLocationId());
}

return ResponseEntity.ok(environment);
return environmentMapper.toDto(environment);
}

/**
Expand All @@ -81,7 +93,8 @@ public ResponseEntity<Environment> createSampleData() {
* @param environmentName the name of the environment to be created
*/
@PostMapping("/create-world-and-place-entity/{environmentName}")
public ResponseEntity<Entity> createWorldAndPlaceEntity(@PathVariable String environmentName) {
@ResponseStatus(HttpStatus.CREATED)
public EntityDto createWorldAndPlaceEntity(@PathVariable @NotBlank String environmentName) {
// create an environment
int numGrids = 1;
int gridSize = 5;
Comment on lines +97 to 100
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

This endpoint assumes a grid was created/fetched successfully. A few lines below it does Grid grid = grids.get(0);, which will throw if gridService.getGridsInEnvironment(...) returns an empty list and will surface as a generic 500. Consider explicitly checking for an empty result and throwing a controlled exception/message before indexing.

Copilot uses AI. Check for mistakes.
Expand All @@ -90,7 +103,10 @@ public ResponseEntity<Entity> createWorldAndPlaceEntity(@PathVariable String env

// get grid info
List<Grid> grids = gridService.getGridsInEnvironment(environment.getEnvironmentId());
Grid grid = grids.get(0);
if (grids.isEmpty()) {
throw new InvalidRequestException("No grids found in environment: " + environment.getEnvironmentId());
}
Grid grid = grids.getFirst();
log.info("Grid created: {} with size {}x{}", grid.getGridId(), grid.getRows(), grid.getColumns());

// create an entity
Expand All @@ -110,11 +126,10 @@ public ResponseEntity<Entity> createWorldAndPlaceEntity(@PathVariable String env
}
}
if (location == null) {
log.error("No valid location found for entity at row {} and column {}", entityRow, entityColumn);
return ResponseEntity.badRequest().body(null);
throw new InvalidRequestException("No valid location found for entity at row " + entityRow + " and column " + entityColumn);
}
locationService.addEntityToLocation(entity.getEntityId(), location.getLocationId());
log.info("Entity {} placed at location ({}, {})", entity.getName(), entityRow, entityColumn);
return ResponseEntity.ok(entity);
return entityMapper.toDto(entity);
}
}
121 changes: 47 additions & 74 deletions src/main/java/preponderous/viron/controllers/EntityController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,120 +2,93 @@

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import preponderous.viron.exceptions.EntityCreationException;
import preponderous.viron.dto.EntityDto;
import preponderous.viron.exceptions.NotFoundException;
import preponderous.viron.exceptions.ServiceException;
import preponderous.viron.factories.EntityFactory;
import preponderous.viron.mappers.EntityMapper;
import preponderous.viron.models.Entity;
import preponderous.viron.repositories.EntityRepository;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import java.util.List;

@RestController
@RequestMapping("/api/v1/entities")
@Slf4j
@RequiredArgsConstructor
@Validated
public class EntityController {
private final EntityRepository entityRepository;
private final EntityFactory entityFactory;
private final EntityMapper entityMapper;

@GetMapping
public ResponseEntity<List<Entity>> getAllEntities() {
try {
return ResponseEntity.ok(entityRepository.findAll());
} catch (Exception e) {
log.error("Error fetching all entities: {}", e.getMessage());
return ResponseEntity.internalServerError().build();
}
public List<EntityDto> getAllEntities() {
List<Entity> entities = entityRepository.findAll();
return entityMapper.toDtoList(entities);
}

@GetMapping("/{id}")
public ResponseEntity<Entity> getEntityById(@PathVariable int id) {
try {
return entityRepository.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
} catch (Exception e) {
log.error("Error fetching entity by id {}: {}", id, e.getMessage());
return ResponseEntity.internalServerError().build();
}
public EntityDto getEntityById(@PathVariable @Min(1) int id) {
Entity entity = entityRepository.findById(id)
.orElseThrow(() -> new NotFoundException("Entity not found with id: " + id));
return entityMapper.toDto(entity);
}

@GetMapping("/environment/{environmentId}")
public ResponseEntity<List<Entity>> getEntitiesInEnvironment(@PathVariable int environmentId) {
try {
return ResponseEntity.ok(entityRepository.findByEnvironmentId(environmentId));
} catch (Exception e) {
log.error("Error fetching entities in environment {}: {}", environmentId, e.getMessage());
return ResponseEntity.internalServerError().build();
}
public List<EntityDto> getEntitiesInEnvironment(@PathVariable @Min(1) int environmentId) {
List<Entity> entities = entityRepository.findByEnvironmentId(environmentId);
return entityMapper.toDtoList(entities);
}

@GetMapping("/grid/{gridId}")
public ResponseEntity<List<Entity>> getEntitiesInGrid(@PathVariable int gridId) {
try {
return ResponseEntity.ok(entityRepository.findByGridId(gridId));
} catch (Exception e) {
log.error("Error fetching entities in grid {}: {}", gridId, e.getMessage());
return ResponseEntity.internalServerError().build();
}
public List<EntityDto> getEntitiesInGrid(@PathVariable @Min(1) int gridId) {
List<Entity> entities = entityRepository.findByGridId(gridId);
return entityMapper.toDtoList(entities);
}

@GetMapping("/location/{locationId}")
public ResponseEntity<List<Entity>> getEntitiesInLocation(@PathVariable int locationId) {
try {
return ResponseEntity.ok(entityRepository.findByLocationId(locationId));
} catch (Exception e) {
log.error("Error fetching entities in location {}: {}", locationId, e.getMessage());
return ResponseEntity.internalServerError().build();
}
public List<EntityDto> getEntitiesInLocation(@PathVariable @Min(1) int locationId) {
List<Entity> entities = entityRepository.findByLocationId(locationId);
return entityMapper.toDtoList(entities);
}

@GetMapping("/unassigned")
public ResponseEntity<List<Entity>> getEntitiesNotInAnyLocation() {
try {
return ResponseEntity.ok(entityRepository.findEntitiesNotInAnyLocation());
} catch (Exception e) {
log.error("Error fetching unassigned entities: {}", e.getMessage());
return ResponseEntity.internalServerError().build();
}
public List<EntityDto> getEntitiesNotInAnyLocation() {
List<Entity> entities = entityRepository.findEntitiesNotInAnyLocation();
return entityMapper.toDtoList(entities);
}

@PostMapping("/{name}")
public ResponseEntity<Entity> createEntity(@PathVariable String name) {
try {
Entity newEntity = entityFactory.createEntity(name);
return ResponseEntity.ok(newEntity);
} catch (EntityCreationException e) {
log.error("Error creating entity with name {}: {}", name, e.getMessage());
return ResponseEntity.badRequest().build();
} catch (Exception e) {
log.error("Unexpected error creating entity: {}", e.getMessage());
return ResponseEntity.internalServerError().build();
}
@ResponseStatus(HttpStatus.CREATED)
public EntityDto createEntity(@PathVariable @NotBlank String name) {
Entity newEntity = entityFactory.createEntity(name);
return entityMapper.toDto(newEntity);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteEntity(@PathVariable int id) {
try {
return entityRepository.deleteById(id)
? ResponseEntity.ok().build()
: ResponseEntity.notFound().build();
} catch (Exception e) {
log.error("Error deleting entity {}: {}", id, e.getMessage());
return ResponseEntity.internalServerError().build();
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteEntity(@PathVariable @Min(1) int id) {
if (entityRepository.findById(id).isEmpty()) {
throw new NotFoundException("Entity not found with id: " + id);
}
if (!entityRepository.deleteById(id)) {
throw new ServiceException("Failed to delete entity with id: " + id);
}
}
Comment on lines 74 to 83
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

@ResponseStatus(HttpStatus.NO_CONTENT) is redundant (and potentially confusing) when the method already returns an explicit ResponseEntity.noContent(). Prefer one mechanism (typically just returning ResponseEntity here) to avoid conflicting sources of truth for the status code.

Copilot uses AI. Check for mistakes.

@PatchMapping("/{id}/name/{name}")
public ResponseEntity<Void> updateEntityName(@PathVariable int id, @PathVariable String name) {
try {
return entityRepository.updateName(id, name)
? ResponseEntity.ok().build()
: ResponseEntity.notFound().build();
} catch (Exception e) {
log.error("Error updating name for entity {}: {}", id, e.getMessage());
return ResponseEntity.internalServerError().build();
public void updateEntityName(@PathVariable @Min(1) int id, @PathVariable @NotBlank String name) {
if (entityRepository.findById(id).isEmpty()) {
throw new NotFoundException("Entity not found with id: " + id);
}
if (!entityRepository.updateName(id, name)) {
throw new ServiceException("Failed to update name for entity " + id);
}
}
}
}
Loading
Loading