Files
jobtrackingapp/Models/TailoredCvDraftJson.cs

255 lines
11 KiB
C#

using System.Text.Json;
using System.Text.Json.Serialization;
namespace JobTrackerApi.Models;
public static class TailoredCvDraftJson
{
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
public static TailoredCvDocument Empty() => Normalize(new TailoredCvDocument());
public static TailoredCvDocument FromDraft(TailoredCvDraft? draft)
{
if (draft is null) return Empty();
var document = new TailoredCvDocument
{
TemplateId = string.IsNullOrWhiteSpace(draft.TemplateId) ? "base" : draft.TemplateId.Trim(),
Headline = TrimOrNull(draft.Headline),
Summary = DeserializeList(draft.SummaryJson),
SelectedSkills = DeserializeList(draft.SelectedSkillsJson),
Experience = DeserializeList<TailoredCvExperienceItem>(draft.ExperienceJson)
.Select(NormalizeExperience)
.Where(item => !string.IsNullOrWhiteSpace(item.Title) || !string.IsNullOrWhiteSpace(item.Company) || item.Bullets.Count > 0)
.ToList(),
Education = DeserializeList<TailoredCvEducationItem>(draft.EducationJson)
.Select(NormalizeEducation)
.Where(item => !string.IsNullOrWhiteSpace(item.Qualification) || !string.IsNullOrWhiteSpace(item.Institution) || item.Details.Count > 0)
.ToList(),
CustomSections = DeserializeList<TailoredCvCustomSection>(draft.CustomSectionsJson)
.Select(NormalizeCustomSection)
.Where(item => !string.IsNullOrWhiteSpace(item.Title) || item.Items.Count > 0)
.ToList(),
RenderOptions = DeserializeObject<TailoredCvRenderOptions>(draft.RenderOptionsJson) ?? new TailoredCvRenderOptions(),
};
return Normalize(document);
}
public static void ApplyToDraft(TailoredCvDraft draft, TailoredCvDocument? document)
{
var normalized = Normalize(document);
draft.TemplateId = normalized.TemplateId;
draft.Headline = TrimOrNull(normalized.Headline);
draft.SummaryJson = JsonSerializer.Serialize(normalized.Summary, SerializerOptions);
draft.SelectedSkillsJson = JsonSerializer.Serialize(normalized.SelectedSkills, SerializerOptions);
draft.ExperienceJson = JsonSerializer.Serialize(normalized.Experience, SerializerOptions);
draft.EducationJson = JsonSerializer.Serialize(normalized.Education, SerializerOptions);
draft.CustomSectionsJson = JsonSerializer.Serialize(normalized.CustomSections, SerializerOptions);
draft.RenderOptionsJson = JsonSerializer.Serialize(normalized.RenderOptions, SerializerOptions);
}
public static TailoredCvDocument Normalize(TailoredCvDocument? document)
{
document ??= new TailoredCvDocument();
document.TemplateId = string.IsNullOrWhiteSpace(document.TemplateId) ? "ats-minimal" : document.TemplateId.Trim();
document.Headline = TrimOrNull(document.Headline);
document.Summary = CleanList(document.Summary);
document.SelectedSkills = CleanList(document.SelectedSkills);
document.Experience = (document.Experience ?? new List<TailoredCvExperienceItem>())
.Select(NormalizeExperience)
.Where(item => !string.IsNullOrWhiteSpace(item.Title) || !string.IsNullOrWhiteSpace(item.Company) || item.Bullets.Count > 0)
.ToList();
document.Education = (document.Education ?? new List<TailoredCvEducationItem>())
.Select(NormalizeEducation)
.Where(item => !string.IsNullOrWhiteSpace(item.Qualification) || !string.IsNullOrWhiteSpace(item.Institution) || item.Details.Count > 0)
.ToList();
document.CustomSections = (document.CustomSections ?? new List<TailoredCvCustomSection>())
.Select(NormalizeCustomSection)
.Where(item => !string.IsNullOrWhiteSpace(item.Title) || item.Items.Count > 0)
.ToList();
document.RenderOptions ??= new TailoredCvRenderOptions();
document.RenderOptions.PageMode = string.IsNullOrWhiteSpace(document.RenderOptions.PageMode) ? "one-page" : document.RenderOptions.PageMode.Trim();
document.RenderOptions.AccentColor = string.IsNullOrWhiteSpace(document.RenderOptions.AccentColor) ? "slate" : document.RenderOptions.AccentColor.Trim();
document.RenderOptions.BulletDensity = string.IsNullOrWhiteSpace(document.RenderOptions.BulletDensity) ? "balanced" : document.RenderOptions.BulletDensity.Trim();
document.RenderOptions.SectionOrder = CleanList(document.RenderOptions.SectionOrder);
if (document.RenderOptions.SectionOrder.Count == 0)
{
document.RenderOptions.SectionOrder = new List<string> { "summary", "skills", "experience", "education", "custom" };
}
return document;
}
public static string RenderPlainText(TailoredCvDocument? document)
{
var normalized = Normalize(document);
var lines = new List<string>();
AddLine(lines, normalized.Headline);
if (normalized.Summary.Count > 0)
{
AddBlock(lines, "Professional Summary", normalized.Summary.Select(item => $"- {item}"));
}
if (normalized.SelectedSkills.Count > 0)
{
AddBlock(lines, "Core Skills", normalized.SelectedSkills);
}
if (normalized.Experience.Count > 0)
{
var block = new List<string>();
foreach (var item in normalized.Experience)
{
AddLine(block, item.Title);
var meta = string.Join(" | ", new[]
{
item.Company,
item.Location,
FormatDateRange(item.Start, item.End, item.IsCurrent)
}.Where(value => !string.IsNullOrWhiteSpace(value)));
AddLine(block, meta);
foreach (var bullet in item.Bullets)
{
AddLine(block, $"- {bullet}");
}
AddLine(block, string.Empty);
}
AddBlock(lines, "Experience", block);
}
if (normalized.Education.Count > 0)
{
var block = new List<string>();
foreach (var item in normalized.Education)
{
AddLine(block, string.IsNullOrWhiteSpace(item.QualificationLevel) ? item.Qualification : $"{item.Qualification} ({item.QualificationLevel})");
var meta = string.Join(" | ", new[]
{
item.Institution,
item.Location,
FormatDateRange(item.Start, item.End, false)
}.Where(value => !string.IsNullOrWhiteSpace(value)));
AddLine(block, meta);
foreach (var detail in item.Details)
{
AddLine(block, $"- {detail}");
}
AddLine(block, string.Empty);
}
AddBlock(lines, "Education", block);
}
foreach (var section in normalized.CustomSections)
{
AddBlock(lines, section.Title ?? "Additional Information", section.Items);
}
return string.Join("\n\n", lines.Where(line => !string.IsNullOrWhiteSpace(line)).Select(line => line.Trim())).Trim();
}
private static TailoredCvExperienceItem NormalizeExperience(TailoredCvExperienceItem? item)
{
item ??= new TailoredCvExperienceItem();
item.Title = TrimOrNull(item.Title);
item.Company = TrimOrNull(item.Company);
item.Location = TrimOrNull(item.Location);
item.Start = TrimOrNull(item.Start);
item.End = TrimOrNull(item.End);
item.Bullets = CleanList(item.Bullets);
item.IsCurrent = item.IsCurrent || string.Equals(item.End, "present", StringComparison.OrdinalIgnoreCase) || string.Equals(item.End, "current", StringComparison.OrdinalIgnoreCase);
return item;
}
private static TailoredCvEducationItem NormalizeEducation(TailoredCvEducationItem? item)
{
item ??= new TailoredCvEducationItem();
item.Qualification = TrimOrNull(item.Qualification);
item.QualificationLevel = TrimOrNull(item.QualificationLevel);
item.Institution = TrimOrNull(item.Institution);
item.Location = TrimOrNull(item.Location);
item.Start = TrimOrNull(item.Start);
item.End = TrimOrNull(item.End);
item.Details = CleanList(item.Details);
return item;
}
private static TailoredCvCustomSection NormalizeCustomSection(TailoredCvCustomSection? item)
{
item ??= new TailoredCvCustomSection();
item.Title = TrimOrNull(item.Title);
item.Items = CleanList(item.Items);
return item;
}
private static List<string> DeserializeList(string? json)
{
return DeserializeList<string>(json)
.Select(value => value?.Trim() ?? string.Empty)
.Where(value => !string.IsNullOrWhiteSpace(value))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
private static List<T> DeserializeList<T>(string? json)
{
try
{
return string.IsNullOrWhiteSpace(json)
? new List<T>()
: JsonSerializer.Deserialize<List<T>>(json, SerializerOptions) ?? new List<T>();
}
catch
{
return new List<T>();
}
}
private static T? DeserializeObject<T>(string? json) where T : class
{
try
{
return string.IsNullOrWhiteSpace(json) ? null : JsonSerializer.Deserialize<T>(json, SerializerOptions);
}
catch
{
return null;
}
}
private static List<string> CleanList(IEnumerable<string>? values)
{
return (values ?? Array.Empty<string>())
.Select(value => value?.Trim() ?? string.Empty)
.Where(value => !string.IsNullOrWhiteSpace(value))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
private static string? TrimOrNull(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
private static string? FormatDateRange(string? start, string? end, bool isCurrent)
{
if (string.IsNullOrWhiteSpace(start) && string.IsNullOrWhiteSpace(end)) return null;
if (string.IsNullOrWhiteSpace(start)) return end;
return $"{start} - {(isCurrent ? "Present" : end ?? "Present")}";
}
private static void AddBlock(List<string> lines, string title, IEnumerable<string> body)
{
var content = string.Join("\n", body.Where(line => !string.IsNullOrWhiteSpace(line)).Select(line => line.Trim())).Trim();
if (string.IsNullOrWhiteSpace(content)) return;
lines.Add($"{title}\n{content}");
}
private static void AddLine(List<string> lines, string? value)
{
if (!string.IsNullOrWhiteSpace(value)) lines.Add(value.Trim());
}
}