Initial commit

This commit is contained in:
2026-07-13 09:25:40 +07:00
parent c08ff54676
commit bccfb156d7
1938 changed files with 641646 additions and 0 deletions

View File

@@ -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" });
}
}
}