Files
novelly/src/Novelly.Api/Scenes/SceneContracts.cs
T
James Wampler 189ebf3237 Add outline import feature; drop Dto naming, map entities at the API boundary
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.
2026-08-06 18:36:40 -07:00

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;
}