Services now return entities; endpoints (and the agent toolsets) map to *Response records instead of services building wire DTOs themselves. Also brings in the outline-import agent, MCP tool, ledger and web dialog that were already in progress on disk.
129 lines
4.2 KiB
C#
129 lines
4.2 KiB
C#
using Novelly.Api.Common;
|
|
using Novelly.Api.Common.Validation;
|
|
|
|
namespace Novelly.Api.Scenes;
|
|
|
|
public record SceneResponse(
|
|
Guid Id,
|
|
Guid ChapterId,
|
|
int SortOrder,
|
|
string Title,
|
|
string? Summary,
|
|
string? Goal,
|
|
string? Conflict,
|
|
string? Outcome,
|
|
Guid? PovCharacterId,
|
|
string? PovCharacterName,
|
|
string? Location,
|
|
string? Prose,
|
|
int WordCount,
|
|
DraftStatus Status,
|
|
DateTimeOffset UpdatedAt);
|
|
|
|
public record CreateSceneRequest(
|
|
string Title,
|
|
int? SortOrder = null,
|
|
string? Summary = null,
|
|
string? Goal = null,
|
|
string? Conflict = null,
|
|
string? Outcome = null,
|
|
Guid? PovCharacterId = null,
|
|
string? Location = null,
|
|
string? Prose = null,
|
|
DraftStatus Status = DraftStatus.Planned);
|
|
|
|
public class CreateSceneRequestValidator : IModelValidator<CreateSceneRequest>
|
|
{
|
|
public ValidationResult Validate(CreateSceneRequest model)
|
|
{
|
|
var result = new ValidationResult();
|
|
|
|
if (string.IsNullOrWhiteSpace(model.Title))
|
|
result.AddError("Title", "'Title' must not be empty.");
|
|
else if (model.Title.Length > 200)
|
|
result.AddError("Title", "'Title' must be 200 characters or fewer.");
|
|
|
|
SceneValidation.OptionalFields(model.SortOrder, model.Summary, model.Goal, model.Conflict, model.Outcome, model.Location, model.Prose, result);
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
public record UpdateSceneRequest(
|
|
string? Title = null,
|
|
int? SortOrder = null,
|
|
string? Summary = null,
|
|
string? Goal = null,
|
|
string? Conflict = null,
|
|
string? Outcome = null,
|
|
Guid? PovCharacterId = null,
|
|
string? Location = null,
|
|
string? Prose = null,
|
|
DraftStatus? Status = null);
|
|
|
|
public class UpdateSceneRequestValidator : IModelValidator<UpdateSceneRequest>
|
|
{
|
|
public ValidationResult Validate(UpdateSceneRequest model)
|
|
{
|
|
var result = new ValidationResult();
|
|
|
|
if (model.Title is not null)
|
|
{
|
|
if (model.Title.Length == 0)
|
|
result.AddError("Title", "'Title' can not be cleared — a scene always needs one.");
|
|
else if (model.Title.Length > 200)
|
|
result.AddError("Title", "'Title' must be 200 characters or fewer.");
|
|
}
|
|
|
|
SceneValidation.OptionalFields(model.SortOrder, model.Summary, model.Goal, model.Conflict, model.Outcome, model.Location, model.Prose, result);
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
file static class SceneValidation
|
|
{
|
|
public static void OptionalFields(
|
|
int? sortOrder, string? summary, string? goal, string? conflict, string? outcome, string? location, string? prose, ValidationResult result)
|
|
{
|
|
if (sortOrder is < 0)
|
|
result.AddError("SortOrder", "'Sort Order' must be zero or greater.");
|
|
|
|
if (summary is { Length: > 20000 })
|
|
result.AddError("Summary", "'Summary' must be 20,000 characters or fewer.");
|
|
|
|
if (goal is { Length: > 20000 })
|
|
result.AddError("Goal", "'Goal' must be 20,000 characters or fewer.");
|
|
|
|
if (conflict is { Length: > 20000 })
|
|
result.AddError("Conflict", "'Conflict' must be 20,000 characters or fewer.");
|
|
|
|
if (outcome is { Length: > 20000 })
|
|
result.AddError("Outcome", "'Outcome' must be 20,000 characters or fewer.");
|
|
|
|
if (location is { Length: > 500 })
|
|
result.AddError("Location", "'Location' must be 500 characters or fewer.");
|
|
|
|
if (prose is { Length: > 100000 })
|
|
result.AddError("Prose", "'Prose' must be 100,000 characters or fewer.");
|
|
}
|
|
}
|
|
|
|
public static class SceneMapping
|
|
{
|
|
public static SceneResponse ToResponse(this Scene s) => new(
|
|
s.Id, s.ChapterId, s.SortOrder, s.Title, s.Summary,
|
|
s.Goal, s.Conflict, s.Outcome,
|
|
s.PovCharacterId, s.PovCharacter?.Name, s.Location,
|
|
s.Prose, s.WordCount, s.Status, s.UpdatedAt);
|
|
|
|
/// <summary>
|
|
/// Whitespace-delimited word count. Good enough for progress tracking, and it costs
|
|
/// nothing to recompute on every save.
|
|
/// </summary>
|
|
public static int CountWords(string? prose) =>
|
|
string.IsNullOrWhiteSpace(prose)
|
|
? 0
|
|
: prose.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries).Length;
|
|
}
|