diff --git a/Controllers/WorkshopsController.cs b/Controllers/WorkshopsController.cs new file mode 100644 index 0000000..aaee027 --- /dev/null +++ b/Controllers/WorkshopsController.cs @@ -0,0 +1,40 @@ + +using CampusWorkshops.Api.Dtos; +using CampusWorkshops.Api.Models; +using CampusWorkshops.Api.Repositories; +using Microsoft.AspNetCore.Mvc; + +namespace CampusWorkshops.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class WorkshopsController : ControllerBase +{ + private readonly IWorkshopRepository _repo; + + public WorkshopsController(IWorkshopRepository repo) => _repo = repo; + + /// Lista workshops com filtros opcionais. + [HttpGet] + public Task GetAll([FromQuery] DateTimeOffset? from, [FromQuery] DateTimeOffset? to, [FromQuery] string? q, CancellationToken ct) + { + // TODO: implementar usando _repo.GetAllAsync e mapear para WorkshopResponse + return Task.FromResult(Ok(Array.Empty())); + } + + /// Obtém um workshop por Id. + [HttpGet("{id:guid}")] + public Task GetById(Guid id, CancellationToken ct) + { + // TODO: implementar usando _repo.GetByIdAsync + return Task.FromResult(NotFound()); + } + + /// Cria um novo workshop. + [HttpPost] + public Task Create([FromBody] CreateWorkshopRequest body, CancellationToken ct) + { + // TODO: validar ModelState, regras de negócio e chamar _repo.AddAsync; retornar CreatedAtAction + return Task.FromResult(BadRequest()); + } +} diff --git a/Dtos/CreateWorkshopRequest.cs b/Dtos/CreateWorkshopRequest.cs new file mode 100644 index 0000000..7686335 --- /dev/null +++ b/Dtos/CreateWorkshopRequest.cs @@ -0,0 +1,13 @@ + +namespace CampusWorkshops.Api.Dtos; + +// DTO para criação de Workshop. +public record CreateWorkshopRequest( + string? Title, + string? Description, + DateTimeOffset StartAt, + DateTimeOffset EndAt, + string? Location, + int Capacity, + bool IsOnline +); diff --git a/Dtos/WorkshopResponse.cs b/Dtos/WorkshopResponse.cs new file mode 100644 index 0000000..161317a --- /dev/null +++ b/Dtos/WorkshopResponse.cs @@ -0,0 +1,14 @@ + +namespace CampusWorkshops.Api.Dtos; + +// DTO de resposta para Workshop +public record WorkshopResponse( + Guid Id, + string? Title, + string? Description, + DateTimeOffset StartAt, + DateTimeOffset EndAt, + string? Location, + int Capacity, + bool IsOnline +); diff --git a/Models/Workshop.cs b/Models/Workshop.cs new file mode 100644 index 0000000..64a19d2 --- /dev/null +++ b/Models/Workshop.cs @@ -0,0 +1,29 @@ + +namespace CampusWorkshops.Api.Models; + +// Modelo mínimo para Workshop.Completar as validações +// e ajustar os tipos/atributos durante a atividade. +public class Workshop +{ + // Identificador único + public Guid Id { get; set; } = Guid.NewGuid(); + + // TODO: adicionar [Required], [StringLength(120, MinimumLength = 3)] + public string? Title { get; set; } + + // TODO: limitar tamanho (ex.: 2000) + public string? Description { get; set; } + + // TODO: usar DateTimeOffset com formato ISO 8601 + public DateTimeOffset StartAt { get; set; } + + public DateTimeOffset EndAt { get; set; } + + // Location deve ser obrigatório se IsOnline == false + public string? Location { get; set; } + + // TODO: validar Capacity >= 1 + public int Capacity { get; set; } = 1; + + public bool IsOnline { get; set; } +} diff --git a/Program.cs b/Program.cs index 3917ef1..29f9166 100644 --- a/Program.cs +++ b/Program.cs @@ -1,41 +1,56 @@ +// Note: repository implementation removed for workshop exercise (TODOs in project files) +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Mvc; + var builder = WebApplication.CreateBuilder(args); -// Add services to the container. -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi -builder.Services.AddOpenApi(); +// Add services +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(o => +{ + o.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo + { + Title = "CampusWorkshops API", + Version = "v1", + Description = "API para gestão de workshops do campus (MVP in-memory)." + }); +}); + +// DI var app = builder.Build(); -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) +// Exception handler that returns RFC7807 ProblemDetails for unhandled errors +app.UseExceptionHandler(errApp => { - app.MapOpenApi(); -} + errApp.Run(async context => + { + var feature = context.Features.Get(); + var ex = feature?.Error; + + var pd = new ProblemDetails + { + Title = "An unexpected error occurred.", + Status = StatusCodes.Status500InternalServerError, + Detail = app.Environment.IsDevelopment() ? ex?.Message : null + }; + + context.Response.StatusCode = pd.Status.Value; + context.Response.ContentType = "application/problem+json"; + await context.Response.WriteAsJsonAsync(pd); + }); +}); app.UseHttpsRedirection(); -var summaries = new[] +app.UseSwagger(); +app.UseSwaggerUI(c => { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; + c.SwaggerEndpoint("/swagger/v1/swagger.json", "CampusWorkshops API v1"); + c.RoutePrefix = "swagger"; // serve at /swagger +}); -app.MapGet("/weatherforecast", () => -{ - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; -}) -.WithName("GetWeatherForecast"); +app.MapControllers(); app.Run(); - -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); -} diff --git a/README.md b/README.md new file mode 100644 index 0000000..bb691bb --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# CampusWorkshops API — Starter for Workshop + +Pré-requisitos: + +* .NET 8 SDK instalado + +Comandos básicos: + +```bash +dotnet restore +dotnet run +``` + +Depois de rodar, abra: https://localhost:5001/swagger + +Objetivos do encontro 1 + +* Entender o contrato REST (recursos, rotas, status codes). +* Implementar/ajustar `GET /api/workshops`, `GET /api/workshops/{id}` e codar o `POST /api/workshops` (com validação e `201 Created + Location`). +* Ver erros formatados como `application/problem+json`. diff --git a/Repositories/IWorkshopRepository.cs b/Repositories/IWorkshopRepository.cs new file mode 100644 index 0000000..ff8bfda --- /dev/null +++ b/Repositories/IWorkshopRepository.cs @@ -0,0 +1,12 @@ + +using CampusWorkshops.Api.Models; + +namespace CampusWorkshops.Api.Repositories; + +// Define repository contract; implementations should be provided by students during the workshop. +public interface IWorkshopRepository +{ + Task> GetAllAsync(DateTimeOffset? from, DateTimeOffset? to, string? q, CancellationToken ct); + Task GetByIdAsync(Guid id, CancellationToken ct); + Task AddAsync(Workshop workshop, CancellationToken ct); +} diff --git a/Repositories/InMemoryWorkshopRepository.cs b/Repositories/InMemoryWorkshopRepository.cs new file mode 100644 index 0000000..e887e44 --- /dev/null +++ b/Repositories/InMemoryWorkshopRepository.cs @@ -0,0 +1,31 @@ + +using CampusWorkshops.Api.Models; + +namespace CampusWorkshops.Api.Repositories; + +// In-memory repository stub +public class InMemoryWorkshopRepository : IWorkshopRepository +{ + public InMemoryWorkshopRepository() + { + // TODO: Adicionar workshops iniciais + } + + public Task> GetAllAsync(DateTimeOffset? from, DateTimeOffset? to, string? q, CancellationToken ct) + { + // TODO: retornar uma lista filtrada e ordenada por StartAt + return Task.FromResult>(Array.Empty()); + } + + public Task GetByIdAsync(Guid id, CancellationToken ct) + { + // TODO: buscar por id + return Task.FromResult(null); + } + + public Task AddAsync(Workshop workshop, CancellationToken ct) + { + // TODO: adicionar à lista em memória e retornar criado + return Task.FromResult(workshop); + } +} diff --git a/Workshop10-API.csproj b/Workshop10-API.csproj index c2caf2d..eb31916 100644 --- a/Workshop10-API.csproj +++ b/Workshop10-API.csproj @@ -9,6 +9,7 @@ +