using Novelly.Api.Beats; using Novelly.Api.Common; using Novelly.Api.Common.Validation; using Novelly.Api.Locations; using Novelly.Api.Tags; namespace Novelly.Api.Chapters; public record ChapterSummaryResponse( Guid Id, Guid NovelId, int Number, ChapterKind Kind, int? DisplayNumber, string Title, string? Summary, IReadOnlyList Locations, DraftStatus Status, int? TargetWordCount, int BeatCount, int WordCount, IReadOnlyList Tags, DateTimeOffset UpdatedAt); public record ChapterResponse( Guid Id, Guid NovelId, int Number, ChapterKind Kind, int? DisplayNumber, string Title, string? Summary, IReadOnlyList Locations, string? Notes, DraftStatus Status, int? TargetWordCount, IReadOnlyList Beats, string? Prose, int WordCount, IReadOnlyList Tags, DateTimeOffset UpdatedAt); public record CreateChapterRequest( string Title, int? Number = null, ChapterKind Kind = ChapterKind.Body, string? Summary = null, IReadOnlyList? Locations = null, string? Notes = null, DraftStatus Status = DraftStatus.Planned, int? TargetWordCount = null, string? Prose = null, IReadOnlyList? Tags = null); public class CreateChapterRequestValidator : IModelValidator { public ValidationResult Validate(CreateChapterRequest model) { var result = new ValidationResult(); result.AddRequiredTextErrors("Title", "Title", model.Title, 200); ChapterValidation.OptionalFields(model.Number, model.Summary, model.Locations, model.Notes, model.TargetWordCount, model.Prose, model.Tags, result); return result; } } public record UpdateChapterRequest( string? Title = null, int? Number = null, ChapterKind? Kind = null, string? Summary = null, IReadOnlyList? Locations = null, string? Notes = null, DraftStatus? Status = null, int? TargetWordCount = null, string? Prose = null, IReadOnlyList? Tags = null); public class UpdateChapterRequestValidator : IModelValidator { public ValidationResult Validate(UpdateChapterRequest model) { var result = new ValidationResult(); result.AddUnclearableTextErrors("Title", "Title", model.Title, "a chapter", 200); ChapterValidation.OptionalFields(model.Number, model.Summary, model.Locations, model.Notes, model.TargetWordCount, model.Prose, model.Tags, result); return result; } } file static class ChapterValidation { public static void OptionalFields( int? number, string? summary, IReadOnlyList? locations, string? notes, int? targetWordCount, string? prose, IReadOnlyList? tags, ValidationResult result) { if (number is <= 0) result.AddError("Number", "'Number' must be greater than zero."); if (summary is { Length: > 20000 }) result.AddError("Summary", "'Summary' must be 20,000 characters or fewer."); if (locations is not null && locations.Any(string.IsNullOrWhiteSpace)) result.AddError("Locations", "'Locations' must not contain blank entries."); if (notes is { Length: > 20000 }) result.AddError("Notes", "'Notes' must be 20,000 characters or fewer."); if (targetWordCount is < 0) result.AddError("TargetWordCount", "'Target Word Count' must be zero or greater."); if (prose is { Length: > 200000 }) result.AddError("Prose", "'Prose' must be 200,000 characters or fewer."); if (tags is not null && tags.Any(string.IsNullOrWhiteSpace)) result.AddError("Tags", "'Tags' must not contain blank entries."); } } public static class ChapterMapping { public static ChapterResponse ToResponse(this Chapter c, int? displayNumber = null) => new( c.Id, c.NovelId, c.Number, c.Kind, displayNumber, c.Title, c.Summary, [.. c.Locations.OrderBy(l => l.Name).Select(l => l.ToResponse())], c.Notes, c.Status, c.TargetWordCount, [.. c.Beats.OrderBy(b => b.SortOrder).Select(b => b.ToResponse())], c.Prose, c.WordCount, [.. c.Tags.OrderBy(t => t.Name).Select(t => t.ToResponse())], c.UpdatedAt); public static ChapterSummaryResponse ToSummaryResponse(this Chapter c, int? displayNumber = null) => new( c.Id, c.NovelId, c.Number, c.Kind, displayNumber, c.Title, c.Summary, [.. c.Locations.OrderBy(l => l.Name).Select(l => l.ToResponse())], c.Status, c.TargetWordCount, c.Beats.Count, c.WordCount, [.. c.Tags.OrderBy(t => t.Name).Select(t => t.ToResponse())], c.UpdatedAt); public static int CountWords(string? prose) => string.IsNullOrWhiteSpace(prose) ? 0 : prose.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries).Length; }