Expose critical curation and source image APIs
This commit is contained in:
@@ -61,6 +61,9 @@
|
|||||||
"rollBand": "66-70",
|
"rollBand": "66-70",
|
||||||
"groupKey": null,
|
"groupKey": null,
|
||||||
"columnKey": "C",
|
"columnKey": "C",
|
||||||
|
"isCurated": false,
|
||||||
|
"sourcePageNumber": 1,
|
||||||
|
"sourceImageUrl": "/api/tables/critical/slash/cells/412/source-image",
|
||||||
"rawCellText": "Original imported full cell text",
|
"rawCellText": "Original imported full cell text",
|
||||||
"descriptionText": "Current curated prose",
|
"descriptionText": "Current curated prose",
|
||||||
"rawAffixText": "+8H - 2S",
|
"rawAffixText": "+8H - 2S",
|
||||||
@@ -77,6 +80,12 @@
|
|||||||
<p class="panel-copy">Use this to retrieve the full editable result graph for one critical-table cell, including nested branches, normalized effects, and review notes for unresolved quick-parse tokens.</p>
|
<p class="panel-copy">Use this to retrieve the full editable result graph for one critical-table cell, including nested branches, normalized effects, and review notes for unresolved quick-parse tokens.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2 class="panel-title">Cell source image</h2>
|
||||||
|
<p class="panel-copy"><code>GET /api/tables/critical/{slug}/cells/{resultId}/source-image</code></p>
|
||||||
|
<p class="panel-copy">Streams the importer-generated PNG crop for the current critical cell. Returns <code>404</code> when the row has no stored crop or the artifact is missing.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2 class="panel-title">Cell re-parse</h2>
|
<h2 class="panel-title">Cell re-parse</h2>
|
||||||
<p class="panel-copy"><code>POST /api/tables/critical/{slug}/cells/{resultId}/reparse</code></p>
|
<p class="panel-copy"><code>POST /api/tables/critical/{slug}/cells/{resultId}/reparse</code></p>
|
||||||
@@ -87,6 +96,7 @@
|
|||||||
"rawAffixText": "+8H",
|
"rawAffixText": "+8H",
|
||||||
"parseStatus": "partial",
|
"parseStatus": "partial",
|
||||||
"parsedJson": "{}",
|
"parsedJson": "{}",
|
||||||
|
"isCurated": false,
|
||||||
"isDescriptionOverridden": true,
|
"isDescriptionOverridden": true,
|
||||||
"isRawAffixTextOverridden": false,
|
"isRawAffixTextOverridden": false,
|
||||||
"areEffectsOverridden": false,
|
"areEffectsOverridden": false,
|
||||||
@@ -103,10 +113,11 @@
|
|||||||
<p class="panel-copy"><code>PUT /api/tables/critical/{slug}/cells/{resultId}</code></p>
|
<p class="panel-copy"><code>PUT /api/tables/critical/{slug}/cells/{resultId}</code></p>
|
||||||
<pre class="code-block">{
|
<pre class="code-block">{
|
||||||
"rawCellText": "Corrected imported text",
|
"rawCellText": "Corrected imported text",
|
||||||
"descriptionText": "Rewritten prose after manual review",
|
"descriptionText": "Rewritten prose after manual review",
|
||||||
"rawAffixText": "+10H - must parry 2 rnds",
|
"rawAffixText": "+10H - must parry 2 rnds",
|
||||||
"parseStatus": "manually_curated",
|
"parseStatus": "manually_curated",
|
||||||
"parsedJson": "{\"reviewed\":true}",
|
"parsedJson": "{\"reviewed\":true}",
|
||||||
|
"isCurated": true,
|
||||||
"isDescriptionOverridden": true,
|
"isDescriptionOverridden": true,
|
||||||
"isRawAffixTextOverridden": false,
|
"isRawAffixTextOverridden": false,
|
||||||
"areEffectsOverridden": false,
|
"areEffectsOverridden": false,
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ public sealed class CriticalCellEditorModel
|
|||||||
public string ColumnKey { get; set; } = string.Empty;
|
public string ColumnKey { get; set; } = string.Empty;
|
||||||
public string ColumnLabel { get; set; } = string.Empty;
|
public string ColumnLabel { get; set; } = string.Empty;
|
||||||
public string ColumnRole { get; set; } = string.Empty;
|
public string ColumnRole { get; set; } = string.Empty;
|
||||||
|
public bool IsCurated { get; set; }
|
||||||
|
public int? SourcePageNumber { get; set; }
|
||||||
|
public string? SourceImageUrl { get; set; }
|
||||||
public string RawCellText { get; set; } = string.Empty;
|
public string RawCellText { get; set; } = string.Empty;
|
||||||
public string QuickParseInput { get; set; } = string.Empty;
|
public string QuickParseInput { get; set; } = string.Empty;
|
||||||
public string DescriptionText { get; set; } = string.Empty;
|
public string DescriptionText { get; set; } = string.Empty;
|
||||||
@@ -43,6 +46,9 @@ public sealed class CriticalCellEditorModel
|
|||||||
ColumnKey = response.ColumnKey,
|
ColumnKey = response.ColumnKey,
|
||||||
ColumnLabel = response.ColumnLabel,
|
ColumnLabel = response.ColumnLabel,
|
||||||
ColumnRole = response.ColumnRole,
|
ColumnRole = response.ColumnRole,
|
||||||
|
IsCurated = response.IsCurated,
|
||||||
|
SourcePageNumber = response.SourcePageNumber,
|
||||||
|
SourceImageUrl = response.SourceImageUrl,
|
||||||
RawCellText = response.RawCellText,
|
RawCellText = response.RawCellText,
|
||||||
QuickParseInput = response.QuickParseInput,
|
QuickParseInput = response.QuickParseInput,
|
||||||
DescriptionText = response.DescriptionText,
|
DescriptionText = response.DescriptionText,
|
||||||
@@ -68,6 +74,7 @@ public sealed class CriticalCellEditorModel
|
|||||||
RawAffixText,
|
RawAffixText,
|
||||||
ResolveParseStatus(Effects, Branches),
|
ResolveParseStatus(Effects, Branches),
|
||||||
SerializeParsedEffects(Effects),
|
SerializeParsedEffects(Effects),
|
||||||
|
IsCurated,
|
||||||
IsDescriptionOverridden,
|
IsDescriptionOverridden,
|
||||||
IsRawAffixTextOverridden,
|
IsRawAffixTextOverridden,
|
||||||
AreEffectsOverridden,
|
AreEffectsOverridden,
|
||||||
@@ -101,6 +108,9 @@ public sealed class CriticalCellEditorModel
|
|||||||
ColumnKey = ColumnKey,
|
ColumnKey = ColumnKey,
|
||||||
ColumnLabel = ColumnLabel,
|
ColumnLabel = ColumnLabel,
|
||||||
ColumnRole = ColumnRole,
|
ColumnRole = ColumnRole,
|
||||||
|
IsCurated = IsCurated,
|
||||||
|
SourcePageNumber = SourcePageNumber,
|
||||||
|
SourceImageUrl = SourceImageUrl,
|
||||||
RawCellText = RawCellText,
|
RawCellText = RawCellText,
|
||||||
QuickParseInput = QuickParseInput,
|
QuickParseInput = QuickParseInput,
|
||||||
DescriptionText = DescriptionText,
|
DescriptionText = DescriptionText,
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ public sealed record CriticalCellEditorResponse(
|
|||||||
string ColumnKey,
|
string ColumnKey,
|
||||||
string ColumnLabel,
|
string ColumnLabel,
|
||||||
string ColumnRole,
|
string ColumnRole,
|
||||||
|
bool IsCurated,
|
||||||
|
int? SourcePageNumber,
|
||||||
|
string? SourceImageUrl,
|
||||||
string RawCellText,
|
string RawCellText,
|
||||||
string QuickParseInput,
|
string QuickParseInput,
|
||||||
string DescriptionText,
|
string DescriptionText,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ public sealed record CriticalCellUpdateRequest(
|
|||||||
string? RawAffixText,
|
string? RawAffixText,
|
||||||
string ParseStatus,
|
string ParseStatus,
|
||||||
string ParsedJson,
|
string ParsedJson,
|
||||||
|
bool IsCurated,
|
||||||
bool IsDescriptionOverridden,
|
bool IsDescriptionOverridden,
|
||||||
bool IsRawAffixTextOverridden,
|
bool IsRawAffixTextOverridden,
|
||||||
bool AreEffectsOverridden,
|
bool AreEffectsOverridden,
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
namespace RolemasterDb.App.Features;
|
||||||
|
|
||||||
|
public sealed class CriticalImportArtifactLocator
|
||||||
|
{
|
||||||
|
public CriticalImportArtifactLocator(IHostEnvironment hostEnvironment)
|
||||||
|
{
|
||||||
|
ArtifactsRootPath = DiscoverArtifactsRootPath(hostEnvironment.ContentRootPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ArtifactsRootPath { get; }
|
||||||
|
|
||||||
|
public string? ResolveStoredPath(string? relativePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(relativePath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidate = Path.GetFullPath(Path.Combine(
|
||||||
|
ArtifactsRootPath,
|
||||||
|
relativePath.Replace('/', Path.DirectorySeparatorChar)));
|
||||||
|
|
||||||
|
return candidate.StartsWith(ArtifactsRootPath, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? candidate
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DiscoverArtifactsRootPath(string contentRootPath)
|
||||||
|
{
|
||||||
|
var probe = new DirectoryInfo(contentRootPath);
|
||||||
|
|
||||||
|
while (probe is not null)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(probe.FullName, "RolemasterDB.slnx")))
|
||||||
|
{
|
||||||
|
return Path.Combine(probe.FullName, "artifacts", "import", "critical");
|
||||||
|
}
|
||||||
|
|
||||||
|
probe = probe.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.Combine(contentRootPath, "artifacts", "import", "critical");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -110,6 +110,7 @@ public sealed record CriticalTableCellDetail(
|
|||||||
string ColumnRole,
|
string ColumnRole,
|
||||||
string? GroupKey,
|
string? GroupKey,
|
||||||
string? GroupLabel,
|
string? GroupLabel,
|
||||||
|
bool IsCurated,
|
||||||
string? Description,
|
string? Description,
|
||||||
IReadOnlyList<CriticalEffectLookupResponse> Effects,
|
IReadOnlyList<CriticalEffectLookupResponse> Effects,
|
||||||
IReadOnlyList<CriticalBranchLookupResponse> Branches);
|
IReadOnlyList<CriticalBranchLookupResponse> Branches);
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ using SharedParsing = RolemasterDb.CriticalParsing;
|
|||||||
|
|
||||||
namespace RolemasterDb.App.Features;
|
namespace RolemasterDb.App.Features;
|
||||||
|
|
||||||
public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbContextFactory)
|
public sealed class LookupService(
|
||||||
|
IDbContextFactory<RolemasterDbContext> dbContextFactory,
|
||||||
|
CriticalImportArtifactLocator? artifactLocator = null)
|
||||||
{
|
{
|
||||||
public async Task<LookupReferenceData> GetReferenceDataAsync(CancellationToken cancellationToken = default)
|
public async Task<LookupReferenceData> GetReferenceDataAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -245,6 +247,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
result.CriticalColumn.Role,
|
result.CriticalColumn.Role,
|
||||||
result.CriticalGroup?.GroupKey,
|
result.CriticalGroup?.GroupKey,
|
||||||
result.CriticalGroup?.Label,
|
result.CriticalGroup?.Label,
|
||||||
|
result.IsCurated,
|
||||||
result.DescriptionText,
|
result.DescriptionText,
|
||||||
result.Effects
|
result.Effects
|
||||||
.OrderBy(effect => effect.Id)
|
.OrderBy(effect => effect.Id)
|
||||||
@@ -300,6 +303,28 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
return CreateCellEditorResponse(result, currentState, generatedContent.ValidationErrors, CreateComparisonState(generatedContent));
|
return CreateCellEditorResponse(result, currentState, generatedContent.ValidationErrors, CreateComparisonState(generatedContent));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetCriticalSourceImagePathAsync(string slug, int resultId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (artifactLocator is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var normalizedSlug = NormalizeSlug(slug);
|
||||||
|
var relativePath = await dbContext.CriticalResults
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(item => item.Id == resultId && item.CriticalTable.Slug == normalizedSlug)
|
||||||
|
.Select(item => item.SourceImagePath)
|
||||||
|
.SingleOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
var fullPath = artifactLocator.ResolveStoredPath(relativePath);
|
||||||
|
return fullPath is not null && File.Exists(fullPath)
|
||||||
|
? fullPath
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<CriticalCellEditorResponse?> ReparseCriticalCellAsync(
|
public async Task<CriticalCellEditorResponse?> ReparseCriticalCellAsync(
|
||||||
string slug,
|
string slug,
|
||||||
int resultId,
|
int resultId,
|
||||||
@@ -363,6 +388,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
result.RawAffixText = NormalizeOptionalText(request.RawAffixText);
|
result.RawAffixText = NormalizeOptionalText(request.RawAffixText);
|
||||||
result.ParseStatus = request.ParseStatus.Trim();
|
result.ParseStatus = request.ParseStatus.Trim();
|
||||||
result.ParsedJson = CriticalCellEditorSnapshot.FromRequest(request).ToJson();
|
result.ParsedJson = CriticalCellEditorSnapshot.FromRequest(request).ToJson();
|
||||||
|
result.IsCurated = request.IsCurated;
|
||||||
|
|
||||||
ReplaceBaseEffects(dbContext, result, request.Effects);
|
ReplaceBaseEffects(dbContext, result, request.Effects);
|
||||||
ReplaceBranches(dbContext, result, request.Branches);
|
ReplaceBranches(dbContext, result, request.Branches);
|
||||||
@@ -450,6 +476,9 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
result.CriticalColumn.ColumnKey,
|
result.CriticalColumn.ColumnKey,
|
||||||
result.CriticalColumn.Label,
|
result.CriticalColumn.Label,
|
||||||
result.CriticalColumn.Role,
|
result.CriticalColumn.Role,
|
||||||
|
state.IsCurated,
|
||||||
|
result.SourcePageNumber,
|
||||||
|
CreateSourceImageUrl(result),
|
||||||
state.RawCellText,
|
state.RawCellText,
|
||||||
state.QuickParseInput,
|
state.QuickParseInput,
|
||||||
state.DescriptionText,
|
state.DescriptionText,
|
||||||
@@ -596,6 +625,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
result.RawAffixText,
|
result.RawAffixText,
|
||||||
result.ParseStatus,
|
result.ParseStatus,
|
||||||
result.ParsedJson,
|
result.ParsedJson,
|
||||||
|
result.IsCurated,
|
||||||
snapshot.IsDescriptionOverridden,
|
snapshot.IsDescriptionOverridden,
|
||||||
snapshot.IsRawAffixTextOverridden,
|
snapshot.IsRawAffixTextOverridden,
|
||||||
snapshot.AreEffectsOverridden,
|
snapshot.AreEffectsOverridden,
|
||||||
@@ -620,6 +650,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
result.RawAffixText,
|
result.RawAffixText,
|
||||||
result.ParseStatus,
|
result.ParseStatus,
|
||||||
result.ParsedJson,
|
result.ParsedJson,
|
||||||
|
result.IsCurated,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@@ -645,6 +676,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
RawAffixText: content.RawAffixText,
|
RawAffixText: content.RawAffixText,
|
||||||
ParseStatus: ResolveParseStatus(content.Effects, content.Branches),
|
ParseStatus: ResolveParseStatus(content.Effects, content.Branches),
|
||||||
ParsedJson: SerializeParsedEffects(content.Effects),
|
ParsedJson: SerializeParsedEffects(content.Effects),
|
||||||
|
IsCurated: false,
|
||||||
IsDescriptionOverridden: false,
|
IsDescriptionOverridden: false,
|
||||||
IsRawAffixTextOverridden: false,
|
IsRawAffixTextOverridden: false,
|
||||||
AreEffectsOverridden: false,
|
AreEffectsOverridden: false,
|
||||||
@@ -663,6 +695,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
currentState.IsRawAffixTextOverridden ? currentState.RawAffixText : generatedState.RawAffixText,
|
currentState.IsRawAffixTextOverridden ? currentState.RawAffixText : generatedState.RawAffixText,
|
||||||
generatedState.ParseStatus,
|
generatedState.ParseStatus,
|
||||||
generatedState.ParsedJson,
|
generatedState.ParsedJson,
|
||||||
|
currentState.IsCurated,
|
||||||
currentState.IsDescriptionOverridden,
|
currentState.IsDescriptionOverridden,
|
||||||
currentState.IsRawAffixTextOverridden,
|
currentState.IsRawAffixTextOverridden,
|
||||||
currentState.AreEffectsOverridden,
|
currentState.AreEffectsOverridden,
|
||||||
@@ -992,4 +1025,9 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
|
|
||||||
private static string NormalizeSlug(string value) =>
|
private static string NormalizeSlug(string value) =>
|
||||||
value.Trim().Replace(' ', '_').ToLowerInvariant();
|
value.Trim().Replace(' ', '_').ToLowerInvariant();
|
||||||
|
|
||||||
|
private static string? CreateSourceImageUrl(CriticalResult result) =>
|
||||||
|
string.IsNullOrWhiteSpace(result.SourceImagePath)
|
||||||
|
? null
|
||||||
|
: $"/api/tables/critical/{result.CriticalTable.Slug}/cells/{result.Id}/source-image";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ var connectionString = builder.Configuration.GetConnectionString("RolemasterDb")
|
|||||||
builder.Services.AddRazorComponents()
|
builder.Services.AddRazorComponents()
|
||||||
.AddInteractiveServerComponents();
|
.AddInteractiveServerComponents();
|
||||||
builder.Services.AddDbContextFactory<RolemasterDbContext>(options => options.UseSqlite(connectionString));
|
builder.Services.AddDbContextFactory<RolemasterDbContext>(options => options.UseSqlite(connectionString));
|
||||||
|
builder.Services.AddSingleton<CriticalImportArtifactLocator>();
|
||||||
builder.Services.AddScoped<LookupService>();
|
builder.Services.AddScoped<LookupService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
@@ -40,6 +41,11 @@ api.MapGet("/tables/critical/{slug}/cells/{resultId:int}", async (string slug, i
|
|||||||
var result = await lookupService.GetCriticalCellEditorAsync(slug, resultId, cancellationToken);
|
var result = await lookupService.GetCriticalCellEditorAsync(slug, resultId, cancellationToken);
|
||||||
return result is null ? Results.NotFound() : Results.Ok(result);
|
return result is null ? Results.NotFound() : Results.Ok(result);
|
||||||
});
|
});
|
||||||
|
api.MapGet("/tables/critical/{slug}/cells/{resultId:int}/source-image", async (string slug, int resultId, LookupService lookupService, CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
var filePath = await lookupService.GetCriticalSourceImagePathAsync(slug, resultId, cancellationToken);
|
||||||
|
return filePath is null ? Results.NotFound() : Results.File(filePath, "image/png");
|
||||||
|
});
|
||||||
api.MapPost("/tables/critical/{slug}/cells/{resultId:int}/reparse", async (string slug, int resultId, CriticalCellReparseRequest request, LookupService lookupService, CancellationToken cancellationToken) =>
|
api.MapPost("/tables/critical/{slug}/cells/{resultId:int}/reparse", async (string slug, int resultId, CriticalCellReparseRequest request, LookupService lookupService, CancellationToken cancellationToken) =>
|
||||||
{
|
{
|
||||||
var result = await lookupService.ReparseCriticalCellAsync(slug, resultId, request.CurrentState, cancellationToken);
|
var result = await lookupService.ReparseCriticalCellAsync(slug, resultId, request.CurrentState, cancellationToken);
|
||||||
|
|||||||
@@ -432,6 +432,7 @@ public sealed class CriticalCellReparseIntegrationTests
|
|||||||
initialResponse.RawAffixText,
|
initialResponse.RawAffixText,
|
||||||
initialResponse.ParseStatus,
|
initialResponse.ParseStatus,
|
||||||
initialResponse.ParsedJson,
|
initialResponse.ParsedJson,
|
||||||
|
initialResponse.IsCurated,
|
||||||
initialResponse.IsDescriptionOverridden,
|
initialResponse.IsDescriptionOverridden,
|
||||||
initialResponse.IsRawAffixTextOverridden,
|
initialResponse.IsRawAffixTextOverridden,
|
||||||
initialResponse.AreEffectsOverridden,
|
initialResponse.AreEffectsOverridden,
|
||||||
@@ -468,6 +469,7 @@ public sealed class CriticalCellReparseIntegrationTests
|
|||||||
initialResponse.RawAffixText,
|
initialResponse.RawAffixText,
|
||||||
initialResponse.ParseStatus,
|
initialResponse.ParseStatus,
|
||||||
initialResponse.ParsedJson,
|
initialResponse.ParsedJson,
|
||||||
|
initialResponse.IsCurated,
|
||||||
initialResponse.IsDescriptionOverridden,
|
initialResponse.IsDescriptionOverridden,
|
||||||
initialResponse.IsRawAffixTextOverridden,
|
initialResponse.IsRawAffixTextOverridden,
|
||||||
initialResponse.AreEffectsOverridden,
|
initialResponse.AreEffectsOverridden,
|
||||||
@@ -493,6 +495,7 @@ public sealed class CriticalCellReparseIntegrationTests
|
|||||||
reopenedResponse.RawAffixText,
|
reopenedResponse.RawAffixText,
|
||||||
reopenedResponse.ParseStatus,
|
reopenedResponse.ParseStatus,
|
||||||
reopenedResponse.ParsedJson,
|
reopenedResponse.ParsedJson,
|
||||||
|
reopenedResponse.IsCurated,
|
||||||
reopenedResponse.IsDescriptionOverridden,
|
reopenedResponse.IsDescriptionOverridden,
|
||||||
reopenedResponse.IsRawAffixTextOverridden,
|
reopenedResponse.IsRawAffixTextOverridden,
|
||||||
reopenedResponse.AreEffectsOverridden,
|
reopenedResponse.AreEffectsOverridden,
|
||||||
@@ -524,6 +527,7 @@ public sealed class CriticalCellReparseIntegrationTests
|
|||||||
rawAffixText,
|
rawAffixText,
|
||||||
"partial",
|
"partial",
|
||||||
"{}",
|
"{}",
|
||||||
|
false,
|
||||||
isDescriptionOverridden,
|
isDescriptionOverridden,
|
||||||
isRawAffixTextOverridden,
|
isRawAffixTextOverridden,
|
||||||
areEffectsOverridden,
|
areEffectsOverridden,
|
||||||
@@ -629,9 +633,16 @@ public sealed class CriticalCellReparseIntegrationTests
|
|||||||
{
|
{
|
||||||
var databasePath = Path.Combine(Path.GetTempPath(), $"rolemaster-app-{Guid.NewGuid():N}.db");
|
var databasePath = Path.Combine(Path.GetTempPath(), $"rolemaster-app-{Guid.NewGuid():N}.db");
|
||||||
File.Copy(Path.Combine(GetRepositoryRoot(), "src", "RolemasterDb.App", "rolemaster.db"), databasePath, true);
|
File.Copy(Path.Combine(GetRepositoryRoot(), "src", "RolemasterDb.App", "rolemaster.db"), databasePath, true);
|
||||||
|
UpgradeDatabase(databasePath).GetAwaiter().GetResult();
|
||||||
return databasePath;
|
return databasePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task UpgradeDatabase(string databasePath)
|
||||||
|
{
|
||||||
|
await using var dbContext = CreateDbContext(databasePath);
|
||||||
|
await RolemasterDbSchemaUpgrader.EnsureLatestAsync(dbContext);
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetRepositoryRoot()
|
private static string GetRepositoryRoot()
|
||||||
{
|
{
|
||||||
var probe = new DirectoryInfo(AppContext.BaseDirectory);
|
var probe = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
using RolemasterDb.App.Data;
|
||||||
|
using RolemasterDb.App.Domain;
|
||||||
|
using RolemasterDb.App.Features;
|
||||||
|
|
||||||
|
namespace RolemasterDb.ImportTool.Tests;
|
||||||
|
|
||||||
|
public sealed class LookupServiceCurationIntegrationTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Lookup_service_surfaces_and_persists_curated_state_and_source_image_metadata()
|
||||||
|
{
|
||||||
|
var databasePath = CreateEmptyDatabasePath();
|
||||||
|
var repositoryRoot = CreateTemporaryRepositoryRoot();
|
||||||
|
var locator = new CriticalImportArtifactLocator(new TestHostEnvironment(Path.Combine(repositoryRoot, "src", "RolemasterDb.App")));
|
||||||
|
|
||||||
|
await SeedCriticalResultAsync(databasePath, "slash/cells/source-cell.png", 2);
|
||||||
|
WriteSourceImage(repositoryRoot, "slash/cells/source-cell.png");
|
||||||
|
|
||||||
|
var lookupService = new LookupService(CreateDbContextFactory(databasePath), locator);
|
||||||
|
var resultId = await GetResultIdAsync(databasePath);
|
||||||
|
|
||||||
|
var initialResponse = await lookupService.GetCriticalCellEditorAsync("slash", resultId);
|
||||||
|
Assert.NotNull(initialResponse);
|
||||||
|
Assert.False(initialResponse!.IsCurated);
|
||||||
|
Assert.Equal(2, initialResponse.SourcePageNumber);
|
||||||
|
Assert.Equal($"/api/tables/critical/slash/cells/{resultId}/source-image", initialResponse.SourceImageUrl);
|
||||||
|
|
||||||
|
var updateRequest = new CriticalCellUpdateRequest(
|
||||||
|
initialResponse.RawCellText,
|
||||||
|
initialResponse.QuickParseInput,
|
||||||
|
initialResponse.DescriptionText,
|
||||||
|
initialResponse.RawAffixText,
|
||||||
|
initialResponse.ParseStatus,
|
||||||
|
initialResponse.ParsedJson,
|
||||||
|
true,
|
||||||
|
initialResponse.IsDescriptionOverridden,
|
||||||
|
initialResponse.IsRawAffixTextOverridden,
|
||||||
|
initialResponse.AreEffectsOverridden,
|
||||||
|
initialResponse.AreBranchesOverridden,
|
||||||
|
initialResponse.Effects,
|
||||||
|
initialResponse.Branches);
|
||||||
|
|
||||||
|
var updatedResponse = await lookupService.UpdateCriticalCellAsync("slash", resultId, updateRequest);
|
||||||
|
Assert.NotNull(updatedResponse);
|
||||||
|
Assert.True(updatedResponse!.IsCurated);
|
||||||
|
|
||||||
|
var tableDetail = await lookupService.GetCriticalTableAsync("slash");
|
||||||
|
Assert.NotNull(tableDetail);
|
||||||
|
Assert.Contains(tableDetail!.Cells, item => item.ResultId == resultId && item.IsCurated);
|
||||||
|
|
||||||
|
var reopenedResponse = await lookupService.GetCriticalCellEditorAsync("slash", resultId);
|
||||||
|
Assert.NotNull(reopenedResponse);
|
||||||
|
Assert.True(reopenedResponse!.IsCurated);
|
||||||
|
Assert.Equal(initialResponse.SourceImageUrl, reopenedResponse.SourceImageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Lookup_service_resolves_source_image_paths_only_when_artifacts_exist()
|
||||||
|
{
|
||||||
|
var databasePath = CreateEmptyDatabasePath();
|
||||||
|
var repositoryRoot = CreateTemporaryRepositoryRoot();
|
||||||
|
var locator = new CriticalImportArtifactLocator(new TestHostEnvironment(Path.Combine(repositoryRoot, "src", "RolemasterDb.App")));
|
||||||
|
|
||||||
|
await SeedCriticalResultAsync(databasePath, "slash/cells/missing.png", 1);
|
||||||
|
|
||||||
|
var lookupService = new LookupService(CreateDbContextFactory(databasePath), locator);
|
||||||
|
var resultId = await GetResultIdAsync(databasePath);
|
||||||
|
|
||||||
|
var missingPath = await lookupService.GetCriticalSourceImagePathAsync("slash", resultId);
|
||||||
|
Assert.Null(missingPath);
|
||||||
|
|
||||||
|
WriteSourceImage(repositoryRoot, "slash/cells/missing.png");
|
||||||
|
|
||||||
|
var resolvedPath = await lookupService.GetCriticalSourceImagePathAsync("slash", resultId);
|
||||||
|
Assert.NotNull(resolvedPath);
|
||||||
|
Assert.True(File.Exists(resolvedPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SeedCriticalResultAsync(string databasePath, string sourceImagePath, int sourcePageNumber)
|
||||||
|
{
|
||||||
|
await using var dbContext = CreateDbContext(databasePath);
|
||||||
|
await dbContext.Database.EnsureCreatedAsync();
|
||||||
|
await RolemasterDbSchemaUpgrader.EnsureLatestAsync(dbContext);
|
||||||
|
|
||||||
|
var table = new CriticalTable
|
||||||
|
{
|
||||||
|
Slug = "slash",
|
||||||
|
DisplayName = "Slash Critical Strike Table",
|
||||||
|
Family = "standard",
|
||||||
|
SourceDocument = "Slash.pdf"
|
||||||
|
};
|
||||||
|
var column = new CriticalColumn
|
||||||
|
{
|
||||||
|
CriticalTable = table,
|
||||||
|
ColumnKey = "B",
|
||||||
|
Label = "B",
|
||||||
|
Role = "severity",
|
||||||
|
SortOrder = 2
|
||||||
|
};
|
||||||
|
var rollBand = new CriticalRollBand
|
||||||
|
{
|
||||||
|
CriticalTable = table,
|
||||||
|
Label = "36-40",
|
||||||
|
MinRoll = 36,
|
||||||
|
MaxRoll = 40,
|
||||||
|
SortOrder = 8
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = new CriticalResult
|
||||||
|
{
|
||||||
|
CriticalTable = table,
|
||||||
|
CriticalColumn = column,
|
||||||
|
CriticalRollBand = rollBand,
|
||||||
|
IsCurated = false,
|
||||||
|
RawCellText = "Imported raw cell text",
|
||||||
|
DescriptionText = "Imported description",
|
||||||
|
ParseStatus = "partial",
|
||||||
|
ParsedJson = "{}",
|
||||||
|
SourcePageNumber = sourcePageNumber,
|
||||||
|
SourceImagePath = sourceImagePath,
|
||||||
|
SourceImageCropJson = "{\"pageNumber\":1}"
|
||||||
|
};
|
||||||
|
|
||||||
|
result.Effects.Add(new CriticalEffect
|
||||||
|
{
|
||||||
|
EffectCode = CriticalEffectCodes.DirectHits,
|
||||||
|
ValueInteger = 5,
|
||||||
|
IsPermanent = false,
|
||||||
|
SourceType = "symbol",
|
||||||
|
SourceText = "+5H"
|
||||||
|
});
|
||||||
|
|
||||||
|
dbContext.CriticalTables.Add(table);
|
||||||
|
dbContext.CriticalResults.Add(result);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteSourceImage(string repositoryRoot, string relativePath)
|
||||||
|
{
|
||||||
|
var fullPath = Path.Combine(repositoryRoot, "artifacts", "import", "critical", relativePath.Replace('/', Path.DirectorySeparatorChar));
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
|
||||||
|
File.WriteAllBytes(fullPath, [137, 80, 78, 71]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<int> GetResultIdAsync(string databasePath)
|
||||||
|
{
|
||||||
|
await using var dbContext = CreateDbContext(databasePath);
|
||||||
|
return await dbContext.CriticalResults
|
||||||
|
.Where(item => item.CriticalTable.Slug == "slash")
|
||||||
|
.Select(item => item.Id)
|
||||||
|
.SingleAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RolemasterDbContext CreateDbContext(string databasePath)
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<RolemasterDbContext>()
|
||||||
|
.UseSqlite($"Data Source={databasePath}")
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
return new RolemasterDbContext(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IDbContextFactory<RolemasterDbContext> CreateDbContextFactory(string databasePath)
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<RolemasterDbContext>()
|
||||||
|
.UseSqlite($"Data Source={databasePath}")
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
return new TestRolemasterDbContextFactory(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateEmptyDatabasePath() =>
|
||||||
|
Path.Combine(Path.GetTempPath(), $"lookup-service-curation-{Guid.NewGuid():N}.db");
|
||||||
|
|
||||||
|
private static string CreateTemporaryRepositoryRoot()
|
||||||
|
{
|
||||||
|
var repositoryRoot = Path.Combine(Path.GetTempPath(), $"rolemaster-repo-{Guid.NewGuid():N}");
|
||||||
|
Directory.CreateDirectory(Path.Combine(repositoryRoot, "src", "RolemasterDb.App"));
|
||||||
|
File.WriteAllText(Path.Combine(repositoryRoot, "RolemasterDB.slnx"), string.Empty);
|
||||||
|
return repositoryRoot;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/RolemasterDb.ImportTool.Tests/TestHostEnvironment.cs
Normal file
12
src/RolemasterDb.ImportTool.Tests/TestHostEnvironment.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace RolemasterDb.ImportTool.Tests;
|
||||||
|
|
||||||
|
internal sealed class TestHostEnvironment(string contentRootPath) : IHostEnvironment
|
||||||
|
{
|
||||||
|
public string EnvironmentName { get; set; } = Environments.Development;
|
||||||
|
public string ApplicationName { get; set; } = "RolemasterDb.Tests";
|
||||||
|
public string ContentRootPath { get; set; } = contentRootPath;
|
||||||
|
public IFileProvider ContentRootFileProvider { get; set; } = new PhysicalFileProvider(contentRootPath);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user