Update entity classes

EntityList's `enforceTypes` setting was removed, as EntityName
enforced entries being unambiguous anyway.
Added a way to enforce specific types on instantiation or else
throw an exception, and updated all existing uses requiring that
check accordingly.
This commit is contained in:
Noi 2022-08-22 19:25:48 -07:00
parent b4db1fcff8
commit 7b29753290
12 changed files with 68 additions and 48 deletions

View file

@ -22,13 +22,12 @@ public class EntityList : IEnumerable<EntityName> {
/// <summary> /// <summary>
/// Creates a new EntityList instance with no data. /// Creates a new EntityList instance with no data.
/// </summary> /// </summary>
public EntityList() : this(null, false) { } public EntityList() : this(null) { }
/// <summary> /// <summary>
/// Creates a new EntityList instance using the given JSON token as input. /// Creates a new EntityList instance using the given JSON token as input.
/// </summary> /// </summary>
/// <param name="input">JSON array to be used for input. For ease of use, null values are also accepted.</param> /// <param name="input">JSON array to be used for input. For ease of use, null values are also accepted.</param>
/// <param name="enforceTypes">Specifies if all entities defined in configuration must have their type specified.</param>
/// <exception cref="ArgumentException">The input is not a JSON array.</exception> /// <exception cref="ArgumentException">The input is not a JSON array.</exception>
/// <exception cref="ArgumentNullException"> /// <exception cref="ArgumentNullException">
/// Unintiutively, this exception is thrown if a user-provided configuration value is blank. /// Unintiutively, this exception is thrown if a user-provided configuration value is blank.
@ -36,7 +35,7 @@ public class EntityList : IEnumerable<EntityName> {
/// <exception cref="FormatException"> /// <exception cref="FormatException">
/// When enforceTypes is set, this is thrown if an EntityName results in having its Type be Unspecified. /// When enforceTypes is set, this is thrown if an EntityName results in having its Type be Unspecified.
/// </exception> /// </exception>
public EntityList(JToken? input, bool enforceTypes) { public EntityList(JToken? input) {
if (input == null) { if (input == null) {
_innerList = new List<EntityName>().AsReadOnly(); _innerList = new List<EntityName>().AsReadOnly();
return; return;
@ -50,8 +49,6 @@ public class EntityList : IEnumerable<EntityName> {
foreach (var item in inputArray.Values<string>()) { foreach (var item in inputArray.Values<string>()) {
if (string.IsNullOrWhiteSpace(item)) continue; if (string.IsNullOrWhiteSpace(item)) continue;
var itemName = new EntityName(item); var itemName = new EntityName(item);
if (enforceTypes && itemName.Type == EntityType.Unspecified)
throw new FormatException($"The following value is not prefixed: {item}");
list.Add(itemName); list.Add(itemName);
} }
_innerList = list.AsReadOnly(); _innerList = list.AsReadOnly();
@ -82,7 +79,7 @@ public class EntityList : IEnumerable<EntityName> {
} else { } else {
foreach (var r in authorRoles) { foreach (var r in authorRoles) {
if (!string.Equals(r.Name, entry.Name, StringComparison.OrdinalIgnoreCase)) break; if (!string.Equals(r.Name, entry.Name, StringComparison.OrdinalIgnoreCase)) break;
if (keepId) entry.SetId(r.Id); if (keepId) entry.Id = r.Id;
return true; return true;
} }
} }
@ -91,7 +88,7 @@ public class EntityList : IEnumerable<EntityName> {
if (entry.Id.Value == channel.Id) return true; if (entry.Id.Value == channel.Id) return true;
} else { } else {
if (!string.Equals(channel.Name, entry.Name, StringComparison.OrdinalIgnoreCase)) break; if (!string.Equals(channel.Name, entry.Name, StringComparison.OrdinalIgnoreCase)) break;
if (keepId) entry.SetId(channel.Id); if (keepId) entry.Id = channel.Id;
return true; return true;
} }
} else { // User } else { // User
@ -99,7 +96,7 @@ public class EntityList : IEnumerable<EntityName> {
if (entry.Id.Value == author.Id) return true; if (entry.Id.Value == author.Id) return true;
} else { } else {
if (!string.Equals(author.Username, entry.Name, StringComparison.OrdinalIgnoreCase)) break; if (!string.Equals(author.Username, entry.Name, StringComparison.OrdinalIgnoreCase)) break;
if (keepId) entry.SetId(author.Id); if (keepId) entry.Id = author.Id;
return true; return true;
} }
} }

View file

@ -6,23 +6,29 @@
/// </summary> /// </summary>
public class EntityName { public class EntityName {
/// <summary> /// <summary>
/// The entity's type, if specified in configuration. /// The entity's type, as specified in configuration.
/// </summary> /// </summary>
public EntityType Type { get; private set; } public EntityType Type { get; private set; }
private ulong? _id;
/// <summary> /// <summary>
/// Entity's unique ID value (snowflake). May be null if the value is not known. /// Entity's unique ID value (snowflake). May be null if the value is not known.
/// </summary> /// </summary>
public ulong? Id { get; private set; } /// <remarks>
/// This value may be updated during runtime if the parent <see cref="EntityList"/> was instructed to
/// update the ID for persistence.
/// </remarks>
public ulong? Id {
get => _id;
internal set => _id ??= value;
}
/// <summary> /// <summary>
/// Entity's name as specified in configuration. May be null if it was not specified. /// Entity's name as specified in configuration. May be null if it was not specified.
/// This value is not updated during runtime.
/// </summary> /// </summary>
/// <remarks>This value is not updated during runtime.</remarks>
public string? Name { get; private set; } public string? Name { get; private set; }
// TODO elsewhere: find a way to emit a warning if the user specified a name without ID in configuration.
/// <summary> /// <summary>
/// Creates a new object instance from the given input string. /// Creates a new object instance from the given input string.
/// Documentation for the EntityName format can be found elsewhere in this project's documentation. /// Documentation for the EntityName format can be found elsewhere in this project's documentation.
@ -72,14 +78,23 @@ public class EntityName {
} }
} }
internal void SetId(ulong id) { /// <summary>
if (!Id.HasValue) Id = id; /// Creates a new object instance from the given input string.
/// Documentation for the EntityName format can be found elsewhere in this project's documentation.
/// </summary>
/// <param name="input">Input string in EntityName format.</param>
/// <param name="expectedType">The <see cref="EntityType"/> expected for this instance.</param>
/// <exception cref="ArgumentNullException">Input string is null or blank.</exception>
/// <exception cref="ArgumentException">Input string cannot be resolved to an entity type.</exception>
/// <exception cref="FormatException">Input string was resolved to a type other than specified.</exception>
public EntityName(string input, EntityType expectedType) : this(input) {
if (Type != expectedType) throw new FormatException("Resolved EntityType does not match expected type.");
} }
/// <summary> /// <summary>
/// Returns the appropriate prefix corresponding to an EntityType. /// Returns the appropriate prefix corresponding to an EntityType.
/// </summary> /// </summary>
public static char Prefix(EntityType t) => t switch { public static char GetPrefix(EntityType t) => t switch {
EntityType.Role => '&', EntityType.Role => '&',
EntityType.Channel => '#', EntityType.Channel => '#',
EntityType.User => '@', EntityType.User => '@',
@ -90,7 +105,7 @@ public class EntityName {
/// Returns a string representation of this item in proper EntityName format. /// Returns a string representation of this item in proper EntityName format.
/// </summary> /// </summary>
public override string ToString() { public override string ToString() {
var pf = Prefix(Type); var pf = GetPrefix(Type);
if (Id.HasValue && Name != null) if (Id.HasValue && Name != null)
return $"{pf}{Id.Value}::{Name}"; return $"{pf}{Id.Value}::{Name}";

View file

@ -3,8 +3,6 @@
/// The type of entity specified in an <see cref="EntityName"/>. /// The type of entity specified in an <see cref="EntityName"/>.
/// </summary> /// </summary>
public enum EntityType { public enum EntityType {
/// <summary>Default value. Is never referenced in regular usage.</summary>
Unspecified,
/// <summary> /// <summary>
/// Userd when the <see cref="EntityName"/> represents a role. /// Userd when the <see cref="EntityName"/> represents a role.
/// </summary> /// </summary>

View file

@ -75,7 +75,7 @@ public class FilterList {
if (incoming.Type != JTokenType.Array) if (incoming.Type != JTokenType.Array)
throw new FormatException("Filtering list must be a JSON array."); throw new FormatException("Filtering list must be a JSON array.");
FilteredList = new EntityList((JArray)incoming, true); FilteredList = new EntityList((JArray)incoming);
// Verify the same for the exemption list. // Verify the same for the exemption list.
if (exemptKey != null) { if (exemptKey != null) {
@ -85,7 +85,7 @@ public class FilterList {
} else if (incomingEx.Type != JTokenType.Array) { } else if (incomingEx.Type != JTokenType.Array) {
throw new FormatException("Filtering exemption list must be a JSON array."); throw new FormatException("Filtering exemption list must be a JSON array.");
} else { } else {
FilterExemptions = new EntityList(incomingEx, true); FilterExemptions = new EntityList(incomingEx);
} }
} else { } else {
FilterExemptions = new EntityList(); FilterExemptions = new EntityList();

View file

@ -26,13 +26,10 @@ class GuildData {
public GuildData(JObject conf, Dictionary<ulong, DateTimeOffset> _waitingList) { public GuildData(JObject conf, Dictionary<ulong, DateTimeOffset> _waitingList) {
WaitingList = _waitingList; WaitingList = _waitingList;
var cfgRole = conf["Role"]?.Value<string>();
if (string.IsNullOrWhiteSpace(cfgRole))
throw new ModuleLoadException("Role value not specified.");
try { try {
TargetRole = new EntityName(cfgRole); TargetRole = new EntityName(conf["Role"]?.Value<string>()!, EntityType.Role);
} catch (ArgumentException) { } catch (Exception) {
throw new ModuleLoadException("Role config value was not properly specified to be a role."); throw new ModuleLoadException("'Role' was not properly specified.");
} }
try { try {

View file

@ -29,10 +29,14 @@ abstract class RoleManipulation : CommandConfig {
// "role" - string; The given role that applies to this command. // "role" - string; The given role that applies to this command.
// "successmsg" - string; Messages to display on command success. Overrides default. // "successmsg" - string; Messages to display on command success. Overrides default.
protected RoleManipulation(ModCommands module, JObject config) : base(module, config) { protected RoleManipulation(ModCommands module, JObject config) : base(module, config) {
var rolestr = config[nameof(Role)]?.Value<string>(); try {
if (string.IsNullOrWhiteSpace(rolestr)) throw new ModuleLoadException($"'{nameof(Role)}' must be provided."); Role = new EntityName(config[nameof(Role)]?.Value<string>()!, EntityType.Role);
Role = new EntityName(rolestr); } catch (ArgumentNullException) {
if (Role.Type != EntityType.Role) throw new ModuleLoadException($"The value in '{nameof(Role)}' is not a role."); throw new ModuleLoadException($"'{nameof(Role)}' must be provided.");
} catch (FormatException) {
throw new ModuleLoadException($"The value in '{nameof(Role)}' is not a role.");
}
SuccessMessage = config[nameof(SuccessMessage)]?.Value<string>(); SuccessMessage = config[nameof(SuccessMessage)]?.Value<string>();
_usage = $"{Command} `user or user ID`\n" + _usage = $"{Command} `user or user ID`\n" +

View file

@ -9,10 +9,11 @@ class ModuleConfig {
public ModuleConfig(JObject config) { public ModuleConfig(JObject config) {
const string RptChError = $"'{nameof(ReportingChannel)}' must be set to a valid channel name."; const string RptChError = $"'{nameof(ReportingChannel)}' must be set to a valid channel name.";
var rptch = config[nameof(ReportingChannel)]?.Value<string>(); try {
if (string.IsNullOrWhiteSpace(rptch)) throw new ModuleLoadException(RptChError); ReportingChannel = new EntityName(config[nameof(ReportingChannel)]?.Value<string>()!, EntityType.Channel);
ReportingChannel = new EntityName(rptch); } catch (Exception) {
if (ReportingChannel.Type != EntityType.Channel) throw new ModuleLoadException(RptChError); throw new ModuleLoadException(RptChError);
}
// Individual logging settings - all default to false // Individual logging settings - all default to false
LogMessageDeletions = config[nameof(LogMessageDeletions)]?.Value<bool>() ?? false; LogMessageDeletions = config[nameof(LogMessageDeletions)]?.Value<bool>() ?? false;

View file

@ -5,11 +5,13 @@ class ModuleConfig {
public EntityName Role { get; } public EntityName Role { get; }
public ModuleConfig(JObject conf) { public ModuleConfig(JObject conf) {
var cfgRole = conf[nameof(Role)]?.Value<string>(); try {
if (string.IsNullOrWhiteSpace(cfgRole)) Role = new EntityName(conf[nameof(Role)]?.Value<string>()!, EntityType.Role);
throw new ModuleLoadException("Role was not specified."); } catch (ArgumentException) {
Role = new EntityName(cfgRole); throw new ModuleLoadException("Role was not properly specified.");
if (Role.Type != EntityType.Role) } catch (FormatException) {
throw new ModuleLoadException("Name specified in configuration is not a role."); throw new ModuleLoadException("Name specified in configuration is not a role.");
}
} }
} }

View file

@ -34,9 +34,11 @@ class ConfDefinition {
var rptch = def[nameof(ReportingChannel)]?.Value<string>(); var rptch = def[nameof(ReportingChannel)]?.Value<string>();
if (rptch != null) { if (rptch != null) {
ReportingChannel = new EntityName(rptch); try {
if (ReportingChannel.Type != EntityType.Channel) ReportingChannel = new EntityName(rptch, EntityType.Channel);
} catch (FormatException) {
throw new ModuleLoadException($"'{nameof(ReportingChannel)}' is not defined as a channel{errpostfx}"); throw new ModuleLoadException($"'{nameof(ReportingChannel)}' is not defined as a channel{errpostfx}");
}
} }
// Regex loading // Regex loading

View file

@ -160,14 +160,14 @@ class ResponseExecutor {
SocketGuildUser? tuser; SocketGuildUser? tuser;
SocketRole? trole; SocketRole? trole;
try { try {
var userName = new EntityName(param[0]); var userName = new EntityName(param[0], EntityType.User);
if (userName.Id.HasValue) tuser = _guild.GetUser(userName.Id.Value); if (userName.Id.HasValue) tuser = _guild.GetUser(userName.Id.Value);
else { else {
if (userName.Name == "_") tuser = _user; if (userName.Name == "_") tuser = _user;
else tuser = userName.FindUserIn(_guild); else tuser = userName.FindUserIn(_guild);
} }
if (tuser == null) return FromError($"Unable to find user '{userName.Name}'."); if (tuser == null) return FromError($"Unable to find user '{userName.Name}'.");
var roleName = new EntityName(param[1]); var roleName = new EntityName(param[1], EntityType.Role);
if (roleName.Id.HasValue) trole = _guild.GetRole(roleName.Id.Value); if (roleName.Id.HasValue) trole = _guild.GetRole(roleName.Id.Value);
else trole = roleName.FindRoleIn(_guild); else trole = roleName.FindRoleIn(_guild);
if (trole == null) return FromError($"Unable to find role '{roleName.Name}'."); if (trole == null) return FromError($"Unable to find role '{roleName.Name}'.");

View file

@ -17,8 +17,12 @@ class ModuleConfig {
var values = new Dictionary<ulong, ulong>(); var values = new Dictionary<ulong, ulong>();
foreach (var item in config.Properties()) { foreach (var item in config.Properties()) {
var name = new EntityName(item.Name); EntityName name;
if (name.Type != EntityType.Role) throw new ModuleLoadException($"'{item.Name}' is not specified as a role."); try {
name = new EntityName(item.Name, EntityType.Role);
} catch (FormatException) {
throw new ModuleLoadException($"'{item.Name}' is not specified as a role.");
}
var role = name.FindRoleIn(g); var role = name.FindRoleIn(g);
if (role == null) throw new ModuleLoadException($"Unable to find role '{name}'."); if (role == null) throw new ModuleLoadException($"Unable to find role '{name}'.");

View file

@ -69,7 +69,7 @@ class ModuleStateService : Service {
} }
// Load moderator list // Load moderator list
var mods = new EntityList(guildConf["Moderators"]!, true); var mods = new EntityList(guildConf["Moderators"]!);
// Create guild state objects for all existing modules // Create guild state objects for all existing modules
var newStates = new Dictionary<Type, object?>(); var newStates = new Dictionary<Type, object?>();