1
0
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:
2026-05-24 08:28:09 +00:00
parent d5f5f54eb7
commit bb6f9d493d
18 changed files with 368 additions and 77 deletions
+59 -30
View File
@@ -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();