mirror of
https://github.com/XFox111/bonch-calendar.git
synced 2026-06-30 10:52:41 +03:00
docs: more code comments and minor style refactoring
This commit is contained in:
+59
-30
@@ -10,14 +10,17 @@ using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args);
|
||||
|
||||
// Adding static JSON serializer since we're running in Native AOT
|
||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default)
|
||||
);
|
||||
|
||||
// Add services to the container.
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
builder.Services.AddOpenApi();
|
||||
builder.Services.AddValidation();
|
||||
builder.Services.AddOpenApi(); // OpenAPI specification generator
|
||||
builder.Services.AddValidation(); // Request validation
|
||||
|
||||
// Customizing non-200 responses to include trace identifier and a no-as-a-service reason
|
||||
builder.Services.AddProblemDetails(configure =>
|
||||
{
|
||||
configure.CustomizeProblemDetails = context =>
|
||||
@@ -28,57 +31,76 @@ builder.Services.AddProblemDetails(configure =>
|
||||
});
|
||||
|
||||
builder.Services
|
||||
.AddSingleton<IssueTrackingService>()
|
||||
.AddScoped<TimetableService>()
|
||||
.AddScoped<ApiService>()
|
||||
.AddScoped<ParsingService>();
|
||||
.AddSingleton<IssueTrackingService>() // Service for tracking latest API request results
|
||||
.AddScoped<TimetableService>() // Service for generating a timetable
|
||||
.AddScoped<ApiService>() // Service for making API calls to sut.ru API
|
||||
.AddScoped<ParsingService>(); // Service for parsing timetable from sut.ru
|
||||
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddCheck<ApiHealthCheck>("timetable_website");
|
||||
.AddCheck<ApiHealthCheck>("timetable_website"); // Healthcheck service
|
||||
|
||||
// Get ORIGIN_DOMAIN environmental variable
|
||||
string? corsDomain = Environment.GetEnvironmentVariable("ORIGIN_DOMAIN");
|
||||
|
||||
// Configure defautl CORS policy
|
||||
builder.Services.AddCors(options =>
|
||||
options.AddDefaultPolicy(policy =>
|
||||
{
|
||||
// Allow only GET requests with any headers
|
||||
policy
|
||||
.WithMethods(["GET"])
|
||||
.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
)
|
||||
.AllowAnyHeader();
|
||||
|
||||
// If ORIGIN_DOMAIN environmental variable is set, allow request only from this domain,
|
||||
// otherwise allow request from any domains.
|
||||
if (string.IsNullOrWhiteSpace(corsDomain))
|
||||
policy.AllowAnyOrigin();
|
||||
else
|
||||
policy.WithOrigins(corsDomain);
|
||||
})
|
||||
);
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
app.UseCors();
|
||||
app.UseStatusCodePages();
|
||||
app.MapOpenApi();
|
||||
// Configure the HTTP request pipeline
|
||||
app.UseCors(); // Enable CORS
|
||||
app.UseStatusCodePages(); // Enable default JSON response body for non-200 responses.
|
||||
app.MapOpenApi(); // Map OpenAPI sepcification. Available at /openapi/v1.json
|
||||
|
||||
// Map healthcheck endpoint with custom response writer
|
||||
// Remark: /health and /openapi/v1.json endpoints are not present in OpenAPI specification.
|
||||
app.MapHealthChecks("/health", new HealthCheckOptions
|
||||
{
|
||||
ResponseWriter = HealthCheckWriter.WriteHealthCheckResponse
|
||||
});
|
||||
|
||||
// Request singleton services which will be used in subsequent endpoints
|
||||
ILogger<Program> logger = app.Services.GetRequiredService<ILogger<Program>>();
|
||||
IssueTrackingService tracker = app.Services.GetRequiredService<IssueTrackingService>();
|
||||
|
||||
// List of identifiers for tracking unique visits to /timetable endpoint (essentially, for unique user counting).
|
||||
List<string> ids = [];
|
||||
|
||||
// Statistics endpoint. Shows number of active users.
|
||||
app.MapGet("/stats", () => Results.Ok(new StatsResponse(ids.Count)))
|
||||
.WithName("GetStats")
|
||||
.WithDescription("Get basic usage statistics.")
|
||||
.Produces<StatsResponse>(StatusCodes.Status200OK);
|
||||
|
||||
// Retrieve list of faculties and their IDs from sut.ru API
|
||||
app.MapGet("/faculties", async ([FromServices] ApiService apiService) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Dictionary<int, string> faculties = await apiService.GetFacultiesListAsync();
|
||||
logger.LogInformation("Fetched {Count} faculties.", faculties.Count);
|
||||
tracker.TrackFacultyFetch(true);
|
||||
tracker.TrackFacultyFetch(true); // Record that last attempt to retrieve faculties was successful
|
||||
return Results.Ok(faculties);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to fetch faculties list.");
|
||||
tracker.TrackFacultyFetch(false);
|
||||
tracker.TrackFacultyFetch(false); // Record that last attempt to retrieve faculties was unsuccessful
|
||||
return Results.Problem("Failed to fetch faculties list.", statusCode: StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
})
|
||||
@@ -87,19 +109,24 @@ app.MapGet("/faculties", async ([FromServices] ApiService apiService) =>
|
||||
.ProducesProblem(StatusCodes.Status500InternalServerError)
|
||||
.Produces<Dictionary<int, string>>(StatusCodes.Status200OK);
|
||||
|
||||
app.MapGet("/groups", async ([FromServices] ApiService apiService, int facultyId, [Range(0, 5)] int year) =>
|
||||
// Retrieve list of groups for a chosen faculty and year.
|
||||
// Year can be in range from 1 to 5.
|
||||
// If year is not specified, all groups for chosen faculty will be retrieved.
|
||||
app.MapGet("/groups", async ([FromServices] ApiService apiService, int facultyId, [Range(1, 5)] int? year) =>
|
||||
{
|
||||
year ??= 0; // Setting year to 0 (show all groups) if not specified in the request.
|
||||
|
||||
try
|
||||
{
|
||||
Dictionary<int, string> groups = await apiService.GetGroupsListAsync(facultyId, year);
|
||||
Dictionary<int, string> groups = await apiService.GetGroupsListAsync(facultyId, year.Value);
|
||||
logger.LogInformation("Fetched {Count} groups (facultyId: {FacultyId}, year: {Year}).", groups.Count, facultyId, year);
|
||||
tracker.TrackGroupFetch(facultyId, year, true);
|
||||
tracker.TrackGroupFetch(facultyId, year.Value, true); // Track whether retrieving groups for this specific faculty and year was successul or not.
|
||||
return Results.Ok(groups);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to fetch groups list (facultyId: {FacultyId}, year: {Year}).", facultyId, year);
|
||||
tracker.TrackGroupFetch(facultyId, year, false);
|
||||
tracker.TrackGroupFetch(facultyId, year.Value, false); // Track whether retrieving groups for this specific faculty and year was successul or not.
|
||||
return Results.Problem(
|
||||
"Failed to fetch groups list.",
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
@@ -117,18 +144,22 @@ app.MapGet("/groups", async ([FromServices] ApiService apiService, int facultyId
|
||||
.ProducesProblem(StatusCodes.Status500InternalServerError)
|
||||
.ProducesValidationProblem();
|
||||
|
||||
// Retrieve timetable for specified group in form of iCal.
|
||||
app.MapGet("/timetable/{facultyId}/{groupId}", async (
|
||||
int facultyId, int groupId, string? id,
|
||||
[FromServices] TimetableService timetableService
|
||||
) =>
|
||||
{
|
||||
string cacheFile = Path.Combine(Path.GetTempPath(), $"bonch_cal_{groupId}.ics");
|
||||
string? content = await timetableService.TryServingFromCacheAsync(groupId);
|
||||
string? content = await timetableService.TryGetTimetableFromCacheAsync(groupId);
|
||||
bool hasId = !string.IsNullOrEmpty(id);
|
||||
|
||||
// If this is the first request with given id, we record it.
|
||||
// "download" is a special "ID" that is used solely for downloading timetable as file.
|
||||
if (hasId && id is not "download" && !ids.Contains(id!))
|
||||
ids.Add(id!);
|
||||
|
||||
// If we have a valid cache, we serve it, instead of retrieving new timetable from sut.ru API
|
||||
// We don't serve cache for requests with no ID, since they have a special behavior.
|
||||
if (content is not null && hasId)
|
||||
{
|
||||
logger.LogInformation("Serving timetable for {FacultyId}/{GroupId} from cache.", facultyId, groupId);
|
||||
@@ -143,6 +174,9 @@ app.MapGet("/timetable/{facultyId}/{groupId}", async (
|
||||
content = await timetableService.GetTimetableAsync(facultyId, groupId, saveToCache: true);
|
||||
else
|
||||
{
|
||||
// For requests with no ID we append an event at 7pm that asks users to update their calendar URL,
|
||||
// since now all /timetable requests must have one.
|
||||
// This is a temporary behavior that will be changed to just sending 4xx response instead later.
|
||||
content = await timetableService.GetTimetableAsync(facultyId, groupId, saveToCache: false, transform: calendar =>
|
||||
calendar.Events.Add(new()
|
||||
{
|
||||
@@ -172,19 +206,13 @@ app.MapGet("/timetable/{facultyId}/{groupId}", async (
|
||||
}
|
||||
|
||||
logger.LogInformation("Fetched timetable for {FacultyId}/{GroupId}.", facultyId, groupId);
|
||||
tracker.TrackTimetableFetch(facultyId, groupId, true);
|
||||
tracker.TrackTimetableFetch(facultyId, groupId, true); // Track whether retrieving timetable for this specific group was successul or not.
|
||||
return Results.Text(content, contentType: "text/calendar");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to generate timetable for {FacultyId}/{GroupId}.", facultyId, groupId);
|
||||
tracker.TrackTimetableFetch(facultyId, groupId, false);
|
||||
|
||||
if (content is not null)
|
||||
{
|
||||
logger.LogWarning("[Fallback] Serving timetable from cache ({CacheFile}).", cacheFile);
|
||||
return Results.Text(content, contentType: "text/calendar");
|
||||
}
|
||||
tracker.TrackTimetableFetch(facultyId, groupId, false); // Track whether retrieving timetable for this specific group was successul or not.
|
||||
|
||||
return Results.Problem(
|
||||
"Failed to fetch timetable",
|
||||
@@ -203,4 +231,5 @@ app.MapGet("/timetable/{facultyId}/{groupId}", async (
|
||||
.ProducesProblem(StatusCodes.Status500InternalServerError)
|
||||
.ProducesValidationProblem();
|
||||
|
||||
// Start the application.
|
||||
app.Run();
|
||||
|
||||
Reference in New Issue
Block a user