Initial commit
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using RobotNet10.NavigationTune.Data;
|
||||
using RobotNet10.NavigationTune.Shared.Models;
|
||||
using StraightLineScenario = RobotNet10.NavigationTune.Scenarios.StraightLineScenario;
|
||||
using CircleScenario = RobotNet10.NavigationTune.Scenarios.CircleScenario;
|
||||
using CustomPathScenario = RobotNet10.NavigationTune.Scenarios.CustomPathScenario;
|
||||
|
||||
namespace RobotNet10.NavigationTune.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// DTO for TestScenario API responses
|
||||
/// </summary>
|
||||
public class TestScenarioDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public TrajectoryType Type { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public bool IsDefault { get; set; }
|
||||
public string ConfigJson { get; set; } = string.Empty;
|
||||
|
||||
public static TestScenarioDto FromTestScenario(TestScenario scenario, TestScenarioEntity entity)
|
||||
{
|
||||
return new TestScenarioDto
|
||||
{
|
||||
Id = scenario.Id,
|
||||
Name = scenario.Name,
|
||||
Description = scenario.Description,
|
||||
Type = scenario.Type,
|
||||
CreatedAt = scenario.CreatedAt,
|
||||
IsDefault = scenario.IsDefault,
|
||||
ConfigJson = entity.ConfigJson
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Create/Update scenario. Uses Type + ConfigJson to avoid deserializing abstract TestScenario.
|
||||
/// </summary>
|
||||
public class CreateOrUpdateScenarioRequest
|
||||
{
|
||||
public Guid? Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public TrajectoryType Type { get; set; }
|
||||
public bool IsDefault { get; set; }
|
||||
/// <summary>JSON string of the scenario-specific config (serialized StraightLineScenario, CircleScenario, or CustomPathScenario).</summary>
|
||||
public string ConfigJson { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// REST API controller for test scenarios management
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ScenariosController(
|
||||
IScenarioRepository scenarioRepository,
|
||||
ILogger<ScenariosController> logger) : ControllerBase
|
||||
{
|
||||
private readonly IScenarioRepository _scenarioRepository = scenarioRepository;
|
||||
private readonly ILogger<ScenariosController> _logger = logger;
|
||||
|
||||
/// <summary>
|
||||
/// Get all scenarios
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<TestScenarioDto>>> GetAll()
|
||||
{
|
||||
try
|
||||
{
|
||||
var entities = await _scenarioRepository.GetAllEntitiesAsync();
|
||||
var dtos = new List<TestScenarioDto>();
|
||||
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
var scenario = entity.ToTestScenario();
|
||||
dtos.Add(TestScenarioDto.FromTestScenario(scenario, entity));
|
||||
}
|
||||
|
||||
return Ok(dtos);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting all scenarios");
|
||||
return StatusCode(500, new { error = "Failed to retrieve scenarios" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get default scenarios
|
||||
/// </summary>
|
||||
[HttpGet("defaults")]
|
||||
public async Task<ActionResult<List<TestScenarioDto>>> GetDefaults()
|
||||
{
|
||||
try
|
||||
{
|
||||
var entities = await _scenarioRepository.GetDefaultScenarioEntitiesAsync();
|
||||
var dtos = new List<TestScenarioDto>();
|
||||
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
var scenario = entity.ToTestScenario();
|
||||
dtos.Add(TestScenarioDto.FromTestScenario(scenario, entity));
|
||||
}
|
||||
|
||||
return Ok(dtos);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting default scenarios");
|
||||
return StatusCode(500, new { error = "Failed to retrieve default scenarios" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get scenario by ID
|
||||
/// </summary>
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<ActionResult<TestScenarioDto>> GetById(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var entity = await _scenarioRepository.GetEntityByIdAsync(id);
|
||||
if (entity == null)
|
||||
return NotFound(new { error = $"Scenario with ID {id} not found" });
|
||||
|
||||
var scenario = entity.ToTestScenario();
|
||||
return Ok(TestScenarioDto.FromTestScenario(scenario, entity));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting scenario {Id}", id);
|
||||
return StatusCode(500, new { error = "Failed to retrieve scenario" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create new scenario. Accepts Type + ConfigJson to avoid deserializing abstract TestScenario.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<TestScenarioDto>> Create([FromBody] CreateOrUpdateScenarioRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var scenario = DeserializeScenarioFromRequest(request);
|
||||
if (scenario.Id == Guid.Empty)
|
||||
scenario.Id = Guid.NewGuid();
|
||||
|
||||
var id = await _scenarioRepository.SaveAsync(scenario);
|
||||
var entity = await _scenarioRepository.GetEntityByIdAsync(id);
|
||||
if (entity == null)
|
||||
return StatusCode(500, new { error = "Failed to retrieve created scenario" });
|
||||
|
||||
var dto = TestScenarioDto.FromTestScenario(entity.ToTestScenario(), entity);
|
||||
return CreatedAtAction(nameof(GetById), new { id }, dto);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error creating scenario");
|
||||
return StatusCode(500, new { error = "Failed to create scenario" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update existing scenario. Accepts Type + ConfigJson to avoid deserializing abstract TestScenario.
|
||||
/// </summary>
|
||||
[HttpPut("{id:guid}")]
|
||||
public async Task<ActionResult> Update(Guid id, [FromBody] CreateOrUpdateScenarioRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existing = await _scenarioRepository.GetEntityByIdAsync(id);
|
||||
if (existing == null)
|
||||
return NotFound(new { error = $"Scenario with ID {id} not found" });
|
||||
|
||||
var scenario = DeserializeScenarioFromRequest(request);
|
||||
scenario.Id = id;
|
||||
await _scenarioRepository.UpdateAsync(scenario);
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error updating scenario {Id}", id);
|
||||
return StatusCode(500, new { error = "Failed to update scenario" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize ConfigJson to concrete TestScenario based on Type (avoids abstract type deserialization).
|
||||
/// </summary>
|
||||
private static TestScenario DeserializeScenarioFromRequest(CreateOrUpdateScenarioRequest request)
|
||||
{
|
||||
var jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||
TestScenario scenario = request.Type switch
|
||||
{
|
||||
TrajectoryType.StraightLine => JsonSerializer.Deserialize<StraightLineScenario>(request.ConfigJson, jsonOptions)
|
||||
?? new StraightLineScenario(),
|
||||
TrajectoryType.Circle => JsonSerializer.Deserialize<CircleScenario>(request.ConfigJson, jsonOptions)
|
||||
?? new CircleScenario(),
|
||||
TrajectoryType.Custom => JsonSerializer.Deserialize<CustomPathScenario>(request.ConfigJson, jsonOptions)
|
||||
?? new CustomPathScenario(),
|
||||
_ => throw new NotSupportedException($"Scenario type {request.Type} is not supported")
|
||||
};
|
||||
scenario.Id = request.Id ?? Guid.Empty;
|
||||
scenario.Name = request.Name;
|
||||
scenario.Description = request.Description;
|
||||
scenario.Type = request.Type;
|
||||
scenario.IsDefault = request.IsDefault;
|
||||
return scenario;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete scenario. Only custom (non-default) scenarios can be deleted.
|
||||
/// </summary>
|
||||
[HttpDelete("{id:guid}")]
|
||||
public async Task<ActionResult> Delete(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existing = await _scenarioRepository.GetByIdAsync(id);
|
||||
if (existing == null)
|
||||
return NotFound(new { error = $"Scenario with ID {id} not found" });
|
||||
|
||||
if (existing.IsDefault)
|
||||
return BadRequest(new { error = "Cannot delete default scenario. Only custom scenarios can be deleted." });
|
||||
|
||||
await _scenarioRepository.DeleteAsync(id);
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting scenario {Id}", id);
|
||||
return StatusCode(500, new { error = "Failed to delete scenario" });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user