240 lines
8.9 KiB
C#
240 lines
8.9 KiB
C#
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" });
|
|
}
|
|
}
|
|
}
|