From 4edfad18b1828ac30ca01636011c31bf59f9a656 Mon Sep 17 00:00:00 2001 From: csc530 <77406318+csc530@users.noreply.github.com> Date: Sat, 16 Dec 2023 23:54:39 -0500 Subject: [PATCH] :art: style(add job command): use the new text prompt constructor simplify the construction of a text prompt to autofill from cmd options Signed-off-by: csc530 <77406318+csc530@users.noreply.github.com> --- resume builder/Helpers.cs | 352 ++++++++++-------- .../cli/commands/add/AddJobCommand.cs | 95 ++--- 2 files changed, 233 insertions(+), 214 deletions(-) diff --git a/resume builder/Helpers.cs b/resume builder/Helpers.cs index 0bea64b..bfb9fcb 100644 --- a/resume builder/Helpers.cs +++ b/resume builder/Helpers.cs @@ -1,6 +1,4 @@ -using System.ComponentModel.DataAnnotations.Schema; using System.Data.Common; -using System.Reflection; using Microsoft.Data.Sqlite; using resume_builder.cli.settings; using resume_builder.models; @@ -10,168 +8,206 @@ namespace resume_builder; public static class Globals { - public const string NullString = ""; - public static DateOnly Today { get; } = DateOnly.FromDateTime(DateTime.Today); - - public static int PrintError(ExitCode exitCode, string message) - { - AnsiConsole.Foreground = Color.Red; - AnsiConsole.WriteLine($"Error {exitCode.ToInt()}: {exitCode}"); - AnsiConsole.WriteLine(message); - return exitCode.ToInt(); - } - - public static int PrintError(CLISettings settings, Exception exception) - { - AnsiConsole.Foreground = Color.Red; - if(exception.GetType() == typeof(SqliteException)) - { - var e = (SqliteException)exception; - var err = $"Database Error {e.SqliteErrorCode}"; - if(settings.Verbose) - err += $"-{e.SqliteExtendedErrorCode}"; - err += $": {((SqlResultCode)e.SqliteErrorCode).GetMessage()}"; - AnsiConsole.WriteLine($"{err}"); - if(settings.Verbose) - AnsiConsole.WriteLine($"{e.Message}"); - return ExitCode.DbError.ToInt(); - } - else if(exception is InvalidOperationException) - { - AnsiConsole.WriteLine("Invalid operation"); - return ExitCode.Fail.ToInt(); - } - else - { - AnsiConsole.WriteLine(exception.Message); - return ExitCode.Error.ToInt(); - } - } + public const string NullString = ""; + public static DateOnly Today { get; } = DateOnly.FromDateTime(DateTime.Today); + + public static int PrintError(ExitCode exitCode, string message) + { + AnsiConsole.Foreground = Color.Red; + AnsiConsole.WriteLine($"Error {exitCode.ToInt()}: {exitCode}"); + AnsiConsole.WriteLine(message); + return exitCode.ToInt(); + } + + public static int PrintError(CLISettings settings, Exception exception) + { + AnsiConsole.Foreground = Color.Red; + if(exception.GetType() == typeof(SqliteException)) + { + var e = (SqliteException)exception; + var err = $"Database Error {e.SqliteErrorCode}"; + if(settings.Verbose) + err += $"-{e.SqliteExtendedErrorCode}"; + err += $": {((SqlResultCode)e.SqliteErrorCode).GetMessage()}"; + AnsiConsole.WriteLine($"{err}"); + if(settings.Verbose) + AnsiConsole.WriteLine($"{e.Message}"); + return ExitCode.DbError.ToInt(); + } + else if(exception is InvalidOperationException) + { + AnsiConsole.WriteLine("Invalid operation"); + return ExitCode.Fail.ToInt(); + } + else + { + AnsiConsole.WriteLine(exception.Message); + return ExitCode.Error.ToInt(); + } + } } -public static partial class Extensions +public static class Extensions { - #region conversions + // todo: inquire about default value being a property - spectre console pr/iss + // .DefaultValue(textPrompt); - public static int ToInt(this ExitCode exitCode) => (int)exitCode; - public static DateOnly ToDateOnly(this DateTime date) => DateOnly.FromDateTime(date); - - #endregion - - // todo: inquire about default value being a property - spectre console pr/iss - // .DefaultValue(textPrompt); - - - public static string GetPrintValue(this string? value, bool allowBlank = false) - { - if(allowBlank) - return value ?? ""; - return string.IsNullOrWhiteSpace(value) ? Globals.NullString : value; - } - - public static string GetPrintValue(this object? value, bool allowBlank = false) => - GetPrintValue(value?.ToString(), allowBlank); - - #region sql values - - public static object? GetNullableValue(this DbDataReader reader, string columnName) - { - var ordinal = reader.GetOrdinal(columnName); - return reader.GetNullableValue(ordinal); - } - - public static object? GetNullableValue(this DbDataReader reader, int ordinal) => - reader.IsDBNull(ordinal) ? null : reader[ordinal]; - - public static T? GetNullableValue(this DbDataReader reader, string columnName) - { - var ordinal = reader.GetOrdinal(columnName); - return reader.GetNullableValue(ordinal); - } - - public static T? GetNullableValue(this DbDataReader reader, int ordinal) - { - var val = GetNullableValue(reader, ordinal); - return val == null ? (T?)val : reader.GetFieldValue(ordinal); - } - - /// - /// because why won't it just auto convert it to a - /// - /// - public static void AddWithNullableValue(this SqliteParameterCollection parameters, string name, object? value) - => parameters.AddWithValue(name, value ?? DBNull.Value); - - #endregion - - #region strings - - public static string Prefix(this string s, string txt) => $"{txt}{s}"; - - public static List Prefix(this IEnumerable strings, string txt) => - strings.Select(s => s.Prefix(txt)).ToList(); - - public static string Surround(this string? s, string txt) => $"{txt}{s}{txt}"; - - - /// surrounds each string within a list with another string. - /// - /// the list strings to be surrounded. - /// - /// The text to surround the strings with. - /// - /// A list of strings with the parameter txt surrounding each string in the original list. - public static List Surround(this IEnumerable strings, string txt) => strings.Select(s => s.Surround(txt)).ToList(); - - public static bool IsNullOrEmpty(this string s) => string.IsNullOrEmpty(s); - public static bool IsNullOrWhiteSpace(this string s) => string.IsNullOrWhiteSpace(s); - public static bool IsBlank(this string? s) => s == null || string.IsNullOrEmpty(s) || string.IsNullOrWhiteSpace(s); - - #endregion - - #region Table - - /// - /// add column values to the table based on conditions - /// - /// the table to append column values - /// a key-value pair of column condition and column values; if the column condition is true, the column value will be added to the table. - /// if the column condition is false, the column (value) will be skipped - public static Table AddTableRow(this Table table, params KeyValuePair[] columns) - { - var row = columns.Where(col => col.Key) - .Select(col => col.Value.GetPrintValue(allowBlank: true)) - .ToArray(); - table.AddRow(row); - return table; - } - - public static Table AddTableColumn(this Table table, string name, bool nowrap = false) => - table.AddColumn(RenderableFactory.CreateTableColumn(name, nowrap)); - - public static Table AddTableColumn(this Table table, bool nowrap = false, params string[] columns) - { - foreach(var column in columns) - table.AddTableColumn(column, nowrap); - return table; - } - - public static Table AddTableColumn(this Table table, params string[] columns) - { - foreach(var column in columns) - table.AddTableColumn(column); - return table; - } - - #endregion + + public static string GetPrintValue(this string? value, bool allowBlank = false) + { + if(allowBlank) + return value ?? ""; + return string.IsNullOrWhiteSpace(value) ? Globals.NullString : value; + } + + public static string GetPrintValue(this object? value, bool allowBlank = false) => + GetPrintValue(value?.ToString(), allowBlank); + + #region conversions + + public static int ToInt(this ExitCode exitCode) => (int)exitCode; + public static DateOnly ToDateOnly(this DateTime date) => DateOnly.FromDateTime(date); + + #endregion + + #region sql values + + public static object? GetNullableValue(this DbDataReader reader, string columnName) + { + var ordinal = reader.GetOrdinal(columnName); + return reader.GetNullableValue(ordinal); + } + + public static object? GetNullableValue(this DbDataReader reader, int ordinal) => + reader.IsDBNull(ordinal) ? null : reader[ordinal]; + + public static T? GetNullableValue(this DbDataReader reader, string columnName) + { + var ordinal = reader.GetOrdinal(columnName); + return reader.GetNullableValue(ordinal); + } + + public static T? GetNullableValue(this DbDataReader reader, int ordinal) + { + var val = GetNullableValue(reader, ordinal); + return val == null ? (T?)val : reader.GetFieldValue(ordinal); + } + + /// + /// because why won't it just auto convert it to a + /// + /// + public static void AddWithNullableValue(this SqliteParameterCollection parameters, string name, object? value) + => parameters.AddWithValue(name, value ?? DBNull.Value); + + #endregion + + #region strings + + public static string Prefix(this string s, string txt) => $"{txt}{s}"; + + public static List Prefix(this IEnumerable strings, string txt) => + strings.Select(s => s.Prefix(txt)).ToList(); + + public static string Surround(this string? s, string txt) => $"{txt}{s}{txt}"; + + + /// surrounds each string within a list with another string. + /// + /// the list strings to be surrounded. + /// + /// The text to surround the strings with. + /// + /// A list of strings with the parameter txt surrounding each string in the original list. + public static List Surround(this IEnumerable strings, string txt) => + strings.Select(s => s.Surround(txt)).ToList(); + + public static bool IsNullOrEmpty(this string s) => string.IsNullOrEmpty(s); + public static bool IsNullOrWhiteSpace(this string s) => string.IsNullOrWhiteSpace(s); + + /// + /// check if a string is null, empty, or whitespace + /// + /// string to check + /// true if the string is null, empty, or whitespace; otherwise false + public static bool IsBlank(this string? s) => s == null || string.IsNullOrEmpty(s) || string.IsNullOrWhiteSpace(s); + + #endregion + + #region Table + + /// + /// add column values to the table based on conditions + /// + /// the table to append column values + /// a key-value pair of column condition and column values; if the column condition is true, the column value will be added to the table. + /// if the column condition is false, the column (value) will be skipped + public static Table AddTableRow(this Table table, params KeyValuePair[] columns) + { + var row = columns.Where(col => col.Key) + .Select(col => col.Value.GetPrintValue(allowBlank: true)) + .ToArray(); + table.AddRow(row); + return table; + } + + public static Table AddTableColumn(this Table table, string name, bool nowrap = false) => + table.AddColumn(RenderableFactory.CreateTableColumn(name, nowrap)); + + public static Table AddTableColumn(this Table table, bool nowrap = false, params string[] columns) + { + foreach(var column in columns) + table.AddTableColumn(column, nowrap); + return table; + } + + public static Table AddTableColumn(this Table table, params string[] columns) + { + foreach(var column in columns) + table.AddTableColumn(column); + return table; + } + + #endregion } public static class RenderableFactory { - public static TableColumn CreateTableColumn(string name, bool nowrap = false) => new TableColumn(name) - { - Footer = new Text(name), - Header = new Text(name), - NoWrap = nowrap - }; + public static TableColumn CreateTableColumn(string name, bool nowrap = false) => new TableColumn(name) + { + Footer = new Text(name), + Header = new Text(name), + NoWrap = nowrap + }; + + + /// + /// The default value of the prompt. + /// If the default value is null and allowEmpty is false, the default value will not be displayed (empty brackets) nor set. + public static TextPrompt CreateTextPrompt(string prompt, T defaultValue, bool allowEmpty = false, + Func? validator = null) + { + if(defaultValue == null && !allowEmpty) + return CreateTextPrompt(prompt, allowEmpty, validator); + return CreateTextPrompt(prompt, allowEmpty, validator).DefaultValue(defaultValue); + } + + /// + /// Creates a text prompt with the specified prompt message, default value, and optional parameters. + /// + /// + /// The type of the prompt result. + /// The prompt message to display to the user. + /// Optional. Specifies whether empty input is allowed. Defaults to false. + /// Optional. A function that validates the input value. Defaults to null. + /// + /// A object representing the created text prompt. + public static TextPrompt CreateTextPrompt(string prompt, bool allowEmpty = false, + Func? validator = null) => + new(prompt) + { + AllowEmpty = allowEmpty, + Validator = validator, + ShowDefaultValue = false + }; } \ No newline at end of file diff --git a/resume builder/cli/commands/add/AddJobCommand.cs b/resume builder/cli/commands/add/AddJobCommand.cs index a3a7197..3fe755c 100644 --- a/resume builder/cli/commands/add/AddJobCommand.cs +++ b/resume builder/cli/commands/add/AddJobCommand.cs @@ -1,7 +1,5 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; -using Microsoft.EntityFrameworkCore; -using resume_builder.models; using resume_builder.models; using Spectre.Console; using Spectre.Console.Cli; @@ -9,7 +7,7 @@ namespace resume_builder.cli.commands.add; -internal sealed class AddJobCommand : Command +internal sealed class AddJobCommand: Command { public override int Execute([NotNull] CommandContext context, [NotNull] AddJobSettings settings) { @@ -23,70 +21,55 @@ public override int Execute([NotNull] CommandContext context, [NotNull] AddJobSe if(settings.PromptUser) { - // todo: show entered fields as default - var jobTitlePrompt = new TextPrompt("Job title: ") - { - ShowDefaultValue = false, - AllowEmpty = false, - Validator = value => string.IsNullOrWhiteSpace(value) + var jobTitlePrompt = RenderableFactory.CreateTextPrompt("Job title: ", jobTitle).Validate(value => + string.IsNullOrWhiteSpace(value) ? ValidationResult.Error("Job title is invalid: cannot be empty") - : ValidationResult.Success(), - }; - var descriptionPrompt = new TextPrompt("Description: ").ShowDefaultValue(false).AllowEmpty(); - var experiencePrompt = new TextPrompt("Experience: ").ShowDefaultValue(false).AllowEmpty(); - var companyPrompt = new TextPrompt("Company: ").ShowDefaultValue(false).AllowEmpty(); - var endDatePrompt = new TextPrompt("End date: ") - .AllowEmpty() - .DefaultValue(null) - .ShowDefaultValue(false) - .Validate(date => date > startDate - ? ValidationResult.Success() - : ValidationResult.Error("[red]End date must be after start date[/]")); + : ValidationResult.Success()); + var startDatePrompt = RenderableFactory.CreateTextPrompt("Start date: ", startDate ?? Today, true) + .Validate(date => date < Today + ? ValidationResult.Error("[red]Start date must be in the past[/]") + : ValidationResult.Success()); + var descriptionPrompt = RenderableFactory.CreateTextPrompt("Description: ", jobDescription); + var experiencePrompt = RenderableFactory.CreateTextPrompt("Experience: ", experience); + var companyPrompt = RenderableFactory.CreateTextPrompt("Company: ", company); + var endDatePrompt = RenderableFactory.CreateTextPrompt("End date: ", endDate, true) + .Validate(date => date > startDate + ? ValidationResult.Success() + : ValidationResult.Error("[red]End date must be after start date[/]")); jobTitle = AnsiConsole.Prompt(jobTitlePrompt); jobDescription = AnsiConsole.Prompt(descriptionPrompt); experience = AnsiConsole.Prompt(experiencePrompt); company = AnsiConsole.Prompt(companyPrompt); - startDate = AnsiConsole.Ask("Start date: ", Today); + startDate = AnsiConsole.Prompt(startDatePrompt); endDate = AnsiConsole.Prompt(endDatePrompt); } if(string.IsNullOrWhiteSpace(jobTitle)) - return PrintError(ExitCode.InvalidArgument, "Job title is invalid, it cannot be empty"); + throw new ArgumentException("Job title is invalid, it cannot be empty"); - try - { - var job = new Job() - { - Title = jobTitle, - Description = jobDescription, - Experience = experience, - Company = company, - StartDate = (DateOnly)startDate!, - EndDate = endDate - - }; - ResumeContext db = new ResumeContext(); - db.Jobs.Add(job); - db.SaveChanges(acceptAllChangesOnSuccess: true); - AnsiConsole.MarkupLine($"✅ Job \"[bold]{job.Title}[/]\" added"); - return ExitCode.Success.ToInt(); - } - catch(Exception e) + var job = new Job() { - return PrintError(settings, e); - } + Title = jobTitle, + Description = jobDescription, + Experience = experience, + Company = company, + StartDate = (DateOnly)startDate!, + EndDate = endDate + }; + var db = new ResumeContext(); + db.Jobs.Add(job); + db.SaveChanges(acceptAllChangesOnSuccess: true); + AnsiConsole.MarkupLine($"✅ Job \"[bold]{job.Title}[/]\" added"); + return ExitCode.Success.ToInt(); } } -public class AddJobSettings : AddCommandSettings + +public class AddJobSettings: AddCommandSettings { public bool PromptUser => - (JobTitle.IsBlank() && Company.IsBlank() && StartDate == null && EndDate == null && - JobDescription.IsBlank() && Experience.IsBlank()) - || - //? why did i do this? shouldn't only be if they're all null??? - (!JobTitle.IsBlank() && !Company.IsBlank() && StartDate != null && EndDate != null && - !JobDescription.IsBlank() && !Experience.IsBlank()) + (JobTitle.IsBlank() && Company.IsBlank() && StartDate == null && EndDate == null && JobDescription.IsBlank() && + Experience.IsBlank()) || Interactive; @@ -116,13 +99,13 @@ public class AddJobSettings : AddCommandSettings public override ValidationResult Validate() { - if(string.IsNullOrWhiteSpace(JobTitle) && JobTitle != null || - (string.IsNullOrWhiteSpace(JobTitle) && !PromptUser)) + if(string.IsNullOrWhiteSpace(JobTitle) && !PromptUser) return ValidationResult.Error("Job title is invalid: cannot be empty"); + // equivalent to + //(StartDate != null && EndDate != null && EndDate < StartDate) return EndDate < StartDate - ? //(StartDate != null && EndDate < StartDate) - ValidationResult.Error($"end date must be after start date: {EndDate} < {StartDate}") + ? ValidationResult.Error($"end date must be after start date: {EndDate} !< {StartDate}") : ValidationResult.Success(); } -} +} \ No newline at end of file