using ErrorOr; using Microsoft.AspNetCore.Mvc; using MuiCharts.Contracts.Track; using MuiCharts.Domain.Models; using MuiCharts.Domain.Repositories; namespace MuiCharts.Api.Controllers; /// /// Controller for managing tracks. /// /// The logger. /// The track repository. public class TracksController( ILogger logger, ITrackRepository trackRepository ) : ApiControllerBase(logger) { private readonly ITrackRepository _repository = trackRepository; /// /// Creates a new track. /// /// The request containing the track details. /// An representing the asynchronous operation result. [HttpPost] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status409Conflict)] [ProducesDefaultResponseType(typeof(ProblemDetails))] public async Task CreateTrackAsync(UpsertTrackRequest request) { // TODO: Check if points exist Logger.LogInformation("Creating track with first ID {FirstId} and second ID {SecondId}", request.FirstId, request.SecondId); if (request.FirstId == request.SecondId) return Problem([Error.Validation(description: "First ID and second ID cannot be the same.")]); Track track = new() { FirstId = request.FirstId, SecondId = request.SecondId, Distance = request.Distance, Surface = request.Surface, MaxSpeed = request.MaxSpeed }; ErrorOr createResult = await _repository.AddTrackAsync(track); if (createResult.IsError) return Problem(createResult.Errors); Logger.LogInformation("Track created with first ID {FirstId} and second ID {SecondId}", createResult.Value.FirstId, createResult.Value.SecondId); return CreatedAtTrackResult(createResult.Value); } /// /// Retrieves a track with the specified first ID and second ID. /// /// The first point ID of the track. /// The second point ID of the track. /// An representing the asynchronous operation result. [HttpGet("{firstId:int}/{secondId:int}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesDefaultResponseType(typeof(ProblemDetails))] public async Task GetTrackAsync(int firstId, int secondId) { Logger.LogInformation("Retrieving track with first ID {FirstId} and second ID {SecondId}", firstId, secondId); ErrorOr getResult = await _repository.GetTrackAsync(firstId, secondId); if (getResult.IsError) return Problem(getResult.Errors); Logger.LogInformation("Track retrieved with first ID {FirstId} and second ID {SecondId}", getResult.Value.FirstId, getResult.Value.SecondId); return Ok(MapTrackResponse(getResult.Value)); } /// /// Retrieves all tracks. /// /// An representing the asynchronous operation result. [HttpGet] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesDefaultResponseType(typeof(ProblemDetails))] public async Task GetAllTracksAsync() { Logger.LogInformation("Retrieving all tracks"); IQueryable tracks = await _repository.GetTracksRangeAsync(); Logger.LogInformation("All tracks retrieved"); return Ok(tracks.Select(MapTrackResponse)); } /// /// Upserts a track with the specified first point ID, second point ID, and request data. /// /// The first point ID of the track. /// The second point ID of the track. /// The request data containing the track details. /// An representing the asynchronous operation result. [HttpPut("{firstId:int}/{secondId:int}")] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesDefaultResponseType(typeof(ProblemDetails))] public async Task UpsertTrackAsync(int firstId, int secondId, UpsertTrackRequest request) { // TODO: Check if points exist Logger.LogInformation("Upserting track with first ID {FirstId} and second ID {SecondId}", firstId, secondId); Track track = new() { FirstId = firstId, SecondId = secondId, Distance = request.Distance, Surface = request.Surface, MaxSpeed = request.MaxSpeed }; ErrorOr upsertResult = await _repository.AddOrUpdateTrackAsync(track); if (upsertResult.IsError) return Problem(upsertResult.Errors); if (upsertResult.Value is Track value) { Logger.LogInformation("Track created with first ID {FirstId} and second ID {SecondId}", value.FirstId, value.SecondId); return CreatedAtTrackResult(value); } Logger.LogInformation("Track updated with first ID {FirstId} and second ID {SecondId}", firstId, secondId); return NoContent(); } /// /// Imports tracks. /// /// The requests containing the track details. /// An representing the asynchronous operation result. [HttpPost("Import")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesDefaultResponseType(typeof(ProblemDetails))] public async Task ImportTracksAsync(Track[] requests) { Logger.LogInformation("Importing tracks"); ErrorOr> importResult = await _repository.AddTracksRangeAsync(requests); if (importResult.IsError) return Problem(importResult.Errors); Logger.LogInformation("Tracks imported"); return Ok(importResult.Value); } /// /// Deletes a track with the specified first point ID and second point ID. /// /// The first point ID of the track to delete. /// The second point ID of the track to delete. /// An representing the asynchronous operation result. [HttpDelete("{firstId:int}/{secondId:int}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesDefaultResponseType(typeof(ProblemDetails))] public async Task DeleteTrackAsync(int firstId, int secondId) { Logger.LogInformation("Deleting track with first ID {FirstId} and second ID {SecondId}", firstId, secondId); ErrorOr deleteResult = await _repository.DeleteTrackAsync(firstId, secondId); if (deleteResult.IsError) return Problem(deleteResult.Errors); Logger.LogInformation("Track deleted with first ID {FirstId} and second ID {SecondId}", firstId, secondId); return NoContent(); } private CreatedAtActionResult CreatedAtTrackResult(Track track) => CreatedAtAction( actionName: nameof(GetTrackAsync), routeValues: new { firstId = track.FirstId, secondId = track.SecondId }, value: MapTrackResponse(track) ); private static TrackResponse MapTrackResponse(Track track) => new( track.FirstId, track.SecondId, track.Distance, track.Surface, track.MaxSpeed ); }